个人随笔
目录
四、架构设计:接口开发过程中,遇到的MySQL和Redis数据一致性问题的解决方案设想
2021-04-10 21:24:34

有时候,我们经常会借助Redis做一些并发控制,比如秒杀库存的控制以及一些抽奖上限和每天数量的控制,比如规定活动每天只能中100个,每个奖品每天只能中20个等,正常这种我们都会用redis来控制提高性能,还有秒杀的库存用redis的自减decr操作老来控制,比如如下业务场景。这里不讨论用redis做缓存,因为用redis做缓存不涉及一致性的问题,坚持的原则都是:先从redis获取,redis获取不到则从数据库中获取然后写回redis中,所以这里值讨论用redis做并发控制导致redis和数据库一致性问题的解决方案和设想。

场景1、数据库操作成功,单个redis操作失败

该场景必须保证redis在最后操作,并且只有一个redis操作,那么解决办法就比较简单,如果redis操作失败,直接抛出异常触发数据库事务回滚即可。

场景2、redis操作成功,数据库操作失败

这里举一个抽奖设计过程中的例子,控制奖品每天的抽奖上限,伪代码如下

  1. //1、从redis获取每天已中奖数目
  2. Long size = redisUtil.getTodayWinSize(key);
  3. //2、如果已中奖数超过限制,则提醒用户不能中奖
  4. if(size>100){
  5. return "已中奖数超过限制,用户不能中奖";
  6. }else{
  7. //3、对每天已中奖数key进行自增
  8. Long new_size = redisUtil.incr(key);
  9. //4、如果自增后的值超过限制,则提醒用户不能中奖,以及回滚自增的值
  10. if(new_size>100){
  11. //5、对自增后的值进行回滚
  12. redisUtil.decr(key);
  13. return "已中奖数超过限制,用户不能中奖";
  14. }else{
  15. //6、用户可以中奖,对数据库进行操作
  16. MySQLUtil.insertAward();
  17. }
  18. }

下面我们列举一下上面的代码逻辑里可能会存在的一些问题

问题1:第1步如果redis中没有数据如何处理

这里我们借助的是redis来操作,那么如果redis中没有值,那么我们需要从数据库中统计回去,但是这里我们并不需要对数据库进行锁表,我们直接借助redis的setNx操作。逻辑如下

  1. Long size = redis中获取值
  2. if(size==null){
  3. //从数据库中获取值不需要锁表
  4. size = 从数据库中获取值
  5. if(size!=null){
  6. Long result = redisUtil.setNx(key,size);
  7. //如果保存成功,则表示获取的值就是最新的值
  8. if(result!=0l){
  9. return size;
  10. }else{
  11. //有其它的线程已经放入redis中了,直接返回即可
  12. size = redis中获取值
  13. return size;
  14. }
  15. }
  16. }

setNx的性质就是如果redis中有值了就不放入redis中了,所以在多线程的情况下,以最先放入的值为准。

问题2:第5步,为啥明明超过了值,还要对自增后的值进行回滚

这里如果不进行回滚,那么如果别的线程数据库操作失败了,回滚了redis,然后你这里确没有回滚的话会导致后面的用户还是中不了奖。

问题3:如果第6步数据库操作失败了怎么办

这里上面redis操作成功,然后这里数据库操作失败了,那么我们需要对redis进行回滚,也就是要对数据库操作的代码进行trycatch捕获,伪代码如下

  1. ...
  2. //6、用户可以中奖,对数据库进行操作
  3. try{
  4. MySQLUtil.insertAward();
  5. }catch{
  6. //7、对redis进行回滚
  7. redisUtil.decr(key);
  8. //触发数据库事务回滚
  9. }
  10. }
  11. }

是不是上面的操作就完美了呢?正常来说,上面的操作可以满足大多数业务场景的需求了,但是如果我们的系统要更高可用一点,在对第7步对redis进行回滚的时候因为网络抖动,redis连接失败了,也就是回滚失败了怎么办呢,那当天的已中奖数目就多统计了一个,会让今天的实际中奖数目少1个,如何解决呢?

如果系统真的考虑到这种程度,那么我们在redis回滚操作失败后进行消息等级,让消费程序来进行回滚,伪代码如下

  1. ...
  2. //6、用户可以中奖,对数据库进行操作
  3. try{
  4. MySQLUtil.insertAward();
  5. }catch{
  6. //7、对redis进行回滚
  7. try{
  8. redisUtil.decr(key);
  9. }catch{
  10. 登记消息,让消费程序来做回滚。
  11. }
  12. //触发数据库事务回滚
  13. }
  14. }
  15. }

也许还有更较真的人就会说,要是消息登记也失败了呢?怎么办呢?这里可以参考我的一篇笔记,将失败的概率再次降低:三、架构设计:对RocketMQ消息登记报错和数据库事务之间关系的一些设想

也许还会有更更更转牛角尖的同学,可能会说,如果在redis回滚的时候,或者登记消息的时候程序挂了怎么办?额,想法是好,这种都要考虑是不是台心累了,你说我一定要考虑,我们的系统可用性就是要超级超级高。那好,这里提供一个终极,也是最最最兜底的解决方案:”最终一致性”,怎么实现呢?

“给key设置有效期”

这样子的话,就算当前key的数据因为回滚的问题导致不正确的,但是等失效重新从数据库中获取后就一致啦!

场景3、redis操作需要进行回滚怎么办

有时候我们的业务场景是要对多个redis进行操作的,但是如果数据库操作失败了,或者其中以恶搞redis操作失败了,前面的redis操作成功的也需要进行回滚。

这种情况,正常来说我们使用场景2的解决方案就可以了,如果有操作失败就捕获异常对之前的数据进行回滚,然后不放心可以采取消息队列,这里需要记得,消息队列进行回滚登记建议有多少个key进行回滚就登记多少条消息,不然到消费程序哪里有涉及到第一个redis回滚成功,第二个redis回滚失败,然后怎么办的恶心问题,如果分开来登记,那么如果还是回滚失败,那就继续返回false重新消费就好了。而登记消息怕失败的话可以参考我的解决方案:三、架构设计:对RocketMQ消息登记报错和数据库事务之间关系的一些设想

总结

最兜底的解决办法就是对redis的key进行有效期设置,这样子就可以保证最终一致性,不需要考虑上面这么多复杂的解决方案。

注:码字不易,转载请注明出处!

 42

啊!这个可能是世界上最丑的留言输入框功能~


当然,也是最丑的留言列表

有疑问发邮件到 : suibibk@qq.com 侵权立删
Copyright : 个人随笔   备案号 : 粤ICP备18099399号