SpringCache从入门到弃坑
死锁问题
在编写接口时,需要将数据库查询出来的结果进行redis缓存,由于之前项目中有使用过对应@Cacheable注解,这次也是使用的@Cacheable接口进行缓存,于是一顿操作完, 开始写单元测试并开始测试, 刚开始的时候还正常执行完,等我修改了部分代码并重新执行单元测试的时候,发现单元测试死锁.代码逻辑很简单,大致如下
这里就很奇怪了,不过之前就踩过一个springCache的坑见之前踩的坑 ,所以源码的断点都还没去掉,开始打开jar包跟着对应代码执行看,对应版本号:
spring-data-redis: 1.8.3
spring-context: 5.2.6
执行流程
Spring 框架支持透明地向现有 Spring 应用程序添加缓存。与事务支持类似,springCache使用了代理模式,缓存抽象允许一致使用各种缓存解决方案,而对代码的影响最小。
cache增强类执行流程
与redis交互的相关流程如上图展示,可见,当使用springCache的时候,执行任意redis操作后都会调用RedisCache的doInRedis
方法,然后再进行值的相关处理与返回,也就是只有在waitForLock
会出现死锁的情况,那出现问题的具体代码,我们还要深入看下
定位结果分析
RedisCache对应模块代码如下
org.springframework.data.redis.cache.RedisCache.AbstractRedisCacheCallback#doInRedis(org.springframework.data.redis.connection.RedisConnection)
发现等待锁的代码,进入看下
很明显, 这里使用了一个dowhile的循环来一直判断当前有没有获取到这个cacheMetadata.getCacheLockKey()
,如果这里的CacheLock一直没有释放,那就会出现死锁的情况,那我们再看下后面的doInRedis
,这里的方法是抽象方法,由下面几个类提供对应具体方法
上面除开本身执行的方法之外,在4个子类中, 只有当使用最后一个RedisWriteThroughCallback
的时候,才会有使用到cacheMetadata.getCacheLockKey()
,而里面执行的代码如下
在这个方法中, 使用了lock
和unlock
方法来保证限制并发执行,可以看下里面的执行代码
lock:
Unlock:
可见如果在这个方法内部执行时中断了程序,那么这个finally是不会执行了,那就会导致cacheMetadata.getCacheLockKey()
永远不会被删除,那么无论下次什么时候访问,无论访问多少次,永远都会在waitForLock
这个方法里面死锁.那接下来让我们看下什么情况下会走到RedisWriteThroughCallback
里面,看调用方,只发现一处:
org.springframework.data.redis.cache.RedisCache#get(java.lang.Object, java.util.concurrent.Callable<T>)
再往上,又回到了我们最初的起点CacheAspectSupport
,即1.2.2中的第二个节点,那看看怎么产生的这个get的调用
正向流程分析
org.springframework.cache.interceptor.CacheAspectSupport#execute(org.springframework.cache.interceptor.CacheOperationInvoker, java.lang.Object, java.lang.reflect.Method, java.lang.Object[])
上面的方法有2个重要步骤
- 生成
CacheOperationSource
列表,并放入CacheOperationContexts
中 - 执行execute方法
生成的CacheOperationSource,进入代码便发现是根据方法上对应注解解析出来的值生成的对象,那接下来看下execute方法
当我们在注解中有设置sync = true
时,便进入了第一个if里面,这里可以看到
正是调用的org.springframework.data.redis.cache.RedisCache#get(java.lang.Object, java.util.concurrent.Callable<T>)
方法,那这里我们可以下对应结论:
当在springCache相关注解中使用了 sync = true
时,springCache 会通过获取redis锁来控制并发,然而如果程序中出现异常的时候,则没删除这个锁,导致后续查询获取不到就一直等待,出现死锁的情况
问题定位
到底发生了什么? 我们来简单复盘下第一步中springCache的使用流程吧
- 开启SpringCache
- 设置RedisCahceManager 去管理对应CacheName
- RedisCacheManager 这个bean 注册完成之后,有对应
afterPropertiesSet
去初始化对应spring缓存配置 - 初始化缓存的时候,根据cacheName 生成对应的
new RedisCache()
并缓存起来 - 在创建RedisCache的时候,会
new RedisCacheMetadata()
,而在这个构造方法里面就生成了对应的cacheLockName即cacheMetadata.getCacheLockKey()
对应的值
- 在指定方法上加上
@Cacheable(cacheNames = "cache", key = "cache_Pagelist", sync = true)
等相关的注解,以便开启对应代理 - 在调用代理方法的时候,判断该方法开启了
sync = true
则会调用redisCache.get()
方法- 在执行get()方法的时候,会执行对应传入的
RedisWriteThroughCallback
里面的doInRedis
方法 - 执行时中途发生中断(如单元测试中断),无法释放redis锁
- 在执行get()方法的时候,会执行对应传入的
- 后续调用该CacheName管理下的方法时,都会进入
waitForLock()
方法从而进入无限等待,导致死锁
解决方法
在1.8.3的版本中,问题在于所有的操作都会被这个锁所控制,而如果不想让这个锁出现,可以不设置sync=true
这个属性,
在2.6.x版本中,这个控制将选择的权利返回到了RedisCacheManager
这样在创建对应bean的时候可以直接控制不开启对应锁.
删除异常
springCache 在项目刚刚开始引入使用的时候,其实就已经出现过一次问题,和刚刚遇到的点一样,都是在doInRedis
中出现,只不过这次是它的另外一个注解:cacheEvict
,其执行方法如下
这注解的作用是执行对应方法之后删除对应缓存,但是问题出在它在不同的版本有不同的处理方式:
zrem or keys?
执行CacheEvict的时候, 删除多个key的时候是有分为2种情况,
- 当CacheEvict 有设置
allEntries= true
时,直接删除CacheName下的所有key - 删除当前CacheEvict指定的key
我们先看1.8.3的当前版本:
这2种不同的方式调用的redis实现方式是不一样的,简单来说 doclear
方法是执行的:
上面2种不同的方法,而doEvict
执行的是
以上方法执行下来,在自定义脚本中keys是有一定风险, 因为在正式的环境中, 是会禁用掉keys的这个命令,
再在另外一个项目用的较高的2.3.0版本:
org.springframework.cache.interceptor.AbstractCacheInvoker#doClear
上面就是doclear方法真正执行的方法,我们再看下doEvict实际执行的方法
org.springframework.cache.interceptor.AbstractCacheInvoker#doEvict
所以从上得知,如果我们使用了allEntries= true
这个属性,可以看到无论是新老版本,都会调用doclear,使用到keys的这个命令,但是由于公司redis的设置,keys无法执行,导致异常报错
解决方法
那spring难道就没有解决这个问题的办法了么? 有的,在翻spring-data-redis当前支持版本的时候翻到了2.6.x,发现他是这么实现的:
org.springframework.data.redis.cache.DefaultRedisCacheWriter#clean
在选择scan还是keys的地方,使用了策略模式,将选择权返回给我们
如果后续有最新版本使用的话,在写RedisCacheManager bean的时候需要改写对应的删除策略为scan
,这样就可以避免上面出现的问题
总结
springCache虽然作为Spring 框架的一员,是Integration 模块中的组成部分,但是每每在使用过程中踩到坑,只能说要理解才能更好的去使用
__EOF__

本文链接:https://www.cnblogs.com/wzqshb/p/16276966.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗