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

完整代码: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. Vue组件间通信6种方式

    摘要: 总有一款合适的通信方式. 作者:浪里行舟 Fundebug经授权转载,版权归原作者所有. 前言 组件是 vue.js 最强大的功能之一,而组件实例的作用域是相互独立的,这就意味着不同组件之间的 ...

  2. CDH报错:ScmActive at bootup: Failed to validate the identity of Cloudera Manager.

    报错原因以及解决办法在官网: https://www.cloudera.com/documentation/enterprise/5-8-x/topics/cm_failover_db.html 1. ...

  3. MSSQL记录表字段数据变化的相关SQl

    在软件实施过程中,也许会有这样的问题: 表中数据出现非预期的结果,此时不确定是程序问题,哪个程序,存储过程,触发器.. 或还是人为修改的结果,此时可以用触发器对特定的表字段做跟踪监视,记录每次新增,修 ...

  4. Linux shell for循环结构

    Linux Shell   for循环结构 循环结构            1:循环开始条件      2:循环操作      3:循环终止的条件 shell语言          for,while ...

  5. Linux的httpd服务介绍和部署

    软件介绍 客户端代理软件     IE,firefox,chroome,opera      服务器端软件      httpd,Nginx,Tengine,ISS,Lighthttp       应 ...

  6. CentOS6.10下yum安装MySQL5.7

    MySQL官网的Yum仓库快速指南:https://dev.mysql.com/doc/mysql-yum-repo-quick-guide/en/ 检查是否安装有MySQL数据库 rpm -qa | ...

  7. 第04节-BLE协议抓包演示

    在上几篇博客中,形象的讲解了BLE各个层的作用,各个层的数据结构.本篇博客将研究BLE协议抓包.在实际开发中,有一个中央设备(central)和一个外设(Peripheral).所谓中央设备就是指它可 ...

  8. mysql常用操作(测试必备)

    现在互联网的主流关系型数据库是mysql,掌握其基本的增.删.改.查是每一个测试人员必备的技能. sql语言分类 1.DDL语句(数据库定义语言): 数据库.表.视图.索引.存储过程,例如:CREAT ...

  9. Pandas | 05 基本功能

    到目前为止,我们了解了三种Pandas数据结构以及如何创建它们.接下来将主要关注数据帧(DataFrame)对象,因为它在实时数据处理中非常重要,并且还讨论其他数据结构. 一.系列基本功能 编号 属性 ...

  10. C#中的Queue集合

    Queue<T>集合 特点:先进先出,简单来说,就是新添加的元素是顺序添加在集合尾部,但是,移除的时候是从顶部开始移除元素. 三个方法: Enqueue(T obj);//顺序添加一个值到 ...