解决并发问题,数据库常用的两把锁

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

它们使用的场景如下:

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

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

 

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

update table set column = value where  version=${version} 
and otherKey = ${otherKey}

B.悲观锁实现的机制一般是在执行更新语句的时候采用for update方式,这种情况where条件呢一定要涉及到数据库对应的索引字段,这样才会是行级锁,否则会是表锁,这样执行速度会变慢,比如

update table set  column='value'  for  update

 

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

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

1.创建数据库表的结构非常简单:  

create table catalog  (





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





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





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





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





PRIMARY KEY(id)





) ENGINE=INNODB DEFAULT CHARSET=utf8;







CREATE table browse (





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





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





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





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





PRIMARY KEY(id)





) ENGINE=INNODB DEFAULT CHARSET=utf8;
View Code

2.配置Pom.xml

<?xml version=
"1.0"
 encoding=
"UTF-8"
?>





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





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





    
<modelVersion>
4.0.0
</modelVersion>





    
<parent>





        
<groupId>
org.springframework.boot
</groupId>





        
<artifactId>
spring-boot-starter-parent
</artifactId>





        
<version>
2.1.1.RELEASE
</version>





        
<relativePath/>
 
<!-- lookup parent from repository -->





    
</parent>





    
<groupId>
com.hqs
</groupId>





    
<artifactId>
dblock
</artifactId>





    
<version>
1.0-SNAPSHOT
</version>





    
<name>
dblock
</name>





    
<description>
Demo project for Spring Boot
</description>







    
<properties>





        
<java.version>
1.8
</java.version>





    
</properties>







    
<dependencies>





        
<dependency>





            
<groupId>
org.springframework.boot
</groupId>





            
<artifactId>
spring-boot-starter-web
</artifactId>





        
</dependency>







        
<dependency>





            
<groupId>
org.springframework.boot
</groupId>





            
<artifactId>
spring-boot-devtools
</artifactId>





            
<scope>
runtime
</scope>





        
</dependency>





        
<dependency>





            
<groupId>
mysql
</groupId>





            
<artifactId>
mysql-connector-java
</artifactId>





            
<scope>
runtime
</scope>





        
</dependency>





        
<dependency>





            
<groupId>
org.springframework.boot
</groupId>





            
<artifactId>
spring-boot-starter-test
</artifactId>





            
<scope>
test
</scope>





        
</dependency>





        
<dependency>





            
<groupId>
org.springframework.boot
</groupId>





            
<artifactId>
spring-boot-starter-data-jpa
</artifactId>





        
</dependency>





        
<dependency>





            
<groupId>
mysql
</groupId>





            
<artifactId>
mysql-connector-java
</artifactId>





        
</dependency>





        
<dependency>





            
<groupId>
org.projectlombok
</groupId>





            
<artifactId>
lombok
</artifactId>





            
<optional>
true
</optional>





        
</dependency>







        
<!-- aop -->





        
<dependency>





            
<groupId>
org.aspectj
</groupId>





            
<artifactId>
aspectjweaver
</artifactId>





            
<version>
1.8.4
</version>





        
</dependency>







    
</dependencies>







    
<build>





        
<plugins>





            
<plugin>





                
<groupId>
org.springframework.boot
</groupId>





                
<artifactId>
spring-boot-maven-plugin
</artifactId>





            
</plugin>





        
</plugins>





    
</build>







</project>
View Code

项目的结构如下:

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

  • 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匹配和更新,是不是很简单。 

public
 
interface
 
CatalogRepository
 
extends
 
CrudRepository
<
Catalog
, 
Long
> {







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





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







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





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





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







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





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





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





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







}
View Code

 

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

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

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

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

package
 com.hqs.dblock;







import
 org.junit.
Test
;





import
 org.junit.runner.
RunWith
;





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





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





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





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





import
 org.springframework.util.
LinkedMultiValueMap
;





import
 org.springframework.util.
MultiValueMap
;







@RunWith
(
SpringRunner
.
class
)





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





public
 
class
 
DblockApplicationTests
 {







    
@Autowired





    
private
 
TestRestTemplate
 testRestTemplate;







    
@Test





    
public
 
void
 browseCatalogTest() {





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





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





            
final
 
int
 num = i;





            
new
 
Thread
(() -> {





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





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





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





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





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





            }





            ).start();





        }





    }







    
@Test





    
public
 
void
 browseCatalogTestRetry() {





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





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





            
final
 
int
 num = i;





            
new
 
Thread
(() -> {





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





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





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





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





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





            }





            ).start();





        }





    }





}
View Code

 

调用100次,即一个商品可以浏览一百次,采用悲观锁,catalog表的数据都是100,并且browse表也是100条记录。

采用乐观锁的时候,因为版本号的匹配关系,那么会有一些记录丢失,但是这两个表的数据是可以对应上的。

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

package
 com.hqs.dblock.annotation;







import
 java.lang.annotation.
ElementType
;





import
 java.lang.annotation.
Retention
;





import
 java.lang.annotation.
RetentionPolicy
;





import
 java.lang.annotation.
Target
;







@Target
(
ElementType
.METHOD)





@Retention
(
RetentionPolicy
.RUNTIME)





public
 
@interface
 
RetryOnFailure
 {





}
View Code

 

针对注解进行切面,见如下代码。

我设置了最大重试次数5,然后超过5次后就不再重试

package
 com.hqs.dblock.aspect;







import
 lombok.
extern
.slf4j.
Slf4j
;





import
 org.aspectj.lang.
ProceedingJoinPoint
;





import
 org.aspectj.lang.annotation.
Around
;





import
 org.aspectj.lang.annotation.
Aspect
;





import
 org.aspectj.lang.annotation.
Pointcut
;





import
 org.hibernate.
StaleObjectStateException
;





import
 org.springframework.orm.
ObjectOptimisticLockingFailureException
;





import
 org.springframework.stereotype.
Component
;







@Slf4j





@Aspect





@Component





public
 
class
 
RetryAspect
 {





    
public
 
static
 
final
 
int
 MAX_RETRY_TIMES = 
5
;
//max retry times







    
@Pointcut
(
"@annotation(com.hqs.dblock.annotation.RetryOnFailure)"
) 
//self-defined pointcount for RetryOnFailure





    
public
 
void
 retryOnFailure(){}







    
@Around
(
"retryOnFailure()"
) 
//around can be execute before and after the point





    
public
 
Object
 doConcurrentOperation(
ProceedingJoinPoint
 pjp) 
throws
 
Throwable
 {





        
int
 attempts = 
0
;







        
do
 {





            attempts++;





            
try
 {





                pjp.proceed();





            } 
catch
 (
Exception
 e) {





                
if
(e 
instanceof
 
ObjectOptimisticLockingFailureException
 ||





                        e 
instanceof
 
StaleObjectStateException
) {





                    log.info(
"retrying....times:{}"
, attempts);





                    
if
(attempts > MAX_RETRY_TIMES) {





                        log.info(
"retry excceed the max times.."
);





                        
throw
 e;





                    }





                }







            }





        } 
while
 (attempts < MAX_RETRY_TIMES);





        
return
  
null
;





    }





}
View Code

 

 

 

 

 

posted @ 2019-04-11 09:35  NingShare  阅读(658)  评论(0编辑  收藏  举报