🔒SQL:共享锁、排他锁、乐观锁、悲观锁,行级锁以及表级锁
🔒锁的产生
基础常识
首先,你需要对于数据库系统原理有一定的认识,你需要了解事务的概念,什么是事务?
⚙️事务 : 指的是满足 ACID 特性的一系列操作。在数据库中,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。
事务的ACID 特性:原子性(Atomicity)、一致性(Consistency)、一致性(Consistency)、持久性(Durability)
-
🚩原子性:
事务被视为不可分割的最小单元,要么全部提交成功,要么全部失败回滚
-
🚩一致性:
事务执行前后都保持一致性状态。在一致性状态下,所有事务对一个数据的读取结果都是相同的
- 🚩隔离性:
一个事务所做的修改在最终提交以前,对其它事务是不可见的
- 🚩持久性:
一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。可以通过数据库备份和恢复来保证 持久性
📉出现问题
在并发环境下,一个事务如果受到另一个事务的影响,那么事务操作就无法满足一致性条件。就可能出现如下的情况
1.丢失修改
T1 和 T2 两个事务都对一个数据进行修改,T1 先修改,T2 随后修改,T2 的修改覆盖了 T1 的修改。
2. 读脏数据
T1 修改一个数据,T2 随后读取这个数据。如果 T1 撤销了这次修改,那么 T2 读取的数据是脏数据。
3. 不可重复读
T1 读取一个数据,T2 对该数据做了修改。如果 T1 再次读取这个数据,此时读取的结果和和第一次读取的结果不同。
4. 幻影读
T1 读取某个范围的数据,T2 在这个范围内插入新的数据,T1 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。
📈解决办法
产生并发不一致性问题主要原因是破坏了事务的隔离性,解决方法是通过并发控制来保证隔离性。
可串行化调度
在没有并发的情况下,事务以串行的方式执行,互不干扰,因此可以保证隔离性。在并发的情况下,如果能通过并发控制,让事务的执行结果和某一个串行执行的结果相同,就认为事务的执行结果满足隔离性要求,也就是说是正确的。把这种事务执行方式称为 可串行化调度 。
封锁
并发控制可以通过封锁来实现,但是封锁操作都要用户自己控制,相当复杂。数据库管理系统提供了事务的隔离级别,让用户以一种更轻松的方式处理并发一致性问题。
🔒锁的类型
🥇排它锁(X锁)与共享锁(S锁)
🔓共享锁(S锁)
共享锁(Shared),简写为 S 锁,又称读锁
一个事务对数据对象 A 加了 S 锁,可以对 A 进行读取操作,但是不能进行更新操作。加锁期间其它事务能对 A 加 S 锁,但是不能加 X 锁。
🔐排它锁(X锁)
排他锁(X锁)又称写锁。:排它锁与共享锁相对应,就是指对于多个不同的事务,对同一个资源只能有一把锁。用于数据修改操作,例如 INSERT、UPDATE 或 DELETE。确保不会同时同一资源进行多重更新。
一个事务对数据对象 A 加了 X 锁,就可以对 A 进行读取和更新。加锁期间其它事务不能对 A 加任何锁。
🥈乐观锁 与 悲观锁
😀乐观锁
乐观锁不是数据库自带的,需要我们自己去实现,是一种思想。乐观锁是指操作数据库时(更新操作),想法很乐观,认为这次的操作不会导致冲突,在操作数据时,并不进行任何其他的特殊处理(也就是不加锁),而在进行更新时,去对比版本(version)字段是否相等,去判断是否有冲突了。
通常实现是这样的:在表中的数据进行操作时(更新),先给数据表加一个版本(version)字段,每操作一次,将那条记录的版本号加1。也就是先查询出那条记录,获取出version字段,如果要对那条记录进行操作(更新),则先判断此刻version的值是否与刚刚查询出来时的version的值相等,如果相等,则说明这段期间,没有其他程序对其进行操作,则可以执行更新,将version字段的值加1;如果更新时发现此刻的version值与刚刚获取出来的version的值不相等,则说明这段期间已经有其他程序对其进行操作了,则不进行更新操作。
举例:
下单操作包括3步骤:
1.查询出商品信息
select (status,status,version)
from t_goods
where id=#{id}
2.根据商品信息生成订单
3.修改商品status为2
update t_goods
set status=2,version=version+1
where id=#{id}
and version=#{version};
除了自己手动实现乐观锁之外,现在网上许多框架已经封装好了乐观锁的实现,如hibernate,需要时,可能自行搜索"hiberate 乐观锁"试试看。
😞悲观锁
与乐观锁相对应的就是悲观锁了。悲观锁就是在操作数据时,认为此操作会出现数据冲突,所以在进行每次操作时都要通过获取锁才能进行对相同数据的操作,所以悲观锁需要耗费较多的时间。悲观锁是由数据库自己实现了的,要用的时候,我们直接调用数据库的相关语句就可以了。
刚刚说了,对于悲观锁,一般数据库已经实现了,共享锁和排它锁也属于悲观锁,那么共享锁在mysql中是通过什么命令来调用呢。通过在执行语句后面加上lock in share mode就代表对某些资源加上共享锁了。比如,我这里通过mysql打开两个查询编辑器,在其中开启一个事务,并不执行commit语句city表DDL如下:
ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8
begin
SELECT * from city where id = "1" lock in share mode
#然后在另一个查询窗口中,对id为1的数据进行更新
update city set name="北京" where id ="1"
#此时,操作界面进入了卡顿状态,过几秒后,也提示错误信息
[SQL]update city set name="上海" where id ="1"
[Err] 1205 - Lock wait timeout exceeded; try restarting transaction
那么证明,对于id=1的记录加锁成功了,在上一条记录还没有commit之前,这条id=1的记录被锁住了,只有在上一个事务释放掉锁后才能进行操作,或用共享锁才能对此数据进行操作。
🥉行级锁 与 表级锁
📑行级锁
行锁,由字面意思理解,就是给某一行加上锁,也就是一条记录加上锁。
比如之前演示的共享锁语句
SELECT * from city where id = "1" lock in share mode
由于对于city表中,id字段为主键,就也相当于索引。执行加锁时,会将id这个索引为1的记录加上锁,那么这个锁就是行锁。
📙表级锁
表锁,和行锁相对应,给这个表加上锁。