Commit dbb674b24f34ef37c0911f5b20d635696d4723be

Authored by YunaiV
1 parent 9b5d3f01

【功能优化】支付:钱包余额更新时,加锁避免并发更新,导致流水不连续的问题

yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/RedisKeyConstants.java
... ... @@ -17,6 +17,15 @@ public interface RedisKeyConstants {
17 17 String PAY_NOTIFY_LOCK = "pay_notify:lock:%d";
18 18  
19 19 /**
  20 + * 支付钱包的分布式锁
  21 + *
  22 + * KEY 格式:pay_wallet:lock:%d
  23 + * VALUE 数据格式:HASH // RLock.class:Redisson 的 Lock 锁,使用 Hash 数据结构
  24 + * 过期时间:不固定
  25 + */
  26 + String PAY_WALLET_LOCK = "pay_wallet:lock:%d";
  27 +
  28 + /**
20 29 * 支付序号的缓存
21 30 *
22 31 * KEY 格式:pay_no:{prefix}
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/redis/wallet/PayWalletLockRedisDAO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.pay.dal.redis.wallet;
  2 +
  3 +import jakarta.annotation.Resource;
  4 +import org.redisson.api.RLock;
  5 +import org.redisson.api.RedissonClient;
  6 +import org.springframework.stereotype.Repository;
  7 +
  8 +import java.util.concurrent.Callable;
  9 +import java.util.concurrent.TimeUnit;
  10 +
  11 +import static cn.iocoder.yudao.module.pay.dal.redis.RedisKeyConstants.PAY_WALLET_LOCK;
  12 +
  13 +/**
  14 + * 支付钱包的锁 Redis DAO
  15 + *
  16 + * @author 芋道源码
  17 + */
  18 +@Repository
  19 +public class PayWalletLockRedisDAO {
  20 +
  21 + @Resource
  22 + private RedissonClient redissonClient;
  23 +
  24 + public <V> V lock(Long id, Long timeoutMillis, Callable<V> callable) throws Exception {
  25 + String lockKey = formatKey(id);
  26 + RLock lock = redissonClient.getLock(lockKey);
  27 + try {
  28 + lock.lock(timeoutMillis, TimeUnit.MILLISECONDS);
  29 + // 执行逻辑
  30 + return callable.call();
  31 + } catch (Exception e) {
  32 + throw e;
  33 + } finally {
  34 + lock.unlock();
  35 + }
  36 + }
  37 +
  38 + private static String formatKey(Long id) {
  39 + return String.format(PAY_WALLET_LOCK, id);
  40 + }
  41 +
  42 +}
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletServiceImpl.java
... ... @@ -2,17 +2,20 @@ package cn.iocoder.yudao.module.pay.service.wallet;
2 2  
3 3 import cn.hutool.core.lang.Assert;
4 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  5 +import cn.iocoder.yudao.framework.common.util.date.DateUtils;
5 6 import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletPageReqVO;
6 7 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
7 8 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
8 9 import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO;
9 10 import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletTransactionDO;
10 11 import cn.iocoder.yudao.module.pay.dal.mysql.wallet.PayWalletMapper;
  12 +import cn.iocoder.yudao.module.pay.dal.redis.wallet.PayWalletLockRedisDAO;
11 13 import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
12 14 import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
13 15 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
14 16 import cn.iocoder.yudao.module.pay.service.wallet.bo.WalletTransactionCreateReqBO;
15 17 import jakarta.annotation.Resource;
  18 +import lombok.SneakyThrows;
16 19 import lombok.extern.slf4j.Slf4j;
17 20 import org.springframework.context.annotation.Lazy;
18 21 import org.springframework.stereotype.Service;
... ... @@ -32,10 +35,17 @@ import static cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum.PAYM
32 35 */
33 36 @Service
34 37 @Slf4j
35   -public class PayWalletServiceImpl implements PayWalletService {
  38 +public class PayWalletServiceImpl implements PayWalletService {
  39 +
  40 + /**
  41 + * 通知超时时间,单位:毫秒
  42 + */
  43 + public static final long UPDATE_TIMEOUT_MILLIS = 120 * DateUtils.SECOND_MILLIS;
36 44  
37 45 @Resource
38 46 private PayWalletMapper walletMapper;
  47 + @Resource
  48 + private PayWalletLockRedisDAO lockRedisDAO;
39 49  
40 50 @Resource
41 51 @Lazy // 延迟加载,避免循环依赖
... ... @@ -121,75 +131,87 @@ public class PayWalletServiceImpl implements PayWalletService {
121 131 }
122 132  
123 133 @Override
  134 + @Transactional(rollbackFor = Exception.class)
  135 + @SneakyThrows
124 136 public PayWalletTransactionDO reduceWalletBalance(Long walletId, Long bizId,
125 137 PayWalletBizTypeEnum bizType, Integer price) {
126 138 // 1. 获取钱包
127 139 PayWalletDO payWallet = getWallet(walletId);
128 140 if (payWallet == null) {
129   - log.error("[reduceWalletBalance],用户钱包({})不存在.", walletId);
  141 + log.error("[reduceWalletBalance][用户钱包({})不存在]", walletId);
130 142 throw exception(WALLET_NOT_FOUND);
131 143 }
132 144  
133   - // 2.1 扣除余额
134   - int updateCounts;
135   - switch (bizType) {
136   - case PAYMENT: {
137   - updateCounts = walletMapper.updateWhenConsumption(payWallet.getId(), price);
138   - break;
  145 + // 2. 加锁,更新钱包余额(目的:避免钱包流水的并发更新时,余额变化不连贯)
  146 + return lockRedisDAO.lock(walletId, UPDATE_TIMEOUT_MILLIS, () -> {
  147 + // 2. 扣除余额
  148 + int updateCounts;
  149 + switch (bizType) {
  150 + case PAYMENT: {
  151 + updateCounts = walletMapper.updateWhenConsumption(payWallet.getId(), price);
  152 + break;
  153 + }
  154 + case RECHARGE_REFUND: {
  155 + updateCounts = walletMapper.updateWhenRechargeRefund(payWallet.getId(), price);
  156 + break;
  157 + }
  158 + default: {
  159 + // TODO 其它类型待实现
  160 + throw new UnsupportedOperationException("待实现");
  161 + }
139 162 }
140   - case RECHARGE_REFUND: {
141   - updateCounts = walletMapper.updateWhenRechargeRefund(payWallet.getId(), price);
142   - break;
  163 + if (updateCounts == 0) {
  164 + throw exception(WALLET_BALANCE_NOT_ENOUGH);
143 165 }
144   - default: {
145   - // TODO 其它类型待实现
146   - throw new UnsupportedOperationException("待实现");
147   - }
148   - }
149   - if (updateCounts == 0) {
150   - throw exception(WALLET_BALANCE_NOT_ENOUGH);
151   - }
152   - // 2.2 生成钱包流水
153   - Integer afterBalance = payWallet.getBalance() - price;
154   - WalletTransactionCreateReqBO bo = new WalletTransactionCreateReqBO().setWalletId(payWallet.getId())
155   - .setPrice(-price).setBalance(afterBalance).setBizId(String.valueOf(bizId))
156   - .setBizType(bizType.getType()).setTitle(bizType.getDescription());
157   - return walletTransactionService.createWalletTransaction(bo);
  166 +
  167 + // 3. 生成钱包流水
  168 + Integer afterBalance = payWallet.getBalance() - price;
  169 + WalletTransactionCreateReqBO bo = new WalletTransactionCreateReqBO().setWalletId(payWallet.getId())
  170 + .setPrice(-price).setBalance(afterBalance).setBizId(String.valueOf(bizId))
  171 + .setBizType(bizType.getType()).setTitle(bizType.getDescription());
  172 + return walletTransactionService.createWalletTransaction(bo);
  173 + });
158 174 }
159 175  
160 176 @Override
  177 + @Transactional(rollbackFor = Exception.class)
  178 + @SneakyThrows
161 179 public PayWalletTransactionDO addWalletBalance(Long walletId, String bizId,
162 180 PayWalletBizTypeEnum bizType, Integer price) {
163   - // 1.1 获取钱包
  181 + // 1. 获取钱包
164 182 PayWalletDO payWallet = getWallet(walletId);
165 183 if (payWallet == null) {
166   - log.error("[addWalletBalance],用户钱包({})不存在.", walletId);
  184 + log.error("[addWalletBalance][用户钱包({})不存在]", walletId);
167 185 throw exception(WALLET_NOT_FOUND);
168 186 }
169   - // 1.2 更新钱包金额
170   - switch (bizType) {
171   - case PAYMENT_REFUND: { // 退款更新
172   - walletMapper.updateWhenConsumptionRefund(payWallet.getId(), price);
173   - break;
174   - }
175   - case RECHARGE: { // 充值更新
176   - walletMapper.updateWhenRecharge(payWallet.getId(), price);
177   - break;
178   - }
179   - case UPDATE_BALANCE: // 更新余额
180   - walletMapper.updateWhenRecharge(payWallet.getId(), price);
181   - break;
182   - default: {
183   - // TODO 其它类型待实现
184   - throw new UnsupportedOperationException("待实现");
  187 +
  188 + // 2. 加锁,更新钱包余额(目的:避免钱包流水的并发更新时,余额变化不连贯)
  189 + return lockRedisDAO.lock(walletId, UPDATE_TIMEOUT_MILLIS, () -> {
  190 + // 2. 更新钱包金额
  191 + switch (bizType) {
  192 + case PAYMENT_REFUND: { // 退款更新
  193 + walletMapper.updateWhenConsumptionRefund(payWallet.getId(), price);
  194 + break;
  195 + }
  196 + case RECHARGE: { // 充值更新
  197 + walletMapper.updateWhenRecharge(payWallet.getId(), price);
  198 + break;
  199 + }
  200 + case UPDATE_BALANCE: // 更新余额
  201 + walletMapper.updateWhenRecharge(payWallet.getId(), price);
  202 + break;
  203 + default: {
  204 + // TODO 其它类型待实现
  205 + throw new UnsupportedOperationException("待实现");
  206 + }
185 207 }
186   - }
187 208  
188   - // 2. 生成钱包流水
189   - WalletTransactionCreateReqBO transactionCreateReqBO = new WalletTransactionCreateReqBO()
190   - .setWalletId(payWallet.getId()).setPrice(price).setBalance(payWallet.getBalance() + price)
191   - .setBizId(bizId).setBizType(bizType.getType()).setTitle(bizType.getDescription());
192   - return walletTransactionService.createWalletTransaction(transactionCreateReqBO);
  209 + // 3. 生成钱包流水
  210 + WalletTransactionCreateReqBO transactionCreateReqBO = new WalletTransactionCreateReqBO()
  211 + .setWalletId(payWallet.getId()).setPrice(price).setBalance(payWallet.getBalance() + price)
  212 + .setBizId(bizId).setBizType(bizType.getType()).setTitle(bizType.getDescription());
  213 + return walletTransactionService.createWalletTransaction(transactionCreateReqBO);
  214 + });
193 215 }
194 216  
195 217 @Override
... ...
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletTransactionServiceImpl.java
... ... @@ -87,8 +87,6 @@ public class PayWalletTransactionServiceImpl implements PayWalletTransactionServ
87 87 @Override
88 88 public AppPayWalletTransactionSummaryRespVO getWalletTransactionSummary(Long userId, Integer userType, LocalDateTime[] createTime) {
89 89 PayWalletDO wallet = payWalletService.getOrCreateWallet(userId, userType);
90   - AppPayWalletTransactionSummaryRespVO summary = new AppPayWalletTransactionSummaryRespVO()
91   - .setTotalExpense(1).setTotalIncome(100);
92 90 return new AppPayWalletTransactionSummaryRespVO()
93 91 .setTotalExpense(payWalletTransactionMapper.selectPriceSum(wallet.getId(), TYPE_EXPENSE, createTime))
94 92 .setTotalIncome(payWalletTransactionMapper.selectPriceSum(wallet.getId(), TYPE_INCOME, createTime));
... ...