title: 数据库事务
date: 2019-05-13 21:38:49
tags:
众所周知,SQL的终极操作归于增删查改,SQL执行的基本单元也就是事务。
事务的基本特性
如何理解事务呢?大家都知道世界是由物质组成的,物质可以分解为分子,分子在切割就是原子。原子是保留原性质最小单位。如果想在切割原子,性质就变了。事务也一样,所以它有了第一个性质,原子性。当原子开始变化,比方它原来的能量值是1KCAL,它正在朝2KCAL这个值演变,中途出现了故障,它只能退回到自己本身的能量值1KCAL来,不能是其他别的值,因为它没变身成功前它还该是它本身,这也就是事务回滚的概念。(此处不能走火入魔,我们这里假设是按正常流程走,若是真走火入魔,事务本身性质也就变了,就不能叫事务了),规定了能量值为1KCAL,变身成功为2KCAL,这个值是要确定的,如果是1.5KCAL就不能叫变身成功,这叫事务的一致性。每个原子变身时候不能被其他原子影响,这叫事务的隔离性,当变身成功,它的能量值是固定的2KCAL,给分子提供的能量就是2KCAL,如果没有下次变身,它将一直是2KCAL,这就是事务的持久性。
回顾下事务的特性:
- 原子性(Atomicity): 事务中的逻辑要全部执行,不可分割。(原子是物理中最小单位)
- 一致性(Consistency): 指事务执行前和执行后, 数据的完整性保持一致
- 隔离性(Isolation): 指一个事务在执行的过程中不应该受其他事务的影响
- 持久性(Durability): 事务执行结束(提交或回滚), 数据都应持久化到数据中
事务是如何来演示的呢?
如果把事务设定为不自动提交,可分为下面几步
- 开启事务:start transaction;
- 提交事务:commit; 数据将会写到磁盘上的数据库
- 回滚事务:rollback; 数据回滚,回到最初的状态。
我们可以写一个多线程程序来演示
public class TIsolation {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
Class.forName("org.postgresql.Driver");
// Connect to the 'bank' database.
Properties props = new Properties();
props.setProperty("user", "wangzn");
//非安全模式
props.setProperty("sslmode", "disable");
while(true) {
//事务1
new Thread() {
Connection db = DriverManager
.getConnection("jdbc:postgresql://xxx.xx.xx.xxx:26257/bank", props);
public void run() {
try {
db.setAutoCommit(false);
ResultSet res = db.createStatement()
.executeQuery("SELECT balance FROM accounts WHERE id = 1");
while (res.next()) {
System.out.println("事务1 \n 余额查询 "+res.getInt("balance"));
}
db.createStatement()
.executeUpdate("UPDATE accounts SET balance = balance - "
+ 100 + " where id = " + 1);
System.out.println("事务1余额减100!");
ResultSet res2 = db.createStatement()
.executeQuery("SELECT balance FROM accounts WHERE id = 1");
while (res2.next()) {
System.out.println(
"事务1 \n余额减少查询 "+res2.getInt("balance"));
}
db.commit();
sleep(1000);
ResultSet res3 = db.createStatement()
.executeQuery("SELECT balance FROM accounts WHERE id = 1");
while (res3.next()) {
System.out.println("事务1 \n 提交后查询 "+res3.getInt("balance"));
}
} catch (SQLException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
try {
db.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
};
}.start();
//事务2
new Thread() {
Connection db = DriverManager
.getConnection("jdbc:postgresql://xxx.xxx.xxx.xxx:26257/bank", props);
public void run() {
try {
db.setAutoCommit(false);
ResultSet res = db.createStatement()
.executeQuery("SELECT balance FROM accounts WHERE id = 1");
while (res.next()) {
System.out.println("事务2 \n 未作修改查询 "+res.getInt("balance"));
}
db.createStatement()
.executeUpdate("UPDATE accounts SET balance = balance + "
+ 200 + " where id = " + 1);
System.out.println("事务2余额加200!");
ResultSet res2 = db.createStatement()
.executeQuery("SELECT balance FROM accounts WHERE id = 1");
while (res2.next()) {
System.out.println("事务2 \n 余额加后查询 "+res2.getInt("balance"));
}
db.commit();
sleep(1000);
ResultSet res3 = db.createStatement()
.executeQuery("SELECT balance FROM accounts WHERE id = 1");
while (res3.next()) {
System.out.println("事务2 \n 提交后查询 "+res3.getInt("balance"));
}
} catch (SQLException | InterruptedException e) {
e.printStackTrace();
}finally {
try {
db.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
};
}.start();
}
}
}
我们很容易看出来,这样跑程序的话,在某个时间段,肯定会出现问题
事务一 | 事务一 |
---|---|
查询1000元 | |
扣款100 | 查询1000 |
增加200 | |
查询1 | |
提交事务1 | |
查询2 | 查询3 |
提交事务2 | |
查询4 |
显而易见,如果不采取一些措施的话,两个事务之间就会互相产生影响。事务管理总结出了如下几点
- 脏读:一个事务读取了另一个未提交的并行事务写的数据。
- 不可重复读:一个事务重新读取前面读取过的数据, 发现该数据已经被另一个已提交的事务修改过。
- 幻读:一个事务重新执行一个查询,返回一套符合查询条件的行,发现这些行因为其他最近提交的事务而发生了改变。
事务的隔离级别
处理这些方法需要为事务增加隔离级别
常见的ANSI隔离级别有这几种:
- Read Uncommitted:最低的隔离级别,什么都不需要做,一个事务可以读到另一个事务未提交的结果。所有的并发事务问题都会发生。读操作不会申请Shared Lock;
- Read Committed:只有在事务提交后,其更新结果才会被其他事务看见。可以解决脏读问题。申请和持有Share Lock;一旦读操作完成,释放Shared Lock;
- Repeated Read:在一个事务中,对于同一份数据的读取结果总是相同的,无论是否有其他事务对这份数据进行操作,以及这个事务是否提交。可以解决脏读、不可重复读。事务会持有Shared Lock,直到事务结束(提交或回滚)
- Serialization:事务串行化执行,隔离级别最高,牺牲了系统的并发性。可以解决并发事务的所有问题。事务会持有范围Shared Lock(Range Lock),锁定一个范围,在事务活跃期间,其他事务不允许在该范围中进行更新(Insert 或 delete)操作;
非ANSI隔离级别
- 快照隔离级别 snapshot isolation (SI)
- 串行化快照隔离级别 serializable snapshot isolation (SSI)
隔离级别控制读操作的行为:
- 在读数据时是否使用共享锁,申请何种类型的锁;
- 事务持有读锁的时间;
- 读操作引用被其他事务更新,但尚未提交的数据行时,控制读操作的行为;
- 被阻塞,等待其他事务释放互斥锁;
- 获取更新之前的数据值,从tempdb中读取行版本,该行版本在事务开始时已经提交;
- 读没有提交的数据,获取更新之后的数据值;
最近的研究表明,使用弱隔离级别会导致基于并发攻击的严重漏洞。
与SNAPSHOT不同,SERIALIZABLE隔离不允许任何异常。为了防止写入偏斜异常,SERIALIZABLE隔离可能需要重新启动事务。 使用SNAPSHOT隔离,事务就像在固定的时间点一致地读取数据库的状态一样。与SERIALIZABLE级别不同,SNAPSHOT隔离允许写入偏斜异常。仍然支持此隔离级别以实现向后兼容性,应该避免使用它。它在性能方面并没有优势,并且可能导致某些复杂工作负载下的状态不一致。基于并发的攻击可以将不一致性强制转化为对对系统状态造成负面影响。
SI隔离级别实现简单,性能较好,但是存在writeskew问题。相比而言,SSI实现上稍微复杂一些,但仍然能保证较高性能(读写冲突严重的情况下稍弱),但是不存在write skew问题。可参考文献Fast Distributed Transactions for Partitioned Database Systems.
网友评论