1. 引言
1.1 为什么需要锁(并发控制)?
在多用户环境中,在同一时间可能会有多个用户更新相同的记录,这会产生冲突。这就是著名的并发性问题。
典型的冲突有:
-
丢失更新
一个事务的更新覆盖了其它事务的更新结果,就是所谓的更新丢失。例如:用户A把值从6改为2,用户B把值从2改为6,则用户A丢失了他的更新。 -
脏读
当一个事务读取其它完成一半事务的记录时,就会发生脏读取。例如:用户A,B看到的值都是6,用户B把值改为2,用户A读到的值仍为6。
1.2 锁一般分为哪几种?
-
悲观锁(Pessimistic Lock)
++假定会发生并发冲突++,屏蔽一切可能违反数据完整性的操作。
Java 中的 synchronized,如果某个资源定义为 synchronized,那么该资源的调用只能排队,一个使用完成后,另外一个才能开始使用。 -
乐观锁(Optimistic Lock)
++假设不会发生并发冲突++,只在提交操作时++检查++是否违反数据完整性。 乐观锁不能解决脏读的问题。
Java JUC中的atomic (原子) 包就是乐观锁的一种实现,AtomicInteger 通过CAS(compare-and-swap)操作实现线程安全的自增。
2. 悲观锁
2.1 概述
悲观锁 ++假定其他用户企图访问或者改变你正在访问、更改的对象的概率是很高的++,因此在悲观锁的环境中,在你开始改变此对象之前就将该对象锁住,并且++直到你提交了所作的更改之后才释放锁++。
悲观的缺陷 是不论是页锁还是行锁,加锁的时间可能会很长,这样可能会++长时间的锁定一个对象,限制其他用户的访问++,也就是说++悲观锁的并发访问性不好++。
悲观锁,主要依赖的是数据库的排他锁来实现
2.2 SQL 实现
实现悲观锁,需要以下两步:
- 使用 transaction,当 commit/rollback 时释放锁。
- 使用 FOR UPDATE 请求对查询的资源进行加锁。
我们使用 SQL 来做一个试验 (基于PostgreSQL),试验假设的内容:
- 第一步,【在查询窗口1】使用悲观锁对资源进行加锁。
- 第二步,【在查询窗口1】使用 sleep 模拟业务处理的等待。
-- 在窗口1先执行以下代码(自行保证数据存在):
BEGIN;
SELECT * FROM charge_stations WHERE id = '0404de7c-ccc5-426f-b1ec-8effa9a31a88' FOR UPDATE;
SELECT pg_sleep(10);
COMMIT;
- 第三步,【在查询窗口2】在另外一个窗口请求对相同资源的访问。分别测试了以下四个情况:
情景 1. 不使用悲观锁对资源进行访问:
SELECT * FROM charge_stations WHERE id = '0404de7c-ccc5-426f-b1ec-8effa9a31a88'
-- 结果:不需要等待窗口1完成,直接输出结果
情景 2. 使用悲观锁进行请求访问:
SELECT * FROM charge_stations WHERE id = '0404de7c-ccc5-426f-b1ec-8effa9a31a88' FOR UPDATE;
-- 结果:需要等待窗口1完成后,才输出结果
情景 3. 使用悲观锁进行请求访问:
SELECT * FROM charge_stations WHERE id = '0404de7c-ccc5-426f-b1ec-8effa9a31a88' FOR UPDATE;
-- 结果:需要等待窗口1完成后,才输出结果
-- 注意:窗口1的记录,它的serial_number是10027
SELECT * FROM charge_stations WHERE serial_number LIKE '1002%' FOR UPDATE;
-- 结果:需要等待窗口1完成后,才输出结果
情景 4. 使用悲观锁进行其它资源访问:
-- 需要保证资源存在,才能达到测试的目的
SELECT * FROM charge_stations WHERE serial_number = '10000' FOR UPDATE;
-- 结果:不需要等待窗口1完成,直接输出结果
==综合可以得出,只有都在使用悲观锁,而且是对相同的资源,才会导致后面的访问等待。==
2.3 Rails 的悲观锁实现 (程序层面使用悲观锁)
锁方法
相关方法有:lock、lock! 和 with_lock.
其中,lock 和 with_lock 都是封装 lock! 而来。
https://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html
方法一:lock
为 ActiveRecord Relation 提供锁请求。
用例:
Account.lock.find(1)
-- Output: select * from accounts where id=1 for update
完整的锁应用例子:
# 需要手动启动事务 和 加锁,不过 lock可以加参数,设置不同的lock模式,如: 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'
Account.transaction do
# select * from accounts where name = 'shugo' limit 1 for update
shugo = Account.where(name: 'shugo').lock(true).first
yuko = Account.where(name: 'yuko').lock(true).first
shugo.balance -= 100
shugo.save!
yuko.balance += 100
yuko.save!
end
方法二:with_lock
自动启动事务,和 请求加锁
用例:
account = Account.find_by(name: 'shugo')
account.with_lock do
# This block is called within a transaction,
# account is already locked.
account.balance -= 100
account.save!
end
3. 乐观锁
3.1 概述
乐观锁认为其他用户企图改变你正在更改的对象的概率是很小的,因此++乐观锁直到你准备提交所作的更改时才将对象锁住++,当你++读取以及改变该对象时并不加锁++。
可见乐观锁++加锁的时间要比悲观锁短++,乐观锁可以++用较大的锁粒度获得较好的并发访问性能++。
脏读导致的失败甚至要重置问题:如果第二个用户恰好在第一个用户提交更改之前读取了该对象,那么当他完成了自己的更改进行提交时,数据库就会发现该对象已经变化了,这样,第二个用户不得不重新读取该对象并作出更改。这说明在乐观锁环境中,++会增加并发用户读取对象的次数++。
乐观锁是一种并发解决的思想,是通过程序层面 + 数据库字段协同实现的。
3.2 SQL 实现
在数据库层面,主要有两种方式协助实现乐观锁:
- 增加 version 字段
Integer 类型,每次修改都增加1;修改当前记录时需要比较 version 与 读取的记录的 version 是否还保持一致。 - 增加 timestamp 字段(名字可以叫 updated_time)
每次记录更新后,都更新该字段为当前时间;修改当前记录时需要比较 timestamp 与 读取的记录的 timestamp 是否还保持一致。
3.3 Rails 的乐观锁实现
https://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html
4. 锁的应用场景
从数据库厂商的角度看,使用乐观的页锁是比较好的,尤其在影响很多行的批量操作中可以放比较少的锁,从而降低对资源的需求提高数据库的性能。再考虑聚集索引。在数据库中记录是按照聚集索引的物理顺序存放的。如果使用页锁,当两个用户同时访问更改位于同一数据页上的相邻两行时,其中一个用户必须等待另一个用户释放锁,这会明显地降低系统的性能。interbase和大多数关系数据库一样,采用的是乐观锁,而且读锁是共享的,写锁是排他的。可以在一个读锁上再放置读锁,但不能再放置写锁;你不能在写锁上再放置任何锁。锁是目前解决多用户并发访问的有效手段。
综上所述:在实际生产环境里边,如果并发量不大且不允许脏读,可以使用悲观锁解决并发问题;
但如果系统的并发非常大的话,悲观锁定会带来非常大的性能问题,所以我们就要选择乐观锁定的方法.
5. 并发锁问题
6. 数据库锁
https://blog.csdn.net/puhaiyang/article/details/72284702
https://www.cnblogs.com/deliver/p/5730616.html
AASM lock。
https://github.com/aasm/aasm/blob/master/README.md
网友评论