1.应用场景
开发过程中经常遇到多线程更新数据实体,例如库存管理(入库与出库,出库与出库)等。
多线程更新数据实体会造成数据覆盖,造成bug.
2.场景模拟
batch(){
for(15){
new Thread(()->{test()}).start()
}
}
test(){
entity = findById(10L)
entity.setNum(entity.getNum()-1)
save(entity);
}
初始化库存100,运行结束库存90。
3.解决方案
可以从两方面解决:应用层面和数据库层面。应用层面使用synchronize或ReentrantLock,数据库层面for update 或者 乐观锁。
4.锁
synchronize 修饰 test() 方法。
ReentrantLock 可重入锁,test() 使用的经典范式:
lock.lock()
try{
//业务代码
}finally{
lock.unlock()
}
synchronize 优化后性能和Lock 差不多;不过ReentrantLock更灵活,可以用非阻塞方式获取锁,可以响应中断,可以设置阻塞时间。ReentrantLock 可以使公平锁或非公平锁,synchronize只能是非公平锁。ReentrantLock是jdk提供的,synchronize是jvm提供的。
事务和锁发生的异常
如果test方法启用@Transational 可能会发生异常,因为Transational真正启动时是业务代码第一条sql语句,提交事务是在执行方法体后。执行过程
lock-->transational start -->unlock -->transaciontal commit ,无法保证事务的原子性。
可以改变执行过程 lock -->transational start -->transational end -->unlock
解决方案是:业务代码单独写一个方法,启动事务。
5.sql
(1)悲观锁
select * from table where id =#{id} for update
查询数据加行锁,更新完成,释放锁。注意id必须是索引,否则行锁会升级为表锁。
(2)乐观锁
在实体类和表加字段version;在更新之前比较版本号,如果版本号不同说明已经有更新了,更新失败,重新读取实体数据进行更新。
update client set num =#{num} ,version = version + 1 where id = #{id} and version = #{version}