为什么会使用到数据库级别的锁?
你可能会有这么一个疑问:现在的程序已经提供了很完善的锁机制来保证多线程的安全问题,还需要用到数据库级别的锁吗?我觉得还是需要的,为什么呢?理由很简单,我们再编程中使用的大部分锁都是单机,尤其是现在分布式集群的流行,这种单机的锁机制就保证不了线程安全了,这个时候,你可能又会想到使用redis的setNX分布式锁或者zookeeper的强一致性来保证线程安全,但是这里我们需要考虑到一个问题,那就是成本问题,有的时候使用redis分布式锁以及zookeeper会增加维护的成本,结合实际出发,再说没有百分百安全的程序,所以再数据库层加锁,也能将安全再提升一级,所以还是有必要的。
什么是悲观锁
悲观锁,正如其名,具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
通俗的讲:开启一个事务之后开启悲观锁,这时候数据库将会锁着你需要查询的某条数据或者某张表,其他事务中的查询将会处于阻塞状态,开启悲观锁事务里面操作不会被阻塞,这点有点类似java中的互斥锁(其中的可重入锁),那什么时候锁记录?什么时候锁整张表呢?接着往下看。
mysql悲观锁如何使用?
1.在查询后加:for update
2.需要先开启事务,否者悲观锁无效
3.执行完查询之后一定要接上update语句,否者其他事物会一直处于阻塞状态,直到第一个事务抛出异常为止。
我们看一个例子,假如用户现在有100块钱,买充电器需要100,买耳机也需要100,这时候用户同时买下这两款商品,会发生什么事情呢?
我们分别说一下正常情况和加了悲观锁的情况,这里暂时不讨论程序锁的问题,如果想了解程序中的锁,请参考:java并发编程之synchronized、java并发编程之ReentrantReadWriteLock读写锁等等。
我在数据库新建了一张表:
表比较简单,我们只需要关注用户id和用户余额,我们等会会用到,我们现在就来模拟一下同时扣款100元,会发生什么情况,直接上代码
单元测试代码:
@Resource
private IUserWalletService userWalletService;
@Test
void deductMoney() throws InterruptedException {
//需要扣除的金额
BigDecimal meney = BigDecimal.valueOf(100l);
//新建第一个线程t1
Thread t1 = new Thread(() ->{
//线程1:让用户1扣除100元
userWalletService.deductMoney(1, meney);
});
//新建第一个线程t2
Thread t2 = new Thread(() ->{
//线程2:让用户1扣除100元
userWalletService.deductMoney(1, meney);
});
//启动线程1
t1.start();
//启动线程1
t2.start();
//让线程同步
t1.join();
t2.join();
System.out.println("执行完毕");
}
service代码:
private UserWalletMapper userWalletMapper;
UserWalletServiceImpl(UserWalletMapper userWalletMapper){
this.userWalletMapper = userWalletMapper;
}
@Override
@Transactional
public void deductMoney(int userId, BigDecimal money) {
//获取线程名
String threadName = Thread.currentThread().getName();
//查询当前用户钱包信息
UserWallet userWallet = userWalletMapper.getWalletByUserId(userId);
log.info("线程&