你有没有遇到过这种情况,明明大家已经评审通过的方案,在code Review的时候,当初的方案被推倒了。
你心里会怎么想?很多程序员会觉得被故意针对了。 其实真的未必是事实。
方案被推倒是非常常见的事情,当初的决策会随着环境、认知的改变而改变,推倒的目的只有一个,现在的方案比之前的好,有更好的方案谁会用退而求其次的方案呢?
不管你是否生气,但我觉得要接受改变,拥抱改变,成长是需要付出代价的,心态要端正。
第一版方案
先来回顾一下当初的我们方案设计,高并发,会遇到那些难点? - 掘金 (juejin.cn)
还记得我们的奖券池吗? 还记得我们要做高并发吗?为了满足高并发的要求,我们把奖券池缓存了。
先看看我们的架构
① 创建奖券池、奖券,数据保存在mysql
② ③ canal监听mysql数据变化,并把变化推送到kafka
④ ⑤assignAwardService消费kafka消息,把奖券池、奖券缓存到Redis
⑥ 用户发起获取奖券的请求,通过传参(奖券池key,用户身份ID)
⑦ 从奖券缓存池自动获取一个奖券,更新缓存池
⑧ 更新之后的缓存池同步到mysql数据
⑨ 用户发起兑奖请求
⑩ 兑奖完成
从2-5步,我们在前边介绍了技术方案可以保证数据一致性,但从第7~8步之间,如何保证数据一致性呢?
我们来推演一下
第7步从缓存中取数据,在未执行第8步之前,缓存是最新的,数据库是旧的,不一致。 执行完成第8步,两边数据最终一致。
但有个问题,在第7步到8步之间,如果有从第1步进来的数据会同步到redis里,两边就形成了各种不一致。
从1-5是去向,7-8是回,这样就形成了环状,数据来回同步会有很大的风险,这种风险是不可接受的,所以我们就推到了之前的方案
第二版方案
第一版方案既然知道问题所在,来看看我们的第二版方案
我们这一版方案把双向数据同步给打掉了,只保留了mysql->redis的单向同步方式。
你可能会有疑问,那性能如何保证呢?
我们先来看看我们的业务场景,从架构图中得知,生成奖券池以及奖券、用户获取奖券是低频操作,为什么用户获取奖券也是低频操作呢?
用户获取奖券的逻辑是,传参:用户ID+奖券池Key,返回:某个具体的奖券。 在第一次请求获取奖券时,奖券ID和用户ID就完成了绑定关系,所以用户再次获取的时候,返回的是已绑定的奖券且不需要从数据库获取,这个时候走的是缓存的读操作。
所以兑换奖券和非第一次获取奖券是高频动作,但这两个都走的是缓存。
所以对数据库的写就不需要太强的性能。
你可能有疑问,奖券什么时候放到缓存中的,在第5步中,已经将生成的奖券放到缓存中了,所以在第9步可以直接获取。
接下来我用了两天的时间把代码改完了,心里也终于松了一口气,至少这版的技术方案比上一版要稳定。
上一次在mysql和Redis如何保持数据一致性(一) - 掘金 (juejin.cn)留了一个问题,如果同步到缓存的canal出现了故障,这就会导致数据库和缓存不一致,那怎么解决呢?
我们最终的方案走向了双删。 数据库和缓存初始的时 a=1
- 先删除缓存 a被删除,数据库a=1,缓存没值。
- 在更新a=2操作之前,红色虚线有读操作 ,此时数据库a=1,缓存a=1
- 数据库执行a=2,接着delete缓存,此时 数据库a=2,缓存被删除。
大家觉得方案有什么问题吗? 在第二次delete之前,数据库a=2,此时缓存a=1,仍然会出现不一致。只不过缩短了不一致的时间。
在我们真实的使用中,我们把第二次的delete操作改成了update操作,这样更有利于提升性能。
有的同学还提到做延时双删,给缓存的值设置过期时间,过期时间多长取决于更新数据库的时间,比如更新操作是1s,那过期时间就设置1s,这也是一个好办法。
总结
我们这次的方案改进得益于code Review,我的理念是不CR的团队不值得加入。
但你知道有很多团队确实是不CR的,在我过去的经历中,我每到一个没有CR的团队都会推动CR落地,我也理解很多团队不CR的原因,本来CR是一件健康的事情,大家共同完善我们的软件,但很多团队CR的过程中就变成了对代码的作者的批判会,久而久之大家都小心翼翼,互相挑毛病,这样的CR谁还愿意参加,失去了本来CR的意义。
CR的目的肯定不是挑对方毛病,而是在一起共同成长,一起避坑。