数据库事务(Database Transaction) ,是指做为单个逻辑工做单元执行的一系列操做,要么彻底地执行,要么彻底地不执行。
ACID,是指在可靠数据库管理系统(DBMS)中,事务(Transaction)所应该具备的四个特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。
隔离级别 | 读数据一致性 | 赃读 | 不可重复读 | 幻读 |
---|---|---|---|---|
Read Uncommitted | 最低级别,只能保证不读取物理上损坏的数据 | √ | √ | √ |
Read Committed | 语句级 | × | √ | √ |
Repeatable Read | 事务级 | × | × | √ |
Serializable | 最高级别,事务级 | × | × | × |
低级别的隔离通常支持更高的并发处理,并拥有更低的系统开销。高级别的隔离可靠性较高,但系统开销较大。spring
建立数据库数据库
CREATE DATABASE IF NOT EXISTS txdemo DEFAULT CHARSET utf8 COLLATE utf8_general_ci;
建立测试表并发
CREATE TABLE `user` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `name` VARCHAR(32) NOT NULL DEFAULT '', `age` INT(16) NOT NULL DEFAULT '30', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
插入测试数据性能
INSERT INTO user (name, age) VALUES ('manerfan', 30), ('Abel', 28), ('Cherry', 42);
Step 1: 设置A的隔离级别为Read Uncommitted,开启事务并读取数据
Step 2: B开启事务,修改数据,但不提交
Step 3: A读取数据,发现数据已变
Step 4: B回滚,但不提交
Step 5: A读取数据,发现数据恢复测试
A事务中能够读取到B事务未修改的数据,发生赃读spa
Step 1: 设置A的隔离级别为Read Committed,开启事务并读取数据
Step 2: B开启事务,修改数据,但不提交
Step 3: A读取数据,发现数据未变
Step 4: B提交事务
Step 5: A读取数据,发现数据改变线程
已提交读隔离级别解决了脏读的问题,可是出现了不可重复读的问题,即事务A在两次查询的数据不一致,由于在两次查询之间事务B更新了一条数据。code
Step 1: 设置A的隔离级别为Repeatable Read,开启事务并读取数据
Step 2: B开启事务,修改数据,但不提交
Step 3: A读取数据,发现数据未变
Step 4: B提交事务
Step 5: A读取数据,发现数据依然未变,解决了不可重复读
Step 6: B插入新数据,并提交
Step 7: A读取数据,发现数据仍是未变,出现幻读
Step 8: A提交事务,再次读取数据,发现数据改变排序
Repeatable Read隔离级别只容许读取已提交记录,并且在一个事务两次读取一个记录期间,其余事务的更新不会影响该事务。但该事务不要求与其余事务可串行化,可能会发生幻读。索引
Step 1: 设置A的隔离级别为Serializable,开启事务并读取数据
Step 2: B开启事务,修改数据,B事务阻塞,A的事务还没有提交,只能等待
Step 3: A事务提交,B事务插入成功,但B事务不提交
Step 4: A事务查询,发现A事务阻塞,B的事务还没有提交,只能等待
Step 5: B事务提交,A事务查询成功
Serializable隔离级别彻底锁定字段,若一个事务来查询同一份数据就必须等待,直到前一个事务完成并解除锁定为止。
乐观锁(Optimistic Lock),是指操做数据库时(更新操做),老是认为此次的操做不会致使冲突,不到万不得已不去拿锁,在更新时采起判断是否冲突,适用于读操做远多于更新操做的状况。
乐观锁并无被数据库实现,须要自行实现,一般的实现方式为在表中增长版本version字段,更新时判断库中version与取出时的version值是否相等,若相等则执行更新并将version加1,若不相等则说明数据被其余线程(进程)修改,放弃修改。
select (age, version) from user where id = #{id}; # 其余操做 update user set age = 18, version = version + 1 where id = #{id} and version = #{version}
悲观锁(Pessimistic Lock),是指操做数据库时(更新操做),老是认为此次的操做会致使冲突,每次都要经过获取锁才能进行数据操做,所以要先确保获取锁成功再进行业务操做。
悲观锁须要数据库自身提供支持,MySql提供了共享锁和排他锁来实现对数据行的锁定,两种锁的介绍以下介绍。
InnoDB实现了如下两种类型的行锁:
X | S | |
---|---|---|
X | 冲突 | 冲突 |
S | 冲突 | 兼容 |
对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X)
对于普通SELECT语句,InnoDB不会加任何锁
事务能够经过如下语句显式地给记录集加共享锁或排他锁:
SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
,等同读锁SELECT * FROM table_name WHERE ... FOR UPDATE
,等同写锁用SELECT ... IN SHARE MODE
得到共享锁,主要用在须要数据依存关系时来确认某行记录是否存在,并确保没有人对这个记录进行UPDATE或者DELETE操做。可是若是当前事务也须要对该记录进行更新操做,则颇有可能形成死锁,对于锁定行记录后须要进行更新操做的应用,应该使用SELECT... FOR UPDATE
方式得到排他锁。
SELECT ... IN SHARE MODE
模式
Step 1: A查询id为1的数据并加共享锁
Step 2: B查询id为2的数据并加共享锁
Step 3: A更新id为2的数据,因为共享锁与排他锁冲突而阻塞
Step 4: B更新id为1的数据,因为A与B互相等待对方释放锁而抛出死锁异常
SELECT... FOR UPDATE
模式
Step 1: A查询id为1的数据并加排他锁
Step 2: B查询id为1的数据不加任何锁,成功
Step 3: B查询id为1的数据并加排他锁,阻塞
Step 4: A更新id为1的数据(数据库自动加排他锁),成功
Step 5: A提交事务,B获取锁查询成功
InnoDB行锁是经过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不一样,后者是经过在数据块中对相应数据行加锁来实现的。InnoDB这种行锁实现特色意味着:只有经过索引条件检索数据,InnoDB才使用行级锁,不然,InnoDB将使用表锁!
在实际应用中,要特别注意InnoDB行锁的这一特性,否则的话,可能致使大量的锁冲突,从而影响并发性能。
Step 1: A查询id为1的数据并加排他锁
Step 2: B查询id为2的数据并加排他锁,成功
Step 3: A开启新的事物,查询age为32的数据并加排他锁
Step 4: B开启新的事物,查询age为92的数据并加排他锁,阻塞,B与A查询的数据并非同一行,但B阻塞,说明A的排他锁为表锁非行锁
Spring的 @Transactional 提供了设置事务隔离级别及事务传播行为的方式
Isolation中定义了DEFAULT及以上介绍的四中隔离级别,这里再也不赘述
package org.springframework.transaction.annotation; public enum Isolation { DEFAULT, READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE; }
Propagation中定义了其中传播行为
package org.springframework.transaction.annotation; public enum Propagation { REQUIRED, SUPPORTS, MANDATORY, REQUIRES_NEW, NOT_SUPPORTED, NEVER, NESTED; }