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

相关推荐
Wang's Blog3 小时前
Kafka: 分布式配置动态更新之微服务总线实现
分布式·微服务·kafka
老马聊技术3 小时前
HBase完全分布式集群搭建详细教程
数据库·分布式·hbase
wtrees_松阳3 小时前
分布式锁实战指南:Redis、ZooKeeper、etcd 三大方案深度对比与避坑指南(附代码)
redis·分布式·zookeeper
Slow菜鸟3 小时前
Java基础架构设计(四)| 通用响应与异常处理(单体/分布式通用增强方案)
java·开发语言·分布式
..空空的人3 小时前
C++基于protobuf实现仿RabbitMQ消息队列---服务器模块认识1
服务器·开发语言·c++·分布式·rabbitmq·protobuf
yumgpkpm3 小时前
Hadoop如何用Flink支持实时数据分析需求
大数据·hadoop·分布式·hdfs·flink·kafka·cloudera
武子康3 小时前
Java-208 RabbitMQ Topic 主题交换器详解:routingKey/bindingKey 通配符与 Java 示例
java·分布式·性能优化·消息队列·系统架构·rabbitmq·java-rabbitmq
..空空的人1 天前
C++基于protobuf实现仿RabbitMQ消息队列---项目设计
分布式·rabbitmq
毕设源码-赖学姐1 天前
【开题答辩全过程】以 基于Spark的电商用户行为分析系统为例,包含答辩的问题和答案
大数据·分布式·spark