1. 问题背景:并发场景下的数据风险
在电商、金融等系统中,诸如优惠券扣减、库存减少、余额变动等操作常面临高并发挑战。以优惠券退还场景为例,若同一用户在退款时触发多次请求,或不同用户同时争夺同一张优惠券,可能引发以下问题:
-
超退:同一张优惠券被多次恢复,导致平台损失。
-
状态不一致:优惠券状态与订单状态不匹配(如订单已退款但优惠券未恢复)。
-
脏数据:并发写操作覆盖彼此结果,导致最终数据错误。
本文将围绕如何保障并发场景下数据一致性展开,结合代码与架构设计,提供系统性解决方案。
2. 核心解决思路
保障数据一致性的核心在于控制临界资源的访问顺序。常见的实现方式可分为三类:
方案 | 原理 | 适用场景 |
---|---|---|
悲观锁 | 操作前先加锁(如SELECT FOR UPDATE) | 写冲突频繁、数据一致性要求高 |
乐观锁 | 通过版本号/条件判断避免冲突 | 读多写少、冲突概率低 |
分布式锁 | 利用外部系统(如Redis/ZooKeeper)协调锁 | 分布式系统、跨服务调用 |
3. 具体方案与代码实战
3.1 数据库悲观锁:行级锁确保串行化
原理:
在事务中通过SELECT ... FOR UPDATE
锁定目标数据行,确保同一时刻仅一个事务可修改该数据。
实现步骤:
-
开启事务,查询优惠券并加锁。
-
执行业务逻辑(如状态变更)。
-
提交事务,释放锁。
@Transactional
public void refundCouponWithPessimisticLock(Long couponId) {
// 1. 查询并锁定优惠券行
Coupon coupon = couponRepository.findCouponByIdForUpdate(couponId);
// 2. 校验状态
if (coupon.getStatus() != CouponStatus.USED) {
throw new BusinessException("优惠券未使用,无需退还");
}
// 3. 更新状态
coupon.setStatus(CouponStatus.UNUSED);
couponRepository.save(coupon);
}
优缺点:
-
✅ 强一致性,适合高并发写场景。
-
❌ 性能较低,可能引发死锁。
3.2 乐观锁:版本号控制
原理:
通过数据库版本号(version
字段)或业务字段(如状态)作为更新条件,若数据已被修改,则更新失败。
实现步骤:
-
查询当前优惠券版本号。
-
更新时携带版本号条件。
-
若更新影响行数为0,说明数据已被修改,重试或抛异常。
// 实体类添加版本字段
@Entity
public class Coupon {
@Id
private Long id;
@Version
private Integer version;
// 其他字段...
}
// 更新逻辑
public void refundCouponWithOptimisticLock(Long couponId) {
int maxRetries = 3;
for (int i = 0; i < maxRetries; i++) {
Coupon coupon = couponRepository.findById(couponId).orElseThrow();
if (coupon.getStatus() != CouponStatus.USED) {
throw new BusinessException("优惠券状态不合法");
}
coupon.setStatus(CouponStatus.UNUSED);
try {
couponRepository.save(coupon);
return; // 更新成功
} catch (OptimisticLockingFailureException e) {
// 版本冲突,重试
log.warn("乐观锁冲突,重试第{}次", i + 1);
}
}
throw new BusinessException("更新失败,请稍后重试");
}
优缺点:
-
✅ 无锁竞争,性能较高。
-
❌ 需处理重试逻辑,不适合高冲突场景。
3.3 分布式锁:Redis RedLock
原理:
利用Redis的原子操作(如SETNX)实现跨服务的互斥锁,确保分布式环境下仅一个节点可修改数据。
实现步骤:
-
尝试获取锁(Key为优惠券ID)。
-
执行业务逻辑。
-
释放锁。
public void refundCouponWithDistributedLock(Long couponId) {
String lockKey = "coupon_lock:" + couponId;
String requestId = UUID.randomUUID().toString();
try {
// 尝试加锁(设置超时防止死锁)
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, 30, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("系统繁忙,请稍后重试");
}
// 执行业务逻辑
Coupon coupon = couponRepository.findById(couponId).orElseThrow();
if (coupon.getStatus() != CouponStatus.USED) {
throw new BusinessException("优惠券状态不合法");
}
coupon.setStatus(CouponStatus.UNUSED);
couponRepository.save(coupon);
} finally {
// 释放锁(需判断requestId避免误删)
String currentRequestId = redisTemplate.opsForValue().get(lockKey);
if (requestId.equals(currentRequestId)) {
redisTemplate.delete(lockKey);
}
}
}
注意事项:
-
锁需设置超时时间,避免死锁。
-
释放锁时需校验请求ID,防止误删其他请求的锁。
3.4 异步队列:串行化处理
原理:
将并发请求写入消息队列(如Kafka、RabbitMQ),由消费者逐个处理,天然避免并发冲突。
架构设计:
-
用户发起退款请求,生成退款任务并投递到队列。
-
消费者顺序处理队列中的任务。
-
更新数据库状态。
// 生产者
public void submitRefundRequest(Long couponId) {
RefundTask task = new RefundTask(couponId);
kafkaTemplate.send("refund_queue", task);
}
// 消费者
@KafkaListener(topics = "refund_queue")
public void processRefundTask(RefundTask task) {
refundService.refundCoupon(task.getCouponId());
}
优缺点:
-
✅ 彻底消除并发,适合高吞吐场景。
-
❌ 系统复杂度增加,数据更新延迟。
4. 方案对比与选型建议
方案 | 一致性强度 | 性能 | 复杂度 | 适用场景 |
---|---|---|---|---|
悲观锁 | 强 | 低 | 低 | 单机事务、写冲突频繁 |
乐观锁 | 中 | 高 | 中 | 读多写少、冲突概率低 |
分布式锁 | 强 | 中 | 高 | 分布式系统、跨服务调用 |
异步队列 | 强 | 高 | 高 | 高吞吐、允许最终一致性 |
选型建议:
-
单体应用:优先选择乐观锁或悲观锁。
-
分布式系统:使用分布式锁或异步队列。
-
超高并发(如秒杀):结合Redis+Lua脚本实现原子操作。
5. 高级优化:Redis+Lua原子脚本
原理:
通过Lua脚本在Redis端原子化执行“查询-判断-更新”操作,彻底避免并发问题。
-- Lua脚本:退还优惠券
local key = KEYS[1]
local currentStatus = redis.call('HGET', key, 'status')
if currentStatus == '1' then
redis.call('HSET', key, 'status', '0')
return 1 -- 成功
else
return 0 -- 失败
end
Java调用:
public boolean refundWithLuaScript(Long couponId) {
String script = "上述Lua脚本内容";
RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long result = redisTemplate.execute(redisScript, Collections.singletonList("coupon:" + couponId));
return result == 1;
}
优势:
-
原子性:操作在Redis单线程中执行,无需额外锁。
-
高性能:无需与数据库交互,适合缓存热点数据。
6. 总结与最佳实践
-
评估冲突概率:低冲突选乐观锁,高冲突选悲观锁或分布式锁。
-
兜底方案:记录操作日志,定期对账修复不一致数据。
-
监控告警:跟踪锁等待时间、重试次数等指标,及时扩容或优化。
-
测试验证:通过Jmeter模拟并发场景,验证方案有效性。
通过合理选择并发控制策略,可有效保障数据一致性,为高并发系统提供稳定基石。