记一次cannot access its superinterface问题的的排查 → 强如Spring也一样写Bug
开心一刻昨天在幼儿园,领着儿子在办公室跟他班主任聊他的情况
班主任:皓瑟,你跟我聊天是不是紧张呀
儿子:是的,老师
班主任:不用紧张,我虽然是你的班主任,但我也才22岁,你就把我当成班上的女同学
班主任继续补充道:你平时跟她们怎么聊,就跟我怎么聊,男孩子要果然,想说啥就说啥
儿子满眼期待的看向我,似乎在征询我的同意,我坚定的点了点头
儿子:老师,看看腿
问题复现
项目基于 Spring Boot 2.4.2,引入了 spring-boot-starter-data-redis 和 mybatis-plus-boot-starter,完整依赖如下
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.2</version></parent><dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.0</version> </dependency> <!--mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency></dependencies>对 RedisTemplate 进行了自定义配置
/** * @author 青石路 */@Configurationpublic class RedisConfig { @Bean RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(factory); StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); Jackson2JsonRedisSerializer<Object> jsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); redisTemplate.setKeySerializer(stringRedisSerializer); redisTemplate.setValueSerializer(jsonRedisSerializer); redisTemplate.setHashKeySerializer(stringRedisSerializer); redisTemplate.setHashValueSerializer(jsonRedisSerializer); redisTemplate.setEnableDefaultSerializer(true); redisTemplate.setDefaultSerializer(jsonRedisSerializer); redisTemplate.setEnableTransactionSupport(true); redisTemplate.afterPropertiesSet(); return redisTemplate; }}需要实现的功能
保存用户:若用户在缓存(Redis)中存在,直接返回成功;若用户在缓存中不存在,将用户信息保存到缓存的同时,还要保存到 MySQL
功能很简单,实现如下
/** * @author: 青石路 */@Servicepublic class UserServiceImpl extends ServiceImpl<UserDao, User> implements IUserService { private static final Logger LOG = LoggerFactory.getLogger(UserServiceImpl.class); @Resource private RedisTemplate<String, Object> redisTemplate; @Override @Transactional(rollbackFor = Exception.class) public String saveNotExist(User user) { Object o = redisTemplate.opsForValue().get("dataredis:user:" + user.getUserName()); if (o != null) { LOG.info("用户已存在"); return "用户已存在"; } redisTemplate.opsForValue().set("dataredis:user:" + user.getUserName(), user); this.save(user); return "用户保存成功"; }}结构还是常规的 Controller -> Service -> Dao;启动项目后,我们直接访问接口
POST http://localhost:8080/user/saveContent-Type: application/json{"userName": "qsl","password": "123456"}毫无意外,接口 500
{"timestamp": "2024-12-28T05:39:49.577+00:00","status": 500,"error": "Internal Server Error","message": "","path": "/user/save"}这么简单的功能,这么完美的实现,为什么也出错?
问题排查
遇到异常我们该如何排查?看 异常堆栈 是最直接的方式
有两点值得我们好好分析下
[*]RedisConnectionUtils.createConnectionSplittingProxy
看方法名就知道,这是要创建 Redis Connection 的代理;咱先甭管创建的是什么代理,咱先弄明白为什么要创建代理?
不就是查 Redis,然后写 Redis,为什么要创建代理?
怎么弄明白了,看谁调用了这个方法不就清楚了?直接从异常堆栈一眼就可以看出 RedisConnectionUtils.java:151 调用了该方法,我们点击跟进看看
所以重点有来到 bindSynchronization 和 isActualNonReadonlyTransactionActive()
[*]bindSynchronization 的值
它的计算逻辑很清楚
TransactionSynchronizationManager.isActualTransactionActive() && transactionSupport;
isActualTransactionActive() 注释如下
/** * Return whether there currently is an actual transaction active. * This indicates whether the current thread is associated with an actual * transaction rather than just with active transaction synchronization. * <p>To be called by resource management code that wants to discriminate * between active transaction synchronization (with or without backing * resource transaction; also on PROPAGATION_SUPPORTS) and an actual * transaction being active (with backing resource transaction; * on PROPAGATION_REQUIRED, PROPAGATION_REQUIRES_NEW, etc). * @see #isSynchronizationActive() */public static boolean isActualTransactionActive() { return (actualTransactionActive.get() != null);}返回当前线程是否是与实际事务相关联;可能你们看的有点迷糊,因为这里还与 Spring 的事务传播机制有关联,结合我给的示例代码来看,可以简单理解成:当前线程是否开启事务
很明显当前线程是开启事务的,所以 TransactionSynchronizationManager.isActualTransactionActive()的值为 true;transactionSupport 的值则需要继续从上游调用方寻找
跟进 RedisTemplate.java:209
enableTransactionSupport 是 RedisTemplate 的成员变量,其默认值是 false
但我们自定义的时候,将 enableTransactionSupport 设置成了 true
这里为什么设置成 true,我问了当时写这个代码的同事,直接从网上复制的,不是刻意开启的!
我是不推荐使用 Redis 事务的,至于为什么,后文会有说明
所以 bindSynchronization 的值为 true
[*]isActualNonReadonlyTransactionActive() 的返回值
从名称就知道,该方法的作用是判断当前事务是不是 非只读 的;其完整代码如下
private static boolean isActualNonReadonlyTransactionActive() { return TransactionSynchronizationManager.isActualTransactionActive() && !TransactionSynchronizationManager.isCurrentTransactionReadOnly();}TransactionSynchronizationManager.isActualTransactionActive() 前面已经分析过,其值是 true;大家还记得事务设置只读是如何设置的吗?@Transactional 注解是不是有 readOnly 配置项?
@Transactional(rollbackFor = Exception.class, readOnly = true)
readOnly 的默认值是 false,而我们的示例代码中又没有将其设置成 true,所以 !TransactionSynchronizationManager.isCurrentTransactionReadOnly() 的值就是 !false,也就是 true
所以 isActualNonReadonlyTransactionActive() 的值为 true
启用 RedisTemplate 事务的同时,又使用了 @Transactional 使得线程关联了实际事务,并且未启用非只读线程,天时地利人和之下创建了 Redis Connection 代理,通过该代理来实现 Redis 事务
Spring 对事务的实现是通用的,都是通过代理的方式来实现,不区分是关系型数据库还是Redis,甚至是其他支持事务的数据源!
[*]cannot access its superinterface
完整信息如下
java.lang.IllegalAccessError: class org.springframework.data.redis.core.Proxy82cannotaccessitssuperinterfaceorg.springframework.data.redis.core.RedisConnectionUtilsRedisConnectionProxy
不合法的访问错误:不能访问父级接口:RedisConnectionUtils$RedisConnectionProxy
关于 Spring 的代理,我们都知道有两种实现:JDK 动态代理 和 CGLIB 动态代理,而 Redis 事务则采用的 JDK 动态代理
JDK 动态代理有哪些限制,你们还记得吗,好好回忆一下
RedisConnectionUtils$RedisConnectionProxy 都没有实现类,为什么代理会涉及到它?我们看下 RedisConnectionUtils.createConnectionSplittingProxy 的实现就明白了
我们再看看 RedisConnectionUtils$RedisConnectionProxy 的具体实现
莫非是因为 RedisConnectionProxy 是内部 interface,并且是 package-protected 的,所以导致不能被访问?如何验证了,我们可以进行类似模拟,但我不推荐,我更推荐从官方找答案,因为这个问题肯定不止我们遇到了;从异常堆栈信息可以很明显的看出,这是 spring-data-redis 引发的,所以我们直接去其 github 寻找相关 issue
正好有一个,点进去看看,正好有我们想要的答案;推荐大家仔细看看这个 issue,我只强调一下重点
[*]将该bug添加到 2.4.7 版本中修复
[*]将 RedisConnectionProxy 修改成 public
[*]代码提交版本:503d639
官方 Release 版本也进行了说明
至此,相信你们都清楚问题原因了
问题修复
既然问题已经找到,修复方法也就清晰了
[*]启用只读事务
这种方式只适用于部分特殊场景,因为它还影响关系型数据库的事务
不推荐使用
[*]停用 RedisTemplate 事务
不设置 enableTransactionSupport,让其保持默认值 false,或者显示设置成 false
redisTemplate.setEnableTransactionSupport(false);
还记不记得我前面跟你们说过:不推荐使用 Redis 事务;至于为什么,我们来看看官网是怎么说明的
Redis不支持事务回滚,因为支持回滚会对Redis的简单性和性能产生重大影响;Redis 事务只能保证两点
[*]事务中的所有命令都被序列化并按顺序执行。Redis执行事务期间,不会被其它客户端发送的命令打断,事务中的所有命令都作为一个隔离操作顺序执行
[*]Redis事务是原子操作,或者执行所有命令或者都不执行。一旦执行命令,即使中间某个命令执行失败,后面的命令仍会继续执行
另外,官网提到了一个另外一个点
Redis 脚本同样具有事务性。你可以用Redis事务做的一切,你也可以用脚本做,通常脚本会更简单、更快。但有些点我们需要注意,Redis 2.6.0 才引进脚本功能,Lua 脚本具备一定的原子性,可以保证隔离性,而且可以完美的支持后面的步骤依赖前面步骤的结果,但同样也不支持回滚
所以如果我们的 Redis 版本满足的话,推荐用 Lua 脚本而非 Redis 事务
推荐使用
[*]升级 spring-data-redis 版本
spring-data-redis 2.4.7 实现了修复,但我们是采用的 starter 的方式引入的依赖,所以升级 spring boot 版本更合适;RedisConnectionUtils$RedisConnectionProxy 是 spring-data-redis 2.4.2 引入的,spring-boot-starter-data-redis 的版本与 spring-boot 版本一致,其 2.4.4、2.4.5 对应的 spring-data-redis 版本是 2.4.6、2.4.8,所以将 spring boot 升级到 2.4.5 或更高即可。如果可以的话,更推荐直接升级到适配 JDK 版本的最新稳定版本
推荐使用
总结
[*]异常堆栈就是发生异常时的调用栈,时间线顺序是 从下往上,也就是下面一行调用上面一行
[*]如果Redis版本是2.6.0或更高,不推荐使用其事务功能,用Lua实现事务更合适
不管是Redis事务,还是Lua脚本,都不支持事务回滚,所以我们要尽量保证Redis命令的正确使用
[*]不管是使用 spring-data-redis 哪个版本,都推荐关闭 RedisTemplate 的 enableTransactionSupport
出于两点考虑
[*]你们可以留意下你们项目中的 Redis 版本,肯定都高于 2.6.0,因为版本越高,功能越强大,性能越稳定;言外之意就是可以使用Lua脚本来实现事务
[*]需要用到Redis事务的场景很少,甚至没有;不怕你们笑话,我还没显示的使用过Redis的事务,当然间接用过(Redisson的锁用到了lua脚本)
页:
[1]