步骤1:线程1更新MySQL,库存=100
步骤2:线程1更新Redis,库存=100
步骤3:线程2更新MySQL,库存=90
步骤4:线程2更新MySQL,库存=90
有并发线程1和2,他们两都需要更新库存,如果此时命令执行顺序按照预期行为走了,好像没什么问题。
但是,如果在远程调用过程中,网络出了问题(抖动、超时、阻塞等)
步骤1:线程1更新MySQL,库存=100
步骤2:线程2更新MySQL,库存=90
步骤3:线程2更新Redis,库存=90
步骤4:线程1更新MySQL,库存=100
发现问题所在了吧,数据不符合基本逻辑了,这时候在Redis获取的数据是错的,和数据库的信息不一致(数据库中是90,而Redis中是100)。
当操作序列没有严格按照请求的先后顺序执行时,会引发并发安全问题。
那我们需要通过以下几种常见的方式来解决数据一致性的问题
1.采用延时双删策略+设置缓存过期时间
在写库前后都进行redis.del(key)(或更新缓存)操作,并且设定合理的超时时间(比如500ms)。因为设置了超时时间,即使有脏数据也是在超时时间内,缓存过期后,最终数据还是会从数据库获取最新的,这样就可以保持数据一致性
至于这个超时时间设置多久,就要看处理业务的耗时情况,这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
public void update(String key,Object data){ redis.delKey(key); db.updateData(data); Thread.sleep(500); redis.delKey(key); }
这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时。
2.通过Canal订阅binlog ,再利用消息队列,推送更新redis缓存数据
canal是阿里的一款开源框架,通过该框架可以对MySQL的binlog进行订阅(类似MySQL主备同步的方式),而 canal 正是模仿了 mysql 的slave 数据库的备份请求,使得 Redis 的数据更新达到了相同的效果
(.Net Core 版本的canal插件 https://github.com/dotnetcore/CanalSharp)