理解并发下的乐观锁与悲观锁

锁( Locking )

锁在我们学习多线程的时候曾经接触过,其实这里的锁和多线程里面处理并发的锁是一个道理,都是暴力的把资源归为自己所有。这里我们用到锁的目的就是通过一些机制来保证一些数据在某个操作过程中不会被外界修改,这样的机制,在这里也就是所谓的“锁”,即给我们选定的目标数据上锁,使其无法被其他程序修改。乐观锁与悲观锁是两种常见的资源并发锁设计思路,也是并发编程中一个非常基础的概念。

乐观锁(Optimistic Locking)

乐观锁不是数据库自带的,需要我们自己去实现。乐观锁是指操作数据库时(更新操作),想法很乐观,认为这次的操作不会导致冲突,在操作数据时,并不进行任何其他的特殊处理(也就是不加锁),而在进行更新后,再去判断是否有冲突了。
乐观锁在不发生取锁失败的情况下开销比悲观锁小,但是一旦发生失败回滚开销则比较大,因此适合用在取锁失败概率比较小的场景,可以提升系统并发性能。

适用场景

乐观锁比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。
乐观锁还适用于一些比较特殊的场景,例如在业务操作过程中无法和数据库保持连接等悲观锁无法适用的地方。

实现方式

1、版本号方式:在数据表中加上一个数据版本号Version字段,表示数据被修改的次数,当数据被修改时,Version值会加1。当线程A要更新数据值时,在读取数据的同时也会读取Version值,在提交更新时,若刚才读取到的Version值为当前数据库中的Version值相等时才更新,否则重试更新操作,直到更新成功。

-- 查询出订单信息
SELECT `status`, `version` FROM `order` WHERE `id` = id;
-- ...
-- 其他业务处理
-- ...
-- 根据获取的数据进行业务操作,得到new_data和new_version
UPDATE `order` SET `status` = "new_status", `version` = `version` + 1 
WHERE `id` = "id" AND `version` = version;

返回更新行数 row,以下交由程序进行处理 

if (row > 0) {
    // 乐观锁获取成功,操作完成
} else {
    // 乐观锁获取失败,回滚并判断是否重新执行业务
}

2、CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

CAS操作包含三个操作数--内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值)。CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”这其实和乐观锁的冲突检查+数据更新的原理是一样的。

悲观锁(Pessimistic Lock)

悲观锁就是在操作数据时,每一个对他操作的程序都有可能产生并发冲突,所以在进行每次操作时都要通过获取锁才能进行对相同数据的操作,所以悲观锁需要耗费较多的时间。悲观锁是由数据库自己实现了的,也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据。

适用场景

比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。

至此由悲观锁涉及到的另外两个锁概念就出来了,它们就是共享锁与排它锁。共享锁和排它锁是悲观锁的不同的实现,它俩都属于悲观锁的范畴。

共享锁(Share Lock)

共享锁又称为读取(Share lock,简记为S锁),指的就是对于多个不同的事务,对同一个资源共享同一个锁,在SQL执行语句后面加上lock in share mode。若事务T对数据对象A加上S锁,则事务T只能读A;其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这就保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。

例如:进程一给对象data表加共享锁,暂时注释COMMIT以做测试

BEGIN;
SELECT * FROM `data` WHERE `id` = 1 LOCK IN SHARE MODE;
-- COMMIT;

进程二可以对data表进行读查询以及添加共享锁,但执行update,insert,delete操作时,会返回错误:

[Err] 1205 - Lock wait timeout exceeded; try restarting transaction

排它锁(Eclusive Lock)

排它锁又称为写锁(eXclusive lock,简记为X锁),指对于多个不同的事务,对同一个资源只能有一把锁,在SQL执行语句后面加上for update。若事物T对数据对象A加上X锁,则只允许T读取和修改A,其它任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。它防止任何其它事务获取资源上的锁,直到在事务的末尾将资源上的原始锁释放为止。

例如:进程一给对象data表加排他锁

BEGIN;
SELECT * FROM `data` WHERE `id` = 1 FOR UPDATE;
-- COMMIT;

进程二可以对data表进行读查询,但执行update,insert,delete操作或添加锁时,会返回错误:

[Err] 1205 - Lock wait timeout exceeded; try restarting transaction

完整实现

-- 开始事务
BEGIN;
-- 查询出订单信息
SELECT `status` FROM `order` WHERE `id` = id FOR UPDATE;
-- ...
-- 其他业务处理
-- ...
UPDATE `order` SET `status` = "new_status" WHERE `id` = "id";
-- 提交事务
COMMIT;

猜你喜欢

转载自blog.csdn.net/wangjianan7357/article/details/81261132