悲观锁
准备工作
本文所讨论的悲观锁,并非普通查询的悲观锁,而是保存指令内部的悲观锁。
面对一个保存操作,在不同的情况下,保存指令会采取两种截然不同的行为:
-
直接利用数据库本身的能力进行upsert操作
-
先通过select操作检查被保存的数据是否存在,再根据查询结果决定后续操作应该是insert还是update
Jimmer会尽可能执行1
,如果无法做到,则执行2
并向用户报告QueryReason
。
当Jimmer不得不执行2
时,存在一个问题:先执行的select操作和后执行insert或update操作之间存在一个时间窗口,其他事务可能挤进这个时间窗口对相同的数据进行修改。
为了解决这个问题,保存指令允许用户启用悲观锁,在执行select操作时加上for update
选项,直到事务提交为止。
在展示悲观锁之前,我们有必要让保存指令执行上面的2
,而非1
。一个有效的办法是启用事务内触发器:
事务内触发器是Jimmer所支持的触发器的一种,Jimmer总是通过select查询修改前的旧数据,并以此模拟出触发器的效果。
事务内触发器是当前尚未介绍的功能,读者不用太在意它,这里,我们只是用它来确保Jimmer会采用先查再改的策略而已。
采用以下任何一种方法开启事务内触发器
-
利用Jimmer的Spring Boot Starter
修改
application.yml
或application.properties
文件,完成配置application.ymljimmer:
trigger-type: TRANSACTION_ONLY -
使用Jimmer的核心API
- Java
- Kotlin
JSqlClient sqlClient = JSqlClient
.newBuilder()
.setTriggerType(TriggerType.TRANSACTION_ONLY)
...省略其他配置...
.build();val sqlClient = sqlClient {
setTriggerType(TriggerType.TRANSACTION_ONLY)
...省略其他配置...
}
通过这样的配置,强迫Jimmer总是先查询再修改,以便于演示悲观锁。
悲观锁展示
我们可以通过setPessimisticLock
方法为保存指令开启悲观锁
- Java
- Kotlin
List<Book> books = Arrays.asList(
Immutables.createBook(draft -> {
draft.setName("GraphQL in Action");
draft.setEdition(3);
draft.setPrice(new BigDecimal("59.9"));
}),
Immutables.createBook(draft -> {
draft.setName("GraphQL in Action");
draft.setEdition(4);
draft.setPrice(new BigDecimal("59.9"));
})
);
sqlClient
.saveEntitiesCommand(books)
.setPessimisticLock(Book.class)
.execute();
val books = listOf(
Book {
name = "SQL in Action"
edition = 3
price = BigDecimal("59.9")
},
Book {
name = "SQL in Action"
edition = 4
price = BigDecimal("59.9")
}
)
sqlClient.saveEntities(books) {
setPessimisticLock(Book::class)
}
这里,setPessimisticLock
为Book
实体开启了悲观锁,如果保存指令需要查询Book
类型的对象,相关select语句会启用for update
选项。
事实上,你也可以调用setPessimisticLockAll
方法盲目地为所有实体类型开启悲观锁
最终生成如下三条SQL
-
查询被保存的对象是否存在,带上
for update
选项Purpose: COMMAND(TRIGGER)
SQL: select
tb_1_.ID,
tb_1_.NAME,
tb_1_.EDITION,
tb_1_.PRICE,
tb_1_.STORE_ID
from BOOK tb_1_
where
(tb_1_.NAME, tb_1_.EDITION) in (
(? /* GraphQL in Action */, ? /* 3 */),
(? /* GraphQL in Action */, ? /* 4 */)
) for update信息这里,查询语句带有
for update
选项,悲观锁已经生效。 -
插入不存在的数据 (非本文重点,默认折叠)
insert into BOOK(
NAME, EDITION, PRICE
) values(
? /* GraphQL in Action */,
? /* 4 */,
? /* 59.9 */
) -
更新已经存在的数据 (非本文重点,默认折叠)
update BOOK
set
PRICE = ? /* 59.9 */
where
ID = ? /* 12 */