Commit dbb674b24f34ef37c0911f5b20d635696d4723be
1 parent
9b5d3f01
【功能优化】支付:钱包余额更新时,加锁避免并发更新,导致流水不连续的问题
Showing
4 changed files
with
122 additions
and
51 deletions
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)); |