拼多多返利app分布式锁设计:解决高并发下的佣金超发问题

拼多多返利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开发者团队,转载请注明出处!

相关推荐
程序员泠零澪回家种桔子3 小时前
分布式事务核心解析与实战方案
分布式
凯子坚持 c3 小时前
CANN 生态中的分布式训练利器:深入 `collective-ops` 项目实现高效多卡协同
分布式
惊讶的猫4 小时前
rabbitmq实践小案例
分布式·rabbitmq
禁默5 小时前
打破集群通信“内存墙”:手把手教你用 CANN SHMEM 重构 AIGC 分布式算子
分布式·重构·aigc
惊讶的猫7 小时前
rabbitmq初步介绍
分布式·rabbitmq
小镇敲码人7 小时前
华为CANN框架中HCCL仓库的全面解析:分布式通信的引擎
分布式·华为
User_芊芊君子8 小时前
【分布式训练】CANN SHMEM跨设备内存通信库:构建高效多机多卡训练的关键组件
分布式·深度学习·神经网络·wpf
酷酷的崽7988 小时前
CANN 开源生态解析(四):`cann-dist-train` —— 构建高效可扩展的分布式训练引擎
分布式·开源
惊讶的猫9 小时前
AMQP 与 RabbitMQ 四大模型
分布式·rabbitmq
灰子学技术9 小时前
istio从0到1:如何解决分布式配置同步问题
分布式·云原生·istio