在复习多线程方面,产生了一些关于事务的疑问。
疑问:mysql数据库的默认级别是‘可重复读’,也就是一个事务中,它读到值就是一个真实的值,既然这个事务保证了真实值,那么我并发还需要加一个同步锁吗?
以下是具体测试:
mysql数据表:
(对stock字段进行操作)
springmvc:
@GetMapping("stock/test1")
public void stockTest(){
goodsService.test1();
}
@GetMapping("stock/test2")
public void stockTest2(){
goodsService.test2();
}
serviceImpl:
@Override
@Transactional
public void test1() {
Long l =2600242L;
Stock stock = stockMapper.selectByPrimaryKey(l);//查询语句
stock.setStock(stock.getStock()-5000);
try {
Thread.sleep(10000L);//线程陷入10s的睡眠
stockMapper.updateByPrimaryKey(stock);//插入语句
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
@Transactional
public void test2() {
Long l =2600242L;
Stock stock = stockMapper.selectByPrimaryKey(l);//查询语句
stock.setStock(stock.getStock()+2000);
stockMapper.updateByPrimaryKey(stock);//插入语句
}
测试环境,在浏览器下用url对stock/test1进行访问,在新开窗口对stock/test2进行访问,
test1会在10s内进行睡眠,相当于tomcat开了2个线程
结果:
大家不妨猜想一下,我们要得到值因该是9999-5000+2000=6999
实际结果:
库存变为了4999,那么事务并没有起到作用,test1查询到的真实值在一个事务中并不是最终的真实值。
那么这个数据库‘可重复读’的作用到底是什么呢?细心的网友可能发现上面网址test1一直在转圈,而test2早就访问完了,这是因为两个线程并没有约束关系。
那么来看下一个测试:
修改代码,将Thread.sleep(10000);移动到插入语句后面。
@Override
@Transactional
public void test1() {
Long l =2600242L;
Stock stock = stockMapper.selectByPrimaryKey(l);//查询语句
stock.setStock(stock.getStock()-5000);
try {
stockMapper.updateByPrimaryKey(stock);//插入语句
Thread.sleep(10000L);//线程陷入10s的睡眠
} catch (InterruptedException e) {
e.printStackTrace();
}
}
测试结果:
发现没有?两者都在转圈,这说明test2在等test1的更新语句执行完毕,这里没有同步锁,说明这是事务在进行一个约束,防止更新丢失,大家可以猜一下数据库的值:
非常遗憾的发现,还是造成了更新丢失,说明事务的约束有其局限性,这里因为读写语句的分离,即读和写的时间不同造成了更新丢失,可参考redis单线程的get(),set(),一样会造成更新丢失。如果是只用update语句,例如:'update bank set money = money+500’就不会产生这样的问题。
在这个情景下,用到同步锁就很必要了:
public static Lock lock = new ReentrantLock();//声明一个全局锁
@Override
@Transactional
public void test1() {
lock.lock();//加锁
Long l =2600242L;
Stock stock = stockMapper.selectByPrimaryKey(l);//查询语句
stock.setStock(stock.getStock()-5000);
try {
Thread.sleep(10000L);//线程陷入10s的睡眠
stockMapper.updateByPrimaryKey(stock);//插入语句
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.unlock();//解锁
}
@Override
@Transactional
public void test2() {
lock.lock();//加锁
Long l =2600242L;
Stock stock = stockMapper.selectByPrimaryKey(l);//查询语句
stock.setStock(stock.getStock()+2000);
stockMapper.updateByPrimaryKey(stock);//插入语句
lock.unlock();//解锁
}
结果:
得到一个想要的值。
结论:
数据库的事务隔离级别有一定作用,但是并没有代码中的同步锁这么灵活,要根据实际业务进行选择判断,还有一个值得思考的点是在代码加了同步锁后,数据库的隔离级别是否能够降低,这样有助于提高数据库的性能,不当之处,欢迎讨论。