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,6 +17,15 @@ public interface RedisKeyConstants {
17 String PAY_NOTIFY_LOCK = "pay_notify:lock:%d"; 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 * KEY 格式:pay_no:{prefix} 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,17 +2,20 @@ package cn.iocoder.yudao.module.pay.service.wallet;
2 2
3 import cn.hutool.core.lang.Assert; 3 import cn.hutool.core.lang.Assert;
4 import cn.iocoder.yudao.framework.common.pojo.PageResult; 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  5 +import cn.iocoder.yudao.framework.common.util.date.DateUtils;
5 import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletPageReqVO; 6 import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.wallet.PayWalletPageReqVO;
6 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO; 7 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
7 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO; 8 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
8 import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO; 9 import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO;
9 import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletTransactionDO; 10 import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletTransactionDO;
10 import cn.iocoder.yudao.module.pay.dal.mysql.wallet.PayWalletMapper; 11 import cn.iocoder.yudao.module.pay.dal.mysql.wallet.PayWalletMapper;
  12 +import cn.iocoder.yudao.module.pay.dal.redis.wallet.PayWalletLockRedisDAO;
11 import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum; 13 import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
12 import cn.iocoder.yudao.module.pay.service.order.PayOrderService; 14 import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
13 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService; 15 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
14 import cn.iocoder.yudao.module.pay.service.wallet.bo.WalletTransactionCreateReqBO; 16 import cn.iocoder.yudao.module.pay.service.wallet.bo.WalletTransactionCreateReqBO;
15 import jakarta.annotation.Resource; 17 import jakarta.annotation.Resource;
  18 +import lombok.SneakyThrows;
16 import lombok.extern.slf4j.Slf4j; 19 import lombok.extern.slf4j.Slf4j;
17 import org.springframework.context.annotation.Lazy; 20 import org.springframework.context.annotation.Lazy;
18 import org.springframework.stereotype.Service; 21 import org.springframework.stereotype.Service;
@@ -32,10 +35,17 @@ import static cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum.PAYM @@ -32,10 +35,17 @@ import static cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum.PAYM
32 */ 35 */
33 @Service 36 @Service
34 @Slf4j 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 @Resource 45 @Resource
38 private PayWalletMapper walletMapper; 46 private PayWalletMapper walletMapper;
  47 + @Resource
  48 + private PayWalletLockRedisDAO lockRedisDAO;
39 49
40 @Resource 50 @Resource
41 @Lazy // 延迟加载,避免循环依赖 51 @Lazy // 延迟加载,避免循环依赖
@@ -121,75 +131,87 @@ public class PayWalletServiceImpl implements PayWalletService { @@ -121,75 +131,87 @@ public class PayWalletServiceImpl implements PayWalletService {
121 } 131 }
122 132
123 @Override 133 @Override
  134 + @Transactional(rollbackFor = Exception.class)
  135 + @SneakyThrows
124 public PayWalletTransactionDO reduceWalletBalance(Long walletId, Long bizId, 136 public PayWalletTransactionDO reduceWalletBalance(Long walletId, Long bizId,
125 PayWalletBizTypeEnum bizType, Integer price) { 137 PayWalletBizTypeEnum bizType, Integer price) {
126 // 1. 获取钱包 138 // 1. 获取钱包
127 PayWalletDO payWallet = getWallet(walletId); 139 PayWalletDO payWallet = getWallet(walletId);
128 if (payWallet == null) { 140 if (payWallet == null) {
129 - log.error("[reduceWalletBalance],用户钱包({})不存在.", walletId); 141 + log.error("[reduceWalletBalance][用户钱包({})不存在]", walletId);
130 throw exception(WALLET_NOT_FOUND); 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 @Override 176 @Override
  177 + @Transactional(rollbackFor = Exception.class)
  178 + @SneakyThrows
161 public PayWalletTransactionDO addWalletBalance(Long walletId, String bizId, 179 public PayWalletTransactionDO addWalletBalance(Long walletId, String bizId,
162 PayWalletBizTypeEnum bizType, Integer price) { 180 PayWalletBizTypeEnum bizType, Integer price) {
163 - // 1.1 获取钱包 181 + // 1. 获取钱包
164 PayWalletDO payWallet = getWallet(walletId); 182 PayWalletDO payWallet = getWallet(walletId);
165 if (payWallet == null) { 183 if (payWallet == null) {
166 - log.error("[addWalletBalance],用户钱包({})不存在.", walletId); 184 + log.error("[addWalletBalance][用户钱包({})不存在]", walletId);
167 throw exception(WALLET_NOT_FOUND); 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 @Override 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,8 +87,6 @@ public class PayWalletTransactionServiceImpl implements PayWalletTransactionServ
87 @Override 87 @Override
88 public AppPayWalletTransactionSummaryRespVO getWalletTransactionSummary(Long userId, Integer userType, LocalDateTime[] createTime) { 88 public AppPayWalletTransactionSummaryRespVO getWalletTransactionSummary(Long userId, Integer userType, LocalDateTime[] createTime) {
89 PayWalletDO wallet = payWalletService.getOrCreateWallet(userId, userType); 89 PayWalletDO wallet = payWalletService.getOrCreateWallet(userId, userType);
90 - AppPayWalletTransactionSummaryRespVO summary = new AppPayWalletTransactionSummaryRespVO()  
91 - .setTotalExpense(1).setTotalIncome(100);  
92 return new AppPayWalletTransactionSummaryRespVO() 90 return new AppPayWalletTransactionSummaryRespVO()
93 .setTotalExpense(payWalletTransactionMapper.selectPriceSum(wallet.getId(), TYPE_EXPENSE, createTime)) 91 .setTotalExpense(payWalletTransactionMapper.selectPriceSum(wallet.getId(), TYPE_EXPENSE, createTime))
94 .setTotalIncome(payWalletTransactionMapper.selectPriceSum(wallet.getId(), TYPE_INCOME, createTime)); 92 .setTotalIncome(payWalletTransactionMapper.selectPriceSum(wallet.getId(), TYPE_INCOME, createTime));