锁是计算机协调多个进程或线程并发访问某一资源的机制。
在数据库中,除传统的计算机资源(如CPU、RAM、I/O等)的挣用外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。
从对数据操作的颗粒度分:
1)表锁:操作时,会锁定整个表
2)行锁:操作时,会锁定当前操作行
从对数据操作的类型分:
1)读锁(共享锁):针对同一份数据,多个读操作可以同时进行而不会互相影响。
2)写锁(排它锁):当前操作没有完成之前,它会阻断其他写锁和读锁。
相对其他数据库而言,MySQL的锁机制比较简单,其最显著的特点是不同的存储引擎支持不同的锁机制。下表中罗列出了各存储引擎对锁的支持情况。
MySQL这3种锁的特性可大致归纳如下:
从上述特点可见,很难笼统的说哪种锁更好,只能就具体应用的特点来说哪种锁更合适。仅从锁的角度来说,表级锁更适合以查询为主,只有少量按索引条件更新数据的应用,如web应用;而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事物处理(OLTP)系统。
MyISAM存储引擎只支持表锁,这也是MySQL开始几个版本中唯一支持的锁类型。
MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加锁,在执行更新操作(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此,用户一般不需要直接LOCK TABLE命令给MyISAM表显式加锁。
显示加表锁语句:
加读锁:lock table table_name read;
加写锁:lock table table_name write;
# 解除所有锁
unlock tables;
1)准备环境(存储引擎设置为MyISAM)
# 创建demo数据库
create database test default charset=utf8mb4;
use test;
CREATE TABLE `country` (
`country_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`country_name` varchar(56) NOT NULL,
PRIMARY KEY (`country_id`)
) ENGINE=myisam AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
INSERT INTO `country` VALUES (1, 'China');
INSERT INTO `country` VALUES (2, 'America');
INSERT INTO `country` VALUES (3, 'Japan');
INSERT INTO `country` VALUES (4, 'UK');
CREATE TABLE `city` (
`city_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`city_name` varchar(56) NOT NULL,
`country_id` int(11) NOT NULL,
PRIMARY KEY (`city_id`)
) ENGINE=myisam AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;
INSERT INTO `city`(`city_id`, `city_name`, `country_id`) VALUES (1, '西安', 1);
INSERT INTO `city`(`city_id`, `city_name`, `country_id`) VALUES (2, 'NewYork', 2);
INSERT INTO `city`(`city_id`, `city_name`, `country_id`) VALUES (3, '北京', 1);
INSERT INTO `city`(`city_id`, `city_name`, `country_id`) VALUES (4, '上海', 1);
2)client1端加读锁,client2端依然能够读取操作,此时client1端无法对加锁表进行写操作,对其他表无法进行读写操作;client2端可以对其他表进行读写操作,但不能对加锁表进行写操作会处于阻塞状态,等client1释放读锁后便可执行写入操作。
# 解除所有锁
unlock tables;
读锁(共享锁):针对同一份数据,多个读操作可以同时进行而不会互相影响。
client2端
1)client1端加写锁,在client1可对当前表可进行读写操作。
写锁(排它锁):当前操作没有完成之前,它会阻断其他写锁和读锁。
2)client2端,在加锁表只能处于阻塞状态,只能等待client1端释放,其他未锁表可进行读写操作。
锁模式的相互兼容性如表中所示:
由上表可见:
1)对MyISAM表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求;
2)对MyISAM表的写操作,则会阻塞其他用户对同一表的读和写操作;
此外,MyISAM的读写锁调度是写优先,这也是MyISAM不适合做写为主的表存储引擎的原因。因为写锁后,其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞。
show open tables;
In_use:表示当前被查询使用的次数。如果该数为零,则表是打开的,但是当前没被使用。
Name_locked:表示是否被锁定。名称锁定用于取消表或对表进行重命名等操作。
show status like 'Table_locks%';
Table_locks_immediate:值的是能够立即获得表级锁的次数,每立即获取锁,值加1。
Table_locks_waited:指的是不能立即获得表级锁而需要等待的次数,每等待一次,该值加1,此值越高说明存在着较为严重的表级锁争抢情况。
行锁特点:偏向InnoDB存储引擎,开销大,加锁慢;会出现死锁;锁定颗粒度最小,发生锁冲突的概率最低,并发度也最高。
InnoDB与MyISAM的最大不同有两点:一是支持事物;而是采用行级锁。
为了解决上述提到的事物并发问题,数据库提供一定的事物隔离机制来解决这个问题。数据库的事物隔离越严格,并发副作用越小,但付出的代价也就越大,因为事物隔离实质上就是使用事物在一定程度上的“串行化”进行,这显然与“并发”是矛盾的。
数据库的隔离级别有4个,由低到高依次为Read uncommitted、Read committed、Repeatable read、Serializable,这四个级别可以逐个解决脏写,脏读、不可重复读、幻读这几类问题。
备注:✔代表可以出现,x代表不会出现。
MySQL的数据库默认隔离级别为Repeatable read,查看方式:
show variables like "tx_isolation";
InnoDB实现了以下两种类型的锁。
1)共享锁(S):又称为读锁,简称S锁,共享锁就是多个事物对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
2)排他锁(X):又称为写锁,简称X锁,排他锁就是不能与其他锁并存,如一个事物获取了一个数据行的排他锁,其他事物就不能获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事物是可以对数据行读写和修改。
对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X):
对于普通SELECT语句,InnoDB不会加任何锁;
可以通过以下语句显示给记录集加共享锁和排他锁。
1)共享锁(S):select * from table_name where ... lock in share mode
2)排他锁(X):select * from table_name where ... for update
# 创建demo数据库
create database demo default charset=utf8mb4;
CREATE TABLE `country` (
`country_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`country_name` varchar(56) NOT NULL,
PRIMARY KEY (`country_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
INSERT INTO `country` VALUES (1, 'China');
INSERT INTO `country` VALUES (2, 'America');
INSERT INTO `country` VALUES (3, 'Japan');
INSERT INTO `country` VALUES (4, 'UK');
CREATE TABLE `city` (
`city_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`city_name` varchar(56) NOT NULL,
`country_id` int(11) NOT NULL,
PRIMARY KEY (`city_id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;
INSERT INTO `city`(`city_id`, `city_name`, `country_id`) VALUES (1, '西安', 1);
INSERT INTO `city`(`city_id`, `city_name`, `country_id`) VALUES (2, 'NewYork', 2);
INSERT INTO `city`(`city_id`, `city_name`, `country_id`) VALUES (3, '北京', 1);
INSERT INTO `city`(`city_id`, `city_name`, `country_id`) VALUES (4, '上海', 1);
#关闭事物自动提交
set autocommit=0;
1)client1在更新时采用的是排他锁
此时修改同一条后的数据在client2中并未读取到,两个事物之间是隔离的,当client1提交后,client2就处于非阻塞状态,便会执行update语句
2)client2处于等待状态,等待client1提交commit才会执行
当client1提交事物后,client2开始执行事物,当client2也进行commit提交后,在client1还是未读取到该剧,需要client1再次进行commit才会读取到。
如果不通过索引条件检索数据,那么InnoDB将对表中的所有记录加锁,实际效果跟表锁一样。
查看当前表的索引:
show index from city;
1)client1使用了无索引行导致行锁升级为表锁
2)client2处理其他行数据此时处于等待状态,此时以由行锁转变为表锁
当我们用范围条件,而不是使用相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据进行加锁;对于键值在条件范围内但并不存在的记录,叫做间隙(GAP),InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。
1)client1更新city_id<3的数据,此时city_id=2的数据是不存在的,但是会给它也加上锁
2)client2在插入city_id=2的数据时会处于阻塞状态
show status like 'innodb_row_lock%';
Innodb_row_lock_current_waits 当前正在等待锁定的数量
Innodb_row_lock_time 从系统启动到现在锁定总时间长度
Innodb_row_lock_time_avg 每次等待所花费平均时长
Innodb_row_lock_time_max 从系统启动到现在等待最长的一次所花的时间
Innodb_row_lock_waits 系统启动后到现在总共等待的次数
当等待的次数很高,而且每次等待的时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手制定优化计划。
InnoDB存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面带来了性能损耗可能比表锁会更高一些,但是在整体并发处理能力方面要远远高于由MyISAM的表锁的。当系统并发量较高的时候,InnoDB的整体性能和MyISAM相比就会有比较明显的优势。
但是,InnoDB的行级锁同样也有脆弱的一面,当我们使用不当的时候,可能会让InnoDB的整体性能表现不仅不能比MyISAM高,甚至可能会更差。
优化建议:
1)尽可能让所有数据检索都能通过索引来完成,避免无索引行锁升级为表锁
2)合理设计索引,尽量缩小锁的范围
3)尽可能减少索引条件,及索引范围,避免间隙锁
4)尽量控制事物大小,减少锁定资源量和时间长度
5)尽可能使用低级别事物隔离(但是需要业务层面满足需求)