乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。而乐观锁机制在一定程度上解决了这个问题。
乐观锁,大多是基于数据版本(Version)记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号等于数据库表当前版本号,则予以更新,否则认为是过期数据。
Spring Data MongoDB 为实现乐观锁提供了一个 @Version 注解, @Version 注解提供了与 MongoDB 上下文中的 JPA 类似的语法,并确保更新只应用于具有匹配版本的文档(即数据库中数据的版本必须和我们更新前查询出来的数据版本一致,否则认为别人修改了数据,更新失败)。
因此,Version 属性的实际值将被添加到更新查询中。这样,如果另一个操作同时更改了文档(数据版本也会变化,+1 了),则更新不会产生任何影响。在这种情况下,将抛出 OptimisticLockingFailureException 异常。
以下示例显示了乐观锁的功能,代码如下:
(1)定义 Person 实体,添加 version 属性。代码如下:
package com.hxstrive.springdata.mongodb.entity; import lombok.Builder; import lombok.Data; import lombok.ToString; import org.springframework.data.annotation.TypeAlias; import org.springframework.data.annotation.Version; import org.springframework.data.mongodb.core.mapping.Document; /** * 用户 * @author hxstrive.com */ @Document("person") @TypeAlias("pers") @Data @Builder @ToString public class Person extends Contact { /** ID,自动映射到 MongoDB 的 _id 字段 */ private String id; /** 姓名 */ private String name; /** 年龄 */ private int age; /** 版本 */ @Version private long version; }
(2)使用客户端代码演示乐观锁的用法,如下:
package com.hxstrive.springdata.mongodb; import com.hxstrive.springdata.mongodb.entity.Person; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Query; import static org.springframework.data.mongodb.core.query.Criteria.where; import static org.springframework.data.mongodb.core.query.Query.query; @SpringBootTest public class OptimisticLockingTest { @Autowired private MongoTemplate mongoTemplate; @Test public void contextLoads() { // 删除集合 mongoTemplate.dropCollection(Person.class); // 插入一条数据,此时数据版本为 1 Person person = mongoTemplate.insert(Person.builder().name("Tom").age(40).build()); // 查询出来的数据版本为 1 Query query = query(where("age").is(40)); Person findPerson = mongoTemplate.findOne(query, Person.class); System.out.println(findPerson); // 更新 person,此时数据版本为 2 person.setName("Helen"); mongoTemplate.save(person); System.out.println(mongoTemplate.findOne(query, Person.class)); // 依然使用旧版本的 person 去进行更新操作 findPerson.setName("Bill"); mongoTemplate.save(findPerson); System.out.println(mongoTemplate.findOne(query, Person.class)); // 抛出如下错误: // org.springframework.dao.OptimisticLockingFailureException: // Cannot save entity 63aef9dd58480d4697f82148 with version 2 to collection person. Has it been modified meanwhile? } }
运行代码,输出如下:
Person(id=63aef9dd58480d4697f82148, name=Tom, age=40, version=1) Person(id=63aef9dd58480d4697f82148, name=Helen, age=40, version=2) org.springframework.dao.OptimisticLockingFailureException: Cannot save entity 63aef9dd58480d4697f82148 with version 2 to collection person. Has it been modified meanwhile? at org.springframework.data.mongodb.core.MongoTemplate.doSaveVersioned(MongoTemplate.java:1509) at org.springframework.data.mongodb.core.MongoTemplate.save(MongoTemplate.java:1472) at org.springframework.data.mongodb.core.MongoTemplate.save(MongoTemplate.java:1458) ...