乐观锁&悲观锁

悲观锁

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

    <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频繁切换线程上下文。

乐观锁

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

CAS原理概述

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

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,所以放弃更新数据的操作。

乐观锁实现

    <update id="updateTest">
        UPDATE table1
        SET stock = stock - 1,
	version = version + 1
	WHERE
	    id = #{id}
            AND version = #{version}
    </update>
        如上所示:每次对stock-1的时候都会对版本号+1,从而避免了ABA问题的出现。

乐观锁重入机制

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

        时间戳方式:

    @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...
        }
    }

        限制重试次数方式:

    @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...
        }
    }

总结

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

参考资料:[1]杨开振 周吉文 梁华辉 谭茂华.Java EE 互联网轻量级框架整合开发:SSM框架(Spring MVC+Spring+MyBatis)和Redis实现[M].北京:电子工业出版社,2017.7

猜你喜欢

转载自blog.csdn.net/weixin_30342639/article/details/80876026