数据库与Redis数据一致性问题思考

1、场景和问题

  我们常用Redis来做缓存减少数据库的访问。一般流程是:

    (1)数据的请求先查缓存中是否存在

    (2)如果缓存命中,则直接返回数据,如果缓存中不存在,则查数据库,然后再写缓存

  可能产生的问题:

1、多线程导致数据不一致

  例如:线程A和线程B同时更新用户积分

    (1)线程A将用户积分更新为100,线程B将用户积分更新为200,这里认为两个更新都是合理的,此时数据库最后更新结果是200

    (2)线程B更新成功后将缓存值设置为200

    (3)线程A更新成功后将缓存值设置为100

    由于网络等原因,无法保证线程A和线程B更新数据库后设置缓存的先后顺序,这样线程A在线程B之后设置缓存,导致数据库值为200,而缓存中值为100。

  针对这种情况可以使用分布式锁来解决,也分为两种:

    方案(1)、粗粒度

      将整个操作数据库和缓存的过程看做一个原子操作,在操作数据库之前先获取分布式锁,获取成功则进入后面的流程,如果获取锁失败则根据具体业务场景选择:阻塞等待、或重试n次、或直接返回操作失败。

    方案(2)、细粒度

      都允许数据库操作,只是在写缓存的时候,设置分布式锁,只允许一个线程去更新缓存:更新之前先查数据库,获取最新的值再更新缓存。

      这样做的好处就是支持的并发更高,坏处就是多了一次数据库查询操作。

  这种方案是使用分布式锁来解决问题,但是分布式锁也会存在超时时间和锁释放产生的问题:

    思考:如果超时时间设置太短,会导致任务流程没结束锁就失效了;如果超时时间设置太长,如果一旦服务挂掉就会导致其他请求等待时间过长。

2、思考:在文章开头以及方案(2)提到的查数据库再写缓存

    (1)如果查完数据库,再设置缓存前数据被修改也会造成数据不一致;

      回答:这种方式现在基本被pass,因为:1、数据安全性,很容易造成数据不一致,2、从业务角度来说,如果写多的场景,这种方式会造成还没读缓存就频繁更新,反而影响性能。

    (2)如果写缓存时Redis.set失败也会导致数据不一致。

3、根据以上考虑,在开发中也会采用删除缓存的方式,也就是操作完数据库之后不去更新缓存,而是直接删除缓存,这样导致数据不一致的原因也就会发生在del失败,概率就会小很多。这样相当于只操作数据库,适合读多写少的场景。

 

2、开发实践

  在实际开发中,以我们的积分操作为例,积分分为两种类型:一是根据游戏场景获得的积分,这种积分只能提现一些小额商品,大额都会被条件卡掉;而是根据广告收益获得的积分,这种积分可以提现更多商品,但是因为这种积分依赖于广告行为,所以相对获得次数相对少。

  针对这种情况,第一种积分完全由Redis维护,使用定时同步落盘机制(前端每30s同步一次数据,所以可能会产生一些丢失风险);第二种积分采用上面第3条删除缓存的策略。

  对于积分提现:db更新时where条件后增加判断余额判断来做一层打底。

update amount = amount - #{cash} from user_amount where user_id = #{userId} and amount >= #{cash}

    

总结:其实采用更新完数据库删缓存的方式已经极小概率会发生数据不一致问题,但是也会有两种可能:

  (1)线程A更新数据库,还没来的及删除缓存,此时线程B来获取数据,从缓存中获得了旧值,这也会造成此次线程B拿到的数据与数据库中数据不一致。

    解决:针对这种场景,如果要严格保证数据不一致,可以采用双删机制,即:先删缓存,然后更新数据库(数据库写自动加锁),然后再删除缓存。

  (2)删除缓存失败导致数据不一致

    解决:使用消息队列,将删除失败的key加入到消息队列再删除。  

posted @ 2022-01-10 18:31  jingyi_up  阅读(116)  评论(0编辑  收藏  举报