一、为什么需要引入事务
使一组SQL语句不可分割,即要么全部执行,要么都不执行。从而保证了数据库数据的一致性。经典转账问题:A向B转账100,A的账户需要扣除100,B的账户需要增加100。两者要么都执行要么都不执行,此时需要事务的保护。
二、四大特性
数据库事务具有ACID四大特性
- Atomic(原子性):将所有SQL作为原子工作单元执行,要么全部执行,要么全部不执行
- Consistency(一致性):事务完成后,所有数据的状态都是一致的,例如A账户只要减去了100,B账户则必定加上了100
- Isolation(隔离性):如果有多个事务并行执行,则某个事务的执行不受其他事务的干扰,事务执行的中间结果对其他事务必须是透明的
- Duration(持久性):对于任意已提交事务,系统必须保证该事务对数据库的改变不被丢失,即使数据库出现故障
三、事务隔离级别
上一节说到数据库事务具有Isolation隔离性,数据库一般具有如下四个隔离级别来调整事务之间的隔离性。不同级别对应隔离效果不同,性能也会随之发生变化,隔离效果越强则性能损耗越大。MySQL默认使用的级别是Repeatable Read。
- 查看事务方式:
select @@transaction_isolation;
- 修改事务方式:
set session transaction isolation level read uncommitted;
其中session可以替换为global,这样做表示修改的是全局的配置即默认隔离级别。
在MySQL中对于单条SQL语句,数据库系统自动将其作为一个事务执行,这种事务被称为隐式事务。这个是由一个参数控制的,查看方法:select @@autocommit;
这个自动提交参数会返回0或者1,其中0表示关闭,1表示开启(默认开启)。开启状态下你的每一条SQL操作(增删改查)会被自动包装成一个事务执行。当然你也可以通过命令来关闭set @@autocommit = 0;
关闭状态下你必须手动提交一条或一组SQL,否则你的写操作不会对数据库造成修改。
与隐式事务相对的那就是显式事务了,显式事务对autocommit没有要求,就算开启autocommit也能实现。它是通过命令begin;
或者start transaction;
来开启,不过需要注意的是输入这个命令并不意味着事务的起点,而是在执行完它们后的第一个sql语句,才表示事务真正的启动。输入完SQL之后,需要通过commit;
提交事务,把事务内的所有SQL所做的修改永久保存。有些时候,我们希望主动让事务失败,这时,可以用rollback;
回滚事务,整个事务会失败,对数据库的修改也如同没有发生一样。
1. Read Uncommitted(读未提交)
读未提交是四种隔离级别最弱的,同时对数据库性能影响也是最小的。顾名思义它表示一个事务可以读到另一个事务尚未提交的SQL对数据库的修改。因此会产生脏读的现象。
如下表所示事务1在t5时刻读到了新增的一个账户,即使这条数据最后被事务2回滚了。
时刻 | 事务1 | 事务2 |
---|---|---|
t1 | set session transaction isolation level read uncommitted; | set session transaction isolation level repeatable read; |
t2 | begin; | begin; |
t3 | select * from account; | |
t4 | insert into account(username) values(‘111’); | |
t5 | select * from account; | |
t6 | commit; | rollback; |
下面用Java代码,在mybatis框架下来实现一下这个场景:
private static void testDirtyRead() {
// mybatis默认关闭autocommit
try (SqlSession session1 = sqlSessionFactory.openSession(TransactionIsolationLevel.READ_UNCOMMITTED);
SqlSession session2 = sqlSessionFactory.openSession(TransactionIsolationLevel.REPEATABLE_READ)) {
AccountDao accountDao1 = session1.getMapper(AccountDao.class);
AccountDao accountDao2 = session2.getMapper(AccountDao.class);
System.out.println("first read:\n" + accountDao1.listAllAccounts());
accountDao2.insertAccount(Account.builder().username("111").build());
// 清除一级缓存,使session1重新去查一次数据库
session1.clearCache();
// 读到脏数据
System.out.println("second read:\n" + accountDao1.listAllAccounts());
session2.rollback();
session1.commit();
}
}
2. Read Committed(读已提交)
顾名思义该级别的隔离性只会读到已经提交的事务,所以脏读就自然而然地避免了。但是正是由于能够读到已提交的事务使得它又会产生新的问题那就是不可重复读,所谓的不可重复读指的是,同一个事务中读取到的结果不一致,即使该事务没有对数据库进行写操作。
如下表所示t3和t6时刻,事务1读取到的结果是不同的,产生了不可重复读。
时刻 | 事务1 | 事务2 |
---|---|---|
t1 | set session transaction isolation level read committed; | set session transaction isolation level repeatable read; |
t2 | begin; | begin; |
t3 | select * from account; | |
t4 | update account set balance=77.7 where id=7; | |
t5 | commit; | |
t6 | select * from account; | |
t7 | commit; |
下面继续用mybatis进行演示:
private static void testNonRepeatableRead() {
try (SqlSession session1 = sqlSessionFactory.openSession(TransactionIsolationLevel.READ_COMMITTED);
SqlSession session2 = sqlSessionFactory.openSession(TransactionIsolationLevel.REPEATABLE_READ)) {
AccountDao accountDao1 = session1.getMapper(AccountDao.class);
AccountDao accountDao2 = session2.getMapper(AccountDao.class);
System.out.println("first read:\n" + accountDao1.listAllAccounts());
accountDao2.updateAccount(Account.builder().id(7L).balance(new BigDecimal("77.7")).build());
session2.commit();
// 清除一级缓存,使session1重新去查一次数据库
session1.clearCache();
// 两次读取结果不一致,即不可重复读
System.out.println("second read:\n" + accountDao1.listAllAccounts());
session1.commit();
}
}
3. Repeatable Read(可重复读)
可重复读这一级别解决了上一级别不可重复读的问题,它在事务开启的时候会创建一个一致性视图,也叫做快照,之后的查询里都共用这个一致性视图,后续其他事务对数据的更改对当前事务是不可见的,这样就实现了可重复读。但这样也并非完全将各个事务隔离开来,该级别会产生幻读 phantom read的现象。幻读指的是修改了一条数据库中已经存在的记录,但这条记录你却读取不到。这条记录就像幽灵(phantom)一样,你看不到却又实际存在。
如下表所示,事务1在t3和t6时刻都没有读取到id为10086的记录,但在t7时刻插入id为10086的记录时,却报主键重复了。
时刻 | 事务1 | 事务2 |
---|---|---|
t1 | set session transaction isolation level repeatable read; | set session transaction isolation level repeatable read; |
t2 | begin; | begin; |
t3 | select * from account where id=10086; | |
t4 | insert into account(id,username) values(10086,‘bbb’); | |
t5 | commit; | |
t6 | select * from account where id=10086; | |
t7 | insert into account(id,username) values(10086,‘aaa’); | |
t8 | commit; |
下面继续用mybatis进行演示:
private static void testPhantomRead(){
try (SqlSession session1 = sqlSessionFactory.openSession(TransactionIsolationLevel.REPEATABLE_READ);
SqlSession session2 = sqlSessionFactory.openSession(TransactionIsolationLevel.REPEATABLE_READ)) {
AccountDao accountDao1 = session1.getMapper(AccountDao.class);
AccountDao accountDao2 = session2.getMapper(AccountDao.class);
System.out.println("first read:\n" + accountDao1.getAccountById(10086L));
accountDao2.insertAccount(Account.builder().id(10086L).username("bbb").build());
session2.commit();
// 清除一级缓存,使session1重新去查一次数据库
session1.clearCache();
// 第二次读到还是null,即使事务2已经插入数据并提交了
System.out.println("second read:\n" + accountDao1.getAccountById(10086L));
// 抛出主键重复异常异常
accountDao1.insertAccount(Account.builder().id(10086L).username("aaa").build());
session1.commit();
}
}
4. Serializable(串行化)
串行化是数据库事务最高隔离级别,当产生冲突时(例如同时修改同一条数据),它要求每个事务按顺序执行。若其中一个事务执行了很长时间则会阻塞后续的其他事务。所以该级别下数据库的吞吐量会大大下降,应用程序的性能会急剧降低。
如下表所示,事务一和事务二想要修改同一条数据(id为10086),若事务2先行修改了数据,但是却迟迟没有提交,则事务1的update语句会一直阻塞,直到事务2在t7时刻提交;若事务1先行修改,则事务2阻塞。
时刻 | 事务1 | 事务2 |
---|---|---|
t1 | set session transaction isolation level serializable; | set session transaction isolation level repeatable read; |
t2 | begin; | begin; |
t3 | update account set username=‘bbb’ where id=10086; | |
t4 | update account set username=‘aaa’ where id=10086; | |
t5 | [blocked] | |
t6 | [blocked] | |
t7 | commit; | |
t8 | commit; |
private static void testSerializable() {
// 两个session必有一个会比另一个多执行1秒
new Thread(() -> {
long begin = System.currentTimeMillis();
try (SqlSession session1 = sqlSessionFactory.openSession(TransactionIsolationLevel.SERIALIZABLE)) {
AccountDao accountDao1 = session1.getMapper(AccountDao.class);
accountDao1.updateAccount(Account.builder().id(10086L).username("aaa").build());
TimeUnit.SECONDS.sleep(1);
session1.commit();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("session1 total time: " + (System.currentTimeMillis() - begin));
}
}).start();
new Thread(() -> {
long begin = System.currentTimeMillis();
try (SqlSession session2 = sqlSessionFactory.openSession(TransactionIsolationLevel.REPEATABLE_READ)) {
AccountDao accountDao2 = session2.getMapper(AccountDao.class);
accountDao2.updateAccount(Account.builder().id(10086L).username("bbb").build());
TimeUnit.SECONDS.sleep(1);
session2.commit();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("session2 total time: " + (System.currentTimeMillis() - begin));
}
}).start();
}
下面做一个小结:
隔离级别 | 脏读(Dirty Read) | 不可重复读(Non Repeatable Read) | 幻读(Phantom Read) |
---|---|---|---|
Read Uncommitted(读未提交) | √ | √ | √ |
Read Committed(读已提交) | × | √ | √ |
Repeatable Read(可重复读) | × | × | √ |
Serializable(串行化) | × | × | × |