榕叶 发表于 2018-9-27 09:54:59

MySQL数据库高级(七)——事务和锁

MySQL数据库高级(七)——事务和锁

一、事务简介

1、事务简介
  事务(Transaction) 是指作为单个逻辑工作单元执行的一系列操作。

2、事务的特性
  A、原子性(Atomicity)
  表示组成一个事务的多个数据库操作是一个不可分隔的原子单元,只有所有的操作执行成功,整个事务才提交,事务中任何一个数据库操作失败,已经执行的任何操作都必须撤销,让数据库返回到初始状态。
  B、一致性(Consistency)
  事务操作成功后,数据库所处的状态和它的业务规则是一致的,即数据不会被破坏。
  C、隔离性(Isolation)
  在并发数据操作时,不同的事务拥有各自数据空间,它们的操作不会对对方产生干扰。数据库规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性越好,但并发性越弱。
  D、持久性(Durabiliy)
  一旦事务提交成功后,事务中所有的数据操作都必须被持久化到数据库中,即使提交事务后,数据库马上崩溃,在数据库重启时,也必须能保证能够通过某种机制恢复数据。

3、事务类型
  A、自动提交事务
  系统默认每个TRANSACT-SQL命令都是一个事务处理,由系统自动开始并提交。
  B、隐式事务
  不需要显示开始事务,需要显示提交,隐式事务是任何单独的INSERT、UPDATE 或者DELETE语句构成。当有大量的DDL和DML命令执行时会自动开始,并一直保持到用户明确提交为止。
  SHOW VARIABLES 查看变量。
  SET AUTOCOMMIT=0,关闭自动提交功能。
  需要显示提交或者回滚。
  

update tablename set sname='孙悟空' where studentid='000000000000003';  
commit;
  

  或
  rollback;
  C、显示事务
  显示事务是用户自定义事务,以START TRANSACTION(事务开始)开头,以 COMMIT(事务提交)或者 ROLLBACK(回滚事务)语句结束。
  

start transaction  
update tablename set sname='孙悟空' where studentid='000000000000003';
  
commit
  

  或
  rollback
  D、分布式事务
  跨越多个服务器的事务称为分布式事务。从MySQL5.03开始支持分布式事务。

4、事务控制
  A、开始事务
  标记一个显式事务的开始点,即事务开始。其语法如下:
  START { TRAN | TRANSACTION }
  B、提交事务
  标记一个成功的隐性事务或显式事务的结束,即事务提交。其语法如下:
  COMMIT
  C、回滚事务
  将显式事务或隐性事务回滚到事务的起点或事务内的某个保存点。其语法如下:
  ROLLBACK
  D、事务设置
  SET AUTOCOMMIT 可以修改当前连接事务提交方式。
  SET AUTOCOMMIT=0,则需要明确的命令进行提交或者回滚。

5、事务并发带来的问题
  脏读(Dirty Read)是指某个事务(A)读取另外事务(B)尚未提交的更改数据,并在读取的数据的基础上操作。如果恰巧 B事务回滚,那么 A事务读到的数据根本是不被承认的。
  不可重复读(Unrepeatable Read)是指A事务读取了B事务已经提交的更改数据。
  幻象读(Phantom Read)
  A事务读取B事务提交的新增数据,这时A事务将出现幻象读的问题。
  第一类丢失更新
  A事务撤销时,把已经提交的B事务的更新数据覆盖。
  第二类丢失更新
  A事务覆盖B事务已经提交的数据,造成B事务所做操作丢失。

二、事务隔离级别

1、事务隔离级别简介
  SQL标准定义了4类隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。
  Read Uncommitted(读取未提交内容)
  本隔离级别,事务可以读取其他未提交事务的执行结果。读取未提交的数据,也被称之为脏读(Dirty Read)。
  Read Committed(读取提交内容)
  大多数数据库系统的默认隔离级别(但不是MySQL默认的)。事务只能读取其他事务已经提交的执行结果。本隔离级别支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。
  Repeatable Read(可重读)
  MySQL默认的事务隔离级别,会给查询的记录做快照,直到事务结束。确保同一事务的多个实例在并发读取数据时,会看到同样的数据行,会导致幻读(Phantom Read)。幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了幻读问题。
  Serializable(可串行化)
  最高的隔离级别,对同一条记录读和修改的多个事务只能结束一个,才能开始下一个。
  通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。在每个读的数据行上加上共享锁,可能导致大量的超时现象和锁竞争。

2、事务隔离级别设置
  用户可以用SET TRANSACTION语句改变单个会话或者所有新进连接的隔离级别。语法如下:
  SET TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}
  默认的行为(不带session和global)是为下一个(未开始)事务设置隔离级别。如果使用GLOBAL关键字,语句在全局对新开始创建的所有新连接设置默认事务级别,需要SUPER权限。使用SESSION关键字为将来在当前连接上执行的事务设置默认事务级别。 任何客户端都能自由改变会话隔离级别,或者为下一个事务设置隔离级别。
  查询全局和会话事务隔离级别:
  

SELECT @@global.tx_isolation;  
SELECT @@session.tx_isolation;
  
SELECT @@tx_isolation;
  

  通过mySQL配置文件修改全局事务隔离级别,设置全局会话默认事务隔离级别。
  

  
xxxxxxx
  
transaction-isolation=read-committed
  

  重启mySQL服务,生效。
  设置当前会隔离级别
  

SETSESSIONTRANSACTION ISOLATION LEVELREAD UNCOMMITTED  
SETSESSIONTRANSACTION ISOLATION LEVELREAD COMMITTED
  
SETSESSIONTRANSACTION ISOLATION LEVELREPEATABLE READ
  
SETSESSIONTRANSACTION ISOLATION LEVELSERIALIZABLE
  

三、事务隔离级别验证

1、不同会话的隔离级别
  不同会话的事务隔离级别不同
  在会话1终端查看当前会话的事务隔离级别
  select @@tx_isolation
  查询结果为:可重复读REPEATABLE-READ
  设置当前会话事务隔离级别为READ UNCOMMITTED
  SET SESSIONTRANSACTION ISOLATION LEVELREAD UNCOMMITTED
  打开另一个SQL Manager终端作为会话2,查看当前会话的事务隔离级别
  select @@tx_isolation
  查询结果为:可重复读REPEATABLE-READ
  创建一张表,含ID、姓名、年龄字段,用于验证不同的事务隔离级别。
  

CREATE TABLE ta  
(
  
id INT NOT NULL PRIMARY KEY,
  
name VARCHAR(10),
  
age INT
  
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
  
insert into ta values(1, '孙悟空', 500);
  
insert into ta values(2, '唐僧', 30);
  

  注:由于本人SQL Manager Lite客户端的事务回滚机制失效,以下实验使用Navicat for MySQL客户端。

2、验证READ UNCOMMITTED隔离级别
  打开一个会话1,设置事务隔离级别为READ UNCOMMITTED
  SET SESSIONTRANSACTION ISOLATION LEVELREAD UNCOMMITTED
  打开会话2,开始一个事务,更新ID为1的记录的age为1000。
  

start TRANSACTION;
  
update ta set age=1000 where>  

  在会话1查看ta表中ID为1的信息,age已经为1000。
  select * from ta;
  会话1的事务隔离级别允许读取未提交的数据。
  在会话2回滚事务
  ROLLBACK;
  会话1和会话2查询ta表中ID为1的记录,age为500

3、验证READ COMMITTED隔离级别
  打开一个会话1,设置事务隔离级别为READ COMMITTED
  SET SESSIONTRANSACTION ISOLATION LEVELREAD COMMITTED
  打开会话2,开始一个事务,更新ID为1的记录的age为5000。
  

start TRANSACTION;
  
update ta set age=5000 where>  

  在会话1查看ta表中ID为1的信息,age为500。
  select * from ta;
  会话1的事务隔离级别不允许读取未提交的数据。
  在会话2提交事务
  COMMIT;
  会话1查询ta表中ID为1的记录,age为5000

4、验证REPEATABLE READ隔离级别
  打开一个会话1,设置事务隔离级别为REPEATABLE READ
  SET SESSIONTRANSACTION ISOLATION LEVELREPEATABLE READ
  在会话1,开始一个事务,查询ID为1的记录的age为5000。
  

start TRANSACTION;
  
SELECT * FROM ta where>  

  在会话2更新ta表中ID为1的信息,age为1000。
  UPDATE ta SET age=1000 WHERE>
  在会话2查看ta表中ID为1的信息,age已经为1000。
  select * from ta WHERE>
  在会话1再次查看ta表中ID为1的信息,age仍旧为5000。
  select * from ta WHERE>
  在会话1提交事务
  COMMIT;
  会话1查询ta表中ID为1的记录,age已经为1000。

5、验证SERIALIZABLE隔离级别
  打开一个会话1,设置事务隔离级别为SERIALIZABLE
  SET SESSIONTRANSACTION ISOLATION LEVELSERIALIZABLE
  打开会话2,开始一个事务,更新ID为1的记录的age为5000。
  

start TRANSACTION;
  
update ta set age=5000 where>  

  在会话1开始一个事务,查看ta表中ID为1的信息,会话1处于等待状态。
  

start TRANSACTION;  
select * from ta;
  

  在会话2提交事务后,
  COMMIT;
  会话1查询SQL执行完毕,结果为5000。

四、锁

1、锁简介
  数据库中的锁是指一种软件机制,用来控制防止某个用户(进程会话)在已经占用了某种数据资源时,其他用户做出影响本用户数据操作或导致数据非完整性和非一致性问题发生的手段。

2、锁的级别
  按照锁级别划分,锁可分为共享锁、排他锁。
  A、共享锁(读锁)
  针对同一块数据,多个读操作可以同时进行而不会互相影响。
  共享锁只针对UPDATE时候加锁,在未对UPDATE操作提交之前,其他事务只能够获取最新的记录但不能够UPDATE操作。
  B、排他锁(写锁)
  当前写操作没有完成前,阻断其他写锁和读锁。

3、锁的粒度
  按锁的粒度划分,锁可分为表级锁、行级锁、页级锁。
  A、行级锁
  开销大,加锁慢,会出现死锁,锁定力度最小,发生锁冲突的概率最低,并发度高。
  B、表级锁
  开销小,加锁快,不会出现死锁,锁定力度大,发生冲突所的概率高,并发度低。
  C、页面锁
  开销和加锁时间介于表锁和行锁之间,会出现死锁,锁定力度介于表和行行级锁之间,并发度一般。

4、MySQL存储引擎和锁机制
  MySQL的锁机制比较简单,最显著的特点是不同的存储引擎支持不同的锁机制。
  MyISAM和MEMORY存储引擎采用表级锁。
  InnoDB支持行级锁、表级锁,默认情况采用行级锁。

五、表级锁

1、表级锁简介
  MyISAM存储引擎和InnoDB存储引擎都支持表级锁。
  MyISAM存储引擎支持表级锁,为了保证数据的一致性,更改数据时,防止其他人更改数据,可以人工添加表级锁。可以使用命令对数据库的表枷锁,使用命令对数据库的表解锁。
  给表加锁的命令Lock Tables,给表解锁的命令Unlock Tables
  MyISAM引擎在用户读数据自动加READ锁,更改数据自动加WRITE锁。使用lock Tables和Unlock Tables显式加锁和解锁。

2、添加表级读锁
  打开会话1,创建表
  

CREATE TABLE tc  
(
  
id INT,
  
name VARCHAR(10),
  
age INT
  
)ENGINE=MyISAM DEFAULT CHARSET=utf8;
  

  插入两条记录:
  

insert into tc values(1, '孙悟空', 500);  
insert into tc values(3, '猪八戒', 100);
  

  对表加READ锁
  lock tables tc read;
  加锁后只可以查询已经加锁的表,
  select * from tc;
  查询没有加锁的表将失败
  select * from ta;
  打开会话2,对已经加锁的表进行查询,成功。
  select * from tc;
  对加锁的表tc进行更新操作,将失败
  update tc set age=100 where>
  会话1中使用LOCK TABLE命令给表加了读锁,会话1可以查询锁定表中的记录,但更新或访问其他表都会提示错误;会话2可以查询表中的记录,但更新就会出现锁等待。
  在会话1对表进行解锁,会话2的更新操作成功。
  unlock tables;
  在会话1,再次锁定表tc,后面带local参数。
  lock tables tc read local;
  Local参数允许在表尾并发插入,只锁定表中当前记录,其他会话可以插入新的记录
  在会话2插入一条记录
  insert into tc values(2, '唐僧', 20);
  在会话1查看tc表的记录,无插入记录
  select * from tc;

3、设置表级锁并发性
  READ锁是共享锁,不影响其他会话的读取,但不能更新已经加READ锁的数据。MyISAM表的读写是串行的,但是总体而言的,在一定条件下,MyISAM表也支持查询和插入操作的并发进行。
  MyISAM存储引擎有一个系统变量concurrent_insert,用以控制其并发插入的行为,其值分别可以为0、1或2。
  0:不允许并发操作
  1:如果MyISAM表中没有空洞(即表的中间没有被删除的行),MyISAM允许在一个进程读表的同时,另一个进程从表尾插入记录,是MySQL的默认设置。
  2:无论MyISAM表中有没有空洞,都允许在表尾并发插入记录。
  在MySQL配置文件添加,concurrent_insert=2,重启mySQL服务设置生效。

4、验证表级锁的并发性
  设置concurrent_insert为0
  在会话1对表tc加锁
  lock tables tc read local;
  在会话2插入一条记录,此时tc表被锁定,进入等待
  insert into tc values(4, '沙悟净', 30);
  在会话1解锁表tc,此时会话2插入成功
  unlock tables;
  设置concurrent_insert为1
  在会话1删除ID为3的记录
  delete from tc where>
  在会话1对表tc加锁
  lock tables tc read local;
  在会话2插入一条记录,此时tc表被锁定,并且表中有空洞,进入等待
  insert into tc values(5, '白骨精', 1000);
  在会话1解锁表tc,此时会话2插入成功,此时表中已经没有空洞
  unlock tables;
  在会话1对表tc加锁
  lock tables tc read local;
  在会话2插入一条记录,插入成功,支持有条件并发插入
  insert into tc values(6, '白骨精', 1000);
  在会话1解锁表tc
  unlock tables;
  设置concurrent_insert为2
  在会话1删除ID为5的记录,创造一个空洞
  delete from tc where>
  在会话1对表tc加锁
  lock tables tc read local;
  在会话2插入一条记录,插入成功,支持无条件并发插入
  insert into tc values(7, '蜘蛛精', 1000);
  在会话1解锁表tc
  unlock tables;

5、添加表级写锁
  添加表级写锁语法如下:
  LOCK TABLES   tablename WRITE;
  不允许其他会话查询、修改、插入记录。

六、行级锁

1、行级锁简介
  InnoDB存储引擎实现的是基于多版本的并发控制协议——MVCC (Multi-Version Concurrency Control)。MVCC的优点是读不加锁,读写不冲突。在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能。
  在MVCC并发控制中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。
  快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁。
  当前读,读取的是记录的最新版本,并且当前读返回的记录都会加上锁,保证其他事务不会再并发修改。事务加锁,是针对所操作的行,对其他行不进行加锁处理。
  快照读:简单的SELECT操作,属于快照读,不加锁。
  select * from table where ?;
  当前读:特殊的读操作,INSERT/UPDATE/DELETE,属于当前读,需要加锁。
  

select * from table where ? lock in share mode;  
select * from table where ? for update;
  
insert into table values (…);
  
update table set ? where ?;
  
delete from table where ?;
  

  以上SQL语句属于当前读,读取记录的最新版本。并且,读取之后,还需要保证其他并发事务不能修改当前记录,对读取记录加锁。其中,除了第一条语句,对读取记录加S锁 (共享锁)外,其他的操作,都加的是X锁 (排它锁)。

2、验证快照读
  打开会话1,创建一个表,含ID、姓名、年龄
  

CREATE TABLE td  
(
  
id INT ,
  
name VARCHAR(10),
  
age INT
  
)ENGINE=innoDB DEFAULT CHARSET=utf8;
  

  在插入两条记录
  

insert into td values(1, '孙悟空', 500);  
insert into td values(2, '猪八戒', 100);
  

  在会话1开始事务
  start transaction;
  在会话1查询ID位1的记录信息
  select * from td where>
  打开会话2,更新ID为1的age为1000
  update td set age=1000 where>
  在会话2查看ID为1的age已经更新为1000。
  select * from td where>
  在会话1查看ID为1的age,仍然为500。
  select * from td where>
  在会话1提交事务
  COMMIT;
  在会话1查看ID为1的age,已经为1000。

3、验证当前读
  在会话1开始事务
  start transaction;
  在会话1给select语句添加共享锁。
  select * from td where>
  在会话2,更新ID为1的age的值为100,进入锁等待
  update td set age=100 where>
  在会话1提交事务
  COMMIT;
  会话2的更新操作成功。

4、验证事务给记录加锁
  在会话1开始事务
  start transaction;
  在会话1更新ID为1的age的值为500。
  update td set age=500 where>
  在会话2开始事务
  start transaction;
  在会话2更新ID为2的age的值为1000,此时进入锁等待
  update td set age=1000 where>
  td表没有指定主键,事务不支持行级锁。会话1的事务给整张表加了锁。
  在会话1提交事务,此时会话2的修改成功
  COMMIT;
  在会话2提交事务,解除对表的锁定
  COMMIT;
  在会话1,给表的ID增加主键
  alter table td add primary key(id);
  在会话1开始事务
  start transaction;
  在会话1更新ID为1的age的值为5000
  update td set age=5000 where>
  在会话2上开始事务
  start transaction;
  在会话2上修改ID为2的get的值为10000,更新成功,说明会话1只锁定了ID为1的行。
  updatetd set age=10000 where>
  在会话2上更新ID是1的age值为100,出现等待。因为会话1给ID为1的行添加了独占锁。
  updatetd set age=5000 where>
  在会话1提交事务
  COMMIT;
  在会话2提交事务
  COMMIT;
  在会话1查询,会话1和会话2对age列的修改都生效
  select * from td;

5、死锁的产生
  A事务添加共享锁后,B事务也可以添加共享锁。A事务UPDATE锁定记录,处于等待中,于此同时B事务也UPDATE更新锁定的记录,就产生死锁。
  在会话1开始事务
  start transaction;
  在会话1查询ID是1的记录,并添加共享锁。
  select * from td where>
  在会话2开始事务
  start transaction;
  在会话2查询ID是1的记录,并添加共享锁。
  select * from td where>
  在会话1更新ID为1的age值为,等待会话2释放共享锁
  update td set age=200 where>
  在会话2更新ID为1的age为,会话2发现死锁,回滚事务。
  update td set age=200 where>
  在会话1提交事务
  COMMIT;

七、事务实例
  事务提交还是回滚,可以在事务结束处判断是否出现错误,如果出现,回滚。如果没有错误,提交事务。
  使用自定义条件来决定事务是提交还是回滚。

1、由错误决定事务提交或回滚
  在存储过程中使用事务,在事务的末尾判断是否有错误,插入失败,则回滚事务。
  创建两张表,存储ID、姓名、年龄,创建存储过程将A表的指定ID的记录转移到B表。
  

CREATE TABLE ta  
(
  
id INT NOT NULL PRIMARY KEY,
  
name VARCHAR(10),
  
age INT
  
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
  
insert into ta values(1, '孙悟空', 500);
  
insert into ta values(2, '唐僧', 30);
  

  
CREATE TABLE tb
  
(
  
id INT NOT NULL PRIMARY KEY,
  
name VARCHAR(10),
  
age INT
  
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
  

  
insert into tb values(1, '孙悟空', 500);
  
insert into tb values(3, '猪八戒', 100);
  
CREATE PROCEDURE move(num INT)
  
BEGIN
  
DECLARE errorinfo INT DEFAULT 0;
  
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET errorinfo=1;
  
START TRANSACTION;

  
INSERT INTO tb SELECT * FROM ta WHERE>
  
DELETE FROM ta WHERE>  
IF errorinfo=1
  THEN ROLLBACK;
  
ELSE
  COMMIT;
  
END IF;
  
END
  

  将ID为2的记录从A表转移到B表
  call move(2);

2、由自定义条件决定事务提交或回滚
  创建两个表,每个表含账户、姓名、余额信息,创建一个存储过程,从A表中的一个账户转账一定金额到B表的一个账户,如果转出账户的余额不足,则回滚,否则提交。
  

create table accountA  
(
  
account INT PRIMARY KEY NOT NULL,
  
name VARCHAR(10),
  
balance DOUBLE
  
)ENGINE=innoDB default CHARSET=utf8;
  

  
insert into accountA VALUES(1, '孙悟空', 10000);
  
insert into accountA VALUES(2, '唐僧', 20000);
  
create table accountB
  
(
  
account INT PRIMARY KEY NOT NULL,
  
name VARCHAR(10),
  
balance DOUBLE
  
)ENGINE=innoDB default CHARSET=utf8;
  

  
insert into accountB VALUES(1, '孙悟空', 10000);
  
insert into accountB VALUES(2, '唐僧', 20000);
  
CREATE PROCEDURE transfer(fromaccout INT,toaccount INT, num DOUBLE)
  
BEGIN
  
DECLARE m DOUBLE;
  
START TRANSACTION;
  
UPDATE accountB SET balance=balance + num WHERE account=toaccount;
  
UPDATE accountA SET balance=balance - num WHERE account=fromaccout;
  
SELECT balance INTO m from accountA WHERE account=fromaccout;
  
IF m < 0
  THEN ROLLBACK;
  
ELSE
  COMMIT;
  
END IF;
  
END
  

  从A表的账户2转出25000元到B表的账户2。
  call transfer(2,2,25000);
  此时A表的余额不足,回滚


页: [1]
查看完整版本: MySQL数据库高级(七)——事务和锁