有时候,我们经常会借助Redis做一些并发控制,比如秒杀库存的控制以及一些抽奖上限和每天数量的控制,比如规定活动每天只能中100个,每个奖品每天只能中20个等,正常这种我们都会用redis来控制提高性能,还有秒杀的库存用redis的自减decr操作老来控制,比如如下业务场景。这里不讨论用redis做缓存,因为用redis做缓存不涉及一致性的问题,坚持的原则都是:先从redis获取,redis获取不到则从数据库中获取然后写回redis中,所以这里值讨论用redis做并发控制导致redis和数据库一致性问题的解决方案和设想。
场景1、数据库操作成功,单个redis操作失败
该场景必须保证redis在最后操作,并且只有一个redis操作,那么解决办法就比较简单,如果redis操作失败,直接抛出异常触发数据库事务回滚即可。
场景2、redis操作成功,数据库操作失败
这里举一个抽奖设计过程中的例子,控制奖品每天的抽奖上限,伪代码如下
//1、从redis获取每天已中奖数目
Long size = redisUtil.getTodayWinSize(key);
//2、如果已中奖数超过限制,则提醒用户不能中奖
if(size>100){
return "已中奖数超过限制,用户不能中奖";
}else{
//3、对每天已中奖数key进行自增
Long new_size = redisUtil.incr(key);
//4、如果自增后的值超过限制,则提醒用户不能中奖,以及回滚自增的值
if(new_size>100){
//5、对自增后的值进行回滚
redisUtil.decr(key);
return "已中奖数超过限制,用户不能中奖";
}else{
//6、用户可以中奖,对数据库进行操作
MySQLUtil.insertAward();
}
}
下面我们列举一下上面的代码逻辑里可能会存在的一些问题
问题1:第1步如果redis中没有数据如何处理
这里我们借助的是redis来操作,那么如果redis中没有值,那么我们需要从数据库中统计回去,但是这里我们并不需要对数据库进行锁表,我们直接借助redis的setNx操作。逻辑如下
Long size = 从redis中获取值
if(size==null){
//从数据库中获取值不需要锁表
size = 从数据库中获取值
if(size!=null){
Long result = redisUtil.setNx(key,size);
//如果保存成功,则表示获取的值就是最新的值
if(result!=0l){
return size;
}else{
//有其它的线程已经放入redis中了,直接返回即可
size = 从redis中获取值
return size;
}
}
}
setNx的性质就是如果redis中有值了就不放入redis中了,所以在多线程的情况下,以最先放入的值为准。
问题2:第5步,为啥明明超过了值,还要对自增后的值进行回滚
这里如果不进行回滚,那么如果别的线程数据库操作失败了,回滚了redis,然后你这里确没有回滚的话会导致后面的用户还是中不了奖。
问题3:如果第6步数据库操作失败了怎么办
这里上面redis操作成功,然后这里数据库操作失败了,那么我们需要对redis进行回滚,也就是要对数据库操作的代码进行trycatch捕获,伪代码如下
...
//6、用户可以中奖,对数据库进行操作
try{
MySQLUtil.insertAward();
}catch{
//7、对redis进行回滚
redisUtil.decr(key);
//触发数据库事务回滚
}
}
}
是不是上面的操作就完美了呢?正常来说,上面的操作可以满足大多数业务场景的需求了,但是如果我们的系统要更高可用一点,在对第7步对redis进行回滚的时候因为网络抖动,redis连接失败了,也就是回滚失败了怎么办呢,那当天的已中奖数目就多统计了一个,会让今天的实际中奖数目少1个,如何解决呢?
如果系统真的考虑到这种程度,那么我们在redis回滚操作失败后进行消息等级,让消费程序来进行回滚,伪代码如下
...
//6、用户可以中奖,对数据库进行操作
try{
MySQLUtil.insertAward();
}catch{
//7、对redis进行回滚
try{
redisUtil.decr(key);
}catch{
登记消息,让消费程序来做回滚。
}
//触发数据库事务回滚
}
}
}
也许还有更较真的人就会说,要是消息登记也失败了呢?怎么办呢?这里可以参考我的一篇笔记,将失败的概率再次降低:三、架构设计:对RocketMQ消息登记报错和数据库事务之间关系的一些设想
也许还会有更更更转牛角尖的同学,可能会说,如果在redis回滚的时候,或者登记消息的时候程序挂了怎么办?额,想法是好,这种都要考虑是不是台心累了,你说我一定要考虑,我们的系统可用性就是要超级超级高。那好,这里提供一个终极,也是最最最兜底的解决方案:”最终一致性”,怎么实现呢?
“给key设置有效期”
这样子的话,就算当前key的数据因为回滚的问题导致不正确的,但是等失效重新从数据库中获取后就一致啦!
场景3、redis操作需要进行回滚怎么办
有时候我们的业务场景是要对多个redis进行操作的,但是如果数据库操作失败了,或者其中以恶搞redis操作失败了,前面的redis操作成功的也需要进行回滚。
这种情况,正常来说我们使用场景2的解决方案就可以了,如果有操作失败就捕获异常对之前的数据进行回滚,然后不放心可以采取消息队列,这里需要记得,消息队列进行回滚登记建议有多少个key进行回滚就登记多少条消息,不然到消费程序哪里有涉及到第一个redis回滚成功,第二个redis回滚失败,然后怎么办的恶心问题,如果分开来登记,那么如果还是回滚失败,那就继续返回false重新消费就好了。而登记消息怕失败的话可以参考我的解决方案:三、架构设计:对RocketMQ消息登记报错和数据库事务之间关系的一些设想
总结
最兜底的解决办法就是对redis的key进行有效期设置,这样子就可以保证最终一致性,不需要考虑上面这么多复杂的解决方案。
注:码字不易,转载请注明出处!