乐观锁和悲观锁
保存指令支持乐观锁和悲观锁。
乐观锁
Jimmer使用注解@org.babyfish.jimmer.sql.Version
支持乐观锁。
修改实体类型
-
修改
BookStore
- Java
- Kotlin
BookStore.java@Entity
public interface BookStore {
@Version
int version();
...省略其他属性...
}BookStore.kt@Entity
public interface BookStore {
@Version
val version: Int
...省略其他属性...
} -
修改
Book
- Java
- Kotlin
Book.java@Entity
public interface Book {
@Version
int version();
...省略其他属性...
}Book.kt@Entity
public interface Book {
@Version
val version: Int
...省略其他属性...
}
示范
乐观锁的特性
-
当插入对象时 (无论明确地进行INSERT操作,还是UPSERT操作被判定为INSERT),将对象的
version
插入到数据库。例子如下
- Java
- Kotlin
BookStore savedData = sqlClient.save(
Immutables.createBookStore(draft -> {
draft.setName("TURING");
draft.addIntoBooks(book -> {
book.setName("Introduction to Algorithms");
book.setEdition(3);
book.setPrice(new BigDecimal("44.99"));
});
draft.addIntoBooks(book -> {
book.setName("The Pragmatic Programmer");
book.setEdition(2);
book.setPrice(new BigDecimal("39.99"));
});
})
).getModifiedEntity();
System.out.println(savedData);val savedData = sqlClient.save(
BookStore {
name = "TURING"
books().addBy {
name = "Introduction to Algorithms"
edition = 3;
price = BigDecimal("44.99")
}
books().addBy {
name = "The Pragmatic Programmer"
edition = 2
price = BigDecimal("39.99")
}
}
).modifiedEntity
println(savedData)提示对插入操作而言,如果对象的version并未被赋值,Jimmer自动插入0。
如果你无法断言某个UPSERT模式的保存指令最终会被判定为
INSERT
还是UPDATE
,则应该坚持指定version
属性。下面的例子,基于一个假设,明确知道
save
操作一定会被判定为INSERT
,而非UPDATE
,所以未指定对象的version
属性。所有对象都没有指定id属性,Jimmer根据每个对象的key去判断它们是否存在。
假设所有对象都不存在,因此,三条新数据会被插入。
所有对象都没有 指定
version
属性,所以,它们会被自动填充为0。最终打印结果为 (为了便于阅读,这里进行了格式化)
{
"id":100,
"name":"TURING",
"version":0,
"books":[
{
"id":100,
"name":"Introduction to Algorithms",
"edition":3,
"price":44.99,
"version":0,
"store":{
"id":100
}
},
{
"id":101,
"name":"The Pragmatic Programmer",
"edition":2,
"price":39.99,
"version":0,
"store":{
"id":100
}
}
]
}信息当然,如果用户为这些对象指 定了
version
属性,这时,会插入用户指定的值,而非0。 -
当修改对象时 (无论明确地进行UPDATE操作,还是UPSERT操作被判定为UPDATE),Jimmer会为每个对象比较用户传递的
version
和数据库中现有的version
,如果二者不一致,抛出异常。我们稍微修改一下代码,再次执行
- Java
- Kotlin
BookStore savedData = sqlClient.save(
Immutables.createBookStore(draft -> {
draft.setName("TURING");
draft.setVersion(0);
draft.addIntoBooks(book -> {
book.setName("Introduction to Algorithms");
book.setEdition(3);
book.setPrice(new BigDecimal("54.99"));
book.setVersion(0);
});
draft.addIntoBooks(book -> {
book.setName("The Pragmatic Programmer");
book.setEdition(2);
book.setPrice(new BigDecimal("39.99"));
// 非法version
book.setVersion(9999);
});
})
).getModifiedEntity();
System.out.println(savedData);val savedData = sqlClient.save(
BookStore {
name = "TURING"
version = 0
books().addBy {
name = "Introduction to Algorithms"
edition = 3;
price = BigDecimal("44.99")
version = 0
}
books().addBy {
name = "The Pragmatic Programmer"
edition = 2
price = BigDecimal("49.99")
// 非法version
version = 9999
}
}
).modifiedEntity
println(savedData)警告对修改操作而言,如果对象的version并未被赋值,Jimmer会抛出异常。
如果你无法判断某个UPSERT模式的保存指令最终会被判定为
INSERT
还是UPDATE
,则应该坚持指定version
属性。执行,由于数据库中已经存在数据,所以三个对象都会被UPDATE。
很明显,最后一本书籍的version
9999
是非法的,上面的的代码将会得到如下异常:-
异常类型:
org.babyfish.jimmer.sql.runtime.SaveException
-
异常编码:
org.babyfish.jimmer.sql.runtime.SaveErrorCode.ILLEGAL_VERSION
-
异常消息:
Save error caused by the path: "<root>.books": Cannot update the entity whose type is "org.doc.j.model.Book", id is "101" and version is "9999"
让我们再修改一下代码,让所有对象都持有正确的
version
,如下- Java
- Kotlin
BookStore savedData = sqlClient.save(
Immutables.createBookStore(draft -> {
draft.setName("TURING");
draft.setVersion(0);
draft.addIntoBooks(book -> {
book.setName("Introduction to Algorithms");
book.setEdition(3);
book.setPrice(new BigDecimal("54.99"));
book.setVersion(0);
});
draft.addIntoBooks(book -> {
book.setName("The Pragmatic Programmer");
book.setEdition(2);
book.setPrice(new BigDecimal("39.99"));
book.setVersion(0);
});
})
).getModifiedEntity();
System.out.println(savedData);val savedData = sqlClient.save(
BookStore {
name = "TURING"
version = 0
books().addBy {
name = "Introduction to Algorithms"
edition = 3;
price = BigDecimal("44.99")
version = 0
}
books().addBy {
name = "The Pragmatic Programmer"
edition = 2
price = BigDecimal("49.99")
version = 0
}
}
).modifiedEntity
println(savedData)最终打印结果为 (为了便于阅读,这里进行了格式化)
{
"id":100,
"name":"TURING",
"version":1,
"books":[
{
"id":100,
"name":"Introduction to Algorithms",
"edition":3,
"price":54.99,
"version":1,
"store":{
"id":100
}
},
{
"id":101,
"name":"The Pragmatic Programmer",
"edition":2,
"price":39.99,
"version":1,
"store":{
"id":100
}
}
]
}信息可见数据被修改后,乐观锁会自动加1。
实际项目中,乐观锁版本号往往是表单界面的隐藏字段。如果某个表单保存后不自动跳转,而是保持界面不变以支持多次提交,则应该在每次保存成功后利用这样的返回信息更新表单界面 的隐藏字段。
悲观锁
和乐观锁不同,悲观锁生命周期很短,仅在一个jdbc事务内有效。
通常,Jimmer会生成一些查询SQL以辅助保存指令的执行,比如
-
判断
UPSERT
操作最终应该判定为INSERT
还是UPDATE
-
判断哪些关联对象需要被脱钩
接下来,我们对比不使用悲观锁和使用悲观锁两种情况,来观察这些查询SQL有何不同。
在前面的例子中,为了介绍乐观锁,假设BookStore
和Book
类型都定义了version
属性。
后续例子为了介绍悲观锁,不再有此假设。
未启用悲观锁
- Java
- Kotlin
sqlClient.save(
Immutables.createBookStore(draft -> {
draft.setName("TURING");
draft.addIntoBooks(book -> {
book.setName("Introduction to Algorithms");
book.setEdition(3);
book.setPrice(new BigDecimal("44.99"));
});
draft.addIntoBooks(book -> {
book.setName("The Pragmatic Programmer");
book.setEdition(2);
book.setPrice(new BigDecimal("39.99"));
});
})
);
sqlClient.save(
BookStore {
name = "TURING"
books().addBy {
name = "Introduction to Algorithms"
edition = 3;
price = BigDecimal("44.99")
}
books().addBy {
name = "The Pragmatic Programmer"
edition = 2
price = BigDecimal("39.99")
}
}
)
生成6条SQL,如下
-
判断书店是否存在
select
tb_1_.ID,
tb_1_.NAME
from BOOK_STORE tb_1_
where
tb_1_.NAME = ? /* TURING */ -
根据前一条查询的结果,决定
INSERT
还是UPDATE
insert或update,略
-
判断第1本书籍是否存在
select
tb_1_.ID,
tb_1_.NAME,
tb_1_.EDITION
from BOOK tb_1_
where
tb_1_.NAME = ? /* Introduction to Algorithms */
and
tb_1_.EDITION = ? /* 3 */ -
根据前一条查询的结果,决定
INSERT
还是UPDATE
insert或update,略
-
判断第2本书籍是否存在
select
tb_1_.ID,
tb_1_.NAME,
tb_1_.EDITION
from BOOK tb_1_
where
tb_1_.NAME = ? /* The Pragmatic Programmer */
and
tb_1_.EDITION = ? /* 2 */ -
根据前一条查询的结果,决定
INSERT
还是UPDATE
insert或update,略
这些查询语句用于进行条件判断,以决定后续SQL该如何生成。
然而,这些查询没有使用悲观锁,经它们判断而成立的条件和假设,有可能被其它并发行为破坏,从 而导致后续SQL执行出现异常。
为了避免这种并发问题,可以启用悲观锁。接下来,我们讨论悲观锁如何实现。
启用悲观锁
有两种启用悲观锁的方法
-
全局配置
有两种方法可以通过全局配置启用悲观锁
-
Spring Boot Starter的配置
修改
application.yml
(或application.properties
)jimmer:
default-lock-mode: PESSIMISTIC -
底层API的配置
- Java
- Kotlin
JSqlClient sqlClient = JSqlClient
.newBuilder()
.setDefaultLockMode(LockMode.PESSIMISTIC)
...省略其他配置...
.build();val sqlClient = newKSqlClient {
setDefaultLockMode(LockMode.PESSIMISTIC)
}
注意此举修改了全局设置,原本的默认值
OPTIMISTIC
被破坏。这意味着,除非将某个保存指令设置为乐观锁模式,前文所讲述的乐观锁的 功能消失。因此,大部分情况下,不推荐全局配置,而更推荐下文即将介绍的指令级配置。
-
-
指令级配置
和影响所有保存指令的全局配置不同,指令级配置仅仅影响当前保存指令
信息如果已经通过全局配置打开了悲观锁控制,就不再需要指令级配置了
调用保存指令的配置方法
setLockMode
,即可启用悲观锁,如下- Java
- Kotlin
sqlClient
.getEntities()
.saveCommand(
Immutables.createBookStore(draft -> {
draft.setName("TURING");
draft.addIntoBooks(book -> {
book.setName("Introduction to Algorithms");
book.setEdition(3);
book.setPrice(new BigDecimal("44.99"));
});
draft.addIntoBooks(book -> {
book.setName("The Pragmatic Programmer");
book.setEdition(2);
book.setPrice(new BigDecimal("39.99"));
});
})
)
.setLockMode(LockMode.PESSIMISTIC)
.execute();sqlClient.save(
BookStore {
name = "TURING"
books().addBy {
name = "Introduction to Algorithms"
edition = 3;
price = BigDecimal("44.99")
}
books().addBy {
name = "The Pragmatic Programmer"
edition = 2
price = BigDecimal("39.99")
}
}
) {
setLockMode(LockMode.PESSIMISTIC)
}
一旦启用了悲观锁,生成的查询语句会有显著变化,如下
-
判断书店是否存在
select
tb_1_.ID,
tb_1_.NAME
from BOOK_STORE tb_1_
where
tb_1_.NAME = ? /* TURING */
/* highlight-next-line */
for update -
根据前一条查询的结果,决定
INSERT
还是UPDATE
insert或update,略
-
判断第1本书籍是否存在
select
tb_1_.ID,
tb_1_.NAME,
tb_1_.EDITION
from BOOK tb_1_
where
tb_1_.NAME = ? /* Introduction to Algorithms */
and
tb_1_.EDITION = ? /* 3 */
/* highlight-next-line */
for update -
根据前一条查询的结果,决定
INSERT
还是UPDATE
insert或update,略
-
判断第2本书籍是否存在
select
tb_1_.ID,
tb_1_.NAME,
tb_1_.EDITION
from BOOK tb_1_
where
tb_1_.NAME = ? /* The Pragmatic Programmer */
and
tb_1_.EDITION = ? /* 2 */
/* highlight-next-line */
for update -
根据前一条查询的结果,决定
INSERT
还是UPDATE
insert或update,略