redis进阶:mysql,redis双写一致性,数据库更新后再删除缓存就够了吗?
创始人
2024-05-29 23:22:18
0

0. 引言

最近线上的一个状态修改功能出现了问题,一开始是运营找了过来,运营告知某条数据的状态已经开启了的,但是实际使用起来还是没有生效,于是拿到这个问题后,首先就去数据库查了这条数据,发现确实如他所说,状态数据是已经更改过的。但是为什么没有生效呢?

于是再次查看了获取数据的方法,发现是优先获取的缓存,于是查询缓存里的数据,发现了问题所在,缓存里并没有将这条数据的状态更改过来,也就是缓存数据不一致问题。

继续追查状态修改的方法,发现采用的更新方式是先更新数据库,然后删除缓存。这是我们常用的双写一致性的处理方法,但也正是这样的方式出现了问题。下面我们来详细讲解

1. 先更新数据库,再删除缓存为什么有问题?

首先原来的方法,大概是这样的,将数据库更新后,再将缓存删除掉

    public R save(UserVO userVO) {User user = new User();BeanUtils.copyProperties(userVO, user);saveUser(user);redisTemplate.delete("userInfo:" + user.getUserName());return R.success("操作成功");}

同时在获取数据时,采用的是先从缓存获取,如果缓存没有,就从数据库查询,并同时存放到缓存上,这样保证了下次访问时数据能直接从缓存获取,减少了数据库压力

    public User getByUserName(String userName) {User user = (User) redisTemplate.opsForValue().get(userName);if (user != null) {return user;}user = this.getOne(Wrappers.lambdaQuery().eq(User::getUserName, userName));redisTemplate.opsForValue().set("userInfo:" + userName, user);return user;}

数据库的数据已经更新成功了,说明数据库是没有报错了,那么哪个地方报错了呢?当然是redis,在执行redis的删除操作时,如果发现错误,比如因为网络问题,或者redis本身服务问题,导致没有正常执行而产生的报错,所以为了捕获这个报错,我们需要添加上数据库事务 @Transactional(rollbackFor = Exception.class)

    @Transactional(rollbackFor = Exception.class)public R save(UserVO userVO) {User user = new User();BeanUtils.copyProperties(userVO, user);saveUser(user);redisTemplate.delete("userInfo:" + user.getUserName());return R.success("操作成功");}

但是这样你以为就结束了吗,这样的操作确实可以实现事务的回滚,但是上述的模式仅仅只是我们用到的Cache Aside模式(这里大家要了解redis缓存的四种模式),而对于另外一种同样经典的Write Through模式就不再适用于这个操作了。

2. Cache Aside模式与Write Through模式的区别

Cache Aside模式采用的是读时放缓存或者预加载缓存,写时更新数据库,删缓存,它适用于读多写少,因为缓存没有时要去数据库查数据,如果多线程场景下,同一时间打进来同样的请求,都会去访问数据库,一方面容易造成缓存击穿,一方面并不能保证数据的强一致性,比如如下场景:

两个请求同时执行:
请求A查询用户A信息,请求B更新用户A信息
请求A查询缓存没有值,于是去查询数据库,获取到值为old(还没有更新缓存)
请求B更新数据库用户A信息为new
请求B删除掉用户A信息的缓存
请求A写入缓存,值为old
至此,就导致用户A的数据缓存为旧数据了

Write Through模式采用的是读缓存,写时先更新缓存,再更新数据库,一般缓存不设置过期时间,适用于频繁查询缓存数据的场景。因为是先更新缓存,再更新数据库,且在查询操作时不做更新缓存,所以保证了数据的一致性,也防止了缓存击穿。

3. Write Through模式下的双写一致性

因为是先更新缓存,再更新数据库,那么更新方法的代码示例就变成了如下所示:

    @Transactional(rollbackFor = Exception.class)public R save3(UserVO userVO) {User user = new User();// 拷贝属性BeanUtils.copyProperties(userVO, user);redisTemplate.opsForValue().set(user.getUserName(), user);userMapper.addUser(user);return R.success("操作成功");}

但是这样的操作,肯定是有问题的,因为一旦数据库报错,@Transactional能够保证数据库回滚,但并不能保证redis的事务性,于是我们需要让redis也能保证事务

方案一: SessionCallback实现redis事务

以为redis本身就自带事务指令,所以最容易想到的就是通过事务指令来实现redis事务,结合redisTemplate实现,需要借助SessionCallback回调类,实现代码如下

   @Transactional(rollbackFor = Exception.class)public R save2(UserVO userVO) {// 本地事务+redis事务 = 双写一致性/缓存强一致性/redis.mysql事务回滚User user = new User();// 拷贝属性BeanUtils.copyProperties(userVO, user);Object execute = redisTemplate.execute(new SessionCallback>() {@Overridepublic List execute(RedisOperations operations) throws DataAccessException {// 事务开启operations.multi();// 执行的操作,redis先操作operations.opsForValue().set(user.getUserName(), user);try {saveUser(user);} catch (Exception e) {// 事务取消operations.discard();e.printStackTrace();return null;}// 事务的提交return operations.exec();}});return R.status(execute != null);}

方案二: setEnableTransactionSupport实现redis事务

这样的代码还是比较长的,书写起来略显麻烦,能不能有更简单的方法,或者说能不能让redis也适配 @Transactional注解,实现事务操作。答案是当然可以的,在redisTemplate中有这样的一个方法setEnableTransactionSupport(true),它可以开启redis支持数据库事务

@Configuration
public class RedisConfig {/*** 创建RedisTemplate* @param redisConnectionFactory* @return*/@Beanpublic RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){// 创建对象RedisTemplate redisTemplate = new RedisTemplate();// 设置连接工厂redisTemplate.setConnectionFactory(redisConnectionFactory);// 设置redis生成的key的序列化器,对key编码进行处理RedisSerializer stringSerializer = new StringRedisSerializer();redisTemplate.setKeySerializer(stringSerializer);redisTemplate.setHashKeySerializer(stringSerializer);// 设置redis支持数据库事务redisTemplate.setEnableTransactionSupport(true);return redisTemplate;}
}

开启之后,我们的更新代码就变成了如下所示,非常的简洁清爽

    @Transactional(rollbackFor = Exception.class)public R save3(UserVO userVO) {User user = new User();// 拷贝属性BeanUtils.copyProperties(userVO, user);redisTemplate.opsForValue().set(user.getUserName(), user);userMapper.addUser(user);return R.success("操作成功");}

测试

最后我们来进行一个模拟测试,我们将sql中的字段故意写成不存在的字段名

在这里插入图片描述

请求这个接口

在这里插入图片描述

报错字段password2不存在

在这里插入图片描述
查看数据库中并没有添加成功

在这里插入图片描述

缓存里同样也没有,说明我们的redis支持了数据库的事物操作

在这里插入图片描述

相关内容

热门资讯

一持有型不动产ABS获批通过 根据上交所网站信息,太保资产-世纪互联数据中心持有型不动产资产支持专项计划获批通过,这是市场通过的第...
美国称其从沙特获得6000亿美... 据报道,美国称其从沙特获得6000亿美元投资。
长三角生态绿色一体化发展示范区... 中新网嘉兴5月13日电(记者 王逸飞)13日,在浙江嘉善举行的2025年国际生物多样性日长三角生态绿...
沙特人工智能公司HUMAIN将... 5月13日,沙特公共投资基金旗下人工智能公司HUMAIN宣布同英伟达建立战略合作伙伴关系。两家公司将...
科技可以如此有温度,多款高科技... 转自:上观新闻外骨骼康复训练器、手语AI主持人……科技助残有了具象。今天下午,以“融合无界 科技无限...
万集科技深耕激光和智能网联方向... 上证报中国证券网讯 5月13日晚,万集科技披露公告,万集科技及下属武汉万集、深圳万集于2025年4月...
官宣!中方调整对美关税 为落实中美经贸高层会谈的重要共识,根据《中华人民共和国关税法》、《中华人民共和国海关法》、《中华人民...
从“小家”到“万家”:闽台夫妻... 中新社福州5月13日电 题:从“小家”到“万家”:闽台夫妻的大陆家装创业路  作者 郑江洛  在福州...
俄副外长:俄美两国将举行双边谈... △俄罗斯副外长里亚布科夫(资料图)当地时间13日,俄罗斯外交部副部长里亚布科夫表示,俄美两国将举行双...
“应急使命·2025”——台风... 中新网南宁5月13日电(林浩 王以照)为提升防范化解重大安全风险和应对突发事件的能力,5月13日,应...
中国国家能源局主要负责人会见洪... 中新社北京5月13日电 (王梦瑶)据中国国家能源局消息,国家能源局局长王宏志日前在北京会见洪都拉斯能...
应对AI对科研诚信的挑战,高校... 转自:中国科学报当前,人工智能(AI)对高校的影响是全面且深入的,这已成为高教界的共识。不过,这种影...
贝莱德CEO:市场在找到新平衡...   尽管中美达成贸易休战协议,但全球金融业一些知名领军者仍在提防市场可能出现的持续波动。不过他们也表...
中方调整对美关税:由34%调整... 国务院关税税则委员会5月13日发布公告称,为落实中美经贸高层会谈的重要共识,根据《中华人民共和国关税...
来伊份再回应:与消费者达成和解 5月13日,来伊份发布关于消费者反映蜜枣粽问题处理的情况说明:事情发生后,公司迅速采取行动,对相关批...
4月份黑龙江省居民消费价格同比... 转自:黑龙江新闻网近日,记者从国家统计局黑龙江调查总队获悉,2025年4月份,黑龙江省消费市场运行总...
同有科技:股东及副总经理拟合计... 格隆汇5月13日|同有科技晚间公告,持股8.38%的股东杨永松计划以集中竞价方式减持公司股份不超过4...
钜泉科技2024年股权激励计划... 上证报中国证券网讯钜泉科技5月13日晚发布公告,公司于近日收到中国证券登记结算有限责任公司上海分公司...
AI热评:百万网红被立案是自导... 转自:法治日报 #律师称网红自导自演自杀已涉嫌违法#【A...
挖地三尺,诱人羊毛来了 图:Isabelle Feliu上次写了支付宝、微信羊毛。点击重温我还担心,知道的人已经很多、没啥参...