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

news/2025/1/7 16:18:22/文章来源:https://www.cnblogs.com/youzhibing/p/18651534

开心一刻

昨天在幼儿园,领着儿子在办公室跟他班主任聊他的情况
班主任:皓瑟,你跟我聊天是不是紧张呀
儿子:是的,老师
班主任:不用紧张,我虽然是你的班主任,但我也才22岁,你就把我当成班上的女同学
班主任继续补充道:你平时跟她们怎么聊,就跟我怎么聊,男孩子要果然,想说啥就说啥
儿子满眼期待的看向我,似乎在征询我的同意,我坚定的点了点头
儿子:老师,看看腿

开心一刻

问题复现

项目基于 Spring Boot 2.4.2,引入了 spring-boot-starter-data-redismybatis-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 青石路*/
@Configuration
public class RedisConfig {@BeanRedisTemplate<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: 青石路*/
@Service
public class UserServiceImpl extends ServiceImpl<UserDao, User> implements IUserService {private static final Logger LOG = LoggerFactory.getLogger(UserServiceImpl.class);@Resourceprivate 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/save
Content-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"
}

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

早知道不学编程了

问题排查

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

异常堆栈信息

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

  1. RedisConnectionUtils.createConnectionSplittingProxy

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

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

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

    createConnectionSplittingProxy调用处

    所以重点有来到 bindSynchronizationisActualNonReadonlyTransactionActive()

    • 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 的事务传播机制有关联,结合我给的示例代码来看,可以简单理解成:当前线程是否开启事务

      saveNotExist开启事务

      很明显当前线程是开启事务的,所以 TransactionSynchronizationManager.isActualTransactionActive() 的值为 truetransactionSupport 的值则需要继续从上游调用方寻找

      redis_trasactionSupportpng

      跟进 RedisTemplate.java:209

      RedisTemplate_enableTrascationSupport

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

      enableTransactionSupport默认值false

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

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

  2. cannot access its superinterface

    完整信息如下

    java.lang.IllegalAccessError: class org.springframework.data.redis.core.$Proxy82 cannot access its superinterface org.springframework.data.redis.core.RedisConnectionUtils$RedisConnectionProxy

    不合法的访问错误:不能访问父级接口:RedisConnectionUtils$RedisConnectionProxy

    关于 Spring 的代理,我们都知道有两种实现:JDK 动态代理CGLIB 动态代理,而 Redis 事务则采用的 JDK 动态代理

    Redis事务实现_JDK代理

    JDK 动态代理有哪些限制,你们还记得吗,好好回忆一下

    RedisConnectionUtils$RedisConnectionProxy 都没有实现类,为什么代理会涉及到它?我们看下 RedisConnectionUtils.createConnectionSplittingProxy 的实现就明白了

    createConnectionSplittingProxy具体代码

    我们再看看 RedisConnectionUtils$RedisConnectionProxy 的具体实现

    RedisConnectionProxy_具体代码

    莫非是因为 RedisConnectionProxy 是内部 interface,并且是 package-protected 的,所以导致不能被访问?如何验证了,我们可以进行类似模拟,但我不推荐,我更推荐从官方找答案,因为这个问题肯定不止我们遇到了;从异常堆栈信息可以很明显的看出,这是 spring-data-redis 引发的,所以我们直接去其 github 寻找相关 issue

    RedisConnectionProxy_github搜索

    正好有一个,点进去看看,正好有我们想要的答案;推荐大家仔细看看这个 issue,我只强调一下重点

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

    2. 将 RedisConnectionProxy 修改成 public

    3. 代码提交版本:503d639

      RedisConnectionProxy_修改记录

    官方 Release 版本也进行了说明

    官方release_2.4.7

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

问题修复

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

  1. 启用只读事务

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

    不推荐使用

  2. 停用 RedisTemplate 事务

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

    redisTemplate.setEnableTransactionSupport(false);

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

    redis事务不支持回滚

    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脚本)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/864819.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何在织梦CMS中修改网站地图模板

织梦CMS是一款广泛使用的开源内容管理系统,适用于各种类型的网站。修改网站地图模板可以帮助您更好地控制网站的搜索引擎优化(SEO)。以下是详细的修改步骤和注意事项:登录织梦CMS后台:使用管理员账户登录织梦CMS后台。通常,后台地址为域名/dede或域名/admin。 输入用户名…

如何轻松修改织梦网站的宽度?

修改织梦CMS(DedeCMS)网站的宽度可以通过调整CSS样式来实现。以下是详细步骤:登录后台: 使用管理员账号登录织梦CMS后台。进入模板管理: 在左侧菜单中找到“模板管理”,点击进入。选择模板文件: 在模板管理页面中,找到包含网站布局的模板文件(如index.htm、header.htm…

如何轻松修改公司网站?

修改公司网站是一个重要的维护任务,可以提升网站的吸引力和用户体验。以下是详细步骤:确定修改需求:分析现状: 评估当前网站的优缺点,确定需要修改的部分。 收集反馈: 收集用户反馈和建议,了解用户的需求和期望。 制定计划: 制定详细的修改计划,包括修改的内容、时间表…

网站内容修改的操作位置及步骤

修改网站内容是日常维护和更新的重要任务。以下是详细的指南,帮助您顺利找到并修改网站内容:确定使用的CMS平台:不同的CMS平台有不同的操作方式。常见的CMS包括WordPress、Joomla、Drupal等。了解您所使用的具体平台及其内容管理机制。登录CMS后台管理系统:进入网站的后台管…

如何在txt网站制作中修改字体以提升视觉效果

在基于文本文件(TXT)的网站制作中修改字体是一项常见的需求。以下是详细的指南,帮助您顺利完成这一任务:选择合适的HTML模板:将纯文本文件转换为HTML格式,以便应用样式和字体设置。 使用简单的HTML结构,如<p>、<h1>、<h2>等标签来组织内容。编写CSS样…

【划重点】90%的人都会忽略!跨网文件安全交换需要注意的问题

跨网文件安全交换是现代企业日常运营中的一个关键环节,尤其是在内外网(例如公司内部网络与外部供应商、客户之间的文件传输),或者内部不同隔离网之间进行数据交换时。由于涉及敏感信息的交换,任何疏忽都可能带来数据泄露、恶意攻击等安全隐患。企业在进行跨网文件传输时,…

学期结束,“物品复活软件”也进行了迭代,谈谈我的心得体会

软件工程作业:“物品复活“软件开发之PSP数据的统计 大学生经常有些物品觉得扔掉可惜,不处理又觉得浪费自己的地方。请你编写一个物品“复活”软件 该程序允许添加物品的信息(物品名称,物品描述,联系人信息),删除物品的信息,显示物品列表,也允许查找物品的信息 加分功…

《深入理解Mybatis原理》MyBatis的sqlSession执行流程

sqlSessionFactory 与 SqlSession 正如其名,Sqlsession对应着一次数据库会话。由于数据库会话不是永久的,因此Sqlsession的生命周期也不应该是永久的,相反,在你每次访问数据库时都需要创建它(当然并不是说在Sqlsession里只能执行一次sql,你可以执行多次,当一旦关闭了Sql…

将 EasySQLite 从 .NET 8 升级到 .NET 9

前言 EasySQLite是一个.NET 8操作SQLite入门到实战的详细教程,主要是对学校班级,学生信息进行管理维护。今天咱们的主要内容是将EasySQLite从.NET 8升级到.NET 9。GitHub开源地址:https://github.com/YSGStudyHards/EasySQLite选型、开发详细教程第一天 SQLite 简介 第二天 …

IDA Pro 9.0 (macOS, Linux, Windows) - 强大的反汇编程序、反编译器和多功能调试器

IDA Pro 9.0 (macOS, Linux, Windows) - 强大的反汇编程序、反编译器和多功能调试器IDA Pro 9.0 (macOS, Linux, Windows) - 强大的反汇编程序、反编译器和多功能调试器 A powerful disassembler, decompiler and a versatile debugger. In one tool. 请访问原文链接:https://…

使用DockerCompose部署服务

由于格式或图片解析问题,为了更好的阅读体验,可前往 阅读原文以前我们总是用命令管理每个容器的启动、停止等等,若有多个容器时可能还存在启动优先级的问题,那就要等到指定的容器启动后再去启动另一个容器,对于整体的应用服务管理极其不方便,简单的docker run命令更适合初…

读数据保护:工作负载的可恢复性27传统的数据保护方案

传统的数据保护方案1. 传统的数据保护方案 1.1. 备份行业有一个比较特殊的地方在于,10年或20年前设计的一些产品至今仍然有许多人在用 1.2. 在20年前,市面上的所有备份方案都是我们现在称之为传统备份产品的那种方案1.2.1. 必须自己动手把上百个UNIX系统里的数据各自备份到单…