14 乐观锁机制和重试机制在实战中的应用

14 乐观锁机制和重试机制在实战中的应用 14.1 什么是乐观锁
乐观锁在实际开发过程中很常?,它没有加锁、没有阻塞,在多线程环境以及?并发的情况下 CPU 的利?率是最?的,吞吐量也是最?的 。
? JavaAPI 协议也对乐观锁的操作做了规定:通过指定 @ 字段对数据增加版本号控制,进?在更新的时候判断版本号是否有变化 。如果没有变化就直接更新;如果有变化,就会更新失败败并抛出“ion”异常 。我们? SQL 表示?下乐观锁的做法,代码如下:
SELECT uid, name, version FROM user WHERE id = 1;UPDATE user SET name = 'jack', version = version + 1 WHERE id = 1 AND version = 1;
假设本次查询的 =1,在更新操作时,加上这次查出来的,这样和我们上?个版本相同,就会更新成功,并且不会出现互相覆盖的问题,保证了数据的原?性 。
这就是乐观锁在数据库??的应? 。那么在Data JPA ??怎么做呢?我们通过?法来了解?下 。
14.2 乐观锁的实现方法
JPA 协议规定,想要实现乐观锁可以通过 @ 注解标注在某个字段上?,并且可以持久化到 DB 即可 。其?持的类型有如下四种:
14.2.1 @ 的用法
这样就可以完成乐观锁的操作 。我?较推荐使?类型的字段,因为这样语义?较清晰、简单 。
注意: Data JPA ??有两个 @ 注解,请使? @javax..,?不是 @org..data.. 。
我们通过如下?个步骤详细讲?下 @ 的?法 。
第?步:实体??添加带 @ 注解的持久化字段 。
我在上?课时讲到了,现在直接在这个基类??添加 @ 即可,当然也可以把这个字段放在 sub-class- ?? 。我?较推荐你放在基类??,因为这段逻辑是公共的字段 。改动完之后我们看看会发?什么变化,如下所示:
@Data@MappedSuperclasspublic class BaseEntity {@Id@GeneratedValue(strategy = GenerationType.AUTO)protected Long id;@Versionprotected Integer version;protected boolean deleted;}
第?步:?实体继承,就可以实现 @ 的效果,代码如下:
@Entity@Data@Builder@AllArgsConstructor@NoArgsConstructor@ToString(callSuper = true)public class User extends BaseEntity {private String name;private String email;@Enumerated(EnumType.STRING)private SexEnum sex;private Integer age;}
第三步:创建,?便进? DB 操作 。
public interface UserInfoRepository extends JpaRepository {}
第四步:创建和,?来模拟的复杂业务逻辑 。
public interface UserService {/*** 根据 UserId 产?的?些业务计算逻辑*/User calculate(Long userId);}@Servicepublic class UserServiceImpl implements UserService {@Autowiredprivate UserRepository userRepository;@Override@Transactionalpublic User calculate(Long userId) {User user = repository.getById(userId);// 模拟复杂的业务计算逻辑耗时操作;try {TimeUnit.SECONDS.sleep(2L);} catch (InterruptedException ignored) {}user.setAge(user.getAge() + 1);return userRepository.saveAndFlush(user);}}
其中,我们通过 @ 开启事务,并且在查询?法后?模拟复杂业务逻辑,?来呈现多线程的并发问题 。
第五步:按照惯例写个测试?例测试?下 。
@ExtendWith(SpringExtension.class)@DataJpaTest@ComponentScan(basePackageClasses = UserServiceImpl.class)class UserServiceTest {@Autowiredprivate UserService userService;@Autowiredprivate UserRepository userRepository;@Testvoid testVersion() {// 加?条数据User user1 = userRepository.save(User.builder().age(20).name("zzn").build());// 验证?下数据库??的值Assertions.assertEquals(0, user1.getVersion());Assertions.assertEquals(20, user1.getAge());userService.calculate(user1.getId());// 验证?下更新成功的值User user2 = userRepository.getById(user1.getId());Assertions.assertEquals(1, user2.getVersion());Assertions.assertEquals(21, user2.getAge());}@SneakyThrows@Test@Rollback(false)@Transactional(propagation = Propagation.NEVER)void testVersionException() {// 加?条数据userRepository.save(User.builder().age(20).name("zzn").build());// 模拟多线程执?两次new Thread(() -> userService.calculate(1L)).start();TimeUnit.SECONDS.sleep(1L);// 如果两个线程同时执?会发?乐观锁异常;Exception exception = Assertions.assertThrows(ObjectOptimisticLockingFailureException.class,() -> userService.calculate(1L));log.info("error info:", exception);}}
从上?的测试得到的结果中,我们执? (),会发现在 save 的时候,会?动 +1,第?次初始化为 0; 的时候也会附带条件,我们通过下图的 SQL,也可以看到的变化 。
?当?我们调? () 测试?法的时候,利?多线程模拟两个并发情况,会发现两个线程同时取到了历史数据,并在稍后都对历史数据进?了更新 。
由此你会发现,第?次测试的结果是乐观锁异常,更新不成功 。
通过?志?会发现,两个 SQL 同时更新的时候,是?样的,是它导致了乐观锁异常 。

14 乐观锁机制和重试机制在实战中的应用

文章插图
注意:乐观锁异常不仅仅是同?个?法多线程才会出现的问题,我们只是为了?便测试?采?同?个?法;不同的?法、不同的项?,都有可能导致乐观锁异常 。乐观锁的本质是 SQL 层?发?的,和使?的框架、技术没有关系 。
那么我们分析?下,@ 对 save 的影响是什么,怎么判断对象是新增还是 ?
14.2.2 @ 对 Save 方法的影响
通过上?的实例,你不难发现,@ 底层实现逻辑和 @ ?点关系没有,底层是通过判断实体??是否有 @ 的持久化字段,利?乐观锁机制来创建和使?的值 。
因此,还是那句话:JavaAPI 负责制定协议,负责实现逻辑,Data JPA 负责封装和使? 。那么我们来看下 Save 对象的时候,如何判断是新增的还是 merge 的逻辑呢?
14.3 isNew 判断的逻辑
通过断点,我们可以进?.class 的 Save ?法中,看到如下图显示的界?:
然后,我们进?.class 的 isNew ?法中,?会看到下图显示的界?:
其中,我们先看第?段逻辑,判断其中是否有 @ 标注的属性,并且该属性是否为基础类型 。如果不满?条件,调? super.isNew() ?法,? super.isNew ??只判断了 ID 字段是否有值 。
第?段逻辑表达的是,如果有 @ 字段,那么看看这个字段是否有值,如果没有就返回 true,如果有值则返回 false 。
由此可以得出结论:如果我们有 @ 注解的字段,就以 @ 字段来判断新增/;如果没有,那么就以 @ID 字段是否有值来判断新增 /。
需要注意的是:虽然我们看到的是 merge ?法,但是不?定会执?操作,??还有很多逻辑,有兴趣的话你可以再 debug 进去看看 。
我直接说?下结论,merge ?法会判断对象是否为游离状态,以及有? ID 值 。它会先触发?条语句,并根据 ID 查?下这条记录是否存在,如果不存在,虽然 ID 和字段都有值,但也只是执?语句;如果本条 ID 记录存在,才会执?的 sql 。?于这个具体的和的 sql、传递的参数是什么,你可以通过控制台研究?下 。
总之,如果我们使?纯粹的 ?法,那么完全不需要??写这?段逻辑,只要保证 ID 和存在该有的值就可以了,JPA 会帮我们实现剩下的逻辑 。
实际?作中,特别是分布式更新的时候,很容易碰到乐观锁,这时候还要结合重试机制才能完美解决我们的问题,接下来看看具体该怎么做 。
14.4 乐观锁机制和重试机制的实战
我们先了解?下?持的重试机制是什么样的 。
14.4.1 重试机制详解
全家桶??提供了 @ 的注解,会帮我们进?重试 。下?看?个 @ 的例? 。
第?步:利? maven 引? -retry 的依赖 jar,如下所示:
org.springframework.retryspring-retry
第?步:在的?法中添加 @ 注解,就可以实现重试的机制了,代码如下:
@Override@Transactional@Retryablepublic User calculate(Long userId) {User user = repository.getById(userId);// 模拟复杂的业务计算逻辑耗时操作;try {TimeUnit.SECONDS.sleep(2L);} catch (InterruptedException ignored) {}user.setAge(user.getAge() + 1);return userRepository.saveAndFlush(user);}
第三步:新增?个并添加@ 注解,是为了开启重试机制,使 @ ?效 。
@Configuration@EnableRetrypublic class RetryConfiguration {}
第四步:新建?个测试?例测试?下 。
@Test@Rollback(false)@SneakyThrows@Transactional(propagation = Propagation.NEVER)void testRetryable() {// 加?条数据userRepository.save(User.builder().age(20).name("zzn").build());// 模拟多线程执?两次new Thread(() -> userService.calculate(1L)).start();TimeUnit.SECONDS.sleep(1L);// 模拟多线程执?两次,由于加了 @EnableRetry,所以这次也会成功User user = userService.calculate(1L);// 经过了两次计算,年龄变成了 22Assertions.assertEquals(22, user.getAge());Assertions.assertEquals(2, user.getVersion());}
这?要说的是,我们在测试?例??执? @(.class),这样就开启了重试机制,然后继续在??模拟了两次线程调?,发现第?次发?了乐观锁异常之后依然成功了 。为什么呢?我们通过?志可以看到,它是失败了?次之后?进?了重试,所以第?次成功了 。
通过案例你会发现 Retry 的逻辑其实很简单,只需要利? @ 注解即可,那么我们看?下这个注解的详细?法 。
14.4.2 @ 的详细用法
这个注解提供了很多的属性,接下来,我们对常用的属性参数做一下说明:
下?是?个关于 @ 扩展的使?例?,具体看?下代码:
@Servicepublic interface MyService {@Retryable( value = http://www.kingceram.com/post/SQLException.class,maxAttempts = 2, backoff = @Backoff(delay = 100))void retryServiceWithCustomization(String sql) throws SQLException; }
可以看到,这?明确指定 .class 异常的时候需要重试两次,每次中间间隔 100 毫秒 。
@Servicepublic interface MyService {@Retryable( value = http://www.kingceram.com/post/SQLException.class, maxAttemptsExpression ="${retry.maxAttempts}",backoff = @Backoff(delayExpression = "${retry.maxDelay}"))void retryServiceWithExternalizedConfiguration(String sql) throws SQLException; }
此外,你也可以利? SpEL 表达式读取配置?件??的值 。
关于的语法就介绍到这?,常?的基本就这些,如果你遇到更复杂的场景,可以到中看?下官?的?档: 。
14.4.3 乐观锁重试机制的实践
我?较建议你使?如下配置:
@Retryable(value = http://www.kingceram.com/post/ObjectOptimisticLockingFailureException.class,backoff = @Backoff(multiplier = 1.5,random = true))
这?明确指定 .class 等乐观锁异常要进?重试,如果引起其他异常的话,重试会失败,没有意义;?采?随机 +1.5 倍的系数,这样基本很少会出现连续 3 次乐观锁异常的情况,并且也很难发?重试?暴?引起系统重试崩溃的问题 。
到这?讲的?直都是乐观锁相关内容,那么 JPA 也?持悲观锁吗?
14.5 悲观锁的实现
JavaAPI 2.0 协议??有?个枚举值,??包含了所有它?持的乐观锁和悲观锁的值,我们看?下 。
public enum LockModeType {// 等同于 OPTIMISTIC,默认,?来兼容 2.0 之前的协议READ,// 等同于 OPTIMISTIC_FORCE_INCREMENT,?来兼容 2.0 之前的协议WRITE,// 乐观锁,默认,2.0 协议新增OPTIMISTIC,// 乐观写锁,强制 version 加 1,2.0 协议新增OPTIMISTIC_FORCE_INCREMENT,// 悲观读锁 2.0 协议新增PESSIMISTIC_READ,// 悲观写锁,version 不变,2.0 协议新增PESSIMISTIC_WRITE,// 悲观写锁,version 会新增,2.0 协议新增PESSIMISTIC_FORCE_INCREMENT,// 2.0 协议新增?锁状态NONE}
悲观锁在Data JPA ??是如何?持的呢?很简单,只需要在??的??覆盖?类的?法,然后添加 @Lock 注解并指定即可,请看如下代码:
public interface UserRepository extends JpaRepository {@Override@Lock(LockModeType.PESSIMISTIC_WRITE)Optional findById(Long aLong);}
你可以看到,??覆盖了?类的?法,并指定锁的类型为悲观锁 。如果我们将改调?为悲观锁的?法,会发?什么变化呢?
这里如果使用 加锁会失败,因为??是利?的 lazy 的加载机制,lazy 的不知道什么时间会上锁,这样?险太?,锁的发?时机不好控制的?度考虑的 。
@Override@Transactionalpublic User calculate(Long userId) {User user = repository.findById(userId).get();// 模拟复杂的业务计算逻辑耗时操作;try {TimeUnit.SECONDS.sleep(2L);} catch (InterruptedException ignored) {}user.setAge(user.getAge() + 1);return repository.saveAndFlush(user);}
再执?上?测试中的?法,跑完测试?例的结果依然是通过的,我们看下?志 。
Hibernate: select user0_.id as id1_1_0_, user0_.create_user_id as create_u2_1_0_, user0_.created_date as created_3_1_0_, user0_.deleted as deleted4_1_0_, user0_.last_modified_date as last_mod5_1_0_, user0_.last_modified_user_id as last_mod6_1_0_, user0_.version as version7_1_0_, user0_.age as age8_1_0_, user0_.email as email9_1_0_, user0_.name as name10_1_0_, user0_.sex as sex11_1_0_ from user user0_ where user0_.id=? for update
你会看到,在查询语句后面都加上了 for,刚才的串?操作完全变成了并?操作 。所以少了?次 Retry 的过程,结果还是?样的 。但是,你在?产环境中要慎?悲观锁,因为它是阻塞的,?旦发?服务异常,可能会造成死锁的现象 。
14.6 本章小结
【14 乐观锁机制和重试机制在实战中的应用】本课时的内容到这?就介绍完了 。在这?课时中,我为你详细讲解了乐观锁的概念及使??法、@ 对 Save ?法的影响,分享了乐观锁与重试机制的最佳实践,此外也提到了悲观锁的使??法(不推荐使?) 。