专栏名称: java那些事
分享java开发中常用的技术,分享软件开发中各种新技术的应用方法。每天推送java技术相关或者互联网相关文章。关注“java那些事”,让自己做一个潮流的java技术人!《java程序员由笨鸟到菜鸟》系列文章火热更新中。
目录
相关文章推荐
芋道源码  ·  四步帮你把Controller 的代码变得简洁 ·  2 天前  
芋道源码  ·  老板爱瞎改权限怎么办:注解+AOP ... ·  2 天前  
芋道源码  ·  腾讯开源:零代码、全功能、强安全 ORM 库 ·  2 天前  
Java编程精选  ·  国产 DeepSeek V3 ... ·  4 天前  
Java编程精选  ·  手把手教你Java文件断点下载 ·  3 天前  
51好读  ›  专栏  ›  java那些事

聊聊数据库乐观锁和悲观锁

java那些事  · 公众号  · Java  · 2019-02-15 16:00

正文

作者:黄青石

链接:www.cnblogs.com/huangqingshi


在写入数据库的时候需要有锁,比如同时写入数据库的时候会出现丢数据,那么就需要锁机制。

数据锁分为乐观锁和悲观锁

它们使用的场景如下:

  • 乐观锁适用于写少读多的情景,因为这种乐观锁相当于JAVA的CAS,所以多条数据同时过来的时候,不用等待,可以立即进行返回。

  • 悲观锁适用于写多读少的情景,这种情况也相当于JAVA的synchronized,reentrantLock等,大量数据过来的时候,只有一条数据可以被写入,其他的数据需要等待。执行完成后下一条数据可以继续。

他们实现的方式上有所不同。

乐观锁采用版本号的方式,即当前版本号如果对应上了就可以写入数据,如果判断当前版本号不一致,那么就不会更新成功,

比如


  1. update table set column = value

  2. where version = $ { version } and otherKey = $ { otherKey }


悲观锁实现的机制一般是在执行更新语句的时候采用for update方式,

比如


  1. update table set column = 'value' for update


这种情况where条件呢一定要涉及到数据库对应的索引字段,这样才会是行级锁,否则会是表锁,这样执行速度会变慢。

下面我就弄一个spring boot(springboot 2.1.1 + mysql + lombok + aop + jpa)工程,然后逐渐的实现乐观锁和悲观锁。

假设有一个场景,有一个catalog商品目录表,然后还有一个browse浏览表,假如一个商品被浏览了,那么就需要记录下浏览的user是谁,并且记录访问的总数。

表的结构非常简单:


  1. create table catalog (

  2. id int ( 11 ) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键' ,

  3. name varchar ( 50 ) NOT NULL DEFAULT '' COMMENT '商品名称' ,

  4. browse_count int ( 11 ) NOT NULL DEFAULT 0 COMMENT '浏览数' ,

  5. version int ( 11 ) NOT NULL DEFAULT 0 COMMENT '乐观锁,版本号' ,

  6. PRIMARY KEY ( id )

  7. ) ENGINE = INNODB DEFAULT CHARSET = utf8 ;


  8. CREATE table browse (

  9. id int ( 11 ) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键' ,

  10. cata_id int ( 11 ) NOT NULL COMMENT '商品ID' ,

  11. user varchar ( 50 ) NOT NULL DEFAULT '' COMMENT '' ,

  12. create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间' ,

  13. PRIMARY KEY ( id )

  14. ) ENGINE = INNODB DEFAULT CHARSET = utf8 ;


POM.XML的依赖如下:


  1. xml version = "1.0" encoding = "UTF-8" ?>

  2. xmlns = "http://maven.apache.org/POM/4.0.0" xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"

  3. xsi:schemaLocation = "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" >

  4. 4.0.0

  5. org.springframework.boot

  6. spring-boot-starter-parent

  7. 2.1.1.RELEASE

  8. com.hqs

  9. dblock

  10. 1.0-SNAPSHOT

  11. dblock

  12. Demo project for Spring Boot


  13. 1.8


  14. org.springframework.boot

  15. spring-boot-starter-web


  16. org.springframework.boot

  17. spring-boot-devtools

  18. runtime

  19. mysql

  20. mysql-connector-java

  21. runtime

  22. org.springframework.boot

  23. spring-boot-starter-test

  24. test

  25. org.springframework.boot

  26. spring-boot-starter-data-jpa

  27. mysql

  28. mysql-connector-java

  29. org.projectlombok

  30. lombok

  31. true


  32. org.aspectj

  33. aspectjweaver

  34. 1.8.4



  35. org.springframework.boot

  36. spring-boot-maven-plugin



项目的结构如下:

介绍一下项目的结构的内容:

  • entity包: 实体类包。

  • repository包:数据库repository

  • service包: 提供服务的service

  • controller包: 控制器写入用于编写requestMapping。相关请求的入口类

  • annotation包: 自定义注解,用于重试。

  • aspect包: 用于对自定义注解进行切面。

  • DblockApplication: springboot的启动类。

  • DblockApplicationTests: 测试类。

咱们看一下核心代码的实现,参考如下,使用dataJpa非常方便,集成了CrudRepository就可以实现简单的CRUD,非常方便,有兴趣的同学可以自行研究。

实现乐观锁的方式有两种:

  1. 更新的时候将version字段传过来,然后更新的时候就可以进行version判断,如果version可以匹配上,那么就可以更新(方法:updateCatalogWithVersion)。

  2. 在实体类上的version字段上加入version,可以不用自己写SQL语句就可以它就可以自行的按照version匹配和更新,是不是很简单。


  1. public interface CatalogRepository extends CrudRepository < Catalog , Long > {


  2. @Query ( value = "select * from Catalog a where a.id = :id for update" , nativeQuery = true )

  3. Optional < Catalog > findCatalogsForUpdate ( @Param ( "id" ) Long id );


  4. @Lock ( value = LockModeType . PESSIMISTIC_WRITE ) //代表行级锁

  5. @Query ( "select a from Catalog a where a.id = :id" )

  6. Optional < Catalog > findCatalogWithPessimisticLock ( @Param ( "id" ) Long id );


  7. @Modifying ( clearAutomatically = true ) //修改时需要带上

  8. @Query ( value = "update Catalog set browse_count = :browseCount, version = version + 1 where id = :id " +

  9. "and version = :version" , nativeQuery = true )

  10. int updateCatalogWithVersion ( @Param ( "id" ) Long id , @Param ( "browseCount" ) Long browseCount , @Param ( "version" ) Long version );


  11. }


实现悲观锁的时候也有两种方式:

  1. 自行写原生SQL,然后写上for update语句。(方法:findCatalogsForUpdate)

  2. 使用@Lock注解,并且设置值为LockModeType.PESSIMISTIC_WRITE即可代表行级锁。

还有我写的测试类,方便大家进行测试:


  1. package com . hqs . dblock ;


  2. import org . junit . Test ;

  3. import org . junit . runner . RunWith ;

  4. import org . springframework . beans . factory . annotation . Autowired ;

  5. import org . springframework . boot . test . context . SpringBootTest ;

  6. import org . springframework . boot . test . web . client . TestRestTemplate ;

  7. import org . springframework . test . context . junit4 . SpringRunner ;

  8. import org . springframework . util . LinkedMultiValueMap ;

  9. import org . springframework . util . MultiValueMap ;


  10. @RunWith ( SpringRunner . class )

  11. @SpringBootTest ( classes = DblockApplication . class , webEnvironment = SpringBootTest . WebEnvironment . RANDOM_PORT )

  12. public class DblockApplicationTests {


  13. @Autowired

  14. private TestRestTemplate testRestTemplate ;


  15. @Test

  16. public void browseCatalogTest () {

  17. String url = "http://localhost:8888/catalog" ;

  18. for ( int i = 0 ; i < 100 ; i ++) {

  19. final int num = i ;

  20. new Thread (() -> {

  21. MultiValueMap < String , String > params = new LinkedMultiValueMap <>();

  22. params . add ( "catalogId" , "1" );

  23. params . add ( "user" , "user" + num );

  24. String result = testRestTemplate . postForObject ( url , params , String . class );

  25. System . out . println ( "-------------" + result );

  26. }

  27. ). start ();

  28. }

  29. }


  30. @Test

  31. public void browseCatalogTestRetry () {

  32. String url = "http://localhost:8888/catalogRetry" ;

  33. for ( int i = 0 ; i < 100 ; i ++) {

  34. final int num = i ;

  35. new Thread (() -> {

  36. MultiValueMap < String , String > params = new LinkedMultiValueMap <>();

  37. params . add ( "catalogId" , "1" );

  38. params . add ( "user" , "user" + num );

  39. String result = testRestTemplate . postForObject ( url , params , String . class );

  40. System . out . println ( "-------------" + result );

  41. }

  42. ). start ();

  43. }

  44. }

  45. }


调用100次,即一个商品可以浏览一百次,采用悲观锁,catalog表的数据都是100,并且browse表也是100条记录。采用乐观锁的时候,因为版本号的匹配关系,那么会有一些记录丢失,但是这两个表的数据是可以对应上的。

乐观锁失败后会抛出ObjectOptimisticLockingFailureException,那么我们就针对这块考虑一下重试,下面我就自定义了一个注解,用于做切面。


  1. package com . hqs . dblock . annotation ;


  2. import







请到「今天看啥」查看全文