开心一刻

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

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

儿子:是的,老师

班主任:不用紧张,我虽然是你的班主任,但我也才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 { @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: 青石路
*/
@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;启动项目后,我们直接访问接口

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 调用了该方法,我们点击跟进看看

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

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

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

  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 动态代理

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

记一次cannot access its superinterface问题的的排查 → 强如Spring也一样写Bug的更多相关文章

  1. 解Bug之路-记一次线上请求偶尔变慢的排查

    解Bug之路-记一次线上请求偶尔变慢的排查 前言 最近解决了个比较棘手的问题,由于排查过程挺有意思,于是就以此为素材写出了本篇文章. Bug现场 这是一个偶发的性能问题.在每天几百万比交易请求中,平均 ...

  2. 记一次生产环境Nginx日志骤增的问题排查过程

    摘要:众所周知,Nginx是目前最流行的Web Server之一,也广泛应用于负载均衡.反向代理等服务,但使用过程中可能因为对Nginx工作原理.变量含义理解错误,或是参数配置不当导致Nginx工作异 ...

  3. 记一次系统稳定性问题的分析处理过程(因CallContext使用不当而造成bug)

    问题描述: 一个项目现场反馈,“差旅费类型的单据审批,在出现业务规则没满足的情况时(即业务报错,需要人机交互),审批仍然通过了”.从技术的角度上说,就是业务构件中的业务规则报错后,事务没有回滚.但是, ...

  4. 记一次yarn导致cpu飙高的异常排查经历

    yarn就先不介绍了,这次排坑经历还是有收获的,从日志到堆栈信息再到源码,很有意思,下面听我说 问题描述: 集群一台NodeManager的cpu负载飙高. 进程还在但是看日志已经不再向Resourc ...

  5. 记一次CPU占用率和load高的排查

    前不久公司进行了一次大促,晚上值班.大促是从晚上8点多开始的,一开始流量慢慢的进来,观察了应用的各项指标,一切都是正常的,因为这是双11过后的第一次大促,想着用户的购买欲应该不会太强,所以我们的运维同 ...

  6. 记一次虚拟机无法挂载volume的怪异问题排查

    故障现象 使用nova volume-attach <server> <volume>命令挂载卷,命令没有返回错误,但是查看虚拟机状态,卷并没有挂载上. 故障原因 疑似虚拟机长 ...

  7. 记一次惊心的网站 TCP 队列问题排查经历

    https://blog.csdn.net/chenlycly/article/details/80868990 http://www.mytju.com/classcode/news_readnew ...

  8. 记一次上线部分docker不打日志的问题排查

    一次正常的上线,发了几台docker后,却发现有的机器打了info.log里面有日志,有的没有.排查问题开始: 第一:确认这台docker是否有流量进来,确认有流量进来. 第二:确认这台docker磁 ...

  9. CAD总记不住?设计达人给你支招,最强口诀40条玩转设计

    绘图界有这样一个准则:绘图越快,玩的越6 相反的,CAD玩的很6 ,你的绘图效率一定不会差到哪里去,虽然不能说的太绝对,但你就操作如果玩转,一定你就操作能给你的绘图带来很多效率的提升. 当然后面就你就 ...

  10. 记一次线上SpringCloud-Feign请求服务超时异常排查

    由于近期线上单量暴涨,第三方反馈部分工单业务存在查询处理失败现象,经排查是当前系统通过FeignClient调用下游系统出现部分超时失败(异常代码贴在下方). Caused by: feign.Ret ...

随机推荐

  1. ToDesk云电脑推出Web端,这意味着什么?

    在数字化转型的浪潮中,云计算技术正在以前所未有的速度改变着我们的生活方式和工作模式.作为云计算领域的一股新生力量,ToDesk云电脑凭借其卓越的性能和便捷的使用体验,一经上线,便赢得了众多用户的青睐. ...

  2. Java高并发之线程的实现方式,含Lamabda表达式

    Java中线程实现的方式 在 Java 中实现多线程有4种手段: 1.继承 Thread 类 2.实现 Runnable 接口 3.匿名内部类 4.Lambda表达式实现 实现 Runnable 接口 ...

  3. 项目中maven依赖无法自动下载

    [解决方法]: 安装目录conf--修改settting.xml文件在mirrors标签下添加子节点 <mirrors> <!-- mirror | Specifies a repo ...

  4. C语言(从入门到入土)

    1.C语言整体上需要记住的 总体上必须清楚的: 1)程序结构是三种: 顺序结构 , 循环结构 (三个循环结构), 选择结构 (if 和 switch) 2)读程序都要从main()入口, 然后从最上面 ...

  5. 在 Kubernetes 中运行 Locust 与 Selenium:安装 Chrome 和 ChromeDriver

    在现代软件开发中,性能和用户体验是至关重要的,而负载测试和自动化测试可以帮助我们实现这一目标.在本文中,我们将讨论如何在 Kubernetes 环境中运行 Locust 和 Selenium,并详细介 ...

  6. Dash 2.18.2版本更新:模式匹配回调性能大提升

    本文示例代码已上传至我的Github仓库:https://github.com/CNFeffery/dash-master Gitee同步仓库地址:https://gitee.com/cnfeffer ...

  7. 使用wxpython开发跨平台桌面应用,设计系统的登录界面

    一般的系统登统界面,设计好看一些,系统会增色不少,而常规的桌面程序,包括Web上的很多界面,都借助于背景图片的效果来增色添彩,本篇随笔介绍基于WxPython来做一个登录界面效果,并对系统登录界面在不 ...

  8. hyperf使用session

    在hyperf里面使用session的时候可以先安装组件包 composer require hyperf/session Session 组件的配置储存于  config/autoload/sess ...

  9. apisix启动报错undefined symbol: EVP_KDF_ctrl, version OPENSSL_1_1_1b

    报错内容 2024/08/06 16:56:13 [error] 154236#154236: *7039 [lua] plugin.lua:110: load_plugin(): failed to ...

  10. Python:pygame游戏编程之旅五(游戏界面文字处理详解)

    再简单的游戏界面中均涉及文字处理,本节主要解读一下pygame模块中对文字及字体的处理方式. 同样,以实例进行讲解,先看看代码: #!/usr/bin/env python # -*- coding: ...