跳到正文
V2EX·· 3 天前AI 评分42

Umem-cache 拒绝最终一致性脏读陷阱:强一致性实现原理

《拒绝最终一致性的脏读陷阱: Umem-cache 强一致性实现原理》

AI 导读

Umem-cache 通过缓存协议与集群设计实现强一致性,代价是牺牲可用性:服务故障时,即使其他服务正常,整个系统也无法工作。数据库更新时,事务和锁确保对应 Key 不在缓存中,也没有未完成的读取;集群则借助 Raft 维护成员与集群版本号,使成员变化后旧缓存自动失效。

正文

原文:consistency.rst

缓存一致性是 Umem-cache 最关键的特性,缓存协议和集群的设计方案都是围绕着它展开的。 缓存一致性能减少软件开发中的复杂性,因为你不需要担心过时的数据,这是最终一致性做不到的。

选择缓存一致性是有代价的:我们牺牲了可用性。 也就是一旦 Umem-cache 挂掉,即使你的其他服务依旧完好,整个系统也无法工作。 但是不必担心,我们有基于 raft 共识算法的集群解决方案,它可以提供很高的容错能力。

缓存协议

我们的缓存协议提供了 GetOrSet 和 Delete 两个命令,是的, 我们把 Get 和 Set 合并了。

缓存一致性的实现是靠在数据库中的 Key 更新完成的瞬间,以下事件确保发生:

  • 没有任何连接正在读取 Key
  • 所有在此之前读到 Key 且未完成 Set 的连接都已被关闭
  • Umem-cache 中不存在该 Key

下面我将详细说明:

缓存命中

缓存命中时,Umem-cache 直接返回数据。

但是如果 key 因为正在 set 而被锁住了,该 get 操作会给该锁设置上过期时间,并排队等待 value 。 另外,如果锁到期了,持有锁的连接会被关闭,并把锁转交给排队中的下一个连接。

::

客户端                  Umem-cache                  数据库
           get key
|------------------------->|
                           | 1. 等待锁释放
           返回 value        |
|<-------------------------|

缓存未命中

当缓存未命中时,我们会锁住 key ,然后尝试 set ,其他 get 请求会排队等待,这种机制是为了防止缓存击穿。

你可能发现了,Umem-cache 不响应 Set ,这让我们省掉了一次网络传输。 因为它大概率会成功,就算失败也不是什么大事。

::

客户端                  Umem-cache                  数据库
           get key
|------------------------->|
                           | 1. 锁住 key
           返回 miss         |
|<-------------------------|

                       get key
|--------------------------------------------------->|
                                                     | 2. 等待读锁
                       返回 value                      |
|<---------------------------------------------------|


|------------------------->| 3. set value
                             4. 释放 key 的锁

更新或删除

事务和锁能保证数据库在更新完成时,该 key 未被缓存,且不在缓存的路上。

将第 4 步放在事务的最后是最佳实践,因为如果该 key 已被缓存,那么在此之前,所有的 Get 都会命中, 而不是在队列中等待。

::

客户端                Umem-cache               数据库

|-------------------------------------------------->| 1. 开启事务
                                                    | 2. 获取 key 的写锁
|<--------------------------------------------------| 3. 更新或删除 key

      删除 key
|-------------------->|	4. 删除 key 或关闭持有 key 锁的连接
|<--------------------|


|-------------------------------------------------->| 5. 提交事务

集群

集群的负载均衡靠的是客户端散列,在集群模式下,每个 key 都携带了 8 字节的版本号前缀, 这个版本号与它要散列到的成员的版本号一致。 一旦 key 散列到的成员发生变化,对应成员的版本号会增长,这个版本号是由 Raft 共识协议来维护的, 并且我们保证了所有成员的版本号都不同。 所以一旦 key 散列到的成员发生变化,旧成员上的 key 就会因为版本号的变化自动失效, 这个机制就保证了同一时间不可能有两个成员上存在相同的有效的 key 。

集群也有一个版本号,这个版本号可以用来快速判断集群是否发生变化,这个版本号也是由 Raft 共识协议来维护的。 每次集群成员变更或者成员可用状态变更都会导致集群版本号增长。 所以集群版本号的增长也代表着集群中 Key 的散列分布发生了变化。

我们的集群协议提供了 Leader ,Authority 和 Cluster 三个命令, 每一个缓存命令都会向集群获取执行授权,这个授权通过 Authority 命令来执行,Authority 命令会返回当前集群的版本号。

下面是客户端实现流程:

::

                            -----
                            |开始|
                            -----
                              |
                              v
                  -------------------------
                  |随机选择服务器查询领导者信息|<----------
                  -------------------------          |
                              |                      |
                              v                      |
             ----------------------------------      |
             |向领导者查询集群信息,得到 version V1|      |
             ----------------------------------      |
                              |                      |
                 ---------------------------         |
                 |           并行           |         |
                 v                         v         |
 ----------------------------------      -----       |
 |向领导者请求认证权威,得到 version V2|      |缓存|       |
 ----------------------------------      -----       |
                 |                         |         |
                 ---------------------------         |
                              |                      |
                              v                      |
                             / \                     |
                            /   \                    |
                           /     \                   |
                          /V1==V2 \                  |
                          \       /--------否---------
                           \     /
                            \   /
                             \ /
                              |
                              是
                              |
                              v
                            -----
                            |结束|
                            -----

来源:V2EX · v2ex.com