MySQL/InnoDB的加锁,一直是一个常见的话题。例如,数据库若是有高并发请求,如何保证数据完整性?产生死锁问题如何排查并解决?下面是不一样锁等级的区别java
查看数据库拥有的存储引擎类型
SHOW ENGINES
mysql
用数据版本(Version)记录机制实现,这是乐观锁最经常使用的一种实现方式。何谓数据版本?即为数据增长一个版本标识,通常是经过为数据库表增长一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加1。当咱们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,若是数据库表当前版本号与第一次取出来的version值相等,则予以更新,不然认为是过时数据。git
举例:github
一、数据库表三个字段,分别是id、value、version
select id,value,version from TABLE where id = #{id}
二、每次更新表中的value字段时,为了防止发生冲突,须要这样操做sql
update TABLE
set value=2,version=version+1
where id=#{id} and version=#{version}
复制代码
与乐观锁相对应的就是悲观锁了。悲观锁就是在操做数据时,认为此操做会出现数据冲突,因此在进行每次操做时都要经过获取锁才能进行对相同数据的操做,这点跟java中的synchronized很类似,因此悲观锁须要耗费较多的时间。另外与乐观锁相对应的,悲观锁是由数据库本身实现了的,要用的时候,咱们直接调用数据库的相关语句就能够了。数据库
说到这里,由悲观锁涉及到的另外两个锁概念就出来了,它们就是共享锁与排它锁。共享锁和排它锁是悲观锁的不一样的实现,它俩都属于悲观锁的范畴。segmentfault
共享锁又称读锁 (read lock),是读取操做建立的锁。其余用户能够并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放全部共享锁。当若是事务对读锁进行修改操做,极可能会形成死锁。以下图所示。bash
若是事务T对数据A加上共享锁后,则其余事务只能对A再加共享锁,不能加排他锁。得到共享锁的事务只能读数据,不能修改数据session
打开第一个查询窗口并发
begin;/begin work;/start transaction; (三者选一就能够)
#(lock in share mode 共享锁)
SELECT * from TABLE where id = 1 lock in share mode;
复制代码
而后在另外一个查询窗口中,对id为1的数据进行更新
update TABLE set name="www.souyunku.com" where id =1;
此时,操做界面进入了卡顿状态,过了好久超时,提示错误信息
若是在超时前,第一个窗口执行commit
,此更新语句就会成功。
[SQL]update test_one set name="www.souyunku.com" where id =1;
[Err] 1205 - Lock wait timeout exceeded; try restarting transaction
复制代码
加上共享锁后,也提示错误信息
update test_one set name="www.souyunku.com" where id =1 lock in share mode;
[SQL]update test_one set name="www.souyunku.com" where id =1 lock in share mode;
[Err] 1064 - You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'lock in share mode' at line 1
复制代码
在查询语句后面增长 LOCK IN SHARE MODE ,Mysql会对查询结果中的每行都加共享锁,当没有其余线程对查询结果集中的任何一行使用排他锁时,能够成功申请共享锁,不然会被阻塞。 其余线程也能够读取使用了共享锁的表,并且这些线程读取的是同一个版本的数据。
加上共享锁后,对于update,insert,delete语句会自动加排它锁。
排他锁 exclusive lock(也叫writer lock)又称写锁。
名词解释:若某个事物对某一行加上了排他锁,只能这个事务对其进行读写,在此事务结束以前,其余事务不能对其进行加任何锁,其余进程能够读取,不能进行写操做,需等待其释放。 排它锁是悲观锁的一种实现,在上面悲观锁也介绍过。
若事务 1 对数据对象A加上X锁,事务 1 能够读A也能够修改A,其余事务不能再对A加任何锁,直到事物 1 释放A上的锁。这保证了其余事务在事物 1 释放A上的锁以前不能再读取和修改A。排它锁会阻塞全部的排它锁和共享锁
读取为何要加读锁呢?防止数据在被读取的时候被别的线程加上写锁。 排他锁使用方式:在须要执行的语句后面加上for update就能够了 select status from TABLE where id=1 for update;
排他锁,也称写锁,独占锁,当前写操做没有完成前,它会阻断其余写锁和读锁。
排它锁-举例:
要使用排他锁,咱们必须关闭mysql数据库的自动提交属性,由于MySQL默认使用autocommit模式,也就是说,当你执行一个更新操做后,MySQL会马上将结果进行提交。
咱们可使用命令设置MySQL为非autocommit模式:
set autocommit=0;
# 设置完autocommit后,咱们就能够执行咱们的正常业务了。具体以下:
# 1. 开始事务
begin;/begin work;/start transaction; (三者选一就能够)
# 2. 查询表信息(for update加锁)
select status from TABLE where id=1 for update;
# 3. 插入一条数据
insert into TABLE (id,value) values (2,2);
# 4. 修改数据为
update TABLE set value=2 where id=1;
# 5. 提交事务
commit;/commit work
复制代码
总结:多个事务操做同一行数据时,后来的事务处于阻塞等待状态。这样能够避免了脏读等数据一致性的问题。后来的事务能够操做其余行数据,解决了表锁高并发性能低的问题。
# Transaction-A
mysql> set autocommit = 0;
mysql> update innodb_lock set v='1001' where id=1;
mysql> commit;
# Transaction-B
mysql> update innodb_lock set v='2001' where id=2;
Query OK, 1 row affected (0.37 sec)
mysql> update innodb_lock set v='1002' where id=1;
Query OK, 1 row affected (37.51 sec)
复制代码
现实:当执行批量修改数据脚本的时候,行锁升级为表锁。其余对订单的操做都处于等待中,,, 缘由:nnoDB只有在经过索引条件检索数据时使用行级锁,不然使用表锁! 而模拟操做正是经过id去做为检索条件,而id又是MySQL自动建立的惟一索引,因此才忽略了行锁变表锁的状况
总结:InnoDB的行锁是针对索引加的锁,不是针对记录加的锁。而且该索引不能失效,不然都会从行锁升级为表锁。
从上面的案例看出,行锁变表锁彷佛是一个坑,可MySQL没有这么无聊给你挖坑。这是由于MySQL有本身的执行计划。 当你须要更新一张较大表的大部分甚至全表的数据时。而你又傻乎乎地用索引做为检索条件。一不当心开启了行锁(没毛病啊!保证数据的一致性!)。可MySQL却认为大量对一张表使用行锁,会致使事务执行效率低,从而可能形成其余事务长时间锁等待和更多的锁冲突问题,性能严重降低。因此MySQL会将行锁升级为表锁,即实际上并无使用索引。 咱们仔细想一想也能理解,既然整张表的大部分数据都要更新数据,在一行一行地加锁效率则更低。其实咱们能够经过explain命令查看MySQL的执行计划,你会发现key为null。代表MySQL实际上并无使用索引,行锁升级为表锁也和上面的结论一致。
注意:行级锁都是基于索引的,若是一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁。
当咱们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫作“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。 举例来讲,假如emp表中只有101条记录,其empid的值分别是 1,2,...,100,101,下面的SQL:
Select * from emp where empid > 100 for update;
复制代码
是一个范围条件的检索,InnoDB不只会对符合条件的empid值为101的记录加锁,也会对empid大于101(这些记录并不存在)的“间隙”加锁。
InnoDB使用间隙锁的目的,一方面是为了防止幻读,以知足相关隔离级别的要求,对于上面的例子,要是不使用间隙锁,若是其余事务插入了empid大于100的任何记录,那么本事务若是再次执行上述语句,就会发生幻读;另一方面,是为了知足其恢复和复制的须要。有关其恢复和复制对锁机制的影响,以及不一样隔离级别下InnoDB使用间隙锁的状况,在后续的章节中会作进一步介绍。
很显然,在使用范围条件检索并锁定记录时,InnoDB这种加锁机制会阻塞符合条件范围内键值的并发插入,这每每会形成严重的锁等待。所以,在实际应用开发中,尤为是并发插入比较多的应用,咱们要尽可能优化业务逻辑,尽可能使用相等条件来访问更新数据,避免使用范围条件。
还要特别说明的是,InnoDB除了经过范围条件加锁时使用间隙锁外,若是使用相等条件请求给一个不存在的记录加锁,InnoDB也会使用间隙锁!
例子:假如emp表中只有101条记录,其empid的值分别是1,2,......,100,101。
InnoDB存储引擎的间隙锁阻塞例子
session_1 | session_2 |
---|---|
mysql> select @@tx_isolation; | mysql> select @@tx_isolation; |
+-----------------+ | +-----------------+ |
@@tx_isolation | @@tx_isolation |
+-----------------+ | +-----------------+ |
REPEATABLE-READ | REPEATABLE-READ |
+-----------------+ | +-----------------+ |
1 row in set (0.00 sec) | 1 row in set (0.00 sec) |
mysql> set autocommit = 0; | mysql> set autocommit = 0; |
Query OK, 0 rows affected (0.00 sec) | Query OK, 0 rows affected (0.00 sec) |
当前session对不存在的记录加for update的锁: | |
mysql> select * from emp where empid = 102 for update; | |
Empty set (0.00 sec) | |
这时,若是其余session插入empid为201的记录(注意:这条记录并不存在),也会出现锁等待: | |
mysql>insert into emp(empid,...) values(201,...); | |
阻塞等待 | |
Session_1 执行rollback: | |
mysql> rollback; | |
Query OK, 0 rows affected (13.04 sec) | |
因为其余session_1回退后释放了Next-Key锁,当前session能够得到锁并成功插入记录: | |
mysql>insert into emp(empid,...) values(201,...); | |
Query OK, 1 row affected (13.35 sec) |
危害(坑):若执行的条件是范围过大,则InnoDB会将整个范围内全部的索引键值所有锁定,很容易对性能形成影响。
如何加表锁? innodb 的行锁是在有索引的状况下,没有索引的表是锁定全表的。
前面提到过,在Innodb引擎中既支持行锁也支持表锁,那么何时会锁住整张表,何时只锁住一行呢? 只有经过索引条件检索数据,InnoDB才使用行级锁,不然,InnoDB将使用表锁!
在实际应用中,要特别注意InnoDB行锁的这一特性,否则的话,可能致使大量的锁冲突,从而影响并发性能。
行级锁都是基于索引的,若是一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁。行级锁的缺点是:因为须要请求大量的锁资源,因此速度慢,内存消耗大。
死锁(Deadlock) 所谓死锁:是指两个或两个以上的进程在执行过程当中,因争夺资源而形成的一种互相等待的现象,若无外力做用,它们都将没法推动下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。因为资源占用是互斥的,当某个进程提出申请资源后,使得有关进程在无外力协助下,永远分配不到必需的资源而没法继续运行,这就产生了一种特殊现象死锁。
解除正在死锁的状态有两种方法:
第一种:
show OPEN TABLES where In_use > 0;
show processlist
kill id
第二种:
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
kill 进程ID
若是系统资源充足,进程的资源请求都可以获得知足,死锁出现的可能性就很低,不然就会因争夺有限的资源而陷入死锁。其次,进程运行推动顺序与速度不一样,也可能产生死锁。 产生死锁的四个必要条件:
虽然不能彻底避免死锁,但可使死锁的数量减至最少。将死锁减至最少能够增长事务的吞吐量并减小系统开销,由于只有不多的事务回滚,而回滚会取消事务执行的全部工做。因为死锁时回滚的操做由应用程序从新提交。
下列方法有助于最大限度地下降死锁:
InnoDB和MyISAM的最大不一样点有两个:
对MyISAM表的读操做(加读锁),不会阻塞其余进程对同一表的读操做,但会阻塞对同一表的写操做。只有当读锁释放后,才能执行其余进程的写操做。在锁释放前不能读其余表。
对MyISAM表的写操做(加写锁),会阻塞其余进程对同一表的读和写操做,只有当写锁释放后,才会执行其余进程的读写操做。在锁释放前不能写其余表。
若是用户想要显示的加锁可使用如下命令:
锁定表:
LOCK TABLES tbl_name {READ | WRITE},[ tbl_name {READ | WRITE},…]
复制代码
解锁表:
UNLOCK TABLES
复制代码
在用 LOCK TABLES
给表显式加表锁时,必须同时取得全部涉及到表的锁。 在执行 LOCK TABLES
后,只能访问显式加锁的这些表,不能访问未加锁的表;
若是加的是读锁,那么只能执行查询操做,而不能执行更新操做。
在自动加锁的状况下也基本如此,MyISAM 老是一次得到 SQL 语句所须要的所有锁。这也正是 MyISAM 表不会出现死锁(Deadlock Free)的缘由。
对表test_table增长读锁:
LOCK TABLES test_table READ
UNLOCK test_table
复制代码
对表test_table增长写锁
LOCK TABLES test_table WRITE
UNLOCK test_table
复制代码
当使用 LOCK TABLES 时,不只须要一次锁定用到的全部表,并且,同一个表在 SQL 语句中出现多少次,就要经过与 SQL 语句中相同的别名锁定多少次,不然也会出错!
好比以下SQL语句:
select a.first_name,b.first_name, from actor a,actor b where a.first_name = b.first_name;
复制代码
该Sql语句中,actor表以别名的方式出现了两次,分别是a,b,这时若是要在该Sql执行以前加锁就要使用如下Sql:
lock table actor as a read,actor as b read;
复制代码
上文说到过 MyISAM 表的读和写是串行的,但这是就整体而言的。在必定条件下,MyISAM表也支持查询和插入操做的并发进行。 MyISAM存储引擎有一个系统变量concurrent_insert,专门用以控制其并发插入的行为,其值分别能够为0、1或2。
能够利用MyISAM存储引擎的并发插入特性,来解决应用中对同一表查询和插入的锁争用。
前面讲过,MyISAM 存储引擎的读锁和写锁是互斥的,读写操做是串行的。那么,一个进程请求某个 MyISAM 表的读锁,同时另外一个进程也请求同一表的写锁,MySQL 如何处理呢?
答案是写进程先得到锁。
不只如此,即便读请求先到锁等待队列,写请求后到,写锁也会插到读锁请求以前!这是由于 MySQL 认为写请求通常比读请求要重要。这也正是 MyISAM 表不太适合于有大量更新操做和查询操做应用的缘由,由于大量的更新操做会形成查询操做很难得到读锁,从而可能永远阻塞。这种状况有时可能会变得很是糟糕!
幸亏咱们能够经过一些设置来调节 MyISAM 的调度行为。
经过指定启动参数low-priority-updates,使MyISAM引擎默认给予读请求以优先的权利。
SET LOWPRIORITYUPDATES=1,
使该链接发出的更新请求优先级下降。经过检查InnoDB_row_lock 状态变量分析系统上中行锁的争夺状况
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 |
+-------------------------------+-------+
复制代码
行锁优化
查看加锁状况 how open tables; 1表示加锁,0表示未加锁。
mysql> show open tables where in_use > 0;
+----------+-------------+--------+-------------+
| Database | Table | In_use | Name_locked |
+----------+-------------+--------+-------------+
| lock | myisam_lock | 1 | 0 |
+----------+-------------+--------+-------------+
复制代码
能够经过检查table_locks_waited 和 table_locks_immediate 状态变量分析系统上的表锁定:show status like 'table_locks%'
mysql> show status like 'table_locks%';
+----------------------------+-------+
| Variable_name | Value |
+----------------------------+-------+
| Table_locks_immediate | 104 |
| Table_locks_waited | 0 |
+----------------------------+-------+
复制代码
此外,MyISAM的读写锁调度是写优先,这也是MyISAM不适合作写为主表的存储引擎。由于写锁后,其余线程不能作任何操做,大量的更新会使查询很可贵到锁,从而形成永久阻塞。
第一种状况:全表更新。事务须要更新大部分或所有数据,且表又比较大。若使用行锁,会致使事务执行效率低,从而可能形成其余事务长时间锁等待和更多的锁冲突。
第二种状况:多表查询。事务涉及多个表,比较复杂的关联查询,极可能引发死锁,形成大量事务回滚。这种状况若能一次性锁定事务涉及的表,从而能够避免死锁、减小数据库因事务回滚带来的开销。
mysql 5.6 在 update 和 delete 的时候,where 条件若是不存在索引字段,那么这个事务是否会致使表锁? 有人回答: 只有主键和惟一索引才是行锁,普通索引是表锁。
结果发现普通索引并不必定会引起表锁,在普通索引中,是否引起表锁取决于普通索引的高效程度。
上文说起的“高效”是相对主键和惟一索引而言,也许“高效”并非一个很好的解释,只要明白在通常状况下,“普通索引”效率低于其余二者便可。 属性值重复率高
当“值重复率”低时,甚至接近主键或者惟一索引的效果,“普通索引”依然是行锁;当“值重复率”高时,MySQL 不会把这个“普通索引”当作索引,即形成了一个没有索引的 SQL,此时引起表锁。
同 JVM 自动优化 java 代码同样,MySQL 也具备自动优化 SQL 的功能。低效的索引将被忽略,这也就倒逼开发者使用正确且高效的索引。
属性值重复率高
为了突出效果,我将“普通索引”创建在一个“值重复率”高的属性下。以相对极端的方式,扩大对结果的影响。
我会建立一张“分数等级表”,属性有“id”、“score(分数)”、“level(等级)”,模拟一个半自动的业务——“分数”已被自动导入,而“等级”须要手工更新。
操做步骤以下:
取消 事务自动提交:
mysql> set autocommit = off;
Query OK, 0 rows affected (0.02 sec)
mysql> show variables like "autocommit";
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| autocommit | OFF |
+--------------------------+-------+
1 rows in set (0.01 sec)
复制代码
建表、建立索引、插入数据:
DROP TABLE IF EXISTS `test1`;
CREATE TABLE `test1` (
`ID` int(5) NOT NULL AUTO_INCREMENT ,
`SCORE` int(3) NOT NULL ,
`LEVEL` int(2) NULL DEFAULT NULL ,
PRIMARY KEY (`ID`)
)ENGINE=InnoDB DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci;
ALTER TABLE `test2` ADD INDEX index_name ( `SCORE` );
INSERT INTO `test1`(`SCORE`) VALUE (100);
……
INSERT INTO `test1`(`SCORE`) VALUE (0);
复制代码
"SCORE" 属性的“值重复率”奇高,达到了 50%,剑走偏锋:
mysql> select * from test1;
+----+-------+-------+
| ID | SCORE | LEVEL |
+----+-------+-------+
| 1 | 100 | NULL |
| 2 | 0 | NULL |
| 5 | 100 | NULL |
| 6 | 100 | NULL |
| 7 | 100 | NULL |
| 8 | 100 | NULL |
| 9 | 100 | NULL |
| 10 | 100 | NULL |
| 11 | 100 | NULL |
| 12 | 100 | NULL |
| 13 | 100 | NULL |
| 14 | 0 | NULL |
| 15 | 0 | NULL |
| 16 | 0 | NULL |
| 17 | 0 | NULL |
| 18 | 0 | NULL |
| 19 | 0 | NULL |
| 20 | 0 | NULL |
| 21 | 0 | NULL |
| 22 | 0 | NULL |
| 23 | 0 | NULL |
| 24 | 100 | NULL |
| 25 | 0 | NULL |
| 26 | 100 | NULL |
| 27 | 0 | NULL |
+----+-------+-------+
25 rows in set
复制代码
开启两个事务(一个窗口对应一个事务),并选定数据:
-- SESSION_1,选定 SCORE = 100 的数据
mysql> BEGIN;
SELECT t.* FROM `test1` t WHERE t.`SCORE` = 100 FOR UPDATE;
Query OK, 0 rows affected
+----+-------+-------+
| ID | SCORE | LEVEL |
+----+-------+-------+
| 1 | 100 | NULL |
| 5 | 100 | NULL |
| 6 | 100 | NULL |
| 7 | 100 | NULL |
| 8 | 100 | NULL |
| 9 | 100 | NULL |
| 10 | 100 | NULL |
| 11 | 100 | NULL |
| 12 | 100 | NULL |
| 13 | 100 | NULL |
| 24 | 100 | NULL |
| 26 | 100 | NULL |
+----+-------+-------+
12 rows in set
复制代码
再打开一个窗口:
-- SESSION_2,选定 SCORE = 0 的数据
mysql> BEGIN;
SELECT t.* FROM `test1` t WHERE t.`SCORE` = 0 FOR UPDATE;
Query OK, 0 rows affected
+----+-------+-------+
| ID | SCORE | LEVEL |
+----+-------+-------+
| 2 | 0 | NULL |
| 14 | 0 | NULL |
| 15 | 0 | NULL |
| 16 | 0 | NULL |
| 17 | 0 | NULL |
| 18 | 0 | NULL |
| 19 | 0 | NULL |
| 20 | 0 | NULL |
| 21 | 0 | NULL |
| 22 | 0 | NULL |
| 23 | 0 | NULL |
| 25 | 0 | NULL |
| 27 | 0 | NULL |
+----+-------+-------+
13 rows in set
复制代码
session_1 窗口,更新“LEVEL”失败:
mysql> UPDATE `test1` SET `LEVEL` = 1 WHERE `SCORE` = 100;
1205 - Lock wait timeout exceeded; try restarting transaction
复制代码
在以前的操做中,session_1 选择了 SCORE
= 100 的数据,session_2 选择了 SCORE
= 0 的数据,看似两个事务井水不犯河水,可是在 session_1 事务中更新本身锁定的数据失败,只能说明在此时引起了表锁。别着急,刚刚走向了一个极端——索引属性值重复性奇高,接下来走向另外一个极端。
属性值重复率低
仍是同一张表,将数据删除只剩下两条,“SCORE” 的 “值重复率” 为 0:
mysql> delete from test1 where id > 2;
Query OK, 23 rows affected
mysql> select * from test1;
+----+-------+-------+
| ID | SCORE | LEVEL |
+----+-------+-------+
| 1 | 100 | NULL |
| 2 | 0 | NULL |
+----+-------+-------+
2 rows in set
复制代码
关闭两个事务操做窗口,从新开启 session_1 和 session_2,并选择各自须要的数据:
-- SESSION_1,选定 SCORE = 100 的数据
mysql> BEGIN;
SELECT t.* FROM `test1` t WHERE t.`SCORE` = 100 FOR UPDATE;
Query OK, 0 rows affected
+----+-------+-------+
| ID | SCORE | LEVEL |
+----+-------+-------+
| 1 | 100 | NULL |
+----+-------+-------+
1 row in set
-- -----------------新窗口----------------- --
-- SESSION_2,选定 SCORE = 0 的数据
mysql> BEGIN;
SELECT t.* FROM `test1` t WHERE t.`SCORE` = 0 FOR UPDATE;
Query OK, 0 rows affected
+----+-------+-------+
| ID | SCORE | LEVEL |
+----+-------+-------+
| 2 | 0 | NULL |
+----+-------+-------+
1 row in set
复制代码
session_1 更新数据成功:
mysql> UPDATE `test1` SET `LEVEL` = 1 WHERE `SCORE` = 100;
Query OK, 1 row affected
Rows matched: 1 Changed: 1 Warnings:0
复制代码
相同的表结构,相同的操做,两个不一样的结果让人出乎意料。第一个结果让人以为“普通索引”引起表锁,第二个结果推翻了前者,两个操做中,惟一不一样的是索引属性的“值重复率”。根据 单一变量 证实法,能够得出结论:当“值重复率”低时,甚至接近主键或者惟一索引的效果,“普通索引”依然是行锁;当“值重复率”高时,MySQL 不会把这个“普通索引”当作索引,即形成了一个没有索引的 SQL,此时引起表锁。
举个栗子:
上面的例子,A同时收到两笔50元转帐,最后的余额应该是200元,但却由于并发的问题变为了150元,缘由是B和C向A发起转帐请求时,同时打开了两个数据库会话,进行了两个事务,后一个事务拿到了前一个事务的中间状态数据,致使更新丢失。
经常使用的解决思路有两种:
要注意悲观锁和乐观锁都是业务逻辑层次的定义,不一样的设计可能会有不一样的实现。在mysql层经常使用的悲观锁实现方式是加一个排他锁。
然而实际上并非这样,实际上加了排他锁的数据,在释放锁(事务结束)以前其余事务不能再对该数据加锁 排他锁之因此能阻止update,delete等操做是由于update,delete操做会自动加排他锁, 也就是说即便加了排他锁也没法阻止select操做。而select XX for update 语法能够对select操做加上排他锁。 因此为了防止更新丢失能够在select时加上for update加锁 这样就能够阻止其他事务的select for update (但注意没法阻止select)
乐观锁example:
begin;
select balance from account where id=1;
-- 获得balance=100;而后计算balance=100+50=150
update account set balance = 150 where id=1 and balance = 100;
commit;
复制代码
如上,若是sql在执行的过程当中发现update的affected为0 说明balance不等于100即该条数据有被其他事务更改过,此时业务上就能够返回失败或者从新select再计算
这是由于我们的 innodb 默认是自动提交的:
须要注意的是,一般还有另一种状况也可能致使部分语句回滚,须要格外留意。在 innodb 里有个参数叫:innodb_rollback_on_timeout
show VARIABLES LIKE 'innodb_rollback_on_timeout'
+----------------------------+---------+
| Variable_name | Value |
|----------------------------+---------|
| innodb_rollback_on_timeout | OFF |
+----------------------------+---------+
复制代码
官方手册里这样描述:
In MySQL 5.1, InnoDB rolls back only the last statement on a transaction timeout by default. If –innodb_rollback_on_timeout is specified, a transaction timeout causes InnoDB to abort and roll back the entire transaction (the same behavior as in MySQL 4.1). This variable was added in MySQL 5.1.15.
解释:这个参数关闭或不存在的话遇到超时只回滚事务最后一个Query,打开的话事务遇到超时就回滚整个事务。
注意:
死锁在行锁及事务场景下很难彻底消除,但能够经过表设计和SQL调整等措施减小锁冲突和死锁,包括:
例子:
DELETE FROM onlineusers WHERE datetime <= now() - INTERVAL 900 SECOND
至
DELETE FROM onlineusers WHERE id IN (SELECT id FROM onlineusers
WHERE datetime <= now() - INTERVAL 900 SECOND order by id) u;
复制代码
尽可能用相等条件访问数据,这样能够避免间隙锁对并发插入的影响; 不要申请超过实际须要的锁级别;除非必须,查询时不要显示加锁; 对于一些特定的事务,可使用表锁来提升处理速度或减小死锁的可能。
# 默认 lock 超时时间 50s,这个时间真心不短了
show variables like 'innodb_lock_wait_timeout';
+--------------------------+---------+
| Variable_name | Value |
|--------------------------+---------|
| innodb_lock_wait_timeout | 50 |
+--------------------------+---------+
复制代码
并且此次 SHOW ENGINE INNODB STATUS\G
也没出现任何死锁信息,而后又将目光转向 MySQL-server 日志,但愿能从日志里看一看那个时刻先后数据究竟在作什么操做。这里先简单的介绍下MySQL日志文件系统的组成:
从上面的介绍能够看到,目前这个问题的日志可能在 2 和 4 中,看了下 4 中没有,那就只能开启 2 了,但 2 对数据库的性能有必定损耗,因为是全量日志,量很是巨大,因此开启必定要谨慎:
-- general_log 日志默认关闭,开启会影响数据库 5% 左右性能:
show variables like 'general%';
+------------------+---------------------------------+
| Variable_name | Value |
|------------------+---------------------------------|
| general_log | OFF |
| general_log_file | /opt/data/mysql/tjtx-103-26.log |
+------------------+---------------------------------+
-- 全局 session 级别开启:
set global general_log=1
-- 若是须要对当前 session 生效须要:
set general_log=1
-- set 指令设置的动态参数在 MySQL 重启后失效,若是须要永久生效须要在 /etc/my.cnf 中配置静态变量/参数。
-- 若是不知道 my.cnf 位置,能够根据 mysql -? | grep ".cnf" 查询
order of preference, my.cnf, $MYSQL_TCP_PORT,
/etc/my.cnf /etc/mysql/my.cnf /usr/etc/my.cnf ~/.my.cnf
复制代码
set 指令设置的动态参数在 MySQL 重启后失效,若是须要永久生效须要在 /etc/my.cnf 中配置静态变量/参数。
更多内容请参考
强烈推荐-何登成的技术博客
何登成 资深技术专家 阿里巴巴数据库内核团队负责人,文章颇有深度
mysql死锁问题分析
mysql中插入,更新,删除锁
MySQL InnoDB 锁——官方文档
show OPEN TABLES where In_use > 0;
show processlist
kill id
第二种:
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
kill 进程ID
# 经过检查 InnoDB_row_lock 状态变量分析系统上中行锁的争夺状况
show status like 'innodb_row_lock%';```
# 查看加锁状况
show open tables where in_use > 0;
#具体使用说明可查看上文内容
show status like 'table_locks%';
show VARIABLES LIKE 'innodb_rollback_on_timeout';
show variables like 'general%';
复制代码