乐观锁
概念
先进行业务操作,不到最后一步不加锁。
适合场景
适合读多写少,如果写过多会导致大量冲突,查询量增加,从而影响读性能。
不能解决脏读问题。
使用形式
- version
update table
set x=x+1, version=version+1
where id=#{id} and version=#{version};
也是用cas的方式自旋,直到更新成功
1. cas
更新时判断传入值与当前内存值是否相同:
——相同则可以修改为新值;
——失败一般重试,进直到更新成功。
悲观锁
概念
在业务处理过程中,加锁保证外部不会修改数据。
适合场景
适合写多读少,如果读过多会导致大量加锁,从而影响写性能。
使用形式
使用数据库提供的方式,保护临界资源不被并发修改
1. 比较
表锁:开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低
页锁:开销和加锁速度介于表锁和行锁之间;会出现死锁;锁定粒度介于表锁和行锁之间,并发度一般
行锁:开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高
- 表锁
分为表共享读锁(Table Read Lock)和表独占写锁(Table Write Lock)。
MYISAM
#表独占写锁
#MyISAM在执行更新操作(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁,无需手动加锁
#锁调度时默认会优先获取写锁,因为重要性写>读
可通过下面方式降低优先级
- 指定INSERT、UPDATE、DELETE语句的LOW_PRIORITY属性,降低该语句的优先级
- 执行命令SET LOW_PRIORITY_UPDATES=1,使该连接发出的更新请求优先级降低。
- 指定启动参数low-priority-updates,使MyISAM引擎默认给予读请求以优先的权利。
获得表film_text的WRITE锁定
mysql> lock table film_text write;
Query OK, 0 rows affected (0.00 sec)
释放锁:
mysql> unlock tables;
Query OK, 0 rows affected (0.00 sec)
#表共享读锁
#获得表film_text的READ锁定
#MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,所以不会出现死锁,而Innodb可能会出现
#加完锁只支持读操作,不会升级,而且只能select锁住的表,否则可能出现
#ERROR 1100 (HY000): Table 'film' was not locked with LOCK TABLES
mysql> lock table film_text read;
Query OK, 0 rows affected (0.00 sec)
#允许在表尾并发插入,但更新会等待
#lock table film_text read local;
#释放锁
mysql> unlock tables;
Query OK, 0 rows affected (0.00 sec)
Innodb
意向锁是InnoDB自动加的,不需用户干预。
意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。
- 行锁
Innodb
检查
#分析系统上的行锁的争夺情况:
mysql> show status like 'innodb_row_lock%';
+-------------------------------+-------+
| Variable_name | Value |
+-------------------------------+-------+
| InnoDB_row_lock_current_waits | 0 |
| InnoDB_row_lock_time | 0 |
| InnoDB_row_lock_time_avg | 0 |
| InnoDB_row_lock_time_max | 0 |
| InnoDB_row_lock_waits | 0 |
+-------------------------------+-------+
行锁模式
共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。
SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE。
可用来防止其它事务对该条数据的更新操作;
排他锁(X):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。另外,为了允许行锁和表锁共存,实现多粒度锁机制
SELECT * FROM table_name WHERE ... FOR UPDATE。
InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。
锁升级
对于普通SELECT语句,InnoDB不会加任何锁;
UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);
行锁是针对索引加的,如果更新语句用不到索引(主键、唯一、普通索引皆可,会互相锁住),那么会加表锁;值得注意的是,如果执行计划觉得扫表效率更高,不走索引,也会导致锁表。
如果使用范围条件检索,并请求共享或排他锁时,会对已有数据的索引项加锁,键值在范围内但不存在记录的数据,叫做间隙Gap,Innodb也会加锁,称之为“间隙锁”,相等的条件索引也会产生。——总结:范围搜索 or 精确搜索但不存在的数据
其目的一是为了实现隔离,防止幻读(基于非唯一索引,主键上没有,只会阻塞insert操作),二是为了恢复和复制的需要。如果把隔离级别降为读已提交,则会禁用间隙锁。
临键锁(Next-Key Locks),特殊的间隙锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据。
Innodb的默认隔离级别RR,该级别下默认使用Next key lock;
Innodb对非唯一索引加锁过程,锁住条件对应的唯一索引,然后对该非唯一索引进行Next-key lock 和 Gap Key lock;
如果是对唯一索引加锁,锁住的只是相应的Record Key。
假设有如下表:
MySql,InnoDB,Repeatable-Read:table(id PK, age KEY, name)
id | age | name |
---|---|---|
1 | 10 | Lee |
3 | 24 | Soraka |
5 | 32 | Zed |
7 | 45 | Talon |
该表中 age 列潜在的临键锁 有: |
||
(-∞, 10], | ||
(10, 24], | ||
(24, 32], | ||
(32, 45], | ||
(45, +∞], |
在事务 A
中执行如下命令:
-- 根据非唯一索引列 UPDATE 某条记录
UPDATE table SET name = Vladimir WHERE age = 24;
-- 或根据非唯一索引列 锁住某条记录
SELECT * FROM table WHERE age = 24 FOR UPDATE;
不管执行了上述 SQL 中的哪一句,之后如果在事务 B
中执行以下命令,则该命令会被阻塞:
INSERT INTO table VALUES(100, 26, 'Ezreal');
很明显,事务 A
在对 age
为 24 的列进行 UPDATE 操作的同时,也获取了 (24, 32)
这个区间内的临键锁。
对主键加了id=3的Record lock;
对索引age加了 (10,24]的Next key lock,和下一个区间(24,32)的Gap key lock。
不仅如此,在执行以下 SQL 时,也会陷入阻塞等待:
INSERT INTO table VALUES(100, 30, 'Ezreal');
那最终我们就可以得知,在根据非唯一索引
对记录行进行 UPDATE \ FOR UPDATE \ LOCK IN SHARE MODE
操作时,InnoDB 会获取该记录行唯一索引的记录锁
,以及非唯一索引的 临键锁
,并同时获取该记录行下一个区间的间隙锁
。
即事务 A
在执行了上述的 SQL 后,最终被锁住的记录区间为 (10, 32)
。
主动使用表锁
SET AUTOCOMMIT=0;#必须加,否则MySQL不会给表加锁
LOCK TABLES t1 WRITE, t2 READ, ...;
[do something with tables t1 and t2 here];
COMMIT; #提交并不会释放lock的锁
UNLOCK TABLES; #会隐含提交事务
第一种情况是:事务需要更新大部分或全部数据,表又比较大,如果使用默认的行锁,不仅这个事务执行效率低,而且可能造成其他事务长时间锁等待和锁冲突,这种情况下可以考虑使用表锁来提高该事务的执行速度。
第二种情况是:事务涉及多个表,比较复杂,很可能引起死锁,造成大量事务回滚。这种情况也可以考虑一次性锁定事务涉及的表,从而避免死锁、减少数据库因事务回滚带来的开销。
这两种事务太多就应该考虑使用MyISAM表了。