目录

幂等实现_基于乐观锁

前言

以账户充值为例,demo基于springboot+mybatis架构,先模拟高并发场景下幂等性问题现象,也就是账户被重复充值的场景。然后通过基于version字段的乐观锁解决方案,解决幂等性问题。

完整demo

问题重现

数据准备

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
-- 初始化脚本
CREATE TABLE `t_account` (
  `account_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '账户ID',
  `account_name` varchar(20) DEFAULT NULL COMMENT '账户名',
  `balance` int(11) DEFAULT 0 COMMENT '账户余额',
  PRIMARY KEY (`account_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

-- 没使用乐观锁的case
CREATE TABLE `t_order` (
  `order_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '订单ID',
  `order_date` datetime DEFAULT NULL COMMENT '下单时间',
  `account_id` int(11) DEFAULT NULL COMMENT '账户ID',
  `amount` int(11) DEFAULT 0 COMMENT '订单金额',
  `status` TINYINT(1) DEFAULT NULL COMMENT '订单支付状态 0-未支付,1-已支付',
  PRIMARY KEY (`order_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

insert  into `t_account`(`account_id`,`account_name`,`balance`) values (1,'jack',100);
insert  into `t_order`(`order_id`,`order_date`,`account_id`,`amount`,`status`) values (1,'2020-08-03 10:44:10',1,10,0);

重置脚本schema.sql

1
2
3
-- 重置脚本
update t_account set balance = 100 where account_id = 1;
update t_order set amount = 10, status = 0 where order_id = 1;

搭建工程

http://img.cana.space/picStore/20201123201713.png

充值服务 RechargeServiceImpl

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.eh.cana.optimisticlock.service.impl;

import cn.hutool.core.util.RandomUtil;
import com.eh.cana.optimisticlock.dao.AccountMapper;
import com.eh.cana.optimisticlock.dao.OrderMapper;
import com.eh.cana.optimisticlock.entity.Order;
import com.eh.cana.optimisticlock.service.RechargeService;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.concurrent.TimeUnit;

@Slf4j
@Service
public class RechargeServiceImpl implements RechargeService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private AccountMapper accountMapper;

    @Transactional
    @SneakyThrows
    @Override
    public boolean recharge(Integer orderId, Integer amount) {
        log.info("查询订单");
        Order order = orderMapper.getByOrderId(orderId);
        if (order.getStatus() == 0) { // check订单支付状态
            log.info("未支付状态");
            TimeUnit.MILLISECONDS.sleep(RandomUtil.randomInt(10, 100)); // 模拟计算时间
            order.setStatus(1);
            log.info("更新支付状态...");
            int affectRow = orderMapper.updateOrderById(order);
            if (affectRow == 1 ) {
                log.info("账户充值...");
                accountMapper.recharge(order.getAccountId(), order.getAmount());
            } else {
                log.info("更新支付状态失败,数据过期");
                return false;
            }
        } else {
            log.info("发现订单已处理");
            return true;
        }
        return false;
    }
}

模拟高并发现象,测试程序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
    @Test
    @SneakyThrows
    public void testProblem() {
        int retryTimes = 10; // 模拟客户端请求次数
        CountDownLatch latch = new CountDownLatch(retryTimes);
        Runnable runnable = () -> {
            rechargeService.recharge(1, 10);
            latch.countDown();
        };
        // 模拟多次重试情况
        IntStream.range(0, retryTimes).forEach(i -> new Thread(runnable, "testProblem-" + i).start());
        latch.await();
        log.info("=====>账户余额:{}", accountMapper.getAccountById(1).getBalance());
    }

日志:

20201123202629

通过日志可以看到重复充值了多次,期望结果是110,结果10次重试导致最终结果是140。

使用乐观锁解决问题

表t_order添加version字段

1
alter table `t_order` add COLUMN version int not null default 1 comment '版本控制';

更新时带上版本号条件并且将版本号加1

1
2
3
<update id="updateOrderById">
        update t_order set status=#{status}, version=version+1 where order_id = #{orderId} and version=#{version}
</update>

再次执行测试程序

http://img.cana.space/picStore/20201123222324.png

这次可以看到账户只充值了一次,账户余额如我们期望的一样。

总结

乐观锁和悲观锁、分布式锁等等幂等性解决方案本质是一样的,就是构造出一个单点,让请求并行变串行,乐观锁将请求放到在数据行,利用行锁来一个个地检测请求是否已处理过。

优点:实现简单

缺点:1.对应用代码有入侵,2.浪费数据库资源。