Hexo

点滴积累 豁达处之

0%

mysql锁与事物

本文介绍锁和事物

锁分类

  • 从性能上分为乐观锁(用版本对比来实现)和悲观锁

  • 从对数据库操作的类型分,分为读锁和写锁(都属于悲观锁) 读锁(共享锁):针对同一份数据,多个读操作可以同时进行而不会互相影响 写锁(排它锁):当前写操作没有完成前,它会阻断其他写锁和读锁

  • 从对数据操作的粒度分,分为表锁和行锁

MyISAM 表锁介绍

每次操作锁住整张表。开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低;

示例表:

1
2
3
4
5
6
7
8
9
10
11
DROP TABLE IF EXISTS `mylock`;
CREATE TABLE `mylock`(
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
)ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

insert into `mylock` (`name`) values ('a');
insert into `mylock` (`name`) values ('b');
insert into `mylock` (`name`) values ('c');
insert into `mylock` (`name`) values ('d');

手动增加表锁

1
lock table 表名称 read(write),表名称2 read(write);

查看表上加过的锁

1
show open tables;

删除表锁

1
unlock tables;

案例分析

加读锁:

1
2
3
4
5
6
-- Session1
lock table mylock read; -- session1 加读锁

select * from mylock limit 10; -- 查看数据 正常

update mylock set name = 'aa' where id = 'a'; -- 修改数据 报错
1
2
3
-- Session2
select * from mylock limit 10; -- -- 查看数据 正常
update mylock set name = 'aa' where id = 'a'; -- 修改数据 等待。。。

当前session和其他session都可以读该表
当前session中插入或者更新锁定的表都会报错,其他session插入或更新则会等待

加写锁

当前session对该表的增删改查都没有问题,其他session对该表的所有操作被阻塞

案例结论

MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行增删改 操作前,会自动给涉及的表加写锁。
1、对MyISAM表的读操作(加读锁) ,不会阻寒其他进程对同一表的读请求,但会阻 赛对同一表的写请求。只有当读锁释放后,才会执行其它进程的写操作。

2、对MylSAM表的写操作(加写锁) ,会阻塞其他进程对同一表的读和写操作,只有 当写锁释放后,才会执行其它进程的读写操作

总结:
简而言之,就是读锁会阻塞写,但是不会阻塞读。而写锁则会把读和写都阻塞。

事务(Transaction)及其ACID属性

事务是由一组SQL语句组成的逻辑处理单元,事务具有以下4个属性,通常简称为事务的ACID属性。

1、原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。

2、一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能 A扣了钱,B却没收到。

3、隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。

4、持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。

事物隔离级别

MySQL事物隔离级别

事物隔离级别 脏读 不可重复读 幻读
读未提交(read-uncommitted)也叫脏读
读已提交(read-committed)
可重复读(repeatable-read)默认级别
串行化(serializable)

事物的并发问题

1、脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据

2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。

3、幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

mysql默认的事务隔离级别为:repeatable-read

1
select @@tx_isolation;

隔离级别详述

1
2
3
4
select @@tx_isolation;      ## 查询当前事物隔离等级
set session transaction isolation level read uncommitted; ## 设置事物隔离等级为读未提交
//read committed repeatable read
start transaction; ## 开始事物

1 读未提交

打开一个客户端A,并设置当前事务模式为read uncommitted(未提交读),查询表test的初始值

Mysql_uncommitted01

在客户端A的事务提交之前,打开另一个客户端B,更新表account:

Mysql_uncommitted02

这时,虽然客户端B的事务还没提交,但是客户端A就可以查询到B已经更新的数据:

Mysql_uncommitted03

此时:一旦客户端B的事务因为某种原因回滚,所有的操作都将会被撤销,那客户端A查询到的数据其实就是脏数据:

2 读已提交

打开一个客户端A,并设置当前事务模式为read committed(未提交读),查询表account的初始值:

Mysql_committed01

在客户端A的事务提交之前,打开另一个客户端B,更新表account:

Mysql_committed02

这时,客户端B的事务还没提交,客户端A不能查询到B已经更新的数据,解决了脏读问题:

Mysql_committed03

客户端B的事务提交

Mysql_committed04

客户端A执行与上一步相同的查询,结果 与上一步不一致,即产生了不可重复读的问题

Mysql_committed05

3 可重复度

打开一个客户端A,并设置当前事务模式为repeatable read,查询表account

Mysql_repeatable01

在客户端A的事务提交之前,打开另一个客户端B,更新表account并提交

Mysql_repeatable02

在客户端A执行步骤(1)的查询:

Mysql_repeatable03

此时:数据一致性问题解决了,接着执行 更新操作

Mysql_repeatable04

数据结果变成了1000 (查出来的数据是2000 ,减去500后变成了1000.由此可判断之前的2000不是最新数据,产生了数据不准确问题)

4 串行化

打开一个客户端A,并设置当前事务模式为serializable,开启事物,查询表account的初始值:

Mysql_serializable01

打开一个客户端B,并设置当前事务模式为serializable,执行更新操作一直等待

Mysql_serializable02

MVCC机制详解

可重复读的 隔离级别下使用了MVCC(multi-version concurrency control)机制,select操作 不会更新版本号,是快照读(历史版本);insert、update和delete会更新版本 号,是当前读(当前版本)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account`(
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`balance` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `account`(`name`,`balance`)VALUES('lilei','450');
INSERT INTO `account`(`name`,`balance`)VALUES('hanmei', '16000');
INSERT INTO `account`(`name`,`balance`)VALUES('lucy','2400');

id name balance 创建事务id 删除事务id
2 1lilei 450 10 13
2 hanmei 16000 11 空
2 han666 16000 13 空
-- mysql 会默认给表加创建事物id和删除事物id属性

mysql_MVCC01

mysql_MVCC02

问题:select1 和select2 的查询语句一样,为什么结果不一样

说明:

  1. 开启五个个session,前三个开启事物,transaction100 , transaction200, transaction300

  2. transaction100 做更新操作:

    1
    update test set c1='123' where id = 1; -- 只为生成事物id,假设trx_id为100
  3. transaction200 做更新操作:

    1
    update test set c1='666' where id = 5;  -- 只为生成事物id,假设trx_id为200
  4. transaction300 做更新操作:

    1
    2
    3
    update account set name = 'lilei300' WHERE id = 1; 
    commit;
    -- 生成事物id300, 然后提交事物

    此时版本链为:

    mysql_MVCC03

  5. select1 做查询:

    1
    2
    3
    select name from account  where id = 1;  
    -- 此时会生成read-view max_id = 300 ,未提交事物数组为[100,200],min_id = 100,;
    -- 根据版本链比对规则trx_id=300的是可见的,

    则结果为”lilei300“

  6. 做更新操作:

    1
    2
    3
    update account set name = 'lilei1' WHERE id = 1; 
    update account set name = 'lilei2' WHERE id = 1;
    -- 连续做更新操作

    此时版本链为:

    mysql_MVCC04

  7. select1 做查询:

    1
    2
    3
    4
    select name from account  where id = 1;  
    -- 此时的read-view会沿用第一次生成的read-view
    -- max_id = 300 ,未提交事物数组为[100,200],min_id = 100,;
    -- 根据版本链比对规则trx_id=300的是可见的,

    则结果为”lilei300“

  8. transaction100提交事物,transaction200做更新操作:

    1
    2
    3
    commit; -- transaction100提交事物
    update account set name = 'lilei3' WHERE id = 1;
    update account set name = 'lilei4' WHERE id = 1;

    此时版本链为:

    mysql_MVCC05

  9. select1 做查询:

    1
    2
    3
    4
    select name from account  where id = 1;  
    -- 此时的read-view还会沿用第一次生成的read-view
    -- max_id = 300 ,未提交事物数组为[100,200],min_id = 100,;
    -- 根据版本链比对规则trx_id=300的是可见的,

    则结果为”lilei300“

  10. Select2 做查询:

    1
    2
    3
    4
    select name from account  where id = 1;  --readview:[200],300   lilei2
    -- 此时会生成新的read-view
    -- max_id = 300 ,未提交事物数组为[200],min_id = 200,;
    -- 根据版本链比对规则遍历:trx_id=100 在已提交事物范围内,可见

    则结果为”lilei2“

注意:

  1. begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作InnoDB 表的语句,事务才真正启动,才会向mysql申请事务id,mysql内部是严格按照事务的启动顺序来分配事务id的
  2. 删除记录,会在undo日志版本链对应记录里面标记删除

mysql 注意事项

SQL规范所规定的标准,不同的数据库具体的实现可能会有些差异

mysql中默认事务隔离级别是可重复读时并不会锁住读取到的行

事务隔离级别为读提交时,写数据只会锁住相应的行

无索引行锁会升级为表锁

1
2
3
4
5
6
7
锁主要是加在索引上,如果对非索引字段更新, 行锁可能会变表锁

session1执行:
update account set balance = 800 where name = 'lilei';
session2对该表任一行操作都会阻塞住 InnoDB的行锁是针对索引加的锁,不是针对记录加的锁。并且该索引不能失 效,否则都会从行锁升级为表锁。

锁定某一行还可以用lock in share mode(共享锁) 和for update(排它锁),例如:select * from test_innodb_lock where a = 2 for update;这样其他session只能读这行数据,修改则会被阻塞,直到锁定行的session提交

事务隔离级别为串行化时,读写数据都会锁住整张表

隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大,鱼和熊掌不可兼得啊。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed,它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。

默认级别情况下间隙锁可以解决幻读

1
要避免幻读可以用间隙锁在Session_1下面执行update account set name ='zhuge' where id > 10 and id <=20;,则其他Session没法在这个范围所包含的间隙里插入或修改任何数据

行锁分析

通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况

1
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:系统启动后到现在总共等待的次数

对于这5个状态变量,比较重要的主要是:

1
2
3
Innodb_row_lock_time_avg (等待平均时长) 
Innodb_row_lock_waits (等待总次数)
Innodb_row_lock_time(等待总时长)

尤其是当等待次数很高,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手制定优化计划。

优化建议

  • 尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁
  • 合理设计索引,尽量缩小锁的范围
  • 尽可能减少检索条件范围,避免间隙锁
  • 尽量控制事务大小,减少锁定资源量和时间长度,涉及事务加锁的sql 尽量放在事务最后执行
  • 尽可能低级别事务隔离

参考链接