乐观锁&悲观锁
目录
悲观锁
悲观锁是一种利用数据库内部机制提供的锁的方法,也就是对更新的数据加锁,这样在并发期间一旦有一个事务持有了数据库记录的锁,其他的线程将不能再对数据进行更新了,这就是悲观锁的实现方式。如下所示:
<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,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。