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

相关推荐
此时不提桶,更待何时4 小时前
06-12-A-Kafka存储深水区与源码解析详解
分布式·kafka·linq
此时不提桶,更待何时7 小时前
06-15-A-Kafka生态集成与流处理详解
分布式·kafka
此时不提桶,更待何时11 小时前
06-13-A-Kafka客户端与协议深入详解
分布式·kafka
程序猿乐锅18 小时前
从0-1一文详解RabbitMQ
java·分布式·后端·中间件·rabbitmq·ruby
老陈说编程2 天前
1. 鸿蒙 (HarmonyOS) 2012 至 2026 年的发展历程
分布式·华为·个人开发·harmonyos·鸿蒙·鸿蒙系统·程序员创富
ly76893 天前
Redis 分布式锁的边界条件:Redlock 争议、锁续期与客户端崩溃后的互斥失效
数据库·redis·分布式·分布式锁·watchdog·redlock
灯澜忆梦3 天前
【minio】#5 | MinIO 分布式部署 + HTTPS 部署
分布式·网络协议·https·对象存储·minio
谢亮_vipxieliang4 天前
Spring Cloud 服务治理入门:注册发现、配置中心、网关限流、熔断降级与分布式一致性方案
分布式·spring·spring cloud
樱花落木兰4 天前
分布式登录实战:Session 会话共享改造,Redis 存储用户登录状态
java·javascript·数据库·redis·分布式·缓存