SpringBootMybatis乐观锁重试机制代码实现
乐观锁重试机制代码实现
有乐观锁,那当然也是有悲观锁的
悲观锁和乐观锁的原理和应⽤场景
悲观锁(Pessimistic Lock)
顾名思义,就是很悲观,每次去拿数据的时候都认为别⼈会修改,所以每次在拿数据的时候都会上锁,这样别⼈想拿这个数据就会block直到它拿到锁。传统的关系型数据库⾥边就⽤到了很多这种锁机制,⽐如⾏锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
乐观锁(Optimistic Lock)
顾名思义,就是很乐观,每次去拿数据的时候都认为别⼈不会修改,所以不会上锁,但是在更新的时候会判断⼀下在此期间别⼈有没有去更新这个数据,可以使⽤版本号等机制。乐观锁适⽤于多读的应⽤类型,这样可以提⾼吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁
应⽤场景
悲观锁:
⽐较适合写⼊操作⽐较频繁的场景,如果出现⼤量的读取操作,每次读取的时候都会进⾏加锁,这样会增加⼤量的锁的开销,降低了系统的吞吐量。
乐观锁:
⽐较适合读取操作⽐较频繁的场景,如果出现⼤量的写⼊操作,数据发⽣冲突的可能性就会增⼤,为了保证数据的⼀致性,应⽤层需要不断的重新获取数据,这样会增加⼤量的查询操作,降低了系统的吞吐量。
总结:两种所各有优缺点,读取频繁使⽤乐观锁,写⼊频繁使⽤悲观锁。
代码实现原理
这⾥采⽤版本号的⽅式
简单例⼦表结构
CREATE TABLE `usr_user_account` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL COMMENT '⽤户ID',
`balance` decimal(50,10) DEFAULT '0.0000000000' COMMENT '余额',
`version` bigint(20) DEFAULT '0' COMMENT '乐观锁版本号',
`create_by` bigint(20) DEFAULT NULL COMMENT ' 创建⼈',
`create_time` datetime NOT NULL COMMENT ' 创建时间',
`modify_by` bigint(20) DEFAULT NULL,
`modify_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='⽤户账户表';
⽐如现在表中有两条数据
现在 user_id = 1001 给 user_id=1002 转账 50 元
转账,⽆⾮就是⼀加⼀减,我就不啰嗦了,先看完整代码实现 如下
@Service
@Transactional
public class UserAccountService implements IUserAccountService{
@Autowired
private UserAccountMapper userAccountMapper;
/**
* 转账
* @param dto
* @return
* @throws Exception
*/
@Transactional
@IsTryAgain
public Result transfter(TransferDTO dto) throws Exception {
FromUserId(),Amount(), Consts.ACCOUNT_OUT);
ToUserId(),Amount(), Consts.ACCOUNT_IN);
return Result.ok();
}
/
**
* 更新账户余额
* @param userId 账户⽤户ID
* @param amount    操作⾦额数量
* @param state    转⼊--1 ,转出 -- 0
* @throws Exception
*/
public void updateAccount( Long userId, BigDecimal amount,int state) throws Exception{
UserAccount userAccount = UserAccountByUserId(userId);
if(userAccount == null){
throw new ApiException(ApiResultEnum.ACCOUNT_NOT_FOUND);
}
BigDecimal afterBalance = BigDecimal.ZERO;
if(state == Consts.ACCOUNT_OUT){
Balance()pareTo(amount)<0){
throw new ApiException(ApiResultEnum.ACCOUNT_NOT_SUFFICIENT);
}
afterBalance = Balance().subtract(amount);
}else if(state == Consts.ACCOUNT_IN){
afterBalance = Balance().add(amount);
}
userAccount.setBalance(afterBalance);
userAccount.setModifyBy(userId);
userAccount.w());
if(!userAccountMapper.updateAccount(userAccount)){
//如果更新失败就抛出去,重试
throw new TryAgainException(ApiResultEnum.ERROR_TRY_AGAIN);
}
}
}
代码不算多
只需要看 updateAccount ⽅法中的 userAccountMapper.updateAccount 这个⽅法,这个⽅法也就是 乐观锁 实现的原理, mapper 如下:
<update id="updateAccount" parameterType="top.lrshuai.ity.UserAccount">
update usr_user_account set
version = version+1
<if test="userId != null">
,user_id = #{userId}
</if>
<if test="balance != null">
,balance = #{balance}
</if>
<if test="createBy != null">
,create_by = #{createBy}
</if>
<if test="createTime != null">
,create_time = #{createTime}
</if>
<if test="modifyBy != null">
,modify_by = #{modifyBy}
</if>
<if test="modifyTime != null">
,modify_time = #{modifyTime}
</if>
where
version=#{version}
<if test="id != null">
and  id=#{id}
</if>
<if test="userId != null">
and user_id=#{userId}
</if>
</update>
重点看 version 的变化
第⼀点: 更新条件 要匹配 version=上⼀次数据的version
第⼆点: 就是 更新成功的时候,把 version + 1
注意:当数据表有⼀个写锁时,其它进程的读写操作都需等待读锁释放后才会执⾏。所以保证了version 的正确性乐观锁实现就是这么简单
如果说更新失败了,那我们就抛异常,这样⽤户体验不是⾮常好,所以就有了重试机制
1、重试原理,利⽤到AOP 切⽚,然后通过 @Around 进⾏⽅法增强。
为了⽅便,创建⼀个⾃定义注解,在需要重试的⽅法上添加 注解即可
@IsTryAgain
重试注解
/**
* 重试注解
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface IsTryAgain {
}
TryAgainException
重试异常
public class TryAgainException extends ApiException {
public TryAgainException(ApiResultEnum apiResultEnum) {
super(apiResultEnum);
}
}
TryAgainAspect
@Aspect
@Configuration
springboot aoppublic class TryAgainAspect  implements Ordered {
/
**
* 默认重试⼏次
*/
private static final int    DEFAULT_MAX_RETRIES = 3;
private int                maxRetries          = DEFAULT_MAX_RETRIES;
private int                order              = 1;
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public int getOrder() {
der;
}
@Pointcut("@annotation(IsTryAgain)")
public void retryOnOptFailure() {
// pointcut mark
}
@Around("retryOnOptFailure()")
@Transactional(rollbackFor = Exception.class)
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int numAttempts = 0;
do {
numAttempts++;
try {
//再次执⾏业务代码
return pjp.proceed();
} catch (TryAgainException ex) {
if (numAttempts > maxRetries) {
//log failure information, and throw exception
//    如果⼤于默认的重试机制次数,我们这回就真正的抛出去了
throw new ApiException(ApiResultEnum.ERROR_TRY_AGAIN_FAILED);
}else{
//如果没达到最⼤的重试次数,将再次执⾏
System.out.println("=====正在重试====="+numAttempts+"次");
}
}
} while (numAttempts <= this.maxRetries);
return null;
}
}
现在,反回去看上⾯的UserAccountService或许就是知道什么意思了,和平常写的业务代码⼀样,唯⼀多的只是⼀个@IsTryAgain注解⽽已
重试可以,但是不能⽆限重试吧,所以也是会有重试失败的时候,如果你给它重试次数越多,失败就越少,我这⾥为了演⽰只有3次机会。测试类,测试
@RunWith(SpringRunner.class)
@SpringBootTest
public class ThreadTest {
@Autowired
private IUserAccountService userAccountService;
/**
* 并发测试重试机制
*
* @throws Exception
*/
@Test
public void test() throws Exception {
TransferDTO dto = new TransferDTO();
dto.setFromUserId(1001l);
dto.setToUserId(1002l);
dto.setAmount(BigDecimal.ONE);
int clientTotal = 100;
// 同时并发执⾏的线程数
int threadTotal = 20;
int count = 0;
ExecutorService executorService = wCachedThreadPool();
//信号量,此处⽤于控制并发的线程数
final Semaphore semaphore = new Semaphore(threadTotal);
//闭锁,可实现计数器递减
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
try {
//执⾏此⽅法⽤于获取执⾏许可,当总计未释放的许可数不超过200时,
//允许通⾏,否则线程阻塞等待,直到获取到许可。
semaphore.acquire();
//释放许可
} catch (Exception e) {
/
/("exception", e);
e.printStackTrace();
}
//闭锁减⼀
});
}
countDownLatch.await();//线程阻塞,直到闭锁值为0时,阻塞才释放,继续往下执⾏
executorService.shutdown();
}
}
结果如下:
100 个请求,20 个并发,好多个都是重试成功了,少部分失败了。完整源代码:
完整源代码:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。