赞
踩
我们都知道,在多线程环境,可以用synchronized来做多线程同步,保护临界区资源,达到线程安全的目的。我们也知道synchronized和ReentrantLock的区别,如果不清楚两者区别的,请参考《synchronized和ReentrantLock的区别与适用场景的解析》。假设读这篇文章前大家已经会使用Springboot+Mysql+Mybatis
,首先在文章前面提出几个问题,看你是否都知道,文章中会通过具体的代码来验证这些问题:
@Transactional(rollbackFor = Exception.class)
public void incrementAge(Long studentId) {
studentMapper.incrementAge(studentId);
}
<update id="incrementAge">
update demo_student
set age=age+1
where id=#{studentId}
</update>
@Transactional(rollbackFor = Exception.class)
public synchronized void syncIncrement(Long studentId) {
Student student = studentMapper.selectOne(studentId);
student.setAge(student.getAge() + 1);
studentMapper.updateStudent(student);
}
<update id="updateStudent">
update demo_student
set age=#{student.age}
where id=#{student.id}
</update>
@Transactional(rollbackFor = Exception.class)
public static void staticIncrement(Long studentId) {
try {
StudentMapper studentMapper = SpringUtils.getBean(StudentMapper.class);
Student student = studentMapper.selectOne(studentId);
student.setAge(student.getAge() + 1);
studentMapper.updateStudent(student);
System.out.println(1/0);
}catch (Exception e){
e.printStackTrace();
}finally {
}
}
@Transactional(rollbackFor = Exception.class)
public int increment(Long studentId) {
try {
StudentMapper studentMapper = SpringUtils.getBean(StudentMapper.class);
Student student = studentMapper.selectOne(studentId);
student.setAge(student.getAge() + 1);
return studentMapper.updateStudent(student);
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(1 / 0);
}
return 0;
}
@Transactional(rollbackFor = Exception.class)
public int increment(Long studentId) {
try {
StudentMapper studentMapper = SpringUtils.getBean(StudentMapper.class);
Student student = studentMapper.selectOne(studentId);
student.setAge(student.getAge() + 1);
return studentMapper.updateStudent(student);
} catch (Exception e) {
e.printStackTrace();
return -1;
} finally {
return 100;
}
}
下面首先贴出本篇文章重要代码,后面解析和验证问题都需要用到
<update id="incrementAge">
update demo_student
set age=age+1
where id=#{studentId}
</update>
<update id="updateStudent">
update demo_student
set age=#{student.age}
where id=#{student.id}
</update>
@Mapper
public interface StudentMapper {
void incrementAge(@Param("studentId") Long studentId);
int updateStudent(@Param("student") Student student);
Student selectOne(@Param("studentId") Long studentId);
}
@Transactional(rollbackFor = Exception.class) public void incrementAge(Long studentId) { studentMapper.incrementAge(studentId); } @Transactional(rollbackFor = Exception.class) public synchronized int syncIncrement(Long studentId) { try { Student student = studentMapper.selectOne(studentId); student.setAge(student.getAge() + 1); return studentMapper.updateStudent(student); } catch (Exception e) { e.printStackTrace(); return -1; } finally { } } @Transactional(rollbackFor = Exception.class) public static int staticIncrement(Long studentId) { try { StudentMapper studentMapper=SpringUtils.getBean(StudentMapper.class); Student student = studentMapper.selectOne(studentId); student.setAge(student.getAge() + 1); return studentMapper.updateStudent(student); } catch (Exception e) { e.printStackTrace(); return -1; } finally { } } @Transactional(rollbackFor = Exception.class) public int increment(Long studentId) { try { Student student = studentMapper.selectOne(studentId); student.setAge(student.getAge() + 1); return studentMapper.updateStudent(student); } catch (Exception e) { e.printStackTrace(); return -1; } finally { } }
@Test void testIncrement() { CountDownLatch countDownLatch = new CountDownLatch(500); ExecutorService executorService = Executors.newFixedThreadPool(50); for (int i = 0; i < 500; i++) { executorService.execute(() -> { try { userService.incrementAge(1L); countDownLatch.countDown(); } catch (Exception e) { e.printStackTrace(); } finally { } }); } try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } }
结论:一条sql 具有原子性,不会有线程安全问题,和事务隔离级别没关系,因此问题1的答案就出来了。
我们知道,synchronized如果加在普通方法上,锁住的就是该类的一个实例对象;synchronized如果加在静态方法上,锁住的就是该类;synchronized如果同步代码块,锁住的就是synchronized(obj)中的obj对象,obj可能是一个对象,或者类,或者当前实例对象。看下面测试代码:
@Test void testIncrement() { CountDownLatch countDownLatch = new CountDownLatch(500); ExecutorService executorService = Executors.newFixedThreadPool(50); for (int i = 0; i < 500; i++) { executorService.execute(() -> { try { userService.syncIncrement(1L); countDownLatch.countDown(); } catch (Exception e) { e.printStackTrace(); } finally { } }); } try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } }
测试结果:
为什么我们在事务方法上加了synchronized同步锁,还会导致线程安全问题,这就跟synchronized锁对象和事务隔离级别有关。
比如在这里,userService.syncIncrement(1L),方法上的synchronized锁就是userService对象,spring aop的原理是JDK动态代理或CGLIB动态代理,在代理对象执行方法之前开启事务,在代理对象执行方法之后提交事务。
因此如果发生高并发,这个同步标志是失效的,原因就是事务在执行的时候,是由spring 生成的代理类执行,在代理方法执行前后锁住的不是同一个对象,如果此时有高并发,请求依然会进入到这个同步的方法内部,造成的结果就是数据库幻读
,由于A线程事务还没有提交,B线程读取到的数据不正确造成最终数据不准确。
解决这种现象的方式有很多,这里列举几种:
executorService.execute(() -> {
try {
synchronized (lock) {
userService.syncIncrement(1L);
countDownLatch.countDown();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
}
});
executorService.execute(() -> {
try {
reentrantLock.lock();
userService.syncIncrement(1L);
countDownLatch.countDown();
} catch (Exception e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
});
isolation = Isolation.SERIALIZABLE
一进入syncIncrement方法就获得了读锁,而是在studentMapper.selectOne才获得了读锁,这里只要把读和写作为临界资源即可保证线程安全。@Transactional(rollbackFor = Exception.class,isolation = Isolation.SERIALIZABLE) public synchronized int syncIncrement(Long studentId) { try { Student student = studentMapper.selectOne(studentId); student.setAge(student.getAge() + 1); return studentMapper.updateStudent(student); } catch (Exception e) { e.printStackTrace(); return -1; } finally { } } @Transactional(rollbackFor = Exception.class) public int syncIncrement(Long studentId) { try { synchronized (this) { Student student = studentMapper.selectOne(studentId); student.setAge(student.getAge() + 1); return studentMapper.updateStudent(student); } } catch (Exception e) { e.printStackTrace(); return -1; } finally { } }
spring默认事务隔离级别与数据库底层保持一致,mysql中默认事务隔离级别为repeatable-read
/**
* Use the default isolation level of the underlying datastore.
* All other levels correspond to the JDBC isolation levels.
* @see java.sql.Connection
*/
int ISOLATION_DEFAULT = -1;
其他方案可以在评论区交流,笔者暂时想不到
我们都知道spring 注解事务底层原理都是spring aop,而spring aop无非就是JDK动态代理和CGLIB动态代理,默认为JDK动态代理,通过@EnableTransactionManagement属性proxyTargetClass=true可以启动CGLIB代理,两种代理的介绍可以参照这篇文章《spring的动态代理模式有几种?默认是那种?如何切换?》
总结事务失效的情况,下面情况事务均会失效
1.静态方法
@Transactional(rollbackFor = Exception.class) public static int incrementThrow(Long studentId) { try { Student student = studentMapper.selectOne(studentId); student.setAge(student.getAge() + 1); return studentMapper.updateStudent(student); } catch (Exception e) { e.printStackTrace(); return -1; } finally { System.out.println(1/0); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
2.private或者protected方法
@Transactional(rollbackFor = Exception.class) private int incrementThrow(Long studentId) {...} @Transactional(rollbackFor = Exception.class) protected int incrementThrow(Long studentId) {...}
- 1
- 2
- 3
- 4
- 5
3.自身非事务方法调用自身的事务方法,如通过调用invokeSelf间接调用事务方法
@Transactional(rollbackFor = Exception.class) public int incrementThrow(Long studentId) {...} public int invokeSelf(Long studentId){ return this.incrementThrow(studentId); }
- 1
- 2
- 3
- 4
- 5
- 6
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。