乐观锁 & 悲观锁

1 悲观锁

         悲观锁是一种利用数据库内部机制提供的锁的方法,也就是对更新的数据加锁,这样在并发期间一旦有一个事务持有了数据库记录的锁,其他的线程将不能再对数据进行更新了,这就是悲观锁的实现方式。如下所示:

<select id="testForUpdate" resultMap="unionColumnMap" parameterType="String">
        SELECT
	    <include refid = "column1" /> 
        FROM
	    table1 pi
	    LEFT JOIN table2 sc ON pi.STAT_CD = sc.STAT_CD 
        WHERE
	    pi.ID = #{standardCode} FOR UPDATE
</select>

        在SQL中加入的for update语句,意味着在高并发的场景下,当一条事务持有了这个更新锁才能往下操作,其他的线程如果要更新这条记录,都需要等待,这样就不会出现数据一致性问题了。

        但是对于悲观锁来说,当一条线程抢占了资源后,其他的线程将得不到资源,那么这个时候,CPU就会将这些得不到资源的线程挂起,挂起的线程也会消耗CPU的资源。一旦释放资源后,就开始抢夺,恢复线程,周而复始直至所有资源被抢完。有些时候,我们也会把悲观锁称为独占锁,毕竟只有一个线程可以独占这个资源,或者称为阻塞锁,因为它会造成其他线程的阻塞。无论如何它都会造成并发能力的下降,从而导致CPU频繁切换线程上下文。


2 乐观锁

        乐观锁是一种不会阻塞其他线程并发的机制,它不会使用数据库的锁进行实现,它的设计里面由于不阻塞其他线程,所以并不会引发线程频繁挂起和恢复,这样便能够提高并发能力,所以也有人把它称为非阻塞锁。乐观锁使用的是CAS原理。

2.1 CAS原理概述

        在CAS原理中,对于多个线程共同的资源,先保存一个旧值。比如一个线程的方法中读到旧值为100,然后经过一定的业务逻辑处理后,再比较数据库当前的值和旧值100是否一致,如果一致则进行更新数据的操作,否则就认为它已经被其他线程修改过了,可以考虑重试或者放弃。

2.2 ABA问题

        对于乐观锁而言,可能存在ABA的问题。如下:

        ABA问题指的是:当一个线程读到旧值X为A,这时另一个线程也读到旧值X为A,它首先将X改为B,并开始处理自己的第一段业务逻辑,然后将X又改回成A,这时第一个线程执行完业务逻辑,判断X=A,所以更新数据。第二个线程处理第二段业务逻辑,然后再判断X=A,更新数据。这时就有两个线程同时对资源进行了操作。

        ABA问题的发生,是因为业务逻辑存在回退的可能性。如果加入一个非业务逻辑的属性,比如在一个数据中加入版本号(version),对于版本号有一个约定,就是只要修改X变量的数据,强制版本号只能递增,而不能回退,即使是其他业务数据回退,它也会递增,那么ABA问题就解决了。

        如上面的例子,一开始version=0。在T2时刻线程2 X=B时将version+1,此时version=1,T4时刻线程2 X=A时version再+1变为2。T5时刻线程1更新数据时发现version变为了2,而不是一开始读到的0,所以放弃更新数据的操作。

2.3 乐观锁实现

<update id="updateTest">
        UPDATE table1
        SET stock = stock - 1,
	version = version + 1
	WHERE
	    id = #{id}
            AND version = #{version}
</update>

        如上所示:每次对stock-1的时候都会对版本号+1,从而避免了ABA问题的出现。

2.4 乐观锁重入机制

        乐观锁可能会造成大量更新失败的问题,使用时间戳或限制重试次数来执行乐观锁重入,是能提高成功率的方法。

2.4.1 时间戳方式

@Override
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public int testForOL(long testId) {
    long start = System.currentTimeMillis();
    while (true) {
        long end = System.currentTimeMillis();
        //如果重入超过100毫秒,则返回失败
        if (end - start > 100) {
            return -1;
        }
        TestVO testVO = testDao.getTest(testId);
        int update = testDao.updateTest(testId, testVO.getVersion());
        if (0 == update) {
            //如果没有数据更新,则说明其他线程已经修改过数据,则重新抢夺
            continue;
        }
        //do something...
    }
}

2.4.2 限制重试次数方式

@Override
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public int testForOL(long testId) {
    for (int i = 0; i < 3; i++) {
        TestVO testVO = testDao.getTest(testId);
        int update = testDao.updateTest(testId, testVO.getVersion());
        if (0 == update) {
            //如果没有数据更新,则说明其他线程已经修改过数据,则重新抢夺
            continue;
        }
        //do something...
    }
}

3 总结

        两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,应用会不断地进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值