适用场景:
悲观锁:比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。
乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。
总结:两种所各有优缺点,读取频繁使用乐观锁,写入频繁使用悲观锁。
一、乐观锁
1、乐观锁:
每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁,由于数据没有进行加锁,期间该数据可以被其他线程进行读写操作。
2、乐观锁的原理
乐观锁,大多是基于数据版本 Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来 实现。 读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提 交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据 版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
3、乐观锁的实现
在实体类中添加一个int型的字段,并标注注解@Version即可,注意该字段要在主键id的后面。
User.java
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Version private int version; .......
Controller.java
@ResponseBody @RequestMapping("/test1") @RetryOnOptimisticLockingFailure//最后一步 public String test() { User user=userRepo.findByNumber("20180716114900229366"); user.setStartTime("22232322"); System.out.println("test1:"+leaveApproval.getVersion()); userRepo.save(user); return "success"; } @ResponseBody @RequestMapping("/test2") @RetryOnOptimisticLockingFailure//最后一步 public String test2() { User user=new User(); user=userRepo.findByNumber("20180716114900229366"); leaveApproval.setEndTime("111111"); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("test2:"+leaveApproval.getVersion()); userRepo.save(leaveApproval); return "success"; }
先执行/test2,然后在5秒之内执行/test1,来模拟多个用户对同一个资源的并发操作。此时便会报错
2018-07-17 17:03:14.254 ERROR 6816 --- [p-nio-80-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.orm.ObjectOptimisticLockingFailureException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1; nested exception is org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1] with root cause
当test2 save时,数据库通过对比version发现 该条数据已经过期,便会终止save 操作。
接下来只要捕获并处理这个异常即可。
3、乐观锁更新失败后的解决方案
用spring AOP思想来实现处理异常并实现重试机制。以下。
首先自定义一个注解:
@RetryOnOptimisticLockingFailure
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RetryOnOptimisticLockingFailure { }
然后 用AOP抛出异常 并进行重试。
注意:捕获异常时,网上的大部分文章都是只有OptimisticLockingFailureException这一种异常,这是不够的,可以先e.printStackTrace();看一看都有哪些异常,再进行捕获。
@Aspect @Component public class RetryOnOptimisticLockingAspect { private static final Logger logger= LoggerFactory.getLogger(RetryOnOptimisticLockingFailure.class); public static final int maxRetries = 5;//最多重试的次数 @Pointcut("@annotation(RetryOnOptimisticLockingFailure)")//自定义的注解作为切点 public void retryOnOptFailure() {} @Around("retryOnOptFailure()")//around注解可以在 目标方法 之前执行 也可以在目标方法之后 public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { int numAttempts = 0; do { numAttempts++; try { return pjp.proceed(); } catch (Exception e) {//此处捕获异常时,网上的大部分文章都是只有OptimisticLockingFailureException这一种异常,这是不够的,可以先e.printStackTrace();看一看都有哪些异常,在进行捕获 if (e instanceof ObjectOptimisticLockingFailureException || e instanceof StaleStateException ||e instanceof JpaSystemException ) { logger.info("更新数据---乐观锁重试中---"); if (numAttempts > maxRetries){ logger.info("抛出异常"); throw e; } } } }while (numAttempts < this.maxRetries); return null; } }
最后在controller的对应的方法上 添加该注解即可。
二、悲观锁
1、 悲观锁
每次在读取或者加载一条记录的时候,都会锁住被加载的记录,此时当其他事务如果要更新或者是加载此条记录就会因为不能获得锁而阻塞,但是其他事务还是可以插入和删除记录的。
2、 实现
在JDBC中使用悲观锁,需要使用select for update,即
select * from A Where id=1 for update;
3、 实例代码
先写查询的Jpa 接口
UserRepo.java
@Lock(LockModeType.PESSIMISTIC_WRITE)//这就相当与 select for update 一会执行的时候看打印的sql语句就知道了 @Query(value = "select u from User u where phoneNumber=?1 ") public User findByPhoneNumber(String phoneNumber);
然后写调用它的service服务,为了便于观察,写两个方法
注意:1、 一定要声明事务管理@Transactional,不添加注解会报错 no transation。
2、 事务管理的注解一定要 包住 对数据库的持久化操作 。即 find--set-save。
AppControllerService.java
@Transactional public User findByPhoneNumber(String phoneNumber){ User user = userRepo.findByPhoneNumber(phoneNumber); user.setUsername("第一步111"); try { Thread.sleep(12000);//线程sleep12秒 } catch (InterruptedException e) { e.printStackTrace(); } userRepo.save(user); return user; } @Transactional public User findByPhoneNumber1(String phoneNumber){ User user = userRepo.findByPhoneNumber(phoneNumber); user.setEmail("第二部222"); userRepo.save(user); return user; }
最后写controller
UserController.java
@GetMapping("/app/getUser1") public String transform(){ User user = appControllerService.findByPhoneNumber("123123"); return user.toString(); } @GetMapping("/app/getUser2") public String transform1(){ User user = appControllerService.findByPhoneNumber1("123123"); return user.toString(); }
首先执行接口 localhost/app/getUser1 该接口执行完成需要12s,在这期间 执行接口2 即localhost/app/getUser2。接口2 没有设置线程sleep。此时会发现接口2 不会立马执行完,而是要等待接口1 (12秒之后)执行完成之后 才会执行接口1。
这样 就表示设置悲观锁成功,方法2修改的数据不会被覆盖。
还在犹豫什么,赶紧关注一波,微信搜索公众号:程序员的成长之路。或者扫描下方二维码进行关注。
欢迎关注公众号,和我一起成长!