Redis和数据库数据一致性问题
Redis作为缓存分两种情形
- 只读缓存, 只读缓存无需考虑数据更新问题, Redis中有则返回Redis中的数据, Redis无则查询数据库
- 读写缓存
- 同步直写策略
- 异步缓写策略
数据读取流程:
正常回写Redis代码流程:
public Object getDataById(String id) {// 1. 先去Redis中查询Object obj;obj = redisTemplate.opsForValue().get(DATA_PR + id);if (obj == null) {// 2. redis中查询出来为null则查询数据库obj = testMapper.getDataById(id);if (obj == null) {// 3.1redis, 数据库都没有数据// 可以给这个id给个空值存在Redis中redisTemplate.opsForValue().set(DATA_PR + id, "null");} else {// 3.2 数据库中有数据将数据写回RedisredisTemplate.opsForValue().set(DATA_PR + id, obj);}}return obj;}
保证并发时, 避免同一数据频繁写入Redis对回写缓存代码进行加锁
public Object getDataById1(String id) {// 1. 先去Redis中查询Object obj;obj = redisTemplate.opsForValue().get(DATA_PR + id);if (obj == null) {// 缓存不存在则加锁// 假设请求量很大synchronized (TestServiceImpl.class) {obj = redisTemplate.opsForValue().get(DATA_PR + id);if (obj != null) {// 查询有数据则直接返回return obj;} else {// 没有数据再查询数据库obj = testMapper.getDataById(id);// 回写缓存if (obj != null) {redisTemplate.opsForValue().setIfAbsent(DATA_PR + id, obj, 20, TimeUnit.SECONDS);return obj;} else {return null;}}}}return obj;}
给缓存设置过期时间, 定期清理缓存并回写, 是保证最终一致性的解决方案
我们可以对存入缓存的数据设置过期时间, 所有的写操作以数据库为准, 对缓存的操作只是尽最大努力即可, 也就是说数据库写成功缓存更新失败, 那么只要达到过期时间, 则后面的读请求自然会从数据库读取新值然后回填缓存,达到一致性, 要以数据库写入库为准
更新数据库并更新Redis有以下几种情况
-
先更新数据库再更新Redis
这种情况会出现的问题:
请求A先将字段x更新为10
请求B后将字段x更新为20
正常流程:
请求A更新数据库将x更新为10
请求A更新Redis 将x更新为10
请求B更新数据库将x更新为20
请求B更新Redis 将x更新为20
异常情况:
请求A更新数据库将x更新为10
请求B更新数据库将x更新为20
请求B更新Redis 将x更新为20
请求A更新Redis 将x更新为10
此时数据库x为10, Redis中x为20出现缓存和数据库数据不一致情况
-
先更新Redis再更新数据库
这种情况会出现的问题:
请求A先将字段x更新为10
请求B后将字段x更新为20
正常流程:
请求A更新Redis 将x更新为10
请求A更新数据库将x更新为10
请求B更新Redis将x更新为20
请求B更新数据库将x更新为20
异常情况:
请求A更新Redis 将x更新为10
请求B更新Redis将x更新为20
请求B更新数据库将x更新为20
请求A更新数据库将x更新为10
此时数据中为20, Redis中为10 出现缓存和数据库数据不一致情况
-
先删除Redis再更新数据库
这种情况会出现的问题:
请求A先将字段x更新为10
请求B查询x的值
正常流程
请求A删除Redis中x的值
请求A更新数据中的值为10
请求B查询Redis中没有值, 查询数据库
请求B回写缓存x的值
异常流程
请求A删除Redis中x的值
请求B查询Redis中没有值, 查询数据库但是此时请求A还未更新数据库或者是还没有commit
请求B查询数据库, 回写缓存
请求A更新数据库
此时缓存中仍然是缓存的旧值, 数据库和缓存值不一致
-
先更新数据库再删除Redis
这种情况会出现的问题:
请求A先将字段x更新为10
请求B查询x的值
正常流程
请求A更新数据库
请求A删除Redis中的缓存值
请求B查询数据库并回写缓存
异常流程
请求A更新数据库但未提交
请求B读取的是旧值
请求A删除缓存
为了解决先删除后修改数据库的异常情况
延时双删
public void updateData(Object obj, String id) {// 先删除缓存中的数据redisTemplate.opsForValue().getAndDelete(DATA_PR + id);// 再更新数据库testMapper.updateData(obj);// 再次删除缓存中的数据避免其他线程读取旧值并回写缓存// 需要在这里等待,等待的原因是如果另外的线程读取的线程还在回写的流程中旧值还未写到缓存中, 那么删除是没有意义的// 这里等待的时间就是大于等待其他线程将旧值写入缓存的时间try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {throw new RuntimeException(e);}redisTemplate.opsForValue().getAndDelete(DATA_PR + id);}
通常我们会采用先更新数据库再删除Redis缓存, 但是依然会存在在更新期间读取到旧值的情况, 还会存在删除缓存失败问题, 此时可引入消息中间件将需要更改的数据推送到MQ, 再通过MQ去对Redis进行删除, 这样也不能保证强一致性, 只是一个比较折中的方案
引入中间件自动同步数据到Redis canal
如果我们的数据库事MySQL可以通过引入开源中间件canal对MySQL的binlog进行监听, 当数据库表发生变化时自动去将MySQL的变更写到我们的缓存中
未完待续…