Mysql 数据锁与事务
一、锁
常用命令
查看表的存储引擎:mysql> show create table myLock;
修改当前表的存储引擎:mysql> alter table myLock engine=myisam;
查看数据库当前默认的存储引擎:mysql> show variables like '%storage_engine%';
1、读写锁(数据的操作类型):
读锁(共享锁):对于同一条记录,多个读操作可以同时进行而不会互相影响,会阻塞写操作。
写锁(排他锁):当前写操作没有完成前,会阻碍其他写锁与读锁。
2、行锁表锁(锁的力度):
表锁
表锁:表级别的锁定是MySQL各存储引擎中最大颗粒度的锁定机制。该锁定机制最大的特点是实现逻辑非常简单,带来的系统负面影响最小。所以获取锁和释放锁的速度很快。由于表级锁一次会将整个表锁定,所以可以很好的避免困扰我们的死锁问题。当然,锁定颗粒度大所带来最大的负面影响就是出现锁定资源争用的概率也会最高,致使并大度大打折扣。(MyISAM存储引擎支持表锁)
创建数据:
mysql> create table myLock(id int not null primary key auto_increment,name varchar(20))engine myisam; Query OK, 0 rows affected (0.01 sec) mysql> insert into myLock (name) values("A"); Query OK, 1 row affected (0.00 sec) mysql> insert into myLock (name) values("B"); Query OK, 1 row affected (0.00 sec)
给表myLock加读锁:mysql> lock table myLock read;
在查看表的锁状态:
mysql> show open tables from jalja_deal; +------------+------------+--------+-------------+ | Database | Table | In_use | Name_locked | +------------+------------+--------+-------------+ | jalja_deal | jalja_user | 0 | 0 | | jalja_deal | myLock | 1 | 0 | | jalja_deal | deal_pay | 0 | 0 | | jalja_deal | deal_order | 0 | 0 | +------------+------------+--------+-------------+
解除表myLock的读锁:
mysql> unlock tables; Query OK, 0 rows affected (0.00 sec) mysql> show open tables from jalja_deal; +------------+------------+--------+-------------+ | Database | Table | In_use | Name_locked | +------------+------------+--------+-------------+ | jalja_deal | jalja_user | 0 | 0 | | jalja_deal | myLock | 0 | 0 | | jalja_deal | deal_pay | 0 | 0 | | jalja_deal | deal_order | 0 | 0 | +------------+------------+--------+-------------+ 4 rows in set (0.00 sec)
模拟读锁:
第一个session:
mysql> lock table myLock read; Query OK, 0 rows affected (0.00 sec) mysql> select * from myLock; +----+------+ | id | name | +----+------+ | 1 | A | | 2 | B | +----+------+ 2 rows in set (0.00 sec)
第二个session:
mysql> select * from myLock; +----+------+ | id | name | +----+------+ | 1 | A | | 2 | B | +----+------+ 2 rows in set (0.00 sec)
从这里可以看出读锁之间是共享的。
我们的第一个session在不释放锁的情况下进行以下操作:
修改当前表的信息:
mysql> update myLock set name='C' where id =1; ERROR 1099 (HY000): Table 'myLock' was locked with a READ lock and can't be updated
查询其他表:
mysql> select * from jalja_user; ERROR 1100 (HY000): Table 'jalja_user' was not locked with LOCK TABLES
第二个session修改myLock表的数据:没有错误也没有执行结果,应为他被阻塞了正在排队等待获取myLock表的锁
mysql> update myLock set name="N" where id =1;
模拟写锁:
#加锁
mysql> lock table myLock write; Query OK, 0 rows affected (0.00 sec) //读数据 mysql> select * from myLock; +----+------+ | id | name | +----+------+ | 1 | N | | 2 | B | +----+------+ 2 rows in set (0.00 sec) //写数据 mysql> update myLock set name="N" where id=1; Query OK, 0 rows affected (0.03 sec) Rows matched: 1 Changed: 0 Warnings: 0 //操作其他表 mysql> select * from jalja_user; ERROR 1100 (HY000): Table 'jalja_user' was not locked with LOCK TABLES
第二个session:
读数据组阻塞
mysql> select * from myLock;
写数据阻塞
mysql> update myLock set name="G" where id=1;
结论:
- MyISAM在执行查询语句前,会自动给涉及到的表加读锁,在执行写操作前,会自动给涉及到的表加写锁。
- 对MyISAM表的读操作,不会阻塞其他进程对同一表的请求,但会阻塞对同一表的写请求。
- 对MyISAM表的写操作,会阻塞其他进程对同一表的读和写操作。
- MyISAM引擎的读写锁调度是写优先,这也是myISAM不适合做写为主表的引擎。因为写锁时,其他线程不能做任何操作,大量的更新操作会使查询很难得到锁,从而造成永远的阻塞
行锁
行锁:行级锁定最大的特点就是锁定对象的颗粒度很小,也是目前各大数据库管理软件所实现的锁定颗粒度最小的。由于锁定颗粒度很小,所以发生锁定资源争用的概率也最小,能够给予应用程序尽可能大的并发处理能力而提高一些需要高并发应用系统的整体性能。虽然能够在并发处理能力上面有较大的优势,但是行级锁定也因此带来了不少弊端。由于锁定资源的颗粒度很小,所以每次获取锁和释放锁需要做的事情也更多,带来的消耗自然也就更大了。此外,行级锁定也最容易发生死锁。使用行级锁定的主要是InnoDB存储引擎。
二、锁分析
A、查看数据的锁状态 (数据库 jalja_deal)
mysql> show open tables from jalja_deal; +------------+----------------+--------+-------------+ | Database | Table | In_use | Name_locked | +------------+----------------+--------+-------------+ | jalja_deal | InnoDB_monitor | 0 | 0 | | jalja_deal | jalja_user | 0 | 0 | | jalja_deal | myLock | 0 | 0 | | jalja_deal | deal_pay | 0 | 0 | | jalja_deal | deal_order | 0 | 0 | +------------+----------------+--------+-------------+
In_use =0 说明没有表被锁
B、分析表的锁定情况
mysql> show status like 'table%'; +----------------------------+-------+ | Variable_name | Value | +----------------------------+-------+ | Table_locks_immediate | 75 | | Table_locks_waited | 0 | | Table_open_cache_hits | 3 | | Table_open_cache_misses | 5 | | Table_open_cache_overflows | 0 | +----------------------------+-------+
Table_locks_immediate:产生表级锁定的次数,表示可以立即获取锁的查询次数,每立即获取锁时该值加1
Table_locks_waited:出现表级锁争用而发生等待的次数(不能立即获取锁的次数,每等待一次该值加1),该值高则说明存在较为严重的表级锁争用的情况。
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 | +-------------------------------+-------+
InnoDB 的行级锁定状态变量不仅记录了锁定等待次数,还记录了锁定总时长,每次平均时长,以及最大时长,此外还有一个非累积状态量显示了当前正在等待锁定的等待数量。对各个状态量的说明如下:
InnoDB_row_lock_current_waits:当前正在等待锁定的数量;
InnoDB_row_lock_time:从系统启动到现在锁定总时间长度;
InnoDB_row_lock_time_avg:每次等待所花平均时间;
InnoDB_row_lock_time_max:从系统启动到现在等待最常的一次所花的时间;
InnoDB_row_lock_waits:系统启动后到现在总共等待的次数;
对于这5个状态变量,比较重要的主要是InnoDB_row_lock_time_avg(等待平均时长),InnoDB_row_lock_waits(等待总次数)以及InnoDB_row_lock_time(等待总时长)这三项。尤其是当等待次数很高,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手指定优化计划。
监控:
如果发现锁争用比较严重,如InnoDB_row_lock_waits和InnoDB_row_lock_time_avg的值比较高,还可以通过设置InnoDB Monitors来进一步观察发生锁冲突的表、数据行等,并分析锁争用的原因。锁冲突的表、数据行等,并分析锁争用的原因。具体方法如下:
mysql> create table InnoDB_monitor(a INT) engine=InnoDB;
然后就可以用下面的语句来进行查看:
mysql> show engine InnoDB status;
监视器可以通过发出下列语句来停止查看:
mysql> drop table InnoDB_monitor;
设置监视器后,会有详细的当前锁等待的信息,包括表名、锁类型、锁定记录的情况等,便于进行进一步的分析和问题的确定。可能会有读者朋友问为什么要先创建一个叫InnoDB_monitor的表呢?因为创建该表实际上就是告诉InnoDB我们开始要监控他的细节状态了,然后InnoDB就会将比较详细的事务以及锁定信息记录进入MySQL的errorlog中,以便我们后面做进一步分析使用。打开监视器以后,默认情况下每15秒会向日志中记录监控的内容,如果长时间打开会导致.err文件变得非常的巨大,所以用户在确认问题原因之后,要记得删除监控表以关闭监视器,或者通过使用“--console”选项来启动服务器以关闭写日志文件。
InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!
三、事务
一个支持事务的数据库必须必须具备ACID的属性
原子性 (Atomicity):
原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,这和前面两篇博客介绍事务的功能是一样的概念,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。
一致性(Consistency):
一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。
例如:
假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。
隔离性(Isolation):
隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。
关于事务的隔离性数据库提供了多种隔离级别,稍后会介绍到。
持久性(durability):
一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,修改的数据也不会丢失。(持久性的安全性与刷新日志级别也存在一定关系,不同的级别对应不同的数据安全级别。)
二、高并发操作会引发的问题
1、丢失更新(Lost Update)
撤销一个事务时,把其他事务已提交的更新数据覆盖
例子:A和B事务并发执行,A事务执行更新后,提交;B事务在A事务更新后,B事务结束前也做了对该行数据的更新操作,然后回滚,则两次更新操作都丢失了
2、脏读(Dirty Reads)
一个事务读到另一个事务未提交的更新数据
例子:A和B事务并发执行,B事务执行更新后,A事务查询B事务没有提交的数据,B事务回滚,则A事务得到的数据不是数据库中的真实数据。也就是脏数据,即和数据库中不一致的数据,不符合一致性要求。
3、不可重复读(Non-Repeatable Reads)
一个事务读到另一个事务已提交的更新数据
例子:A和B事务并发执行,A事务查询数据,然后B事务更新该数据,A再次查询该数据时,发现该数据变化了
4、覆盖更新
这是不可重复读中的特例,一个事务覆盖另一个事务已提交的更新数据
例子:A事务更新数据,然后B事务更新该数据,A事务查询发现自己更新的数据变了
5、虚读(幻读 Phantom Reads)
一个事务读到另一个事务已提交的新插入的数据
例子:A和B事务并发执行,A事务查询数据,B事务插入或者删除数据,A事务再次查询发现结果集中有以前没有的数据或者以前有的数据消失了
四、隔离级别
隔离级别 | 读数据一致性 | 脏读 | 不可重复读 | 幻读 |
未提交读(Read uncommitted) | 最低级别隔离,只能保证不读取物理上损坏的数据 | 是 | 是 | 是 |
已提交读(Read committed) | 语句级别 | 否 | 是 | 是 |
可重复读(Repeatable read) | 事务级别 | 否 | 否 | 是 |
可序列化(Serializable) | 最高级别,事务级 | 否 | 否 | 否 |
事务的隔离级别越高,并发引发的副作用就越小,但付出的代价越大,因为事务隔离实质上就是使事务在一定程度上“串行化”进行,这显然与“并发”是矛盾的。同时,不同的应用对读一致性和事务隔离程度的要求也是不同的,比如许多应用对“不可重复读” 和“幻读”并不敏感,可能更关心数据库并发访问能力。
查看当前数据库的隔离级别:
mysql> show variables like 'tx_isolation'; +---------------+-----------------+ | Variable_name | Value | +---------------+-----------------+ | tx_isolation | REPEATABLE-READ | +---------------+-----------------+