1、问题场景

  以用户账户为例,如果允许同时对某个用户的账户进行修改的话,会导致某些修改被覆盖,使最后的结果不正确。

  如:1.1、张三的账户中有100元。

    1.2、张三的账户消费了50元。

    1.3、张三的账户充值了100元。

  我们希望的张三账户最终的结果是150元。如果1.2、1.3是并发执行的,按下面的方式执行的话,回事怎样的呢?

账户实体:

/**
* 账户实体
*
* @author caofanqi
*/
@Slf4j
@Data
@EqualsAndHashCode(callSuper = true)
@Entity
@Builder
@Table(name = "jpa_account")
@NoArgsConstructor
@AllArgsConstructor
public class Account extends AbstractID { /**
* 简单代表一下账户所属人
*/
private String accountName; @Column(columnDefinition = "DECIMAL(19, 2)")
private BigDecimal balance; }

Repository接口:

/**
* @author caofanqi
*/
public interface AccountRepository extends JpaRepositoryImplementation<Account,Long> { Account findByAccountName(String accountName); }

Service:

/**
*
* @author caofanqi
*/
@Service
public class AccountServiceImpl implements AccountService { @Resource
private AccountRepository accountRepository; @Override
@Transactional(rollbackFor = Exception.class)
public String addAccountMoney(String accountName, BigDecimal money){ System.out.println(Thread.currentThread().getName() + ",addAccountMoney start...");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Account account = accountRepository.findByAccountName(accountName);
System.out.println(Thread.currentThread().getName() + ",find balance : " + account.getBalance());
account.setBalance(account.getBalance().add(money)); try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Account result = accountRepository.save(account);
System.out.println(Thread.currentThread().getName() + ", update balance end ,balance : " + result.getBalance()); System.out.println(Thread.currentThread().getName() + ",addAccountMoney sleep...");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
} System.out.println(Thread.currentThread().getName() + ",addAccountMoney end..."); return "success";
} }

数据库表中数据:

  

测试用例:

    @Test
void addAccountMoney() throws InterruptedException { CountDownLatch count = new CountDownLatch(2); ExecutorService executorService = Executors.newFixedThreadPool(2); executorService.execute(() -> {
String result = accountService.addAccountMoney("张三的账户", BigDecimal.valueOf(-50));
System.out.println(Thread.currentThread().getName() + ",result : " + result);
count.countDown();
}); TimeUnit.SECONDS.sleep(1); executorService.execute(() -> {
String result = accountService.addAccountMoney("张三的账户", BigDecimal.valueOf(100));
System.out.println(Thread.currentThread().getName() + ",result : " + result);
count.countDown();
}); count.await(10, TimeUnit.SECONDS); Account endAccount = accountRepository.findByAccountName("张三的账户");
System.out.println("final balance :" + endAccount.getBalance()); }

控制台打印及数据库结果:

  

  这明显不是我们想要的正确答案,那怎么解决呢?这里提供几个方法,①如果是单JVM的话,可以使用Java的同步机制和Lock(估计这种情况很少见吧...)。②使用JPA为我们提供的乐观锁@Version。

③使用JPA为我们提供的@Lock中的悲观锁。

2、@Version

  JPA提供的乐观锁,指定实体中的字段或属性作为乐观锁的version,该version用于确保并发操作的正确性。每个实体只能使用一个version属性或字段。version支持(int, Integer, short, Short, long, Long, java.sql.Timestamp)类型的属性或字段。

  使用起来非常方便,我们只需要在实体中添加一个字段,并添加@Version注解就可以了。加了@Version后,insert和update的SQL语句都会带上version的操作。当乐观锁更新失败的时候,会抛出异常org.springframework.orm.ObjectOptimisticLockingFailureException。我们自己进行业务处理。

实体修改如下:

/**
* 账户实体
*
* @author caofanqi
*/
@Slf4j
@Data
@EqualsAndHashCode(callSuper = true)
@Entity
@Builder
@Table(name = "jpa_account")
@NoArgsConstructor
@AllArgsConstructor
public class Account extends AbstractID { /**
* 简单代表一下账户所属人
*/
private String accountName; @Column(columnDefinition = "DECIMAL(19, 2)")
private BigDecimal balance; /**
* 乐观锁version
*/
@Version
private Integer version; }

重新插入一条数据,可以看到数据库中如下

  

修改Service方法如下:

    @Override
@Transactional(rollbackFor = Exception.class)
public String addAccountMoney(String accountName, BigDecimal money){ try {
updateAccount(accountName, money);
return "success";
}catch (ObjectOptimisticLockingFailureException e){
//记录日志,重新操作...
return "fail";
} } @Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
public void updateAccount(String accountName, BigDecimal money) {
System.out.println(Thread.currentThread().getName() + ",addAccountMoney start...");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Account account = accountRepository.findByAccountName(accountName);
System.out.println(Thread.currentThread().getName() + ",find balance : " + account.getBalance());
account.setBalance(account.getBalance().add(money)); try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Account result = accountRepository.save(account);
System.out.println(Thread.currentThread().getName() + ", update balance end ,balance : " + result.getBalance()); System.out.println(Thread.currentThread().getName() + ",addAccountMoney sleep...");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
} System.out.println(Thread.currentThread().getName() + ",addAccountMoney end...");
}

重新运行测试用例:

 

  这样只有和我们上次版本一样的时候才会更新,就不会出现互相覆盖的问题,保证了数据的原子性。但是如果我们的业务就是需要让两次都必须成功,那么可以使用下面的悲观锁来实现。

3、@Lock

      spring-data-jpa为我们提供了@Lock注解,指定查询方法要使用的锁定模式。可以添加在派生查询上,也可以重写父类CRUD的方法,添加该注解。@Lock只有一个value属性,为LockModeType枚举类型,我们主要看以下里面的悲观锁PESSIMISTIC_WRITE。

  修改Repository如下:

/**
* @author caofanqi
*/
public interface AccountRepository extends JpaRepositoryImplementation<Account,Long> { @Lock(LockModeType.PESSIMISTIC_WRITE)
Account findByAccountName(String accountName); }

  恢复数据库表数据为100,并将@Version注解去掉,运行测试用例控制台打印如下:

pool-1-thread-1,addAccountMoney start...
pool-1-thread-2,addAccountMoney start...
Hibernate: select account0_.id as id1_0_, account0_.account_name as account_2_0_, account0_.balance as balance3_0_, account0_.version as version4_0_ from cfq_jpa_account account0_ where account0_.account_name=? for update
pool-1-thread-1,find balance : 100.00
Hibernate: select account0_.id as id1_0_, account0_.account_name as account_2_0_, account0_.balance as balance3_0_, account0_.version as version4_0_ from cfq_jpa_account account0_ where account0_.account_name=? for update
pool-1-thread-1, update balance end ,balance : 50.00
pool-1-thread-1,addAccountMoney sleep...
pool-1-thread-1,addAccountMoney end...
Hibernate: update cfq_jpa_account set account_name=?, balance=?, version=? where id=?
pool-1-thread-2,find balance : 50.00
pool-1-thread-1,result : success
pool-1-thread-2, update balance end ,balance : 150.00
pool-1-thread-2,addAccountMoney sleep...
Hibernate: select account0_.id as id1_0_, account0_.account_name as account_2_0_, account0_.balance as balance3_0_, account0_.version as version4_0_ from cfq_jpa_account account0_ where account0_.account_name=? for update
pool-1-thread-2,addAccountMoney end...
Hibernate: update cfq_jpa_account set account_name=?, balance=?, version=? where id=?
final balance :150.00
2019-12-08 17:20:43.915 INFO 4160 --- [ main] o.s.t.c.transaction.TransactionContext : Committed transaction for test: [DefaultTestContext@7674f035 testClass = AccountServiceImplTest, testInstance = cn.caofanqi.study.studyspringdatajpa.service.impl.AccountServiceImplTest@46d69ca4, testMethod = addAccountMoney@AccountServiceImplTest, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@69e153c5 testClass = AccountServiceImplTest, locations = '{}', classes = '{class cn.caofanqi.study.studyspringdatajpa.StudySpringDataJpaApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@9353778, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@1700915, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@31c88ec8, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@20ce78ec], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true]]

  可以看到查询语句通过for update进行加锁。得到了我们想要的150结果。

  注意:for update ,如果不通过索引条件检索数据,那么InnoDB将对表中的所有记录加锁,实际效果跟锁表一样。我们进行测试,在数据库中在添加一条记录,如下:

  

  执行下面测试用例:

    /**
* for update ,如果不通过索引条件检索数据,那么InnoDB将对表中的所有记录加锁,实际效果跟锁表一样
*/
@Test
void addAccountMoney2() throws InterruptedException { CountDownLatch count = new CountDownLatch(2); ExecutorService executorService = Executors.newFixedThreadPool(2); executorService.execute(() -> {
String result = accountService.addAccountMoney("张三的账户", BigDecimal.valueOf(-50));
System.out.println(Thread.currentThread().getName() + ",result : " + result);
count.countDown();
}); TimeUnit.SECONDS.sleep(1); executorService.execute(() -> {
String result = accountService.addAccountMoney("李四的账户", BigDecimal.valueOf(100));
System.out.println(Thread.currentThread().getName() + ",result : " + result);
count.countDown();
}); count.await(20, TimeUnit.SECONDS); }

控制台打印结果:

可以看到并不是并行进行的更新,我们就该实体类,重新生成数据库表,并插入数据(或直接修改数据库)

/**
* 账户实体
*
* @author caofanqi
*/
@Slf4j
@Data
@EqualsAndHashCode(callSuper = true)
@Entity
@Builder
@Table(name = "jpa_account")
@NoArgsConstructor
@AllArgsConstructor
public class Account extends AbstractID { /**
* 简单代表一下账户所属人
*/
@Column(unique = true,nullable = false)
private String accountName; @Column(columnDefinition = "DECIMAL(19, 2)")
private BigDecimal balance; /**
* 乐观锁version
*/
// @Version
private Integer version; }

   

重新运行测试用例:

  我们在使用的过程中要根据自己的业务进行选择。

参考连接:https://blog.csdn.net/u014316026/article/details/78726459

       https://blog.csdn.net/loophome/article/details/79867174

源码地址:https://github.com/caofanqi/study-spring-data-jpa

学习Spring-Data-Jpa(十六)---@Version与@Lock的更多相关文章

  1. 学习Spring Data JPA

    简介 Spring Data 是spring的一个子项目,在官网上是这样解释的: Spring Data 是为数据访问提供一种熟悉且一致的基于Spring的编程模型,同时仍然保留底层数据存储的特​​殊 ...

  2. 学习-spring data jpa

    spring data jpa对照表 Keyword Sample JPQL snippet And findByLastnameAndFirstname - where x.lastname = ? ...

  3. 快速搭建springmvc+spring data jpa工程

    一.前言 这里简单讲述一下如何快速使用springmvc和spring data jpa搭建后台开发工程,并提供了一个简单的demo作为参考. 二.创建maven工程 http://www.cnblo ...

  4. 【Spring Data 系列学习】了解 Spring Data JPA 、 Jpa 和 Hibernate

    在开始学习 Spring Data JPA 之前,首先讨论下 Spring Data Jpa.JPA 和 Hibernate 之前的关系. JPA JPA 是 Java Persistence API ...

  5. Spring Boot入门系列(二十六)超级简单!Spring Data JPA 的使用!

    之前介绍了Mybatis数据库ORM框架,也介绍了使用Spring Boot 的jdbcTemplate 操作数据库.其实Spring Boot 还有一个非常实用的数据操作框架:Spring Data ...

  6. 解决neo4j @Transactional 与Spring data jpa @Transactional 冲突问题,@CreatedBy,@CreatedDate,@LastModifiedBy,@LastModifiedDate,以及解决@Version失效问题

    之前mybatis特别流行,所以前几个项目都是用@SelectProvider,@InsertProvider,@UpdateProvider,@DeleteProvider 加反射泛型封装了一些通用 ...

  7. 一步步学习 Spring Data 系列之JPA(二)

    继上一篇文章对Spring Data JPA更深( )一步剖析. 上一篇只是简单的介绍了Spring Data JPA的简单使用,而往往在项目中这一点功能并不能满足我们的需求.这是当然的,在业务中查询 ...

  8. Spring Data JPA 学习记录1 -- 单向1:N关联的一些问题

    开新坑 开新坑了(笑)....公司项目使用的是Spring Data JPA做持久化框架....学习了一段时间以后发现了一点值得注意的小问题.....与大家分享 主要是针对1:N单向关联产生的一系列问 ...

  9. spring data jpa入门学习

    本文主要介绍下spring data jpa,主要聊聊为何要使用它进行开发以及它的基本使用.本文主要是入门介绍,并在最后会留下完整的demo供读者进行下载,从而了解并且开始使用spring data ...

随机推荐

  1. 抽象工厂(AbstractFactory)模式

    抽象工厂模式又称工具箱模式.其实抽象工厂模式可以简单的理解为一个工厂生成一个产品族的产品. 抽象工厂模式可以向客户端提供一个接口,使得客户端在不指定产品的具体类型的情况下,创建多个产品族中的产品对象. ...

  2. Django框架之第八篇(模型层补充)--数据库的查询与优化:only/defer,select_related与prefetch_related,事务

    在设置外键字段时需要注意: 当你使用django2.x的版本时候,在建立外键关系时,需要你手动添加几个关键点参数 models.cascade #设置级联删除 db_constraints 数据库查询 ...

  3. C语言学习笔记01——C语言概述

    作者:Eventi 出处:http://www.cnblogs.com/Eventi 欢迎转载,也请保留这段声明.谢谢! 1 C语言的起源 1972年,贝尔实验室的丹尼斯·里奇(Dennis Ritc ...

  4. libevent源码分析二--timeout事件响应

    libevent不仅支持io事件,同时还支持timeout事件与signal事件,这篇文件将分析libevent是如何组织timeout事件以及如何响应timeout事件. 1.  min_heap ...

  5. Spring概述学习笔记

    1.Spring概述 Spring致力于J2EE应用的各种解决方案,而不仅仅专注于某一层的方案. 可以说,Spring是企业应用开发的一站式选择,他贯穿于表现层.业务层.持久层. Spring并不想取 ...

  6. DEDE5.5招聘模板

    <channel:id>18</channel:id> <channel:nid>zhaopin</channel:nid> <channel:t ...

  7. 利用nfs-client-provisioner动态提供Kubernetes后端存储卷

    原文:https://www.kubernetes.org.cn/3894.html 利用NFS client provisioner动态提供Kubernetes后端存储卷 本文翻译自nfs-clie ...

  8. flask 与 flask_migrate的使用

    flask 与 flask_migrate的使用 一.安装 pip install Flask-Migrate 二.简单使用 # 文件:manage.py from flask_migrate imp ...

  9. mysql-多表联查(实例)

    目录 多表查询 笛卡尔积查询 内连接查询 左外连接查询 右外连接查询 全外连接查询 多表查询 笛卡尔积查询 笛卡尔积查询:就是两张表相乘,若左边表有M条信息,右边表有N条信息,那么查询显示的信息总共为 ...

  10. Commander基本使用

    随着NodeJs的不断发展,对于前端来说要做的东西也就更多,Vue脚手架React脚手架等等等一系列的东西都脱颖而出,进入到人们的视野当中,对于这些脚手架工具来讲也只是停留在应用阶段,从来没有想过脚手 ...