事务篇:Spring事务并发问题解决
Posted 青梅主码
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了事务篇:Spring事务并发问题解决相关的知识,希望对你有一定的参考价值。
大家好,我是杰哥
上一篇,讲到了项目中使用 Spring 开发中,带有 @Transactional 注解和 synchronized 关键字的方法,运行时依旧存在的问题
其原因在于:线程在提交事务之前,便释放了锁,导致其他线程与自己处在同一事务的情况
一 解决方案
于是乎,我们上次也顺着这个原因,直接给出了其中的最直接的一种解决方案:
在开启事务之前,便对操作加锁。即将该方法的 sychronized 关键字,移到父级方法上,具体实现如下方法一所示:
1、为父级方法加悲观锁
1)事务方法:
/**
* 方法一:为上层方法加锁:synchronized
*/
@Transactional
public void transactionalMethod(){
User user = userDao.findOne(2);
if (user !=null){
user.setAge(user.getAge()+1);
userDao.updateUser(user);
}
}
取消掉 transactionalMethod()
方法上的 synchronized
关键字,将其添加至其调用方法上
2)父级方法
/**
* 1-直接调用加了锁的方法:testTransactionalWithSynchronized()
*/
@GetMapping("/testTransactionalWithSynchronized")
public void invokeMethod() {
final CountDownLatch latch = new CountDownLatch(1000);
try {
for (int i = 0; i < latch.getCount() ; i++) {
new Thread(() -> {
testTransactionalWithSynchronized();
latch.countDown();
}).start();
}
}catch (Exception e){
System.out.println(e.getMessage());
}finally {
latch.countDown();
}
}
/**
* 加锁,然后调用transactionalMethod()方法
*/
private synchronized void testTransactionalWithSynchronized() {
userService.transactionalMethod();
}
testTransactionalWithSynchronized()
为直接调用事务的方法,为该方法添加 synchronized
关键字,然后在 invokeMethod()
中构造并发请求,直接调 testTransactionalWithSynchronized()
方法即可
最终,正如我们看到的那样,成功解决并发问题
那么,除了这种直接在方法加锁的方案,还有没有其他什么方案呢?
分析一下,实际上我们只需要保证的是,每个线程获取到的数据,必须是上一个线程提交事务之后的数据
。顺着调整锁位置的这个思路走下去,我们想到,实际上也可以在数据库上加锁啊,也就是说在sql语句查询中添加关键字 for update
一起来验证一下:
2、为查询语句加悲观锁
1)为查询语句加 for update
原来的 findOne()
方法
/**
* 根据id获取用户记录
*/
public User findOne(Integer id) {
//采用RowMapper的方式,获取结果对象。
String sql = "select * from t_user where id = "+id;
User user = jdbcTemplate.queryForObject(sql, (rs, rowNum) -> User.builder()
.id(rs.getLong(1))
.name(rs.getString("name"))
.age(rs.getInt("age"))
.build());
return user;
}
更新后的 findOne()
方法
/**
* 根据id获取用户记录
*/
public User findOne(Integer id) {
//采用RowMapper的方式,获取结果对象。
String sql = "select * from t_user where id = "+id +" for update";
User user = jdbcTemplate.queryForObject(sql, (rs, rowNum) -> User.builder()
.id(rs.getLong(1))
.name(rs.getString("name"))
.age(rs.getInt("age"))
.build());
return user;
}
调用说明:
/**
* 2-直接调用transactionalMethod()方法,解决并发问题
*/
@GetMapping("/transactionalWithUpdate")
public void transactionalWithUpdate() {
final CountDownLatch latch = new CountDownLatch(10);
try {
for (int i = 0; i < latch.getCount() ; i++) {
new Thread(() -> {
userService.transactionalWithUpdate();
latch.countDown();
}).start();
}
}catch (Exception e){
System.out.println(e.getMessage());
}finally {
latch.countDown();
}
}
测试结果,年龄被更新为 1000
,没毛病~
ok,问题再次被解决
对比一下直接在方法中加锁,这种方案的话,能够直接在底层就保证了执行是线程安全的。也就是说,在当前事务未提交之前,是不会允许其他事务来访问该条记录的。这样的话,即使以后其他的方法对这个表有类似的更新操作,便不用再在方法上考虑并发的控制问题啦~
再来分析一下,你会发现,这两种都采用的是 悲观锁
先来普及一下,悲观锁
和乐观锁
的概念吧~
悲观锁
,顾名思义,总是比较悲观,它认为数据总是有可能被更新,所以在每个线程进行更新的时候,都会先加上锁,再更新。比如说java中的 sychronized
关键字,就是最明显的悲观锁了
乐观锁
,就是比较乐观了。它保持着一种侥幸心理:认为在自己更新的时候,暂时是没有其他线程来更新的,于是便大胆地直接进行更新。只是在提交的时候,需要判断一下当前的资源是否有其他线程更新,若没有,那就直接提交成功了~ 也不用加锁,也就省了耗费加锁、释放锁的步骤;若资源发生了变化,那就没辙了,只能提示更新失败了。
比如说,git、svn的提交,就是这样的思想了,先让你修改自己的代码,只是提交的时候再去判断有没有其他人更新你所修改文件的部分。若是有,就会发生冲突,需要处理冲突之后再去提交;若没有,那么就直接提交成功了
总的来说,
对于读多于写的场景,冲突几率小,就可以采用乐观锁
对于写多于读的场景,冲突几率大,就可以采用悲观锁
而真正的业务中,往往总是读多于写的,所以有时候可能更适合于使用乐观锁机制
那么,如何实现乐观锁呢?
一般情况是采用 版本号机制
来实现乐观锁的,实际上这个方法应该是大家最常用以及最靠谱的方法了
为资源加一个版本号 version
,在更新之前记录这个 version
值 ,提交的时候判断最新的vversion
与这个 version
的值是否相等,若相等,表示没有其他人更新;否则就说明自己在更新过程中,已经有其他人抢先更新了一版了,自己拿到的数据不是最新的,只能提示失败了
那么,我们也按照这种思想来实现一下:乐观锁方式
3、采用乐观锁
1)为 t_user 表添加version 字段
表的初始状态:
即:version 和 age 的值都为 0
2)每次更新字段,也要判断其 version 字段 是否发生变化,并同时更新 version 的值
/**
* 更新用户(t_user,判断version)
*/
public int updateUserWithVersion(User user) {
String sql = "update t_user set age = ?,version = version + 1 where id = ? " +" and version = ?";
int result = jdbcTemplate.update(sql, user.getAge(), user.getId(),user.getVersion());
return result;
}
3)若 version 字段发生了变化,则抛出异常;否则,提示更新成功
/**
* 方法三:使用乐观锁(Optimistic lock)
*/
@Transactional
@Override
public void transactionalWithOptimisticLock(){
User user = userDao.findOne(2);
if (user !=null){
user.setAge(user.getAge()+1);
int result = userDao.updateUserWithVersion(user);
if (result == 0){
log.error("更新用户年龄时,出现异常");
throw new RuntimeException("更新用户年龄时,出现异常");
}
log.info("成功更新用户:{},年龄为:{}",user.getName(),user.getAge());
}
}
调用 updateUserWithVersion()
方法,若更新记录为 0,表示未更新成功,则抛出异常;若更新结果不为 0,则记录日志:更新成功
好了,理论上没啥问题,我们继续构造 10 个并发请求,看看结果
部分日志截取如下:
正如我们预期的那样,乐观锁虽然不能保证每个线程都执行成功,但是可以保证不出现混乱更新但对于更新不那么频繁的场景,使用乐观锁实际上就跟悲观锁的效果一样了~
二 总结
好了,针对这个问题,我们列出了三个方案,一起来总结一下这三个方案吧
当然,从悲观锁与乐观锁的角度出发,还会有很多其他的解决方案。需要大家根据不同的业务场景去进行选型了~
嗯,就这样。每天学习一点,时间会见证你的强大~
往期精彩回顾
以上是关于事务篇:Spring事务并发问题解决的主要内容,如果未能解决你的问题,请参考以下文章