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

相关推荐
【D'accumulation】18 分钟前
Kafka地址映射不通(很常见的问题)
分布式·kafka
数翊科技6 小时前
深度解析 HexaDB分布式 DDL 的全局一致性
分布式
Tony Bai10 小时前
【分布式系统】03 复制(上):“权威中心”的秩序 —— 主从架构、一致性与权衡
大数据·数据库·分布式·架构
txinyu的博客17 小时前
HTTP服务实现用户级窗口限流
开发语言·c++·分布式·网络协议·http
独自破碎E17 小时前
RabbitMQ中的Prefetch参数
分布式·rabbitmq
深蓝电商API18 小时前
Scrapy+Rredis实现分布式爬虫入门与优化
分布式·爬虫·scrapy
回家路上绕了弯19 小时前
定期归档历史数据实战指南:从方案设计到落地优化
分布式·后端
rchmin21 小时前
Distro与Raft协议对比分析
分布式·cap
小辉笔记21 小时前
kafka原理总结
分布式·kafka
实战项目21 小时前
分布式协作入侵检测系统的报警信息管理
分布式