1. Spring事务与分布式锁的本质区别
在Java企业级开发中,Spring事务和分布式锁是解决并发问题的两种不同武器。很多开发者容易混淆二者的适用场景,我曾经在一个电商库存扣减系统中同时错误使用了两者,导致性能下降了60%。让我们先解剖它们的本质差异:
Spring事务的核心是保证数据库操作的ACID特性,它通过@Transactional注解在单个数据库连接内实现:
- 原子性(Atomicity):要么全部成功,要么全部回滚
- 一致性(Consistency):数据从一个有效状态变为另一个有效状态
- 隔离性(Isolation):事务间的可见性控制(通过隔离级别实现)
- 持久性(Durability):提交后的修改永久保存
而分布式锁解决的是跨进程/跨服务的互斥访问问题,典型场景如:
- 防止重复订单提交
- 秒杀系统中的库存超卖
- 定时任务的集群防重执行
关键区别:事务针对数据一致性,锁针对资源独占。事务像合同公证员,锁像洗手间门锁。
2. Spring事务的实战陷阱与解决方案
2.1 事务失效的七大经典场景
在我参与的金融项目中,曾因事务失效导致资金流水对账不平。以下是血泪总结的失效场景:
- 自调用问题(最常见坑点)
java复制public class OrderService {
public void createOrder() {
this.updateStock(); // 事务失效!
}
@Transactional
public void updateStock() {
// 库存操作
}
}
解决方法:通过AopContext.currentProxy()获取代理对象调用
- 异常类型不匹配
java复制@Transactional(rollbackFor = Exception.class) // 必须指定
public void method() throws IOException {
// 默认只回滚RuntimeException
}
-
数据库引擎不支持:MyISAM引擎不支持事务
-
传播行为误用:PROPAGATION_NOT_SUPPORTED会挂起当前事务
-
多数据源未配置事务管理器
-
异步方法调用:@Async和@Transactional混用
-
私有方法注解:@Transactional在private方法上无效
2.2 隔离级别实战选择建议
不同业务场景需要不同的隔离级别配置:
- 读未提交(READ_UNCOMMITTED):几乎不用,存在脏读
- 读已提交(READ_COMMITTED):Oracle默认,适合多数OLTP系统
- 可重复读(REPEATABLE_READ):MySQL默认,存在幻读
- 串行化(SERIALIZABLE):完全隔离,性能最差
金融支付系统推荐配置:
java复制@Transactional(isolation = Isolation.REPEATABLE_READ,
timeout = 30,
rollbackFor = Exception.class)
3. 分布式锁的三种实现方式对比
3.1 Redis分布式锁深度解析
基于Redis的SETNX命令是实现分布式锁最常见的方式,但这里有三个进阶知识点:
- 锁续期机制(关键!)
java复制// Redisson客户端示例
RLock lock = redisson.getLock("orderLock");
try {
// 自动续期,默认30秒过期
lock.lock();
// 业务逻辑
} finally {
lock.unlock();
}
-
红锁算法(RedLock):需要至少3个独立的Redis主节点,防止单点故障
-
锁粒度控制:商品ID级别的锁比全局锁并发度高100倍
3.2 Zookeeper实现方案
Zookeeper通过临时顺序节点实现更可靠的锁:
java复制public boolean tryLock() {
// 创建临时顺序节点
String node = zk.create("/lock/order_",
new byte[0],
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
// 判断是否是最小序号节点
List<String> children = zk.getChildren("/lock", false);
Collections.sort(children);
return node.equals("/lock/" + children.get(0));
}
优势:天然具备锁释放机制(会话断开自动删除节点)
3.3 数据库悲观锁方案
在极端情况下可考虑SELECT...FOR UPDATE:
sql复制BEGIN;
SELECT * FROM inventory WHERE product_id=1001 FOR UPDATE;
UPDATE inventory SET stock=stock-1 WHERE product_id=1001;
COMMIT;
性能警告:QPS超过500时慎用,会导致连接池耗尽
4. 混合使用时的死亡陷阱
在微服务架构下,我曾遇到一个经典死锁场景:
- 服务A先获取分布式锁X
- 然后在事务中操作数据库表T
- 同时服务B的事务正在操作表T
- 服务B在事务中尝试获取锁X
解决方案:遵守"锁粒度由大到小"原则
- 先获取分布式锁(粗粒度)
- 再开启数据库事务(细粒度)
- 最后操作具体数据
正确代码结构:
java复制// 先获取分布式锁
RLock lock = redisson.getLock("resource_lock");
lock.lock();
try {
// 再开启事务
transactionTemplate.execute(status -> {
// 数据库操作
return Boolean.TRUE;
});
} finally {
lock.unlock();
}
5. 性能优化实战技巧
5.1 事务优化三板斧
- 缩短事务生命周期:不要在事务中包含RPC调用和IO操作
- 合理设置超时:@Transactional(timeout=5) 防止长时间占用连接
- 按需选择传播行为:默认REQUIRED适合多数场景
5.2 分布式锁优化方案
- 分段锁提升并发度:将商品库存拆分为10个段,锁粒度从1000qps提升到10000qps
java复制// 原始锁
String lockKey = "product_1001";
// 分段锁(假设分成10段)
int segment = productId.hashCode() % 10;
String lockKey = "product_1001_segment_" + segment;
- 锁等待优化:使用tryLock代替lock,设置合理等待时间
java复制if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
// 业务逻辑
} finally {
lock.unlock();
}
} else {
throw new BusyException("系统繁忙,请重试");
}
- 锁降级模式:写锁降级为读锁,提升并发读取能力
6. 监控与问题排查
6.1 事务监控方案
- Spring Actuator端点:
code复制/actuator/transactions
- Druid监控:配置Filter统计事务执行时间
properties复制spring.datasource.druid.filter.stat.enabled=true
spring.datasource.druid.filter.stat.log-slow-sql=true
spring.datasource.druid.filter.stat.slow-sql-millis=200
- 自定义事务拦截器:
java复制@Aspect
@Component
public class TransactionMonitorAspect {
@Around("@annotation(transactional)")
public Object monitor(ProceedingJoinPoint pjp, Transactional transactional) {
long start = System.currentTimeMillis();
try {
return pjp.proceed();
} finally {
long cost = System.currentTimeMillis() - start;
Metrics.timer("transaction.cost").record(cost);
}
}
}
6.2 分布式锁监控
Redis锁监控关键指标:
- 锁等待时间分布
- 锁持有时间超过阈值告警
- 死锁检测(通过锁TTL和线程堆栈分析)
推荐使用Redisson的监控功能:
java复制RedissonClient redisson = Redisson.create();
RBatch batch = redisson.createBatch();
batch.getLock("lock1").lockAsync();
batch.getLock("lock2").lockAsync();
BatchResult<?> result = batch.execute();
7. 真实案例:秒杀系统优化实践
去年优化某电商秒杀系统时,我们经历了三个阶段:
第一阶段:纯数据库事务
java复制@Transactional
public void seckill(Long productId) {
Product product = productMapper.selectById(productId);
if (product.getStock() > 0) {
product.setStock(product.getStock() - 1);
productMapper.updateById(product);
}
}
结果:100并发时TPS只有23,数据库CPU飙升至90%
第二阶段:增加Redis分布式锁
java复制public void seckill(Long productId) {
String lockKey = "seckill:" + productId;
RLock lock = redisson.getLock(lockKey);
try {
lock.lock();
Product product = productMapper.selectById(productId);
if (product.getStock() > 0) {
product.setStock(product.getStock() - 1);
productMapper.updateById(product);
}
} finally {
lock.unlock();
}
}
结果:TPS提升到120,但锁竞争成为新瓶颈
第三阶段:Redis预减库存+异步落库
- 用Redis原子操作预减库存
lua复制local stock = redis.call('get', KEYS[1])
if tonumber(stock) > 0 then
redis.call('decr', KEYS[1])
return 1
end
return 0
- 通过消息队列异步创建订单
- 最终TPS达到2100,数据库负载低于30%
