经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 数据库/运维 » MyBatis » 查看文章
SpringBoot整合MyBatis实现乐观锁和悲观锁的示例
来源:jb51  时间:2019/9/20 12:15:55  对本文有异议

本文以转账操作为例,实现并测试乐观锁和悲观锁。

全部代码:https://github.com/imcloudfloating/Lock_Demo

GitHub Page:https://cloudli.top

死锁问题

当 A, B 两个账户同时向对方转账时,会出现如下情况:

时刻 事务 1 (A 向 B 转账) 事务 2 (B 向 A 转账)
T1 Lock A Lock B
T2 Lock B (由于事务 2 已经 Lock A,等待) Lock A (由于事务 1 已经 Lock B,等待)

由于两个事务都在等待对方释放锁,于是死锁产生了,解决方案:按照主键的大小来加锁,总是先锁主键较小或较大的那行数据。

建立数据表并插入数据(MySQL)

  1. create table account
  2. (
  3. id int auto_increment
  4. primary key,
  5. deposit decimal(10, 2) default 0.00 not null,
  6. version int default 0 not null
  7. );
  8.  
  9. INSERT INTO vault.account (id, deposit, version) VALUES (1, 1000, 0);
  10. INSERT INTO vault.account (id, deposit, version) VALUES (2, 1000, 0);
  11. INSERT INTO vault.account (id, deposit, version) VALUES (3, 1000, 0);
  12. INSERT INTO vault.account (id, deposit, version) VALUES (4, 1000, 0);
  13. INSERT INTO vault.account (id, deposit, version) VALUES (5, 1000, 0);
  14. INSERT INTO vault.account (id, deposit, version) VALUES (6, 1000, 0);
  15. INSERT INTO vault.account (id, deposit, version) VALUES (7, 1000, 0);
  16. INSERT INTO vault.account (id, deposit, version) VALUES (8, 1000, 0);
  17. INSERT INTO vault.account (id, deposit, version) VALUES (9, 1000, 0);
  18. INSERT INTO vault.account (id, deposit, version) VALUES (10, 1000, 0);
  19.  

Mapper 文件

悲观锁使用 select ... for update,乐观锁使用 version 字段。

  1. <?xml version="1.0" encoding="UTF-8" ?>
  2. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  3. "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
  4. <mapper namespace="com.cloud.demo.mapper.AccountMapper">
  5. <select id="selectById" resultType="com.cloud.demo.model.Account">
  6. select *
  7. from account
  8. where id = #{id}
  9. </select>
  10. <update id="updateDeposit" keyProperty="id" parameterType="com.cloud.demo.model.Account">
  11. update account
  12. set deposit=#{deposit},
  13. version = version + 1
  14. where id = #{id}
  15. and version = #{version}
  16. </update>
  17. <select id="selectByIdForUpdate" resultType="com.cloud.demo.model.Account">
  18. select *
  19. from account
  20. where id = #{id} for
  21. update
  22. </select>
  23. <update id="updateDepositPessimistic" keyProperty="id" parameterType="com.cloud.demo.model.Account">
  24. update account
  25. set deposit=#{deposit}
  26. where id = #{id}
  27. </update>
  28. <select id="getTotalDeposit" resultType="java.math.BigDecimal">
  29. select sum(deposit) from account;
  30. </select>
  31. </mapper>

Mapper 接口

  1. @Component
  2. public interface AccountMapper {
  3. Account selectById(int id);
  4. Account selectByIdForUpdate(int id);
  5. int updateDepositWithVersion(Account account);
  6. void updateDeposit(Account account);
  7. BigDecimal getTotalDeposit();
  8. }

Account POJO

  1. @Data
  2. public class Account {
  3. private int id;
  4. private BigDecimal deposit;
  5. private int version;
  6. }

AccountService

在 transferOptimistic 方法上有个自定义注解 @Retry,这个用来实现乐观锁失败后重试。

  1. @Slf4j
  2. @Service
  3. public class AccountService {
  4.  
  5. public enum Result{
  6. SUCCESS,
  7. DEPOSIT_NOT_ENOUGH,
  8. FAILED,
  9. }
  10.  
  11. @Resource
  12. private AccountMapper accountMapper;
  13.  
  14. private BiPredicate<BigDecimal, BigDecimal> isDepositEnough = (deposit, value) -> deposit.compareTo(value) > 0;
  15.  
  16. /**
  17. * 转账操作,悲观锁
  18. *
  19. * @param fromId 扣款账户
  20. * @param toId 收款账户
  21. * @param value 金额
  22. */
  23. @Transactional(isolation = Isolation.READ_COMMITTED)
  24. public Result transferPessimistic(int fromId, int toId, BigDecimal value) {
  25. Account from, to;
  26.  
  27. try {
  28. // 先锁 id 较大的那行,避免死锁
  29. if (fromId > toId) {
  30. from = accountMapper.selectByIdForUpdate(fromId);
  31. to = accountMapper.selectByIdForUpdate(toId);
  32. } else {
  33. to = accountMapper.selectByIdForUpdate(toId);
  34. from = accountMapper.selectByIdForUpdate(fromId);
  35. }
  36. } catch (Exception e) {
  37. log.error(e.getMessage());
  38. TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
  39. return Result.FAILED;
  40. }
  41.  
  42. if (!isDepositEnough.test(from.getDeposit(), value)) {
  43. TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
  44. log.info(String.format("Account %d is not enough.", fromId));
  45. return Result.DEPOSIT_NOT_ENOUGH;
  46. }
  47.  
  48. from.setDeposit(from.getDeposit().subtract(value));
  49. to.setDeposit(to.getDeposit().add(value));
  50.  
  51. accountMapper.updateDeposit(from);
  52. accountMapper.updateDeposit(to);
  53.  
  54. return Result.SUCCESS;
  55. }
  56.  
  57. /**
  58. * 转账操作,乐观锁
  59. * @param fromId 扣款账户
  60. * @param toId 收款账户
  61. * @param value 金额
  62. */
  63. @Retry
  64. @Transactional(isolation = Isolation.REPEATABLE_READ)
  65. public Result transferOptimistic(int fromId, int toId, BigDecimal value) {
  66. Account from = accountMapper.selectById(fromId),
  67. to = accountMapper.selectById(toId);
  68.  
  69. if (!isDepositEnough.test(from.getDeposit(), value)) {
  70. TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
  71. return Result.DEPOSIT_NOT_ENOUGH;
  72. }
  73.  
  74. from.setDeposit(from.getDeposit().subtract(value));
  75. to.setDeposit(to.getDeposit().add(value));
  76.  
  77. int r1, r2;
  78.  
  79. // 先锁 id 较大的那行,避免死锁
  80. if (from.getId() > to.getId()) {
  81. r1 = accountMapper.updateDepositWithVersion(from);
  82. r2 = accountMapper.updateDepositWithVersion(to);
  83. } else {
  84. r2 = accountMapper.updateDepositWithVersion(to);
  85. r1 = accountMapper.updateDepositWithVersion(from);
  86. }
  87.  
  88. if (r1 < 1 || r2 < 1) {
  89. // 失败,抛出重试异常,执行重试
  90. throw new RetryException("Transfer failed, retry.");
  91. } else {
  92. return Result.SUCCESS;
  93. }
  94. }
  95. }
  96.  

使用 Spring AOP 实现乐观锁失败后重试

自定义注解 Retry

  1. @Retention(RetentionPolicy.RUNTIME)
  2. @Target(ElementType.METHOD)
  3. public @interface Retry {
  4. int value() default 3; // 重试次数
  5. }

重试异常 RetryException

  1. public class RetryException extends RuntimeException {
  2. public RetryException(String message) {
  3. super(message);
  4. }
  5. }

重试的切面类

tryAgain 方法使用了 @Around 注解(表示环绕通知),可以决定目标方法在何时执行,或者不执行,以及自定义返回结果。这里首先通过 ProceedingJoinPoint.proceed() 方法执行目标方法,如果抛出了重试异常,那么重新执行直到满三次,三次都不成功则回滚并返回 FAILED。

  1. @Slf4j
  2. @Aspect
  3. @Component
  4. public class RetryAspect {
  5.  
  6. @Pointcut("@annotation(com.cloud.demo.annotation.Retry)")
  7. public void retryPointcut() {
  8.  
  9. }
  10.  
  11. @Around("retryPointcut() && @annotation(retry)")
  12. @Transactional(isolation = Isolation.READ_COMMITTED)
  13. public Object tryAgain(ProceedingJoinPoint joinPoint, Retry retry) throws Throwable {
  14. int count = 0;
  15. do {
  16. count++;
  17. try {
  18. return joinPoint.proceed();
  19. } catch (RetryException e) {
  20. if (count > retry.value()) {
  21. log.error("Retry failed!");
  22. TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
  23. return AccountService.Result.FAILED;
  24. }
  25. }
  26. } while (true);
  27. }
  28. }
  29.  

单元测试

用多个线程模拟并发转账,经过测试,悲观锁除了账户余额不足,或者数据库连接不够以及等待超时,全部成功;乐观锁即使加了重试,成功的线程也很少,500 个平均也就十几个成功。

所以对于写多读少的操作,使用悲观锁,对于读多写少的操作,可以使用乐观锁。

完整代码请见 Github:https://github.com/imcloudfloating/Lock_Demo

  1. @Slf4j
  2. @SpringBootTest
  3. @RunWith(SpringRunner.class)
  4. class AccountServiceTest {
  5.  
  6. // 并发数
  7. private static final int COUNT = 500;
  8.  
  9. @Resource
  10. AccountMapper accountMapper;
  11.  
  12. @Resource
  13. AccountService accountService;
  14.  
  15. private CountDownLatch latch = new CountDownLatch(COUNT);
  16. private List<Thread> transferThreads = new ArrayList<>();
  17. private List<Pair<Integer, Integer>> transferAccounts = new ArrayList<>();
  18.  
  19. @BeforeEach
  20. void setUp() {
  21. Random random = new Random(currentTimeMillis());
  22. transferThreads.clear();
  23. transferAccounts.clear();
  24.  
  25. for (int i = 0; i < COUNT; i++) {
  26. int from = random.nextInt(10) + 1;
  27. int to;
  28. do{
  29. to = random.nextInt(10) + 1;
  30. } while (from == to);
  31. transferAccounts.add(new Pair<>(from, to));
  32. }
  33. }
  34.  
  35. /**
  36. * 测试悲观锁
  37. */
  38. @Test
  39. void transferByPessimisticLock() throws Throwable {
  40. for (int i = 0; i < COUNT; i++) {
  41. transferThreads.add(new Transfer(i, true));
  42. }
  43. for (Thread t : transferThreads) {
  44. t.start();
  45. }
  46. latch.await();
  47.  
  48. Assertions.assertEquals(accountMapper.getTotalDeposit(),
  49. BigDecimal.valueOf(10000).setScale(2, RoundingMode.HALF_UP));
  50. }
  51.  
  52. /**
  53. * 测试乐观锁
  54. */
  55. @Test
  56. void transferByOptimisticLock() throws Throwable {
  57. for (int i = 0; i < COUNT; i++) {
  58. transferThreads.add(new Transfer(i, false));
  59. }
  60. for (Thread t : transferThreads) {
  61. t.start();
  62. }
  63. latch.await();
  64.  
  65. Assertions.assertEquals(accountMapper.getTotalDeposit(),
  66. BigDecimal.valueOf(10000).setScale(2, RoundingMode.HALF_UP));
  67. }
  68.  
  69. /**
  70. * 转账线程
  71. */
  72. class Transfer extends Thread {
  73. int index;
  74. boolean isPessimistic;
  75.  
  76. Transfer(int i, boolean b) {
  77. index = i;
  78. isPessimistic = b;
  79. }
  80.  
  81. @Override
  82. public void run() {
  83. BigDecimal value = BigDecimal.valueOf(
  84. new Random(currentTimeMillis()).nextFloat() * 100
  85. ).setScale(2, RoundingMode.HALF_UP);
  86.  
  87. AccountService.Result result = AccountService.Result.FAILED;
  88. int fromId = transferAccounts.get(index).getKey(),
  89. toId = transferAccounts.get(index).getValue();
  90. try {
  91. if (isPessimistic) {
  92. result = accountService.transferPessimistic(fromId, toId, value);
  93. } else {
  94. result = accountService.transferOptimistic(fromId, toId, value);
  95. }
  96. } catch (Exception e) {
  97. log.error(e.getMessage());
  98. } finally {
  99. if (result == AccountService.Result.SUCCESS) {
  100. log.info(String.format("Transfer %f from %d to %d success", value, fromId, toId));
  101. }
  102. latch.countDown();
  103. }
  104. }
  105. }
  106. }
  107.  

MySQL 配置

  1. innodb_rollback_on_timeout='ON'
  2. max_connections=1000
  3. innodb_lock_wait_timeout=500

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持w3xue。

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号