作者:黄青石
链接:www.cnblogs.com/huangqingshi
在写入数据库的时候需要有锁,比如同时写入数据库的时候会出现丢数据,那么就需要锁机制。
数据锁分为乐观锁和悲观锁
它们使用的场景如下:
他们实现的方式上有所不同。
乐观锁采用版本号的方式,即当前版本号如果对应上了就可以写入数据,如果判断当前版本号不一致,那么就不会更新成功,
比如
-
update table
set
column
=
value
-
where
version
=
$
{
version
}
and
otherKey
=
$
{
otherKey
}
悲观锁实现的机制一般是在执行更新语句的时候采用for update方式,
比如
-
update table
set
column
=
'value'
for
update
这种情况where条件呢一定要涉及到数据库对应的索引字段,这样才会是行级锁,否则会是表锁,这样执行速度会变慢。
下面我就弄一个spring boot(springboot 2.1.1 + mysql + lombok + aop + jpa)工程,然后逐渐的实现乐观锁和悲观锁。
假设有一个场景,有一个catalog商品目录表,然后还有一个browse浏览表,假如一个商品被浏览了,那么就需要记录下浏览的user是谁,并且记录访问的总数。
表的结构非常简单:
-
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
;
POM.XML的依赖如下:
-
xml version
=
"1.0"
encoding
=
"UTF-8"
?>
-
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"
>
-
4.0.0
-
-
org.springframework.boot
-
spring-boot-starter-parent
-
2.1.1.RELEASE
-
-
-
com.hqs
-
dblock
-
1.0-SNAPSHOT
-
dblock
-
Demo project for Spring Boot
-
-
-
1.8
-
-
-
-
-
org.springframework.boot
-
spring-boot-starter-web
-
-
-
-
org.springframework.boot
-
spring-boot-devtools
-
runtime
-
-
-
mysql
-
mysql-connector-java
-
runtime
-
-
-
org.springframework.boot
-
spring-boot-starter-test
-
test
-
-
-
org.springframework.boot
-
spring-boot-starter-data-jpa
-
-
-
mysql
-
mysql-connector-java
-
-
-
org.projectlombok
-
lombok
-
true
-
-
-
-
-
org.aspectj
-
aspectjweaver
-
1.8.4
-
-
-
-
-
-
-
-
org.springframework.boot
-
spring-boot-maven-plugin
-
-
-
-
-
项目的结构如下:
介绍一下项目的结构的内容:
-
entity包: 实体类包。
-
repository包:数据库repository
-
service包: 提供服务的service
-
controller包: 控制器写入用于编写requestMapping。相关请求的入口类
-
annotation包: 自定义注解,用于重试。
-
aspect包: 用于对自定义注解进行切面。
-
DblockApplication: springboot的启动类。
-
DblockApplicationTests: 测试类。
咱们看一下核心代码的实现,参考如下,使用dataJpa非常方便,集成了CrudRepository就可以实现简单的CRUD,非常方便,有兴趣的同学可以自行研究。
实现乐观锁的方式有两种:
-
更新的时候将version字段传过来,然后更新的时候就可以进行version判断,如果version可以匹配上,那么就可以更新(方法:updateCatalogWithVersion)。
-
在实体类上的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
);
-
-
}
实现悲观锁的时候也有两种方式:
-
自行写原生SQL,然后写上for update语句。(方法:findCatalogsForUpdate)
-
使用@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
();
-
}
-
}
-
}
调用100次,即一个商品可以浏览一百次,采用悲观锁,catalog表的数据都是100,并且browse表也是100条记录。采用乐观锁的时候,因为版本号的匹配关系,那么会有一些记录丢失,但是这两个表的数据是可以对应上的。
乐观锁失败后会抛出ObjectOptimisticLockingFailureException,那么我们就针对这块考虑一下重试,下面我就自定义了一个注解,用于做切面。
-
package
com
.
hqs
.
dblock
.
annotation
;
-
-
import