记一次cannot access its superinterface问题的的排查 → 强如Spring也一样写Bug

开心一刻

昨天在幼儿园,领着儿子在办公室跟他班主任聊他的情况

班主任:皓瑟,你跟我聊天是不是紧张呀

儿子:是的,老师

班主任:不用紧张,我虽然是你的班主任,但我也才22岁,你就把我当成班上的女同学

班主任继续补充道:你平时跟她们怎么聊,就跟我怎么聊,男孩子要果然,想说啥就说啥

儿子满眼期待的看向我,似乎在征询我的同意,我坚定的点了点头

儿子:老师,看看腿

问题复现

项目基于 Spring Boot 2.4.2,引入了 spring-boot-starter-data-redismybatis-plus-boot-starter,完整依赖如下

xml 复制代码
<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 进行了自定义配置

java 复制代码
/**
 * @author 青石路
 */
@Configuration
public 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

功能很简单,实现如下

java 复制代码
/**
 * @author: 青石路
 */
@Service
public 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;启动项目后,我们直接访问接口

htt 复制代码
POST http://localhost:8080/user/save
Content-Type: application/json

{
  "userName": "qsl",
  "password": "123456"
}

毫无意外,接口 500

http 复制代码
{
  "timestamp": "2024-12-28T05:39:49.577+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "message": "",
  "path": "/user/save"
}

这么简单的功能,这么完美的实现,为什么也出错?

问题排查

遇到异常我们该如何排查?看 异常堆栈 是最直接的方式

有两点值得我们好好分析下

  1. RedisConnectionUtils.createConnectionSplittingProxy

    看方法名就知道,这是要创建 Redis Connection 的代理;咱先甭管创建的是什么代理,咱先弄明白为什么要创建代理?

    不就是查 Redis,然后写 Redis,为什么要创建代理?

    怎么弄明白了,看谁调用了这个方法不就清楚了?直接从异常堆栈一眼就可以看出 RedisConnectionUtils.java:151 调用了该方法,我们点击跟进看看

    所以重点有来到 bindSynchronizationisActualNonReadonlyTransactionActive()

    • bindSynchronization 的值

      它的计算逻辑很清楚

      TransactionSynchronizationManager.isActualTransactionActive() && transactionSupport;

      isActualTransactionActive() 注释如下

      java 复制代码
      /**
       * 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() 的值为 truetransactionSupport 的值则需要继续从上游调用方寻找

      跟进 RedisTemplate.java:209

      enableTransactionSupport 是 RedisTemplate 的成员变量,其默认值是 false

      但我们自定义的时候,将 enableTransactionSupport 设置成了 true

      这里为什么设置成 true,我问了当时写这个代码的同事,直接从网上复制的,不是刻意开启的!

      我是不推荐使用 Redis 事务的,至于为什么,后文会有说明

      所以 bindSynchronization 的值为 true

    • isActualNonReadonlyTransactionActive() 的返回值

      从名称就知道,该方法的作用是判断当前事务是不是 非只读 的;其完整代码如下

      java 复制代码
      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,甚至是其他支持事务的数据源!

  2. cannot access its superinterface

    完整信息如下

    java.lang.IllegalAccessError: class org.springframework.data.redis.core.Proxy82 cannot access its superinterface org.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,我只强调一下重点

    1. 将该bug添加到 2.4.7 版本中修复

    2. 将 RedisConnectionProxy 修改成 public

    3. 代码提交版本:503d639

    官方 Release 版本也进行了说明

至此,相信你们都清楚问题原因了

问题修复

既然问题已经找到,修复方法也就清晰了

  1. 启用只读事务

    这种方式只适用于部分特殊场景,因为它还影响关系型数据库的事务

    不推荐使用

  2. 停用 RedisTemplate 事务

    不设置 enableTransactionSupport,让其保持默认值 false,或者显示设置成 false

    redisTemplate.setEnableTransactionSupport(false);

    还记不记得我前面跟你们说过:不推荐使用 Redis 事务;至于为什么,我们来看看官网是怎么说明的

    Redis不支持事务回滚,因为支持回滚会对Redis的简单性和性能产生重大影响;Redis 事务只能保证两点

    • 事务中的所有命令都被序列化并按顺序执行。Redis执行事务期间,不会被其它客户端发送的命令打断,事务中的所有命令都作为一个隔离操作顺序执行
    • Redis事务是原子操作,或者执行所有命令或者都不执行。一旦执行命令,即使中间某个命令执行失败,后面的命令仍会继续执行

    另外,官网提到了一个另外一个点

    Redis 脚本同样具有事务性。你可以用Redis事务做的一切,你也可以用脚本做,通常脚本会更简单、更快。但有些点我们需要注意,Redis 2.6.0 才引进脚本功能,Lua 脚本具备一定的原子性,可以保证隔离性,而且可以完美的支持后面的步骤依赖前面步骤的结果 ,但同样也不支持回滚

    所以如果我们的 Redis 版本满足的话,推荐用 Lua 脚本而非 Redis 事务

    推荐使用

  3. 升级 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 版本的最新稳定版本

    推荐使用

总结

  1. 异常堆栈就是发生异常时的调用栈,时间线顺序是 从下往上,也就是下面一行调用上面一行

  2. 如果Redis版本是2.6.0或更高,不推荐使用其事务功能,用Lua实现事务更合适

    不管是Redis事务,还是Lua脚本,都不支持事务回滚,所以我们要尽量保证Redis命令的正确使用

  3. 不管是使用 spring-data-redis 哪个版本,都推荐关闭 RedisTemplate 的 enableTransactionSupport

    出于两点考虑

    • 你们可以留意下你们项目中的 Redis 版本,肯定都高于 2.6.0,因为版本越高,功能越强大,性能越稳定;言外之意就是可以使用Lua脚本来实现事务
    • 需要用到Redis事务的场景很少,甚至没有;不怕你们笑话,我还没显示的使用过Redis的事务,当然间接用过(Redisson的锁用到了lua脚本)
相关推荐
ToPossessLight090228 分钟前
Spring 容器的基本实现
spring
程序定小飞3 小时前
基于springboot的学院班级回忆录的设计与实现
java·vue.js·spring boot·后端·spring
郝开5 小时前
Spring Boot 2.7.18(最终 2.x 系列版本)1 - 技术选型:连接池技术选型对比;接口文档技术选型对比
java·spring boot·spring
知兀6 小时前
【Spring/SpringBoot】SSM(Spring+Spring MVC+Mybatis)方案、各部分职责、与Springboot关系
java·spring boot·spring
伊布拉西莫8 小时前
Spring 6.x HTTP interface 使用说明
spring·restclient
YDS82910 小时前
苍穹外卖 —— Spring Cache和购物车功能开发
java·spring boot·后端·spring·mybatis
Elieal10 小时前
Spring 框架核心技术全解析
java·spring·sqlserver
组合缺一10 小时前
(对标 Spring)OpenSolon v3.7.0, v3.6.4, v3.5.8, v3.4.8 发布(支持 LTS)
java·后端·spring·web·solon
♡喜欢做梦10 小时前
Spring IOC
java·后端·spring
葡萄城技术团队20 小时前
迎接下一代 React 框架:Next.js 16 核心能力解读
javascript·spring·react.js