深度实战:多线程在支付场景中的实际应用与避坑指南

8 阅读4分钟

深度实战:多线程在支付场景中的实际应用与避坑指南

支付系统是对并发安全要求最高的业务场景之一:用户同时发起支付、多线程处理回调、批量对账并发拉取渠道数据……任何一处线程安全问题都可能造成资金损失或死锁。 本文结合生产实践,系统梳理多线程在支付场景中的 5 大核心应用,并给出对应的实现方案与踩坑经验。


一、支付场景对多线程的典型诉求

场景1: 用户并发下单 → 同一账户扣款,需要线程安全的余额操作
场景2: 第三方回调并发到达 → 同一订单被多线程同时处理
场景3: 批量对账 → 上万条流水需要并发拉取渠道数据
场景4: 支付超时检查 → 定时任务并发扫描待支付订单
场景5: 异步通知业务系统 → 支付成功后并发通知多个下游

这五个场景,每一个都有独特的并发挑战,不能用同一个方案解决。


二、账户扣款:乐观锁 + CAS 保障余额一致性

2.1 问题复现

// 危险写法:读-改-写不是原子操作
public boolean deduct(Long accountId, BigDecimal amount) {
    Account account = accountRepository.findById(accountId);
    if (account.getBalance().compareTo(amount) < 0) {
        return false;  // 余额不足
    }
    // 两线程都通过了余额检查,都执行到这里 → 超扣!
    account.setBalance(account.getBalance().subtract(amount));
    accountRepository.save(account);
    return true;
}

并发场景下,两个线程同时读到余额 100 元,同时扣 80 元,结果账户变成 20 元(实际应该变成 -60 元并拒绝第二笔)。

2.2 数据库乐观锁方案

-- 账户表加版本号字段
ALTER TABLE account ADD COLUMN version BIGINT NOT NULL DEFAULT 0;
@Service
public class AccountService {

    @Autowired
    private AccountRepository accountRepository;

    /**
     * 扣款:乐观锁保障原子性
     * 底层执行: UPDATE account SET balance=?, version=version+1
     *           WHERE id=? AND version=? AND balance>=?
     */
    @Transactional
    public boolean deductWithOptimisticLock(Long accountId, BigDecimal amount) {
        int maxRetry = 3;
        for (int i = 0; i < maxRetry; i++) {
            Account account = accountRepository.findById(accountId)
                .orElseThrow(() -> new AccountNotFoundException(accountId));

            if (account.getBalance().compareTo(amount) < 0) {
                throw new InsufficientBalanceException("余额不足");
            }

            // 乐观锁更新:只有 version 匹配时才更新成功
            int rows = accountRepository.deductBalance(
                accountId,
                amount,
                account.getVersion()  // 携带版本号
            );

            if (rows == 1) {
                log.info("[扣款成功] accountId={}, amount={}", accountId, amount);
                return true;
            }

            // 版本冲突:被其他线程抢先修改,重试
            log.warn("[版本冲突] 第{}次重试, accountId={}", i + 1, accountId);
        }

        throw new ConcurrentModificationException("扣款并发冲突,请稍后重试");
    }
}
// Repository 层
@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {

    @Modifying
    @Query("""
        UPDATE Account a SET a.balance = a.balance - :amount, a.version = a.version + 1
        WHERE a.id = :accountId AND a.version = :version AND a.balance >= :amount
        """)
    int deductBalance(@Param("accountId") Long accountId,
                      @Param("amount") BigDecimal amount,
                      @Param("version") Long version);
}

2.3 Redis 原子扣款方案(高并发场景)

当 TPS 极高时(秒杀支付、大促),数据库乐观锁重试会造成大量 DB 压力,改用 Redis Lua 脚本实现原子扣款:

-- deduct_balance.lua
-- KEYS[1] = account:{accountId}:balance
-- ARGV[1] = 扣款金额(分)
local balance = tonumber(redis.call('GET', KEYS[1]))
local amount  = tonumber(ARGV[1])

if balance == nil then
    return -2  -- 账户不存在
end

if balance < amount then
    return -1  -- 余额不足
end

redis.call('DECRBY', KEYS[1], amount)
return balance - amount  -- 返回扣款后余额
@Service
public class RedisAccountService {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    private static final DefaultRedisScript<Long> DEDUCT_SCRIPT;

    static {
        DEDUCT_SCRIPT = new DefaultRedisScript<>();
        DEDUCT_SCRIPT.setScriptSource(new ClassPathResource("lua/deduct_balance.lua"));
        DEDUCT_SCRIPT.setResultType(Long.class);
    }

    /**
     * Lua 脚本原子扣款(单位:分)
     */
    public long deduct(Long accountId, long amountCents) {
        String key = "account:" + accountId + ":balance";
        Long result = redisTemplate.execute(
            DEDUCT_SCRIPT,
            Collections.singletonList(key),
            String.valueOf(amountCents)
        );

        if (result == null || result == -2) {
            throw new AccountNotFoundException("账户不存在");
        }
        if (result == -1) {
            throw new InsufficientBalanceException("余额不足");
        }

        // 异步同步到数据库(最终一致)
        asyncSyncToDB(accountId, amountCents);

        return result;  // 返回扣款后余额(分)
    }
}

三、回调幂等:ReentrantLock + 本地锁防并发处理

3.1 问题场景

第三方支付平台(如微信、支付宝)的回调通知可能在短时间内并发多次到达(网络重试机制)。如果处理逻辑包含账户入账,并发处理会导致重复入账。

时间线:
T1: 微信回调#1 到达 → 线程A 开始处理,查询订单状态=PENDING
T2: 微信回调#2 到达 → 线程B 开始处理,查询订单状态=PENDINGA还没更新)
T3: 线程A 完成入账,更新状态=PAID
T4: 线程B 完成入账,更新状态=PAID(重复入账!)

3.2 分布式锁方案(Redisson)

@Service
@Slf4j
public class PayCallbackService {

    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private PayOrderRepository orderRepository;
    @Autowired
    private AccountService accountService;

    /**
     * 处理支付回调(防并发重复处理)
     */
    public void handleCallback(String outTradeNo, CallbackResult callback) {
        // 同一订单加分布式锁,防止并发处理
        String lockKey = "pay:callback:lock:" + outTradeNo;
        RLock lock = redissonClient.getLock(lockKey);

        boolean acquired = false;
        try {
            // 尝试获取锁,最多等待3秒,锁自动释放时间30秒
            acquired = lock.tryLock(3, 30, TimeUnit.SECONDS);
            if (!acquired) {
                log.warn("[回调] 获取锁超时,可能有并发处理, outTradeNo={}", outTradeNo);
                return;
            }

            // 加锁后再次检查状态(双重检测)
            PayOrder order = orderRepository.findByOutTradeNo(outTradeNo);
            if (order.getStatus() == PayStatus.PAID) {
                log.info("[回调] 订单已处理,幂等跳过, outTradeNo={}", outTradeNo);
                return;
            }

            // 执行业务逻辑
            processCallback(order, callback);

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.error("[回调] 锁获取被中断, outTradeNo={}", outTradeNo, e);
        } finally {
            if (acquired && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    private void processCallback(PayOrder order, CallbackResult callback) {
        // 状态机更新:PENDING → PAID
        orderRepository.updateStatusPaid(order.getId(), callback.getChannelTradeNo());
        // 账户入账
        accountService.credit(order.getAccountId(), order.getAmount());
        log.info("[回调] 支付成功处理完成, payNo={}", order.getPayNo());
    }
}

四、批量对账:线程池并发拉取,CountDownLatch 同步结果

4.1 场景描述

日终对账需要对接多个渠道(微信、支付宝、银联),每个渠道需要调用远程接口拉取对账文件,串行处理耗时太长(每个渠道约 5 秒,10 个渠道就是 50 秒)。

4.2 CompletableFuture 并发对账

@Service
public class ReconcileService {

    // 对账专用线程池(IO 密集型,线程数多一些)
    private final ExecutorService reconcileExecutor = new ThreadPoolExecutor(
        10,                           // 核心线程数
        20,                           // 最大线程数
        60L, TimeUnit.SECONDS,        // 空闲线程存活时间
        new LinkedBlockingQueue<>(50), // 任务队列
        new ThreadFactoryBuilder().setNameFormat("reconcile-%d").build(),
        new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略:调用者自己跑
    );

    @Autowired
    private List<ChannelAdapter> channelAdapters;

    /**
     * 多渠道并发对账
     */
    public ReconcileReport dailyReconcile(String date) {
        log.info("[对账] 开始并发对账, date={}, 渠道数={}", date, channelAdapters.size());
        long start = System.currentTimeMillis();

        // 并发提交各渠道对账任务
        List<CompletableFuture<ChannelReconcileResult>> futures = channelAdapters.stream()
            .map(adapter -> CompletableFuture
                .supplyAsync(() -> doChannelReconcile(adapter, date), reconcileExecutor)
                .exceptionally(ex -> {
                    log.error("[对账] 渠道{}对账失败", adapter.getChannel(), ex);
                    return ChannelReconcileResult.failure(adapter.getChannel(), ex.getMessage());
                })
            )
            .collect(Collectors.toList());

        // 等待所有渠道完成
        List<ChannelReconcileResult> results = futures.stream()
            .map(CompletableFuture::join)
            .collect(Collectors.toList());

        long elapsed = System.currentTimeMillis() - start;
        log.info("[对账] 并发对账完成, 耗时={}ms", elapsed);

        return buildReport(results);
    }

    private ChannelReconcileResult doChannelReconcile(ChannelAdapter adapter, String date) {
        log.info("[对账] 渠道={}, 线程={}", adapter.getChannel(), Thread.currentThread().getName());
        try {
            // 1. 拉取渠道对账文件
            List<ReconcileItem> channelItems = adapter.fetchReconcileData(date);

            // 2. 查本地流水
            List<PayOrder> localOrders = orderRepository.findByDateAndChannel(date, adapter.getChannel());

            // 3. 核对差异
            List<ReconcileDiff> diffs = compare(channelItems, localOrders);

            return ChannelReconcileResult.success(adapter.getChannel(), channelItems.size(), diffs);
        } catch (Exception e) {
            return ChannelReconcileResult.failure(adapter.getChannel(), e.getMessage());
        }
    }
}

4.3 线程池配置要点

/**
 * 支付系统线程池配置(不同场景隔离,防止互相影响)
 */
@Configuration
public class PayThreadPoolConfig {

    /**
     * 对账线程池(IO 密集型)
     * 线程数 = CPU核心数 * (1 + 等待时间/计算时间),IO 密集设置 2N
     */
    @Bean("reconcileExecutor")
    public ExecutorService reconcileExecutor() {
        int cores = Runtime.getRuntime().availableProcessors();
        return new ThreadPoolExecutor(
            cores * 2,
            cores * 4,
            60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(200),
            new ThreadFactoryBuilder().setNameFormat("reconcile-%d").build(),
            new ThreadPoolExecutor.CallerRunsPolicy()
        );
    }

    /**
     * 支付通知线程池(CPU 密集型)
     */
    @Bean("notifyExecutor")
    public ExecutorService notifyExecutor() {
        int cores = Runtime.getRuntime().availableProcessors();
        return new ThreadPoolExecutor(
            cores + 1,
            cores * 2,
            30L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(100),
            new ThreadFactoryBuilder().setNameFormat("pay-notify-%d").build(),
            new ThreadPoolExecutor.AbortPolicy()
        );
    }
}

五、异步通知下游:线程池 + 重试机制

5.1 场景描述

支付成功后,可能需要通知多个下游系统(订单系统、会员积分系统、库存系统、营销系统)。如果串行通知,任何一个下游慢都会阻塞主线程。

5.2 并发通知 + 失败重试

@Service
public class PayNotifyService {

    @Autowired
    @Qualifier("notifyExecutor")
    private ExecutorService notifyExecutor;

    @Autowired
    private List<PaySuccessHandler> handlers;  // 所有下游处理器

    /**
     * 支付成功后并发通知所有下游
     */
    public void notifyAll(PaySuccessEvent event) {
        List<CompletableFuture<Void>> futures = handlers.stream()
            .map(handler -> CompletableFuture
                .runAsync(() -> notifyWithRetry(handler, event), notifyExecutor)
            )
            .collect(Collectors.toList());

        // 不等待结果(全异步),主线程直接返回
        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
            .exceptionally(ex -> {
                log.error("[支付通知] 部分下游通知失败", ex);
                return null;
            });
    }

    /**
     * 单个下游通知(带重试)
     */
    private void notifyWithRetry(PaySuccessHandler handler, PaySuccessEvent event) {
        int maxRetry = 3;
        long[] delays = {1000L, 3000L, 10000L};  // 退避间隔:1s, 3s, 10s

        for (int i = 0; i < maxRetry; i++) {
            try {
                handler.onPaySuccess(event);
                log.info("[支付通知] 成功, handler={}, payNo={}", handler.getClass().getSimpleName(), event.getPayNo());
                return;
            } catch (Exception e) {
                log.warn("[支付通知] 第{}次失败, handler={}, payNo={}, error={}",
                    i + 1, handler.getClass().getSimpleName(), event.getPayNo(), e.getMessage());

                if (i < maxRetry - 1) {
                    try {
                        Thread.sleep(delays[i]);
                    } catch (InterruptedException ie) {
                        Thread.currentThread().interrupt();
                        return;
                    }
                }
            }
        }

        // 三次重试全失败,发告警 + 写补偿表
        alertService.sendAlert("支付通知失败,需人工处理: " + event.getPayNo());
        compensateService.saveForManual(handler.getClass().getSimpleName(), event);
    }
}

六、定时超时检查:分片并行扫描

6.1 问题场景

超时关单通常用定时任务扫描待支付订单,当数据量大时(每次扫描上万条),单线程扫描耗时过长,还可能出现任务重叠执行(上一次还没跑完,下一次就开始了)。

6.2 分片 + 并行扫描

@Component
@Slf4j
public class OrderTimeoutJob {

    @Autowired
    private PayOrderRepository orderRepository;
    @Autowired
    private OrderService orderService;

    // 分片线程池
    private final ExecutorService scanExecutor = Executors.newFixedThreadPool(4,
        new ThreadFactoryBuilder().setNameFormat("timeout-scan-%d").build());

    /**
     * 每分钟执行一次,使用 @ScheduledLock 防止集群重复执行
     */
    @Scheduled(fixedDelay = 60_000)
    @SchedulerLock(name = "orderTimeoutJob", lockAtLeastFor = "PT55S", lockAtMostFor = "PT5M")
    public void checkTimeout() {
        LocalDateTime expireTime = LocalDateTime.now().minusMinutes(30);

        // 分4片并行扫描(按 id mod 4 分片)
        int shards = 4;
        List<CompletableFuture<Integer>> futures = IntStream.range(0, shards)
            .mapToObj(shard -> CompletableFuture.supplyAsync(
                () -> processShard(shard, shards, expireTime),
                scanExecutor
            ))
            .collect(Collectors.toList());

        int totalClosed = futures.stream()
            .mapToInt(f -> {
                try { return f.get(4, TimeUnit.MINUTES); }
                catch (Exception e) { return 0; }
            })
            .sum();

        log.info("[超时关单] 本次关单总数={}", totalClosed);
    }

    private int processShard(int shard, int totalShards, LocalDateTime expireTime) {
        List<PayOrder> orders = orderRepository.findTimeoutOrdersByShard(
            expireTime, shard, totalShards, 500  // 每片最多处理500条
        );

        int closed = 0;
        for (PayOrder order : orders) {
            try {
                orderService.closeOrder(order.getId(), CloseReason.TIMEOUT);
                closed++;
            } catch (Exception e) {
                log.warn("[超时关单] 关单失败, orderId={}", order.getId(), e);
            }
        }
        log.info("[超时关单] shard={}/{}, 关单={}", shard, totalShards, closed);
        return closed;
    }
}
-- 分片查询(按 id mod 取余)
SELECT * FROM pay_order
WHERE status = 'PENDING'
  AND create_time < :expireTime
  AND id % :totalShards = :shard
LIMIT :limit
FOR UPDATE SKIP LOCKED  -- 跳过已被其他线程锁定的行,防止并发重复处理

七、核心避坑总结

场景错误做法正确做法风险等级
账户扣款读-改-写不加锁乐观锁/Redis Lua⭐⭐⭐⭐⭐
回调处理直接处理不检查分布式锁 + 双重检测⭐⭐⭐⭐⭐
线程池直接 Executors.newFixedThreadPool自定义 ThreadPoolExecutor⭐⭐⭐⭐
异步通知同步串行通知下游CompletableFuture 并发 + 退避重试⭐⭐⭐
定时扫描单线程全量扫描分片并行 + SKIP LOCKED⭐⭐⭐
线程隔离所有场景共用线程池按业务场景独立线程池⭐⭐⭐⭐

记忆口诀

扣款用乐观锁,回调加分布锁;
对账并发跑,通知异步绕;
线程池要隔离,超时分片扫。

八、总结

支付场景中多线程的应用,本质是在吞吐量数据安全之间找到平衡:

  • 扣款:用锁保原子性,乐观锁优先,高并发转 Redis Lua
  • 回调:用分布式锁防并发,状态机 + 双重检测保幂等
  • 对账:用 CompletableFuture 并发,线程池 IO 密集型配置
  • 通知:全异步不阻塞,退避重试兜底,人工补偿收尾
  • 定时任务:分片并行 + SKIP LOCKED,防止集群重复处理

线程安全问题在测试环境几乎不会暴露,只有上生产、遇到并发才会炸。
养成在设计阶段就考虑并发模型的习惯,比出了事再排查要省力得多。