java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > java 乐观锁与悲观锁

java中乐观锁与悲观锁区别及使用场景分析

作者:kerwin_code

本文主要介绍了java中乐观锁与悲观锁区别及使用场景分析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

一、什么是乐观锁?什么是悲观锁

个人理解一句话概括就是,在应用层面会造成线程阻塞的是悲观锁,而不会造成线程阻塞的是乐观锁,为什么这么说会在后续的内容中做详细介绍。

1.1、悲观锁

1.2、乐观锁

二、乐观锁与悲观锁分别适用于什么场景

2.1、悲观锁适用场景

2.2、乐观锁适用场景

三、乐观锁与悲观锁各自优缺点

3.1、悲观锁

优点

缺点

3.2、乐观锁

优点

缺点

四、乐观锁与悲观锁使用示例

这里举例会以操作数据库实现乐观锁与悲观锁示例,在实际开发中一般在操作数据库时经常会使用到乐观锁与悲观锁的实现思路来确保数据一致性问题,这里会以一个更新用户钱包举例。

用户钱包表

CREATE TABLE `customer_wallet` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `customer_id` bigint(20) DEFAULT NULL COMMENT '客户ID',
  `balance_amount` bigint(20) DEFAULT NULL COMMENT '剩余金额',
  `version` bigint(20) DEFAULT '1' COMMENT '版本锁',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `idx_customer_id` (`customer_id`)
) COMMENT='客户钱包信息';

4.1、悲观锁实现更新用户钱包示例

这里提供一个使用悲观锁扣减钱包余额的示例,在第一次查询时添加for update操作,那么其它线程进入该方法时则会阻塞等待上一个方法事务提交才能继续执行,在这整个方法中都是线程安全的,这就是常见的结合数据库实现悲观锁更新数据的示例,所有线程都必须排队串行更新数据。

    @Transactional(rollbackFor = Exception.class)
    public boolean pessimisticLockSubAmount(Long customerId, Long happenAmount) {
        // 1、查询用户钱包 - 并且添加for update 锁,这里customer_id字段添加了索引最终锁定的还是索引定义行的ID,和直接使用ID区别不大
        // 这段代码相当于 select * from customer_wallet where customer_id = ? for update
        CustomerWallet customerWallet = lambdaQuery()
                .eq(CustomerWallet::getCustomerId, customerId)
                .last("for update")
                .one();
        if(customerWallet == null){
            throw new RuntimeException("用户钱包不存在");
        }
        // 2、校验用户余额是否足够
        Long balanceAmount = customerWallet.getBalanceAmount() - happenAmount;
        if(balanceAmount < 0){
            throw new RuntimeException("用户余额不足");
        }
        // 3、更新钱包余额 update customer_wallet set balance_amount = ? where id = ?
        boolean update = lambdaUpdate()
                .eq(CustomerWallet::getId, customerWallet.getId())
                .set(CustomerWallet::getBalanceAmount, balanceAmount)
                .update();
        if(!update){
            throw new RuntimeException("钱包更新失败");
        }
        // 4、添加余额明细
        addWalletDetail(customerWallet.getId(),2,happenAmount,balanceAmount);
        return update;
    }

4.2、乐观锁实现更新用户钱包示例1

使用乐观锁更新数据时,执行更新语句时通过判断version是否有变动来确认数据是否有过变更,如果数据库当前version值和查询出来的version值相等则代表数据没有变更可以更新,因为数据库指定ID更新某一行数据时是在数据库层面会添加行锁,确保只能有一个事务进行这行数据更新,这样就保证了数据的一致性。

    @Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class)
    public boolean subAmount(Long customerId, Long happenAmount) {
        // 1、获取用户钱包
        CustomerWallet customerWallet = lambdaQuery().eq(CustomerWallet::getCustomerId, customerId).one();
        if (customerWallet == null) {
            throw new RuntimeException("用户钱包不存在");
        }
        // 2、判断用户余额是否足够
        Long balanceAmount = customerWallet.getBalanceAmount() - happenAmount;
        if(balanceAmount < 0){
            throw new RuntimeException("用户余额不足");
        }
        // 3、进行乐观锁更新 
        // 这段代码相当于 update customer_wallet set balance_amount = ?, version = ? where id = ? and version = ?
        boolean update = lambdaUpdate()
                .eq(CustomerWallet::getId, customerWallet.getId())
                .eq(CustomerWallet::getVersion, customerWallet.getVersion())
                .set(CustomerWallet::getBalanceAmount, balanceAmount)
                .set(CustomerWallet::getVersion, customerWallet.getVersion() + 1)
                .update();
        if(!update){
            log.info("乐观锁更新失败,开始自旋");
            return subAmount(customerId,happenAmount);
        }
        // 4、添加余额明细
        addWalletDetail(customerWallet.getId(),2,happenAmount,balanceAmount);
        return update;
    }

PS:注意,在使用乐观锁更新数据时,事务隔离级别必须设置为READ_COMMITTED,在最后注意事项中会进行分析。

4.3、乐观锁实现更新用户钱包示例2

乐观锁实现更新用户钱包示例1中使用了一个version字段来作为乐观锁更新的标记,其实对于这种更新钱包的业务想使用乐观锁完全没有必要单独加一个version字段,可以直接使用余额字段作为这个乐观锁的比较字段,因为我们这里拟定的是用户余额需要足够才能支付,那么在更新钱包时判断一下当前余额是否大于等于所需金额,如果满足调整则

    @Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class)
    public boolean subAmountV2(Long customerId, Long happenAmount) {
        // 1、获取用户钱包
        CustomerWallet customerWallet = lambdaQuery().eq(CustomerWallet::getCustomerId, customerId).one();
        if (customerWallet == null) {
            throw new RuntimeException("用户钱包不存在");
        }

        // 2、直接使用余额作为乐观锁更新依据进行乐观锁更新
        // 这段代码相当于 update customer_wallet set balance_amount = balance_amount + ? where id = ? and balance_amount >= ?
        boolean update = lambdaUpdate()
                .eq(CustomerWallet::getId, customerWallet.getId())
                .ge(CustomerWallet::getBalanceAmount, happenAmount)
                .setSql("balance_amount = balance_amount - "+happenAmount)
                .update();
        if(!update){
            log.info("乐观锁更新失败,用户余额不足");
            throw new RuntimeException("用户余额不足");
        }
        // 3、添加余额明细 注意这里需要从新查询一次数据
        CustomerWallet customerWalletNew = lambdaQuery().eq(CustomerWallet::getCustomerId, customerId).one();
        addWalletDetail(customerWallet.getId(),2,happenAmount,customerWalletNew.getBalanceAmount());
        return update;
    }

PS:注意,在使用乐观锁更新数据时,事务隔离级别必须设置为READ_COMMITTED,在最后注意事项中会进行分析。

五、注意事项

在使用乐观锁更新数据时要注意一个事务隔离级别的问题,我这里使用的是READ COMMITTED(读已提交),如果使用的是REPEATABLE READ(可重复读)会存在两个问题,分别对应4.2 和 4.3中的示例。

到此这篇关于java中乐观锁与悲观锁区别及使用场景分析的文章就介绍到这了,更多相关java 乐观锁与悲观锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家! 

您可能感兴趣的文章:
阅读全文