在使用进行余额扣减时,可能会遇到并发请求同时扣减同一余额的情况,这可能会导致脏读和负数余额的问题。为了解决这些问题,可以使用分布式锁来保证同一时间只有一个请求能够进行余额扣减操作。
分布式锁可以使用redis或zookeeper等工具实现。在进行余额扣减前,先获取分布式锁。如果获取锁成功,则进行余额扣减操作,扣减完成后释放锁。如果获取锁失败,则说明有其他请求正在进行余额扣减操作,此时需要等待一段时间后重新尝试获取锁。
在进行余额扣减操作时,需要先查询当前余额数量。如果当前余额数量小于等于0,则不能进行余额扣减,否则会导致负数余额的问题。如果当前余额数量大于0,则进行余额扣减操作,并更新数据库中的余额数量。
同样我们也可以使用乐观锁。使用乐观锁时,先查询当前余额数量,并记录版本号。扣减余额时,检查当前余额数量和版本号是否一致,如果一致则进行扣减操作并更新版本号,否则说明有其他请求已经进行了余额扣减操作,此时需要重新查询当前余额数量和版本号,然后重新尝试扣减余额。
总之,使用分布式锁和乐观锁等机制可以保证余额扣减操作的正确性和并发性,避免出现脏读和负数余额等问题
首先通过sql对加减余额做个限制, 通过 balance + #{amount} >= 0; 限制修改余额的操作必须大于0
<update id="updateBalance">
update `user`
set balance = balance - #{amount}
where id = #{userId}
and balance - #{amount} >= 0;
</update>
- 乐观锁
mybatis-plus 自带乐观锁功能 , 具体可以查看官方文档
- 分布式锁
@Override
public Boolean transfer(PlatformTransferReq req) throws InterruptedException {
//定义锁 最好是根据人进行加锁
RLock lock = redissonClient.getLock(RedisKey.BALANCE + req.getUserId());
try {
if (lock.tryLock(3, 30, TimeUnit.SECONDS)) {
// 校验余额
MerUser merUser = getById(req.getId());
BigDecimal balance = merUser.getBalance();
Assert.isTrue(NumberUtil.isGreaterOrEqual(balance, req.getAmount()), "账户余额不足");
// 减掉余额
return SqlHelper.retBool(baseMapper.updateBalance(req.getId(), req.getAmount()));
}
} catch (InterruptedException e) {
throw new ServiceException("转账失败 :" + e.getMessage());
} finally {
if (lock.isLocked()) {
lock.unlock();
}
}
return false;
}