热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

springboot基于数据库的乐观锁实现

悲观锁与乐观锁比

何谓悲观锁与乐观锁
  • 悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

  • 乐观锁

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

两种锁的使用场景

从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

乐观锁常见的两种实现方式
  1. 版本号机制

一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

举一个简单的例子:

更新帐号余额,有如下表:

线程1:扣款操作,读取id为100的用户,当前版本为1;

线程2:充值操作,读取id为100的用户,当前版本为1;

线程2:执行更新操作,update t_account set mOney= money + 10,version=version+1 where id = 100 and version = 1。执行成功此时数据库中id为100的帐号信息如下:

此时当前账户的version已经被更新成2了。

线程1:执行更新操作,update t_account set mOney= money - 10,version = version + 1 where id= 1,执行失败了。当线程1在执行此更新操作的时候version字段已经变成了2,所以更新失败了。

通过这种机制来保证了数据的安全。

  1. CAS算法

compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数

  • 需要读写的内存值 V

  • 进行比较的值 A

  • 拟写入的新值 B

当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试

  • 乐观锁的缺点

ABA 问题是乐观锁一个常见的问题

  • ABA 问题

如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。

JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。


接下来通过基于数据库版本号的方式来实现乐观锁。

环境:springboot2.3.6.RELEASE + spring data jpa

  • 配置

pom.xml依赖包

<dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-data-jpaartifactId> dependency> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-webartifactId> dependency> <dependency> <groupId>org.mybatis.spring.bootgroupId> <artifactId>mybatis-spring-boot-starterartifactId> <version>2.1.4version> dependency> <dependency> <groupId>mysqlgroupId> <artifactId>mysql-connector-javaartifactId> <scope>runtimescope> dependency>

application.yml

spring: datasource: driverClassName: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/testjpa?serverTimezOne=GMT%2B8 username: root password: xxxxxx type: com.zaxxer.hikari.HikariDataSource hikari: minimumIdle: 10 maximumPoolSize: 200 autoCommit: true idleTimeout: 30000 poolName: MasterDatabookHikariCP maxLifetime: 1800000 connectionTimeout: 30000 connectionTestQuery: SELECT 1---spring: jpa: generateDdl: false hibernate: ddlAuto: update openInView: true show-sql: true

  • 实体对象

@Entity@Table(name = "t_account")public class Account { @Id private Long id; private String userId; private BigDecimal money; @Version private Integer version ;}

注意这里的version字段加了@Version注解,以实现乐观锁。

  • Service

@Servicepublic class AccountService { @Resource private AccountDAO accountDAO ; /** * 扣款操作 * @param id * @param money */ @Transactional public Account deduction(Long id, BigDecimal money) { Account account = accountDAO.findById(id).orElse(null) ; try { TimeUnit.SECONDS.sleep(3) ; } catch (InterruptedException e) {} if (account != null) { account.setMoney(account.getMoney().subtract(money)) ; return accountDAO.saveAndFlush(account) ; } return null ; } /** * 充值操作 * @param id * @param money * @return */ @Transactional public Account recharge(Long id, BigDecimal money) { Account account = accountDAO.findById(id).orElse(null) ; if (account != null) { account.setMoney(account.getMoney().add(money)) ; return accountDAO.saveAndFlush(account) ; } return null ; } }

扣款操作deduction方法做了睡眠操作,为了模拟效果。这里你不能使用getOne方法获取Account对象,getOne方法返回的是代理对象,只有你真正去用的时候才去数据库中做查询。

源码:

这里的getReference方法:EntityManager会创建一个新的实体,但是不会立即访问数据库来加载持久状态,而是在第一次访问某个属性的时候才加载。此外,getReference()方法不返回null,如果数据库找不到相应的实体,这个方法会抛出javax.persistence.EntityNotFoundException。

  • 测试

@SpringBootTest@RunWith(SpringRunner.class)public class SpringBootLockRetryApplicationTests { @Resource private AccountService accountService ; private CountDownLatch cdl = new CountDownLatch(2) ; @Test public void testMoneyOperator() { Thread t1 = new Thread(() -> { accountService.deduction(100L, BigDecimal.valueOf(10)) ; cdl.countDown() ; }) ; Thread t2 = new Thread(() -> { accountService.recharge(100L, BigDecimal.valueOf(10)) ; cdl.countDown() ; }) ; t1.start() ; t2.start() ; try { cdl.await(); } catch (InterruptedException e) { e.printStackTrace(); } }}

运行结果:

注意这里的sql语句和上面所说的基于版本号实现的乐观锁一样更新时需要更新版本号和比对当前数据库中的版本号和当前持有的版本是否相同。

接下来通过AOP来处理这种乐观锁的异常,这里通过AOP拦截有特定注解的方法进行重试。

自定义需要重试的注解类:

@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface RetryProcess { int value() default 3 ; }

默认重试3次,这里可以自定义重试的测试。

修改service类:

这里模拟的是扣款操作发生异常,那这里需要修改扣款的操作添加注解及事务注解不能在这里添加,需要放到AOP类的Around方法上。

/** * 扣款操作 * @param id * @param money */ @RetryProcess public Account deduction(Long id, BigDecimal money) { Account account = accountDAO.findById(id).orElse(null) ; try { TimeUnit.SECONDS.sleep(3) ; } catch (InterruptedException e) {} if (account != null) { account.setMoney(account.getMoney().subtract(money)) ; return accountDAO.saveAndFlush(account) ; } return null ; }

AOP注解类:

@Component@Aspectpublic class RetryAspect { private int max_retry_times = 3 ; private static Logger logger = LoggerFactory.getLogger(RetryAspect.class) ; @Pointcut("@annotation(com.pack.annotation.RetryProcess)") public void retry() {} @Around("retry()") @Transactional public Object arround(ProceedingJoinPoint pjp) throws Throwable { MethodSignature msig = (MethodSignature) pjp.getSignature(); Class[] parameterTypes = msig.getMethod().getParameterTypes(); Method method = pjp.getTarget().getClass().getMethod(pjp.getSignature().getName(), parameterTypes); this.max_retry_times = method.getAnnotation(RetryProcess.class).value() ; int attempts = 0 ; Object result = null ; do { attempts++; try { result = pjp.proceed(); return result ; } catch (Exception e) { e.printStackTrace() ; if(e instanceof ObjectOptimisticLockingFailureException || e instanceof StaleObjectStateException) { logger.info("retrying....times:{}", attempts); if(attempts > max_retry_times) { logger.info("retry excceed the max times.."); throw e; } } } } while (attempts return result ; } }

注意这里的arround方法添加了@Transactional 如果不添加到这里会发生如下错误:

2020-12-08 15:33:07.568 INFO 19340 --- [ Thread-3] com.pack.aop.retry.RetryAspect : retrying....times:3Exception in thread "Thread-3" org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:752) at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:711) at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:633) at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:386) at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:118) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:95) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691) at com.pack.service.AccountService$$EnhancerBySpringCGLIB$$934e6b25.deduction(<generated>) at com.pack.SpringBootLockRetryApplicationTests.lambda$0(SpringBootLockRetryApplicationTests.java:26) at java.lang.Thread.run(Thread.java:745)

意思是:之前的事务发生了错误并且将事务设置了 rollback-only了,但是这个异常并没有被抛出导致执行到最后执行了commit,所以会出现这个错误。

再次测试:

Hibernate: select account0_.id as id1_0_0_, account0_.money as money2_0_0_, account0_.user_id as user_id3_0_0_, account0_.version as version4_0_0_ from t_account account0_ where account0_.id=?Hibernate: select account0_.id as id1_0_0_, account0_.money as money2_0_0_, account0_.user_id as user_id3_0_0_, account0_.version as version4_0_0_ from t_account account0_ where account0_.id=?Hibernate: select account0_.id as id1_0_0_, account0_.money as money2_0_0_, account0_.user_id as user_id3_0_0_, account0_.version as version4_0_0_ from t_account account0_ where account0_.id=?Hibernate: update t_account set mOney=?, version=? where id=? and version=?Hibernate: select account0_.id as id1_0_0_, account0_.money as money2_0_0_, account0_.user_id as user_id3_0_0_, account0_.version as version4_0_0_ from t_account account0_ where account0_.id=?org.springframework.orm.ObjectOptimisticLockingFailureException: Object of class [com.pack.domain.Account] with identifier [100]: optimistic locking failed; nested exception is org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.pack.domain.Account#100] at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:337) at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:255) at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:528) at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212) at com.sun.proxy.$Proxy95.saveAndFlush(Unknown Source) at com.pack.service.AccountService.deduction(AccountService.java:34) at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:88) at com.pack.aop.retry.RetryAspect.arround(RetryAspect.java:41) at java.lang.Thread.run(Thread.java:745)Caused by: org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.pack.domain.Account#100] at org.hibernate.event.internal.DefaultMergeEventListener.entityIsDetached(DefaultMergeEventListener.java:341) at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:172) at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:70) at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:102) at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:118) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:139) ... 31 more2020-12-08 15:43:47.975 INFO 12412 --- [ Thread-3] com.pack.aop.retry.RetryAspect : retrying....times:1Hibernate: select account0_.id as id1_0_0_, account0_.money as money2_0_0_, account0_.user_id as user_id3_0_0_, account0_.version as version4_0_0_ from t_account account0_ where account0_.id=?Hibernate: select account0_.id as id1_0_0_, account0_.money as money2_0_0_, account0_.user_id as user_id3_0_0_, account0_.version as version4_0_0_ from t_account account0_ where account0_.id=?Hibernate: update t_account set mOney=?, version=? where id=? and version=?2020-12-08 15:43:51.022 INFO 12412 --- [extShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'2020-12-08 15:43:51.030 INFO 12412 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'2020-12-08 15:43:51.031 INFO 12412 --- [extShutdownHook] com.zaxxer.hikari.HikariDataSource : MasterDatabookHikariCP - Shutdown initiated...2020-12-08 15:43:51.053 INFO 12412 --- [extShutdownHook] com.zaxxer.hikari.HikariDataSource : MasterDatabookHikariCP - Shutdown completed.

控制台输出了:com.pack.aop.retry.RetryAspect : retrying....times:1

重试了一次成功了。查看数据:

钱没有发生变化(扣减 都是10元),版本变成了3。

完毕!!!

来源:

https://www.toutiao.com/i6903789542680068611/

“IT大咖说”欢迎广大技术人员投稿,投稿邮箱:aliang@itdks.com

来都来了,走啥走,留个言呗~

 IT大咖说  |  关于版权 

由“IT大咖说(ID:itdakashuo)”原创的文章,转载时请注明作者、出处及微信公众号。投稿、约稿、转载请加微信:ITDKS10(备注:投稿),茉莉小姐姐会及时与您联系!

感谢您对IT大咖说的热心支持!

相关推荐

推荐文章

  • 微服务架构即将被淘汰

  • 如何写好业务代码?

  • 高效交付的秘诀,开源 DevOps 运维平台合集

  • 海量订单系统微服务开发:使用MongoDB支持海量数据

  • 微服务网关——设计篇

  • 一个开源的密码管理器



推荐阅读
  • Spring框架《一》简介
    Spring框架《一》1.Spring概述1.1简介1.2Spring模板二、IOC容器和Bean1.IOC和DI简介2.三种通过类型获取bean3.给bean的属性赋值3.1依赖 ... [详细]
  • Spring常用注解(绝对经典),全靠这份Java知识点PDF大全
    本文介绍了Spring常用注解和注入bean的注解,包括@Bean、@Autowired、@Inject等,同时提供了一个Java知识点PDF大全的资源链接。其中详细介绍了ColorFactoryBean的使用,以及@Autowired和@Inject的区别和用法。此外,还提到了@Required属性的配置和使用。 ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • 本文介绍了Java工具类库Hutool,该工具包封装了对文件、流、加密解密、转码、正则、线程、XML等JDK方法的封装,并提供了各种Util工具类。同时,还介绍了Hutool的组件,包括动态代理、布隆过滤、缓存、定时任务等功能。该工具包可以简化Java代码,提高开发效率。 ... [详细]
  • 本文讨论了在Spring 3.1中,数据源未能自动连接到@Configuration类的错误原因,并提供了解决方法。作者发现了错误的原因,并在代码中手动定义了PersistenceAnnotationBeanPostProcessor。作者删除了该定义后,问题得到解决。此外,作者还指出了默认的PersistenceAnnotationBeanPostProcessor的注册方式,并提供了自定义该bean定义的方法。 ... [详细]
  • 在springmvc框架中,前台ajax调用方法,对图片批量下载,如何弹出提示保存位置选框?Controller方法 ... [详细]
  • 本文由编程笔记小编整理,主要介绍了使用Junit和黄瓜进行自动化测试中步骤缺失的问题。文章首先介绍了使用cucumber和Junit创建Runner类的代码,然后详细说明了黄瓜功能中的步骤和Steps类的实现。本文对于需要使用Junit和黄瓜进行自动化测试的开发者具有一定的参考价值。摘要长度:187字。 ... [详细]
  • 2018深入java目标计划及学习内容
    本文介绍了作者在2018年的深入java目标计划,包括学习计划和工作中要用到的内容。作者计划学习的内容包括kafka、zookeeper、hbase、hdoop、spark、elasticsearch、solr、spring cloud、mysql、mybatis等。其中,作者对jvm的学习有一定了解,并计划通读《jvm》一书。此外,作者还提到了《HotSpot实战》和《高性能MySQL》等书籍。 ... [详细]
  • 开发笔记:spring boot项目打成war包部署到服务器的步骤与注意事项
    本文介绍了将spring boot项目打成war包并部署到服务器的步骤与注意事项。通过本文的学习,读者可以了解到如何将spring boot项目打包成war包,并成功地部署到服务器上。 ... [详细]
  • Java如何导入和导出Excel文件的方法和步骤详解
    本文详细介绍了在SpringBoot中使用Java导入和导出Excel文件的方法和步骤,包括添加操作Excel的依赖、自定义注解等。文章还提供了示例代码,并将代码上传至GitHub供访问。 ... [详细]
  • 测绘程序设计Excel度分秒转换模板附代码超实用版
    本文介绍了测绘程序设计Excel度分秒转换模板附代码超实用版的相关知识,包括准备工作、编写表达式和注意事项。在实际工作中,将GPS实测的经纬度度转换为度分秒是常见需求,本文提供了在Excel中快速进行转换的方法,以提高工作效率。 ... [详细]
  • 原文地址:https:www.cnblogs.combaoyipSpringBoot_YML.html1.在springboot中,有两种配置文件,一种 ... [详细]
  • 重入锁(ReentrantLock)学习及实现原理
    本文介绍了重入锁(ReentrantLock)的学习及实现原理。在学习synchronized的基础上,重入锁提供了更多的灵活性和功能。文章详细介绍了重入锁的特性、使用方法和实现原理,并提供了类图和测试代码供读者参考。重入锁支持重入和公平与非公平两种实现方式,通过对比和分析,读者可以更好地理解和应用重入锁。 ... [详细]
  • 本文介绍了Java集合库的使用方法,包括如何方便地重复使用集合以及下溯造型的应用。通过使用集合库,可以方便地取用各种集合,并将其插入到自己的程序中。为了使集合能够重复使用,Java提供了一种通用类型,即Object类型。通过添加指向集合的对象句柄,可以实现对集合的重复使用。然而,由于集合只能容纳Object类型,当向集合中添加对象句柄时,会丢失其身份或标识信息。为了恢复其本来面貌,可以使用下溯造型。本文还介绍了Java 1.2集合库的特点和优势。 ... [详细]
author-avatar
沈巛小糖meimei昌策_247
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有