【MySQL笔记】事务
事务操作
默认每一条sql语句都是一个事务,事务自动提交,查看事务的提交方式:
select @@autocommit; -- 1
结果是1,代表自动提交,设置事务不自动提交:
set @@autocommit=0;
提交事务:
COMMIT;
回滚事务:
ROLLBACK;
测试数据:
CREATE TABLE account(
ID int(11) primary key auto_increment comment '主键ID',
name varchar(20) comment '姓名',
money int(11) comment '金额'
)
insert into account(name,money) values('张三',2000);
insert into account(name,money) values('李四',2000);
测试事务的两种方式
在一个会话中,设置事务为不自动提交,执行多条sql属于同一个事务,如下:
set @@autocommit=0;
update account set money=money-1000 where name='张三';
update account set money=money+1000 where name='李四';
commit;
第二种方式,显式开启事务,不需要将@@autocommit
设置为0
:
start transaction; -- 或 BEGIN
update account set money=money-1000 where name='张三';
update account set money=money+1000 where name='李四';
commit;
事务的四大特性(ACID)
原子性(Atomicity)
事务是不可分隔的最小单元,多个操作要么全部成功,要么全部失败。原子性只能保证单个事务的一致性
一致性(Consistency)
事务结束时,必须所有数据都保持一致,所有内部结构(如B树索引或双向链表)都必须正确
隔离性(Isolation)
保证事务在不受外部并发操作影响的独立环境下运行。并发执行的事务不会相互影响。由并发事务所作的修改必须与任何其它并发事务所作的修改隔离。事务查看数据时数据所处的状态,要么另一并发事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看中间状态的数据。
持久性(Durability)
事务一旦提交,对数据库的更新就是持久的
事务的最终目的就是为了保证数据的一致性,所以一致性是事务最重要的特性
并发事务问题
- 脏读
在一个事务里读取了另一个未提交的事务中的数据。 - 不可重复读
在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,数据被另一个事务修改并提交了。 - 幻读
一个事务按照条件查询数据时,没有对应的数据行,但在插入数据时,又发现这条数据已经存在,就好象发生了幻影。
事务的隔离级别
事务的隔离级别就时为了解决并发中存在的问题,事务的隔离级别是通过锁、MVCC的方式实现
MySQL中事务的默认隔离级别是REPEATABLE-READ
Read uncommitted
(读未提交)
最低隔离级别,以上并发问题都有可能发生
实现机制:在前文有说到所有写操作都会加排它锁,那还怎么读未提交呢?因为排他锁会阻止其它事务再对其锁定的数据加读或写的锁,但是对不加锁的读就不起作用了。READ UNCOMMITTED
隔离级别下, 读不会加任何锁。而写会加排他锁,并到事务结束之后释放。Read committed
(读已提交)
可防止数据脏读
实现机制:事务中的修改操作会加排他锁,直到事务提交时才释放锁。读取数据不加锁而是使用了MVCC
机制。因此在读已提交的级别下,都会通过MVCC
获取当前数据的最新快照,不加任何锁,也无视任何锁(因为历史数据是构造出来的,身上不可能有锁)。
为什么Read committed
可以防止数据脏读:脏读是因为读取了其他事务未提交的数据,之后事务回滚了,导致脏读。但是如果在事务中修改数据时加了排他锁,并且直到事务提交时才释放排他锁,在这之间不允许其他事务查询此记录,所以不会出现脏读。
为什么遗留了不可重复读和幻读问题:MVCC
版本的生成时机: 是每次select时。这就意味着,如果我们在事务A中执行多次的select
,在每次select
之间有其他事务更新了我们读取的数据并提交了,那就出现了不可重复读,即:重复读时,会出现数据不一致问题,后面我们会讲解超支现象,就是这种引起的。Repeatable read
实现机制:READ COMMITTED
级别不同的是MVCC版本的生成时机,即:一次事务中只在第一次select时生成版本,后续的查询都是在这个版本上进行,从而实现了可重复读。Serializable
可以解决全部事务并发问题
实现机制:所有的读操作均为当前读,读加读锁 (S锁
),写加写锁 (X锁
)。采用的是范围锁RangeS RangeS_S模式,锁定检索范围为只读,这样就避免了幻影读问题。
Serializable
隔离级别下,读写冲突,因此并发度急剧下降,在MySQL/InnoDB
下不建议使用。
隔离级别命令
查看当前数据库隔离级别:
show global variables like '%isolation%';
设置事务隔离级别:
SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL {Read uncommitted|Read committed|Repeatable read|Serializable}
如下:
set session transaction isolation level read uncommitted; -- 设置read uncommitted级别
set session transaction isolation level read committed; -- 设置read committed级别
set session transaction isolation level repeatable read; -- 设置repeatable read级别
set session transaction isolation level serializable; -- 设置serializable级别
ADO.NET设置事务隔离级别
var tran = conn.BeginTransaction(IsolationLevel.ReadUncommitted)
测试几种隔离级别:
read uncommitted:
打开两个会话,先后执行会话1、会话2中的代码
会话1:
set autocommit=0; -- 设置不自动提交
update account set name='fan' where id=1; -- 将姓名修改为fan,不提交
会话2:
set session transaction isolation level read uncommitted; -- 将当前会话隔离级别设置为read uncommitted
select * from account where id=1; -- 可以读取到会话1修改后的数据
read committed:
会话1:
set autocommit=0;
update account set name='fan' where id=1;
会话2:
set session transaction isolation level read committed; -- 将当前会话隔离级别设置为read committed
select * from account where id=1; -- 会话2读取到的还是原来的数据,直到会话1提交后,会话2才可以读到修改后的数据
再测试一下是否可以重复读:
会话1:
set session transaction isolation level read committed;
set autocommit=0;
select * from account where id=1; -- 先执行这个sql,查看结果。然后再执行会话2
select * from account where id=1; -- 执行完会话2后再执行一次查询,对比两次结果是否一致
会话2:
update account set name='fan' where id=1; -- 将姓名修改为fan
结果是会话1中两次查询结果不一致
repeatable read:
会话1:
set session transaction isolation level repeatable read;
set autocommit=0;
select * from account where id=1; -- 先执行这个sql,查看结果。然后再执行会话2
select * from account where id=1; -- 执行完会话2后再执行一次查询,对比两次结果是否一致
会话2:
update account set name='fan' where id=1; -- 将姓名修改为fan
结果是会话1中两次查询结果一致
serializable:
会话1:
set session transaction isolation level serializable;
set autocommit=0;
select * from account where id=1;
会话2:
update account set name='fan' where id=1;
执行完会话1,此时会话1未提交,id=1的记录加了读锁,会话2执行update会被阻塞。直到会话1执行了commit;
,会话2才会update成功