解决redis多表更新的事务一致性问题

前言

由于对接的系统越来越多,接口访问并发量日渐增加,遂决定在项目中加入redis。

认识redis

redis做为一个缓存中间件经常被用于提高接口并发量来使用。你可以把他看做数据库的缓存来使用,类似于上次我们说的mybatis的缓存机制。也可以把他作为一个高性能数据库来使用,支持持久化。
类比于mybatis的缓存机制不同的是,我们使用redis作为缓存不会出现查一次做修改不保存再去查就是新值的问题,也比mybatis的缓存机制可操作性更强。比如当更新一个表时,mybatis自己会把所有缓存全部清空,保证时效性。而redis更新一个表时,你可以去只更新对应的一条数据,可操作性更强,同时支持设置失效时间,如10分钟,10分钟后自动删除。

redis内部将key-value保存到了内存中,查询时,通过key返回value值。处理value,也就是String类型,还支持同时还提供list,set,zset,hash等数据结构的存储。最常用的就是String类型。
以为保存在内存中,所以比数据库读写速度快。

安装可以参考菜鸟教程安装。他的使用类似于mysql,提供命令行操作,支持可视化软件链接。

使用redis

springboot项目接入redis,同样需要引入redis包,在yaml文件中加入配置连接。

项目中使用redis两种方式,一种是使用缓存注解形式,一种是使用RedisTemplate的方法操作。
注解形式
首先在项目启动类上加入@EnableCaching注解表示启用缓存

@Cacheable注解,在查询方法上添加,调用此方法前先通过key查询缓存,若查不到,则再执行此方法,并将返回结果和key保存到缓存中。若查得到,直接返回value,不在执行此方法。其中参数cacheNames指定缓存组件名,此名字在yaml文件中定义,key代表key-value中的key,使用spEL表达式去编写,如果入参是对象,可写作key = "#user.usrNo"

@Cacheable(cacheNames = "XXX", key = "#usrNo")
public User queryByUsrNo(String usrNo) {
}

需要注意的是,这里的key是不区分表的,假如两个表的key值相同,那么就会产生冲突,所以我们的key值前要加上表明前缀,格式为表名:key,写作key = "'user:' + '#usrNo'" 。这里的:号有分组的意思,类似于文件路径的/,这个可以在可视化redis工具中看到。

@CachePut注解,在变更方法上添加,在执行完目标方法后,缓存中的数据也会更新。

@CacheEvict注解,删除缓存,在执行完目标方法后,清除对应的key-value。

代码形式
以上功能手动在代码中添加,使用RedisTemplate中的方法实现。
例如需要手动在查询方法上加查询数据库的判断操作和查询完数据库的保存操作。

解决redis多表更新的事务一致性问题

redis支持事务,但是跟mysql的事务并不一样,redis事务没有回滚机制。那么假如一个变更交易更新两个表,当更新第一个表时mysql成功更新,redis也更新,当更新第二个表时报错mysql回滚,第一个表还是原来的数据,但是redis无法回滚之前的数据,导致数据不一致的问题。

解决这个问题其实我们要保证redis在所有表都成功更新后再去统一更新。这个我想到了前段时间接触的spring event机制。其中有一个发布多个事件,监听事件者可以在事务提交后执行。利用此原理。切面数据库更新删除方法,每当更新删除一个表时,把key发布出去。同时设置一个监听者,加入@TransactionalEventListener注解,当整个事务提交时,删除发布的所有key-value。
具体可参考spring event机制
这里需要我们着重注意一下key的设置,我们需要给每一个表设置一个业务要素。业务要素可以简单理解为我们查询次数最多的条件。如学生表的话就是学生编号,学生班级关联表也是学生编号。这样保证每一个表的key对应的字段相同,删除缓存时才不会漏删。

总结

redis适用于一些不经常变的数据或者对变化时效性要求不高的数据缓存,如果对时效性要求高并且又不得不加redis,可以使用我上边提到的解决办法。

作者:小强Zzz原文地址:https://segmentfault.com/a/1190000043760157

%s 个评论

要回复文章请先登录注册