一、事务方法的同步问题
例如以下这段代码
@RequestMapping("/test")
@Transactional
public int test(){
Test test = testMapper.selectById(1);
int max = test.getMax() + 1;
test.setMax(max);
testMapper.updateById(test);
return max;
}
当用jmeter并发访问1000次这个接口,发现max的值为532,跟预期的1000不一样。
这是因为有些请求查询不到数据库最新的值,导致的同步问题。
这里我使用的项目是springboot+mybatisplus+mysql。
二、解决方法
1.使用select for update解决
@Select("select * from test where id = #{id} for update")
Test selectForUpdate(@Param("id") long id);
for update是一种行级锁,又叫排它锁,一旦用户对某个行施加了行级加锁,则该用户可以查询也可以更新被加锁的数据行,其它用户只能查询但不能更新被加锁的数据行.如果其它用户想更新该表中的数据行,则也必须对该表施加行级锁.即使多个用户对一个表均使用了共享更新,但也不允许两个事务同时对一个表进行更新,真正对表进行更新时,是以独占方式锁表,一直到提交或复原该事务为止。行锁永远是独占方式锁。
只有当出现如下之一的条件,才会释放共享更新锁:
1、执行提交(COMMIT)语句
2、退出数据库(LOG OFF)
3、程序停止运行
2.使用手动事务+同步锁synchronized
@Autowired
private PlatformTransactionManager platformTransactionManager;
@Autowired
private TransactionDefinition transactionDefinition;
@RequestMapping("/test")
public synchronized int test(){
int max = 0;
TransactionStatus transactionStatus = platformTransactionManager.getTransaction(transactionDefinition);
try{
Test test = testMapper.selectById(1);
max = test.getMax() + 1;
test.setMax(max);
testMapper.updateById(test);
platformTransactionManager.commit(transactionStatus);
}catch (Exception e){
platformTransactionManager.rollback(transactionStatus);
}
return max;
}
这里使用手动事务的原因是,spring aop中,在方法执行前开启事务,方法执行后提交事务,也就是说事务的开启和提交过程是没有加锁的,因此无法实现同步。
以下方式是无法实现同步的:
@RequestMapping("/test")
@Transactional
public synchronized int test(){
Test test = testMapper.selectById(1);
int max = test.getMax() + 1;
test.setMax(max);
testMapper.updateById(test);
return max;
}
将数据库值清0,再次使用jmeter测试,发现max的值为647。
说明依然没有解决同步问题。
就算把synchronized放到方法体里也是一样无法实现同步的,锁住的只是方法里的代码块,没有锁住事务的开启和提交过程。
private Object object = new Object();
@RequestMapping("/test")
@Transactional
public int test(){
int max = 0;
synchronized (object) {
Test test = testMapper.selectById(1);
max = test.getMax() + 1;
test.setMax(max);
testMapper.updateById(test);
}
return max;
}
3.使用乐观锁
数据库添加version字段。
public class Test {
private Long id;
private Integer max;
@Version
private Integer version;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Integer getMax() {
return max;
}
public void setMax(Integer max) {
this.max = max;
}
public Integer getVersion() {
return version;
}
public void setVersion(Integer version) {
this.version = version;
}
}
@RequestMapping("/test")
@Transactional(isolation = Isolation.READ_COMMITTED)
public int test(){
int i = 0;
Test test = null;
while(i<1){
test = testMapper.selectById(1);
test.setMax(test.getMax()+1);
i = testMapper.updateById(test);
}
System.out.println(test.getMax());
return test.getMax();
}
这里因为使用了mybatisplus框架,只需要在bean类version属性添加@Version注释,然后使用框架提供的方法查询和修改,默认支持乐观锁。
需要注意的是,这里@Transactional注释中标明了隔离级别为读取已提交。
否则会导致在事务中查询不到数据库最新的数据,导致一直无法更新成功,一直回旋。
原因是@Transactional默认的隔离级别是根据数据库的隔离级别,由于我用的是mysql的innodb引擎,默认的隔离级别是支持可重复读的,导致在事务中,查询数据库的值都是一致的可重复读的,导致无法获取数据库最新的值。
因此将事务隔离级别改为读取已提交就可以解决。
三、总结
事务方法的同步可以使用以下方法解决:
①select for update
②手动事务+同步锁synchronized(可将锁细化)
③乐观锁
并发量不是特别高的话,使用乐观锁的效率最高。