首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

当 Redis 碰上 @Transactional,有大坑!

嗨,我是东哥!今天聊点“血泪经验”:当 Redis 遇上 @Transactional,真的是个隐藏大坑,稍有不慎就会踩进去。

这个事儿我是真经历过,每次出了问题,都是一通排查,结果发现问题根本不是写代码能解决的,这坑到底有多深,咱们一起来挖掘看看!

最近我负责的一个项目,生产环境突然出现了一个诡异的情况。

客服人员每天早上想创建客服事件时,总是失败。但奇怪的是,重启服务后居然就好了,可到第二天早上又会出现同样的错误——非重启不能解决,循环往复。这类问题真是头疼得很,程序员的梦魇没跑了。

于是开始排查,发现代码里使用了 Redis 的递增操作来生成唯一的分布式 ID,代码如下:

return redisTemplate.opsForValue().increment("count", 1);

按理说,这个递增操作应该每次都返回一个整数。但偏偏到早上这段代码就返回 null,导致后续一系列逻辑全部卡壳,事件无法保存。

最骚的是,重启之后又正常了!这“玄学”特性可真让人抓狂。

既然问题在 Redis 操作上,我们的排查方向也就定了下来,为什么 Redis 操作会返回 null?而且重启后为什么又好使了?到底问题出在哪?

排查过程:疑云渐散,锁定“锅”在 @Transactional

先说结论,问题出在 @Transactional 和 Redis 的组合上。讲真,起初也没想到是它,咱们一步步来还原这过程:

监控 Redis 连接:刚开始我以为是 Redis 本身的问题,于是开启 Redis 监控,看看是不是 Redis 连接超时或者连接数不足。结果 Redis 正常得很,连接也很稳定。

日志排查:既然 Redis 本身没问题,那就看看服务端的日志。于是我在递增操作周围加了各种日志,查看每一步的返回值。发现出错的早上,这段代码里 increment 操作返回了 null,但重启后又正常返回整数值,这让我开始怀疑是不是某种环境问题。

锁定 @Transactional:这时我留意到,递增操作是在一个 @Transactional 方法中进行的,而这可能就是问题的核心。回忆起来,@Transactional 这个注解和 Redis 可不怎么对付,特别是在某些场景下会导致 Redis 操作异常。

经过分析,这里出现问题的根本原因其实是:Spring 的事务机制和 Redis 操作在某些情况下并不兼容,特别是事务回滚时会干扰 Redis 操作,导致 Redis 返回 null。

了解 @Transactional 如何“惹祸”

知道问题在 @Transactional 上后,我们深入了解一下 @Transactional 到底如何“惹祸”的。

首先要知道,@Transactional 是 Spring 管理事务的注解。事务开启后,Spring 会创建一个新的数据库连接来执行 SQL 操作,事务结束时则决定是否提交或回滚。

在 Spring 的事务回滚机制中,如果某个操作需要回滚,事务管理器会自动清除当前事务中执行的操作(包括 Redis 的操作)。然而 Redis 操作通常是不带事务的,当 @Transactional 要求回滚时,Redis 的连接偶尔会受到影响,比如被释放或者被清空,从而导致后续 Redis 操作返回 null。

具体来说,当 Redis 在事务管理中执行时,事务的回滚可能会使 Redis 操作变得不稳定,导致返回 null。而一旦服务重启,连接重置,Redis 恢复正常,直到事务再次触发。

为了加深理解,看看源码可以更清晰:

@Transactional

public void createCustomerEvent() {

Long id = redisTemplate.opsForValue().increment("count", 1);

if (id == null) {

throw new RuntimeException("Redis increment returned null!");

}

// 其他数据库操作...

}

在事务中,Redis 的递增操作被干扰了,最终返回 null,导致异常。而在 Spring 事务管理中,Redis 连接和数据库连接一旦发生交叉,就可能触发这类问题。

修复方案

既然找到了问题根源,修复方法也就清晰了。以下几种方案可以避免 @Transactional 和 Redis 冲突:

方案一:Redis 操作移出事务

最直接有效的方式就是把 Redis 操作移出事务。Redis 本身是一个内存型数据库,性能非常高,一般不需要事务管理,因此可以单独处理它的操作:

public void createCustomerEvent() {

Long id = redisTemplate.opsForValue().increment("count", 1);

if (id == null) {

throw new RuntimeException("Redis increment returned null!");

}

// Redis 操作完毕,开始事务

saveEventWithTransaction(id);

}

@Transactional

private void saveEventWithTransaction(Long id) {

// 数据库操作...

}

这样一来,Redis 操作与数据库事务分开,避免了 Redis 操作被事务回滚影响的情况。

方案二:使用 Redis 的 Lua 脚本

如果业务逻辑复杂,必须在事务中使用 Redis,可以考虑用 Lua 脚本来保证 Redis 操作的原子性。Lua 脚本在 Redis 中是原子执行的,可以确保递增操作的稳定性:

// Lua 脚本

String script = "return redis.call('INCRBY', KEYS[1], ARGV[1])";

DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();

redisScript.setScriptText(script);

redisScript.setResultType(Long.class);

Long id = redisTemplate.execute(redisScript, Collections.singletonList("count"), 1);

这种方式避免了 @Transactional 带来的回滚问题,因为 Lua 脚本本身是一个整体,即使 Redis 内部有小问题,也不会导致 null 返回。

方案三:使用分布式 ID 生成器

如果 Redis 的递增操作不可靠,还可以使用分布式 ID 生成器,比如雪花算法(Snowflake)或者 UUID。这类算法不依赖 Redis,可以单独生成唯一 ID,避免了 Redis 的不确定性。

public Long generateUniqueId() {

return System.currentTimeMillis(); // 简单的 ID 生成示例

}

当然,雪花算法更推荐,它能生成长整型 ID,不容易冲突。

@Transactional 是个强大的注解,但用不好就会带来意想不到的麻烦,特别是与 Redis 一起使用时。通过这次经历,我总结了几条经验:

Redis 操作尽量放在事务外部,让 Redis 操作与事务解耦。

必要时使用 Lua 脚本,确保操作的原子性。

ID 生成尽量使用分布式算法,减少对 Redis 的依赖。

总之,@Transactional 和 Redis 的组合要格外小心,否则就会像我这样掉进坑里,天天 debug 到天亮

  • 发表于:
  • 原文链接https://page.om.qq.com/page/OITG-5Gxek--YEPMf7zQWWDw0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券