请求 A 更新数据 请求B读数据
在高并发情况下,A、B请求过程步骤相互穿插,就会出现图中的问题。
期望redis 的数据是11,最后变成了10
场景:先删除Redis,再更新 MySQL,不主动更新Redis,访问redis 没有数据,再访问MySQL 把数据回写到Redis。
请求 A、B 都是更新 Redis,然后再更新MySQL
在高并发情况下,如果请求 A 在更新MySQL 时卡了一会,请求 B 已经依次完成数据的更新,就会出现图中的问题。
期望redis 的数据是11,最后变成了10
分析:MySQL事物功能比Redis强大,可以严格保证原子性。 Redis宕机可能会丢失部分数据。以Redis为主处理异常成本高。
请求 A、B 都是先写 MySQL,然后再写 Redis
在高并发情况下,如果请求 A 在写 Redis 时卡了一会,请求 B 已经依次完成数据的更新,就会出现图中的问题。
期望redis 的数据是11,最后变成了10
场景:以MySQL 为主,事物成功,才更新Redis,
对于上面这种情况,对于第一次查询,请求 B 查询的数据是 10,但是 MySQL 的数据是 11,
存在这一次不一致的情况,对于不是强一致性要求的业务,可以容忍。
秒杀业务、库存服务等,需要考虑这个风险点。
这里需要满足条件:
1、缓存刚好自动失效或者删除;
2、B C请求同时访问,key未命中
3、B查询MySQL数据回写到redis 10。
4、A请求更新MySQL 为11 并且删除缓存
5、C回写之前的数据10。
因为正常情况更新MySQL 要比更新Redis耗时要长,这种情况发生概率极低, 在服务高负载情况下可能发生。
请求 A 更新数据 请求B读数据
在高并发情况下,A、B请求过程步骤相互穿插,如果“删除缓存 10”必须在“回写缓存10”前面会有异常。
期望redis 的数据是11,可能最后变成了10
优化:“删除缓存 10”必须在“回写缓存10”后面,为了让请求 A 的最后一次删除,等待一段时间,缺点可能会有性能问题造成进程排队。
场景: 先删除 Redis,更新 MySQL,再删除 Redis,用户读redis 没数据再访问MySQL回写Redis
通过消息队列的异步串行,实现最后一次删除。
删除失败加重试
尽量不用sleep 的方式,可能有性能风险。
这个方案可以保证数据最终一致性,但是带来了性能问题。对数据一致性比较敏感的服务可以采用这个方案。
这个方案,会保证 MySQL 和 Redis 的最终一致性,
但是如果中途请求 B 需要查询数据,如果缓存无数据,就直接查 DB;
如果缓存有数据,查询的数据也会存在不一致的情况。
总结:
除非读也要加锁, 不论采用那种方案极端情况下都会有某一个时刻数据不一致的情况。从技术上来说,无法保证Redis 和数据库的严格一致,所有的方案都是尽可能降低不一致的可能性和不一致时间。
方案4,5 加redis key设置过期时间 可以实现数据的最终一致。
方案6 牺牲了性能保证了数据最终一致性。
方案7 既保证了性能又能保证数据最终一致性,引入的中间件多,维护成本高。
方案6和方案7 因为所有的更新都会同步更新 redis ,redis 存储了所有数据,对redis内存空间要求大。
备注:绘图工具。
https://excalidraw.com/