Redis和MySQL数据一致性解决方案(动态KEY)
2019-04-10 本文已影响0人
醒了_2440
背景
我们经常使用redis作为缓存,以减轻直接访问数据度带来的并发压力。基本操作如下:
查询
请求进来,先查询redis,如果有值则直接返回;如果没有则查询数据库(不考虑并发情景)并更新redis,再返回结果
更新
先删除redis原数据,成功后再更新数据库,然后再写入redis。
在更新redis时,就有可能带来数据库和redis数据不一致的问题:
如果在删除redis后,立刻有新的请求进来(很有可能,本来redis就在高并发下使用),则"删除失效"。因为当前数据库事务未提交,查询数据库依旧能读到旧值(数据库事务隔离要保证无脏读,如RC, RR或串行化),redis就被写入旧值。因此可直接考虑在更新redis前,redis还保留旧值的情况
- 更新redis成功,但是返回exception,此时如果贸然回退数据库,将导致redis保存了最新的值,但是数据库中依旧是旧值
- 更新redis失败,但是返回exception,此时如果直接提交数据库事务,将导致数据库保留最新值,redis保留旧值。
上面这种redis写入异常,就会带来"数据不一致"的问题
思考方向
有一种解决数据一致性的方案是使用补偿机制,原理大体在更新redis后,数据状态无法确定时,保留异常记录id,后续使用补偿查询的方式以获知"未知记录"的真实情况。并且在明确数据状态之前,记录都无法使用。
这种方案有时延和数据不可用的缺陷,直接导致服务中断一段时间。
其实我们不一定要让"数据一致",只要请求得到的结果是预计正确即可。设想一下,在发生写入异常的时候,不管redis更新成功还是失败,只要我们让这条redis记录得不到使用,再把数据库回退,是不是也能让请求的结果是预计正确的。
设计方案
- 现有一个key的结构为PREFIX+参数值,其中PREFIX为常量,如表名等能表示数据含义的字符串。另有一个静态的、初始化后的HashMap<String,String> suffixMap,其key为PREFIX,value表示数据库当前值的,用V表示。"版本"值是uiid。
- 更新开始()
(1)请求的KEY=PREFIX+参数值+Vi+1,其中Vi = suffixMap.get(PREFIX),Vi表示上一个版本,Vi+1表示的是即将生效版本。
(2)先update数据库,如果成功,则利用RedisTemplate.opsForValue().set(KEY,记录值)。如果更新数据库失败,程序退出。其中写入redis会出现以下a,b两种情况:
a.写入异常
- 回退数据库,其中redis写的情况分为写入成功或者写入失败
- 如果写入redis成功,Redis和数据库中记录不一致。但是redis中保存的KEY= PREFIX+参数值+Vi+1,而suffixMap并未更新,因此在执行查询操作时,使用到的KEY= PREFIX+参数值+Vi也无法从Redis中读取到“脏数据”。
- 如果写入失败,数据库回退后两者的数据仍一样。
b.写入成功 - 将版本更新,suffixMap.put(PREFIX,Vi+1)
- 删除redis中的旧值,KEY=PREFIX+参数值+Vi,RedisTemplate.opsForValue().del(KEY)。因为写入redis成功,此时删除失败的概率很低。即使删除旧值失败,也只是在redis中遗留历史数据,在Expired Time过后就会被清理,不会导致redis内存消耗高。
评价
暂未进行实际测试,待完善!