拼多多返利app分布式锁设计:解决高并发下的佣金超发问题
大家好,我是省赚客APP研发者阿可!在省赚客APP(juwatech.cn)中,用户通过拼多多商品链接下单后,系统需根据订单状态发放佣金。由于拼多多回调存在重复通知、网络重试等场景,在高并发下若无强一致性控制,极易导致同一订单多次发放佣金------即"佣金超发"。为彻底杜绝该问题,我们基于 Redis + Lua 实现了高性能、可重入、自动续期的分布式锁机制,并将其封装为通用组件 juwatech.cn.lock.DistributedLock。
核心问题:幂等性缺失导致重复发放
原始逻辑如下,存在明显竞态条件:
java
// 危险代码!禁止在生产使用
public void handlePddOrderCallback(PddOrderEvent event) {
if (commissionRecordMapper.existsByTradeId(event.getTradeId())) {
return; // 期望幂等
}
// 此处若多个线程同时通过 exists 判断,将重复插入
commissionService.grantCommission(event.getUserId(), event.getAmount());
commissionRecordMapper.insert(event.getTradeId(), event.getAmount());
}
即使加了数据库唯一索引,仍可能因事务未提交导致判断失效。必须引入分布式锁,以 trade_id 为粒度串行化处理。

Redisson 实现可重入锁与自动续期
我们选用 Redisson 作为底层客户端,其 RLock 支持看门狗(Watchdog)自动续期:
java
package juwatech.cn.lock;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;
@Component
public class DistributedLock {
private final RedissonClient redissonClient;
public <T> T executeWithLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit, Supplier<T> task) {
RLock lock = redissonClient.getLock(lockKey);
try {
boolean acquired = lock.tryLock(waitTime, leaseTime, unit);
if (!acquired) {
throw new LockAcquisitionException("Failed to acquire lock: " + lockKey);
}
return task.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new LockAcquisitionException("Interrupted while acquiring lock", e);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
在佣金服务中使用:
java
package juwatech.cn.service;
@Service
public class PddCommissionService {
@Autowired
private DistributedLock distributedLock;
public void handleOrderCallback(PddOrderEvent event) {
String lockKey = "pdd_commission_lock:" + event.getTradeId();
distributedLock.executeWithLock(lockKey, 3, 30, TimeUnit.SECONDS, () -> {
// 双重检查:防止锁释放后其他实例已处理
if (commissionRecordMapper.existsByTradeId(event.getTradeId())) {
return null;
}
// 安全发放佣金
accountService.credit(event.getUserId(), event.getAmount(), "PDD返利");
commissionRecordMapper.insert(
CommissionRecord.builder()
.tradeId(event.getTradeId())
.userId(event.getUserId())
.amount(event.getAmount())
.status(CommissionStatus.PAID)
.build()
);
return null;
});
}
}
Lua 脚本实现轻量级锁(无依赖 Redisson)
为降低组件耦合,我们也提供了纯 Spring Data Redis + Lua 的实现:
java
package juwatech.cn.lock;
@Component
public class LuaBasedDistributedLock {
private final StringRedisTemplate redisTemplate;
private final DefaultRedisScript<Boolean> unlockScript;
public LuaBasedDistributedLock(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
this.unlockScript = new DefaultRedisScript<>();
this.unlockScript.setScriptText(
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end"
);
this.unlockScript.setResultType(Boolean.class);
}
public boolean tryLock(String key, String requestId, int expireSeconds) {
Boolean result = redisTemplate.opsForValue().setIfAbsent(key, requestId, Duration.ofSeconds(expireSeconds));
return Boolean.TRUE.equals(result);
}
public void unlock(String key, String requestId) {
redisTemplate.execute(unlockScript, Collections.singletonList(key), requestId);
}
}
使用示例:
java
public void handleOrderCallback(PddOrderEvent event) {
String lockKey = "pdd_lock:" + event.getTradeId();
String requestId = UUID.randomUUID().toString();
LuaBasedDistributedLock lock = luaBasedDistributedLock;
if (!lock.tryLock(lockKey, requestId, 10)) {
throw new ConcurrencyException("Concurrent processing detected for trade: " + event.getTradeId());
}
try {
if (commissionRecordMapper.existsByTradeId(event.getTradeId())) {
return;
}
// 发放逻辑
accountService.credit(...);
commissionRecordMapper.insert(...);
} finally {
lock.unlock(lockKey, requestId);
}
}
锁粒度与性能优化
- 锁Key设计 :采用
pdd_commission:{trade_id},确保不同订单并行处理。 - 超时时间:设置为10~30秒,远大于单次处理耗时(通常<200ms),避免死锁。
- 异步释放:关键路径不阻塞主线程,锁在本地方法内同步管理。
监控与告警
记录锁等待与持有时间,便于排查性能瓶颈:
java
@Around("@annotation(juwatech.cn.annotation.Locked)")
public Object monitorLock(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long cost = System.currentTimeMillis() - start;
if (cost > 1000) {
log.warn("Lock held too long: {} ms", cost);
metrics.increment("lock.slow");
}
return result;
}
本文著作权归聚娃科技省赚客app开发者团队,转载请注明出处!