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

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

GitHub Page:http://blog.cloudli.top/posts/Spring-Boot-&-MyBatis-实现乐观锁和悲观锁/

死锁问题

当 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)

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

Mapper 文件

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

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.cloud.demo.mapper.AccountMapper">
<select id="selectById" resultType="com.cloud.demo.model.Account">
select *
from account
where id = #{id}
</select>
<update id="updateDeposit" keyProperty="id" parameterType="com.cloud.demo.model.Account">
update account
set deposit=#{deposit},
version = version + 1
where id = #{id}
and version = #{version}
</update>
<select id="selectByIdForUpdate" resultType="com.cloud.demo.model.Account">
select *
from account
where id = #{id} for
update
</select>
<update id="updateDepositPessimistic" keyProperty="id" parameterType="com.cloud.demo.model.Account">
update account
set deposit=#{deposit}
where id = #{id}
</update>
<select id="getTotalDeposit" resultType="java.math.BigDecimal">
select sum(deposit) from account;
</select>
</mapper>

Mapper 接口

@Component
public interface AccountMapper {
Account selectById(int id);
Account selectByIdForUpdate(int id);
int updateDepositWithVersion(Account account);
void updateDeposit(Account account);
BigDecimal getTotalDeposit();
}

Account POJO

@Data
public class Account {
private int id;
private BigDecimal deposit;
private int version;
}

AccountService

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

@Slf4j
@Service
public class AccountService { public enum Result{
SUCCESS,
DEPOSIT_NOT_ENOUGH,
FAILED,
} @Resource
private AccountMapper accountMapper; private BiPredicate<BigDecimal, BigDecimal> isDepositEnough = (deposit, value) -> deposit.compareTo(value) > 0; /**
* 转账操作,悲观锁
*
* @param fromId 扣款账户
* @param toId 收款账户
* @param value 金额
*/
@Transactional(isolation = Isolation.READ_COMMITTED)
public Result transferPessimistic(int fromId, int toId, BigDecimal value) {
Account from, to; try {
// 先锁 id 较大的那行,避免死锁
if (fromId > toId) {
from = accountMapper.selectByIdForUpdate(fromId);
to = accountMapper.selectByIdForUpdate(toId);
} else {
to = accountMapper.selectByIdForUpdate(toId);
from = accountMapper.selectByIdForUpdate(fromId);
}
} catch (Exception e) {
log.error(e.getMessage());
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return Result.FAILED;
} if (!isDepositEnough.test(from.getDeposit(), value)) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
log.info(String.format("Account %d is not enough.", fromId));
return Result.DEPOSIT_NOT_ENOUGH;
} from.setDeposit(from.getDeposit().subtract(value));
to.setDeposit(to.getDeposit().add(value)); accountMapper.updateDeposit(from);
accountMapper.updateDeposit(to); return Result.SUCCESS;
} /**
* 转账操作,乐观锁
* @param fromId 扣款账户
* @param toId 收款账户
* @param value 金额
*/
@Retry
@Transactional(isolation = Isolation.REPEATABLE_READ)
public Result transferOptimistic(int fromId, int toId, BigDecimal value) {
Account from = accountMapper.selectById(fromId),
to = accountMapper.selectById(toId); if (!isDepositEnough.test(from.getDeposit(), value)) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return Result.DEPOSIT_NOT_ENOUGH;
} from.setDeposit(from.getDeposit().subtract(value));
to.setDeposit(to.getDeposit().add(value)); int r1, r2; // 先锁 id 较大的那行,避免死锁
if (from.getId() > to.getId()) {
r1 = accountMapper.updateDepositWithVersion(from);
r2 = accountMapper.updateDepositWithVersion(to);
} else {
r2 = accountMapper.updateDepositWithVersion(to);
r1 = accountMapper.updateDepositWithVersion(from);
} if (r1 < 1 || r2 < 1) {
// 失败,抛出重试异常,执行重试
throw new RetryException("Transfer failed, retry.");
} else {
return Result.SUCCESS;
}
}
}

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

自定义注解 Retry

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

重试异常 RetryException

public class RetryException extends RuntimeException {
public RetryException(String message) {
super(message);
}
}

重试的切面类

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

@Slf4j
@Aspect
@Component
public class RetryAspect { @Pointcut("@annotation(com.cloud.demo.annotation.Retry)")
public void retryPointcut() { } @Around("retryPointcut() && @annotation(retry)")
@Transactional(isolation = Isolation.READ_COMMITTED)
public Object tryAgain(ProceedingJoinPoint joinPoint, Retry retry) throws Throwable {
int count = 0;
do {
count++;
try {
return joinPoint.proceed();
} catch (RetryException e) {
if (count > retry.value()) {
log.error("Retry failed!");
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return AccountService.Result.FAILED;
}
}
} while (true);
}
}

单元测试

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

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

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

@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
class AccountServiceTest { // 并发数
private static final int COUNT = 500; @Resource
AccountMapper accountMapper; @Resource
AccountService accountService; private CountDownLatch latch = new CountDownLatch(COUNT);
private List<Thread> transferThreads = new ArrayList<>();
private List<Pair<Integer, Integer>> transferAccounts = new ArrayList<>(); @BeforeEach
void setUp() {
Random random = new Random(currentTimeMillis());
transferThreads.clear();
transferAccounts.clear(); for (int i = 0; i < COUNT; i++) {
int from = random.nextInt(10) + 1;
int to;
do{
to = random.nextInt(10) + 1;
} while (from == to);
transferAccounts.add(new Pair<>(from, to));
}
} /**
* 测试悲观锁
*/
@Test
void transferByPessimisticLock() throws Throwable {
for (int i = 0; i < COUNT; i++) {
transferThreads.add(new Transfer(i, true));
}
for (Thread t : transferThreads) {
t.start();
}
latch.await(); Assertions.assertEquals(accountMapper.getTotalDeposit(),
BigDecimal.valueOf(10000).setScale(2, RoundingMode.HALF_UP));
} /**
* 测试乐观锁
*/
@Test
void transferByOptimisticLock() throws Throwable {
for (int i = 0; i < COUNT; i++) {
transferThreads.add(new Transfer(i, false));
}
for (Thread t : transferThreads) {
t.start();
}
latch.await(); Assertions.assertEquals(accountMapper.getTotalDeposit(),
BigDecimal.valueOf(10000).setScale(2, RoundingMode.HALF_UP));
} /**
* 转账线程
*/
class Transfer extends Thread {
int index;
boolean isPessimistic; Transfer(int i, boolean b) {
index = i;
isPessimistic = b;
} @Override
public void run() {
BigDecimal value = BigDecimal.valueOf(
new Random(currentTimeMillis()).nextFloat() * 100
).setScale(2, RoundingMode.HALF_UP); AccountService.Result result = AccountService.Result.FAILED;
int fromId = transferAccounts.get(index).getKey(),
toId = transferAccounts.get(index).getValue();
try {
if (isPessimistic) {
result = accountService.transferPessimistic(fromId, toId, value);
} else {
result = accountService.transferOptimistic(fromId, toId, value);
}
} catch (Exception e) {
log.error(e.getMessage());
} finally {
if (result == AccountService.Result.SUCCESS) {
log.info(String.format("Transfer %f from %d to %d success", value, fromId, toId));
}
latch.countDown();
}
}
}
}

MySQL 配置

innodb_rollback_on_timeout='ON'
max_connections=1000
innodb_lock_wait_timeout=500

Spring Boot 整合 MyBatis 实现乐观锁和悲观锁的更多相关文章

  1. Spring Boot整合Mybatis并完成CRUD操作

    MyBatis 是一款优秀的持久层框架,被各大互联网公司使用,本文使用Spring Boot整合Mybatis,并完成CRUD操作. 为什么要使用Mybatis?我们需要掌握Mybatis吗? 说的官 ...

  2. spring boot 整合 mybatis 以及原理

    同上一篇文章一样,spring boot 整合 mybatis过程中没有看见SqlSessionFactory,sqlsession(sqlsessionTemplate),就连在spring框架整合 ...

  3. Spring Boot 整合mybatis时遇到的mapper接口不能注入的问题

    现实情况是这样的,因为在练习spring boot整合mybatis,所以自己新建了个项目做测试,可是在idea里面mapper接口注入报错,后来百度查询了下,把idea的注入等级设置为了warnin ...

  4. Spring Boot整合Mybatis报错InstantiationException: tk.mybatis.mapper.provider.base.BaseSelectProvider

    Spring Boot整合Mybatis时一直报错 后来发现原来主配置类上的MapperScan导错了包 由于我使用了通用Mapper,所以应该导入通用mapper这个包

  5. Spring Boot整合MyBatis(非注解版)

    Spring Boot整合MyBatis(非注解版),开发时采用的时IDEA,JDK1.8 直接上图: 文件夹不存在,创建一个新的路径文件夹 创建完成目录结构如下: 本人第一步习惯先把需要的包结构创建 ...

  6. Spring Boot整合Mybatis完成级联一对多CRUD操作

    在关系型数据库中,随处可见表之间的连接,对级联的表进行增删改查也是程序员必备的基础技能.关于Spring Boot整合Mybatis在之前已经详细写过,不熟悉的可以回顾Spring Boot整合Myb ...

  7. Spring Boot系列(三):Spring Boot整合Mybatis源码解析

    一.Mybatis回顾 1.MyBatis介绍 Mybatis是一个半ORM框架,它使用简单的 XML 或注解用于配置和原始映射,将接口和Java的POJOs(普通的Java 对象)映射成数据库中的记 ...

  8. 太妙了!Spring boot 整合 Mybatis Druid,还能配置监控?

    Spring boot 整合 Mybatis Druid并配置监控 添加依赖 <!--druid--> <dependency> <groupId>com.alib ...

  9. Spring Boot 整合 Mybatis 实现 Druid 多数据源详解

    摘要: 原创出处:www.bysocket.com 泥瓦匠BYSocket 希望转载,保留摘要,谢谢! “清醒时做事,糊涂时跑步,大怒时睡觉,独处时思考” 本文提纲一.多数据源的应用场景二.运行 sp ...

随机推荐

  1. windows docker 安装 Kitematic

    在已经安装好docker for windows的基础上, 右键docker任务栏小图标, 选择Kitematic 然后放在docker的安装目录中C:\Program Files\Docker: 文 ...

  2. 是否忘记了向源中添加“#include "StdAfx.h"”?

    错误分析 此错误发生的原因是编译器在寻找预编译指示头文件(默认#include "stdafx.h")时,文件未预期结束.没有找到预编译指示信息的头文件"stdafx.h ...

  3. MySQL的select详细介绍

    MySQL 查询数据 MySQL 数据库使用SQL SELECT语句来查询数据. 你可以通过 mysql> 命令提示窗口中在数据库中查询数据 语法 以下为在MySQL数据库中查询数据通用的 SE ...

  4. grep基本详细使用

    过滤文件类容---grep grep正则表达式应用: #grep [option]"pattern" 文件名称 pattern模式 由普通字符和正则表达式的元字符组构成的条件 简单 ...

  5. Linux计划任务管理

    计划任务 类型:     一次性计划任务     周期性计划任务      一次性计划任务 前提:  atd服务必须运行 [root@wei init.d]# yum -y install at   ...

  6. rhel7学习第四天

    学习<Linux就该这么学>第二周,学习了最重要的几个文件操作命令

  7. Python3+HTMLTestRunner+SMTP生成测试报告后发送邮件

    在前一篇https://www.cnblogs.com/zhengyihan1216/p/11549820.html 中记录了如何生成html格式的报告, 这篇记录下怎么将测试报告通过邮件发出 1.对 ...

  8. 9-3 deepsort在win10配置

    win10+python3.5+tensorflow https://blog.csdn.net/Draw_1999/article/details/79735677 deep-sort https: ...

  9. win +R

    一.电脑设置免登录密码及修改密码 1.win+R 2.输入control userpasswords2 3.勾选免密码账号登陆,修改密码 二.打开性能监视器 1.win+R 2.输入perfmon.e ...

  10. manacher算法笔记

    模板 [模板]manacher算法 不妨先只考虑如何求长度为奇数的回文串 记\(P[i]\)表示以\(i\)为中心最多向两边扩展几个字符,满足回文 如串\(ababa\), \(P[1]=0,P[2] ...