似乎是在试图重新发明 Raft ?
- Raft 里有 ReadIndex
- 你的方案中,"向领导者请求认证权威" 这一步在脑裂情况下会有问题,旧的 leader 短时间内可能还会认为自己是合法 leader 。Raft 里的方案是:在读时向过半数的 follower 确认自己仍是 leader 。
对于自己设计的算法,一个好的方法是让 AI 用形式化验证帮你证明一下 serializability/linearizability ,尤其是在换主、过半数断连、脑裂等情况下。
- 你的延迟周期我是不认同的,它是很侥幸的,没有任何保证的。
- 我不能同意你说的 mysql 在 select by id 这一块,速度高的,因为我一旦同意这个,缓存就没有存在的意义了。
- Umem-cache 和 etcd 是不一样的东西。etcd 的本质是持久化的数据库,它的集群中的每一个节点都存相同的数据,它的命令都是要靠 leader 来执行,它要求多数节点完成执行,而且这个执行是要确定持久化到磁盘上,它碰磁盘,它自然慢。而 Umem-cache 是绝不会碰磁盘的,Umem-cache 的集群的每个节点存的也是完全不相同的数据,资源的利用率是很高的。
- 我的 raft 设计跟 etcd 是不一样的,这是得益于缓存的特性:允许少数据。在集群中执行的每个命令要向领导者请求认证权威,这个操作其实就是 leader 向集群发一次 heartbeat ,不需要携带业务数据,只要多数节点响应,我们就可以确认 leader 的权威。而且这步权威认证我是支持多个命令打包一起确认的。然后缓存命令也就是 GetOrSet 和 Delete 是可以异步执行的因为客户端可以缓存集群的状态,而且集群大多数情况下是稳定的。集群发生变更的情况下,重新执行就行了。缓存命令的压力是分散到集群每个节点的,效率不是 leader 统一处理可以比的。
- 我的 raft 是没有锁的,你关于 raft 锁的说法我不明白。在集群中,锁的情况和单节点是一样的,都是在节点上的。
- 关于集群的测试我还没做,现在还没条件做。所以我没有数据去分析 qps ,将来有测试结果了我会再发帖的。
是 get 两次,防止中间有被驱逐的更新。
看具体业务吧,“读都是命中缓存”只适合读多写少的情况,但在更新和删除场景,你的方案也无法保证强一致性,因为数据库的事务作用域无法跨越到外部缓存。比如在更新流程中,如果客户端在删除了缓存之后、提交数据库事务之前发生异常崩溃或回滚,此时数据库数据未变,但外部缓存已经被改了/删了。
你认为 cloudzhou 给出的方案会在缓存驱逐时无法达到最终一致,实际这种情况只有在非常极端情况才会发生(缓存空间太小,导致刚更新的缓存立马遭到驱逐),退一步来讲,如果 key_v2 真遭到驱逐,key_v1 被缓存成功,给 key_v1 设置一个可以最大可以忍受不一致的过期时间就可以了,最终一致性还是可以达到的。
我是保证强一致的。第一,Umem-cache 不提供修改的选项,更新和删除在 Umem-cache 都是删除
第二,提交数据库事务之前发生异常崩溃或回滚,Umem-cache 的数据最多是被删掉了,不影响一致性。你这个过期时间确实可以达到最终一致,这跟定时清空缓存差不多,只要缓存没数据,就是一致的。你一旦提供 ttl ,你就没法处理用户会续期的行为,只要用户持续续期,这个不一致就会影响很久。 你可能要重新描述以下整体的实现流程,我才能跟上你的解决最终一致性的思路。然后,我这下理解你说的 raft 锁了,我的 heartbeat 其实就是相当于一个写指令,只不过是什么都没写。
我是有用 raft 的需要的,因为我需要协商集群的状态,也就是当前是哪些节点组成了整个集群。集群的 qps 我目前是还没有设备去测,但是就单节点的性能我已经是碾压 Redis 了,集群我是有信心的。
如果是推广的话 技术思路值得学习; 经历过电商库存超售的事件 我还是会选择一楼的方案
到了这一级别的高一致性,简单依赖 db 就好了只依赖 db 记录的库存数 宁可查询慢一点。- matrix10104小时前⭐️ 新访客5168
建议发 Hacker News 。虽然我认为分布式缓存发展了这么多年,很难有什么新突破了,所有优化都是相对的取舍。但 Hacker News 的技术氛围还是比 G站 浓的。另外也可以 X 上 at 或者私信一下 antirez 看看他的看法。
你一旦提供 ttl ,你就没法处理用户会续期的行为
cache ttl 是没有续约概念的,只会中途刷新,或者到期消失,影响范围只有 ttl 时间我再次总结不出现 ttl 范围 cache 不一致的设计:
触发事件然后缓存集中处理,也就是没有并发态:
- 读触发 cache 更新,触发 event: (read, table, id)
- 驱逐、写触发 cache 可能更新,触发 event: (write/volatile, table, id)
- 事件流式处理,同一个 table 事件只有一个进程顺序处理
- 无论是 1 ,2 事件,从 db 拉去新数据,read 操作直接 cas 更新 cache ,write/volatile 一样,cas and only exists
重点:同一个 table 只有一个处理点,消息不管先后乱序,都是 select by id 拉最新数据和 cache 同步。
续期的意思就是 GETEX 传入相同的过期时间
过期时间是否相同一点都没影响啊,下次更新就是新的数据了,我们的核心是,旧数据最多 ttl 时间内
你的意思是每次 get 的时候,还延续 ttl ?这个我的实践上,没有这么做的你不能把发送 event z 失败,这个事情纳进来,正如 raft 脑裂整个系统失败了一样。
这么说的话不用举这么复杂的例子,只要 version=1 事件设置 cache 成功,version=2 事件因为各种原因失败就行了,缓存已经不一致CAP 理论里,只能有两个,而 cache ,实践上都是纳入弱一致性范围。
#43 唯一的问题就是第 4 步和第 6 步不可以失败。
监听 redis Keyspace Notifications 是改不了现状的,只是延长了步骤 10 的时间,只要二次驱逐发生在步骤 11 之前,就没有意义,因为步骤 11 的 version 是在第 5 步拿到的。引入 redis Keyspace Notifications 只是把极限的情况拉到更加极限罢了,此时业务又在不知不觉中变复杂了。另外 Umem-cache 是一个缓存,它有内存上限,如果把 Redis 当作数据库用而不是缓存,我们的讨论没有意义,因为此时 Umem-cache 和 Redis 就是截然不同的东西。而在把 Redis 当作数据库使用的情况下,再引入 DB 和 CDC 我感觉也没什么意义了。
https://chatgpt.com/share/6aeae799-2e2c-83e8-842e-4ed59b47ccca
ai 分析蛮有意思,我是理清每个人的逻辑了
参与者 最突出的贡献 我的评价 ( OP ) 提出双锁协议,尝试实现强一致 Cache 技术创造力强,但系统级正确性尚未证明 发现竞态、修正判断、讨论消息原子性 最像认真做协议 Review 的人 CDC 、版本号、异步处理、工程取舍 工程经验视角强,但部分论证依赖时间窗口假设 指出 Raft ReadIndex 、脑裂和形式化验证 分布式系统审查方向很准确 强调真实业务价值与选型 业务判断正确,技术分析不够深入 指出读后使用期间仍可能过期 业务语义问题成立,但结论过于绝对。
是的,但是流程简化了好多,可以考虑作为工程实践了,有意思。
强一致本身就很难实现,你给出的方法大体可行,代价就是丢失了性能,读写都需要锁,几乎等同于串行,如果涉及到集群,就更复杂了。 #1 基于 Redis 给出的最终一致性(允许短暂的不一致)方案,即便在这个基础上再做一些修改,还是无法保证强一致性,优点就是性能更好。
绝大部分的读都是缓存命中,不会涉及数据库的锁。然后修改数据,数据库先持有写锁也很必要,能避免并发的修改,这一点不管你用不用缓存都是要这样操作的。
然后我跟 讨论的内容是不涉及强一致性的,他的修改是由 CDC 提交到 redis 的,强一致是不可能达成的。我一直要表的的观点是他的方案保证不了最终一致。
Umem-cahce 是 Redis 作为缓存的替代品,我设计它的初衷就是提供强一致性,这是 Memcached 和 Redis 都做不到的。
一个概念我觉得首先要明确到底是为了解决什么问题,github 上 readme 写的特点是 cache 命中和吞吐超过 redis 和 memcached ,如果在同基准下能做到就已经很好了,可以详细说说这个是怎么做的。
但这篇文章的一致性问题
站在大的架构概念上考虑,这已经不是一个纯粹的缓存服务了,更像是一个分布式锁,或者说都不是分布式锁,而是数据库锁的外置版本。使用它有非常重的架构成本,不是直接引入就好的,举几个例子- 你的逻辑会直接依赖于数据库的隔离级别,你的更新写入依赖于数据库的事务,写入的 key 按你上面说的是先 select for update 查出来的,就不提你这样可能查出一大堆的数据直接打爆应用,数据库在 rr 和 rc 的隔离级别下锁住的范围都不是一致的,这里就已经可能出现冲突了。
- 你需要业务强依赖缓存组件,本来增加缓存是为了给数据库增加容错,缓存挂了降级到数据库也能撑一会,现在缓存一挂服务直接不可用这不可以接受
- 如果你的缓存仅有 kv 写入,而且也没有过期的能力,业务为了这个功能引入这个组件的目的是什么
站小的细节考虑,锁有很多问题需要考虑,redis 最有名的锁应该是 redlock 吧,但它的一致性仍有非常多的讨论,可以参看 https://zhangtielei.com/posts/blog-redlock-reasoning.html
我没翻源码,仅从文章上说几个点- 如果你想维持完美的一致性,那缓存读锁的时间是组件决定还是业务决定,太长影响 set 失败后的自然过期,太短数据库的查询又可能超时。
- 更新后,你有一步是删除 key ,如果一个连接超时,客户端拿到的是超时的结果,你是会重试还是事务直接结束,如果结束已经更新的 key 要不要回滚,如果重试,那之前的连接发送的 delete 在组件里可能已经成功了,现在是另一个连接拿到了这个 key 的锁,之前的连接重试能不能直接删除这个 key ?
- 集群的方案没有那么简单的,就算用 raft ,你可以解决选主的问题,那主从直接的同步是怎么做的,是写入的时候全部节点成功才算成功?异常节点重启后怎么恢复缓存,什么时候才能认为缓存已经和集群同步可以加入集群?如果集群全挂了重启怎么办?
一共有两个锁。第 4 步的 key 锁是在 Umem-cache 上的。而事务里拿到的写锁是在数据库的,通过 select for update 拿到的,这个写锁在事务提交后才会自动释放。你说的竟态是被数据库的锁保护住了的。任何试图缓存的客户端都要先读数据库才能取到数据,它会被更新占有的写锁阻塞。
#37 的方案:
我现在有点不理解你说的竞争是什么意思了,我一直是把它当作不一致来理解的。你可以看#38 我写的流程,在第 8 步的时候,数据库里的 version=2,而缓存里的 version=1,是不一致的。但是后面会跟着一个擦屁股的第 9 步,它发送的 event Z 会同步 cache 和 db ,使得它们再次一致。我大概理解你,你这里就充当了 cache 的角色,通过 rpc 然后触发 cache 读取/更新,存储 cache 的就是你这服务,通过 raft lock 的机制,保证了读写的时序和并发安全。
然而,你这个机制略显尴尬:
- to c 端,大流量大并发,几乎不可用,raft 用来保证集群仲裁而已,连 kv 都谨慎使用,没见过谁敢接 c 端请求的。讲究啥一致性,cache 周期一过能刷最新值就好了
- to b 端,尤其是银行等强一致性,db 走起,硬件堆上,让 db 自己做好 cache 缓存就好了
你这套机制,适用场景,非常少。
发过 Hacker News ,AI 的产出泛滥了,哪里都一样。
按你的理解其实也是一样,这个方案的问题就是向 kafka 的请求是不允许失败的:
- 客户端 A:Get cache miss, 发送 event A
- event A 执行,set cache version = 1
- 客户端 B: Update DB version=2,发送 event B 失败。