96SEO 2026-04-24 07:05 3
说实话,这绝对是我职业生涯里Zui让人头皮发麻的一次经历。前阵子接了个需求,逻辑简单得让人想笑:用户并发操作某个资源,比如抢限量优惠券,或者是高频修改某个核心配置。这种防并发超卖的场景,对于咱们这种老 Java 开发来说闭着眼睛douNeng写出一套“标准答案”:Spring Boot 搭配 @Transactional 注解,再加上 Redisson 的分布式锁,这不就是铁桶江山吗?

当时我咔咔一顿敲,代码写得极其丝滑,本地单线程测了没毛病,直接提测上线。结果上线第一天运营那边直接炸锅了跑过来拍桌子:“后台数据不对啊,怎么同一个资源被重复扣了两次?库存怎么变成负数了?”
我当时心里一句“卧槽”,赶紧滚去查kan日志。不kan不知道,一kan整个人dou麻了:虽然加了分布式锁,但在极高并发下锁竟然“失效”了!
kan似无懈可击的“标准”代码为了复盘这次惨痛的教训,我把当时的业务代码简化了一下。大体逻辑非常清晰:查询数据库中的剩余量 -> 判断是否足够 -> 扣减库存 -> 保存结果。
各位大佬kankan,下面这段代码是不是特别眼熟?是不是觉得这就是教科书级别的写法?@Transactional 负责保证事务的原子性,try...finally 块负责保证锁必定会被释放。这代码不管怎么 Code Review,逻辑上似乎dou没毛病啊!
@Service
public class ResourceServiceImpl implements ResourceService {
@Autowired
private RedissonClient redissonClient;
@Autowired
private ResourceMapper resourceMapper;
@Override
@Transactional
public void consumeResource {
String lockKey = "lock:resource:" + resourceId;
RLock lock = redissonClient.getLock;
try {
// 尝试获取锁,Zui多等3秒
if ) {
// 1. 查数据库当前库存
Resource res = resourceMapper.selectById;
if > 0) {
// 2. 扣减
res.setStock - 1);
resourceMapper.updateById;
} else {
throw new RuntimeException;
}
} else {
throw new RuntimeException;
}
} catch {
Thread.currentThread.interrupt;
} finally {
// 3. 释放锁
if && lock.isHeldByCurrentThread) {
lock.unlock;
}
}
}
}
但是现实狠狠地给了我一巴掌。并发量一上来数据库的库存还是出现了负数。这到底是为什么?
致命的陷阱:Spring AOP 的执行顺序排查了大半天抓了 MySQL 的 binlog 和 Redis 的执行日志进行对比,终于发现了问题所在:这锅得 Spring AOP 背。
大家回忆一下 @Transactional 的底层原理。Spring 是通过 AOP 动态代理来实现事务管理的,它相当于在你的业务方法外面包了一层代理壳子。我们Ke以用一段伪代码来模拟这个 Spring 代理类的逻辑:
// Spring 代理类的伪代码
public void proxyConsumeResource {
// 1. 开启数据库事务
connection.setAutoCommit;
try {
// 2. 执行你的真实业务逻辑
target.consumeResource;
// 3. 提交事务
connection.commit;
} catch {
connection.rollback;
}
}
kan出致命问题了吗?!
在我的业务代码里finally 块执行了 lock.unlock,此时分布式锁Yi经被释放了。但是!此时 target.consumeResource 方法才刚刚执行完,Spring 代理类的 connection.commit 还没执行呢!
也就是说锁Yi经没了但数据还没落盘。
这个时间窗口虽然极短,但就是致命的漏洞。这时候,Ru果有另一个线程并发打进来:
线程A执行完业务逻辑,释放了锁。
线程B立马尝试加锁,成功!因为它发现锁Yi经被A释放了。
线程A的 Spring 代理才开始提交事务,写入数据库。
线程B查询数据库,读到的可Neng还是旧数据,或者直接开始修改。
完美,脏写产生了数据被彻底打穿。有时候真不是底层组件不行,纯粹是我们没把 Spring AOP 的执行顺序盘明白。
如何修复?锁的范围必须大于事务找到原因后解决起来就非常简单了。核心思想只有一个:必须保证事务提交之后再释放锁。
也就是说锁的粒度要足够大,必须把 Spring 的事务提交过程包裹在锁的范围内。
方案一:调整代码结构直接把加锁的逻辑往上提,放到 Controller 层,或者再包一层 Service。确保锁的范围大于事务的范围。
@Service
public class ResourceLockService {
@Autowired
private RedissonClient redissonClient;
@Autowired
private ResourceServiceImpl resourceService; // 注入原来的事务Service
public void safeConsume {
String lockKey = "lock:resource:" + resourceId;
RLock lock = redissonClient.getLock;
try {
if ) {
// 调用事务方法,由于事务方法是一个独立的 proxy,
// 执行完毕返回到这里时事务Yi经 commit 啦!
resourceService.consumeResource;
}
} catch {
Thread.currentThread.interrupt;
} finally {
if && lock.isHeldByCurrentThread) {
lock.unlock;
}
}
}
}
注:注意不要在同一个类里写这两个方法直接 this 调用,会导致 AOP 失效,这是老生常谈的坑了。
方案二:手动控制事务边界Ru果你不想多写一层类,也Ke以使用 TransactionTemplate 手动控制事务的边界,把事务提交显式地写在锁释放之前。
// 在获取锁之后执行:
transactionTemplate.execute(status -> {
// 查库、扣减、geng新
return null;
});
// 事务提交完毕,再进入 finally 释放锁
顺便提一嘴的坑:Redisson kan门狗失效
借着这个机会,再分享一个hen多人用 Redisson 容易踩的坑。有些哥们喜欢在加锁的时候传 leaseTime
// 试图加锁,等待3秒,锁定10秒后自动释放
lock.tryLock;
一旦你显式传入了 leaseTime,Redisson 的 WatchDog机制就会失效!
正确Zuo法是Ru果不知道业务具体执行多久,千万别传 leaseTime
// 只传等待时间,不传 leaseTime,kan门狗机制生效,会自动帮你续期
lock.tryLock;
Ru果你的业务逻辑执行时间超过了你设置的 10 秒,锁会自动释放,其他线程就会趁虚而入。这时候,并发冲突依然会发生,而且geng难排查,因为锁是“正常”过期的。
分布式锁与并发控制的geng多思考其实解决分布式开发中的抢锁并发现象,除了 Redis 锁,还有hen多其他的路子。比如 MVCC 方案、单独的去重表、插入数据的唯一索引约束、状态机幂等性设计等等。
为了保证数据的Zui终一致性,我们需要hen多的技术方案来支持,比如分布式事务、分布式锁等。
乐观锁Ke以多线程同时读取数据,若出现冲突,也Ke以依赖上层逻辑修改,Neng够保证高并发下的读取,适用于读取频率hen高而修改频率较少的场景。本文详细介绍了分布式锁的三种实现方式:数据库锁、基于 Redis 的分布式锁以及基于 Zookeeper 的分布式锁。
至于 Zookeeper 分布式锁,它利用 ZK 的 ZAB 分布式一致性协议。Ru果 ZK 的主节点挂了协议Neng保证一定是数据同步完成的节点被选举为主节点,所以就不会发生分布式锁的失效问题。ZK Zuo分布式协调比较流程,大数据应用里面 Hadoop、Storm dou是基于 ZK 去Zuo分布式协调。
今天的这次复盘,希望Neng帮大家少掉几根头发。平时的业务开发中,@Transactional 和各种锁一起用的时候,一定要多留个心眼,画一画它们的作用域边界。
问题源于分布式锁可Neng失效,导致数据被并发处理。设计目标本是在定时扫描执行的代码中增加分布式锁,获取锁之后将查询到的数据中间表数据的状态由 1 改成 0,标识Yi经在处理了另一台服务器就不会再去扫描了。但Ru果锁释放早了一切努力dou白费。
Java Redis 分布式锁是指使用 Redis 实现的分布式锁机制,旨在解决分布式系统中的并发问题。但是随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的 Java API 并不Neng提供分布式Neng力。
希望我这次的血泪教训,Neng成为大家避坑的指南。大家在线上还踩过什么离谱的并发坑?欢迎评论区交流一波~
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback