【DataBase】事务
一、事务概述
- 事务的概念:事务是指逻辑上的一组操作,这组操作要么同时完成要么同时不完成。
- 假如小明要给小红转账1000元,这个转账会涉及到两个关键操作就是:将小明的余额减少1000元,将小红的余额增加1000元。万一在这两个操作之间突然出现错误比如银行系统崩溃,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。
- 如果你自己不去控制事务,数据库默认一条sql语句就处在自己单独的事务当中。
也可以使用命令去开启一个事务:
start transaction
:开启事务,这条语句之后的sql语句将处在一个事务当中,这些sql语句并不会立即执行Commit
:提交事务,一旦提交事务,事务中的所有sql语句才会执行。Rollback
:回滚事务,将之前所有的SQL取消。
JDBC中管理事务:
conn.setAutoCommit(false); conn.commit();
conn.rollback();
conn.setSavePoint();
conn.rollback(sp);
二、事务的四大特性(ACID)
- 原子性:事务的一组操作是原子的不可再分割的,这组操作要么同时完成要么同时不完成。
- 一致性: 事务在执行前后数据的完整性保持不变。数据库在某个状态下符合所有的完整性约束的状态叫做数据库具有完整性。在解散一个部门时应该同时处理员工表中的员工保证这个事务结束后,仍然保证所有的员工能找到对应的部门,满足外键约束。
- 隔离性:当多个事务同时操作一个数据库时,可能存在并发问题,此时应保证各个事务要进行隔离,事务之间不能互相干扰。
- 持久性:持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,不能再回滚。
三、事务的隔离性导致的问题
(所有的问题都是在某些情况下才会导致问题)
- 将数据库设计成单线程的数据库,可以防止所有的线程安全问题,自然就保证了隔离性.但是如果数据库设计成这样,那么效率就会极其低下.
- 如果是两个线程并发修改,一定会互相捣乱,这时必须利用锁机制防止多个线程的并发修改
- 如果两个线程并发查询,没有线程安全问题
- 如果两个线程一个修改,一个查询......
(一)、脏读:一个事务读取到了另一个事务未提交的数据。
#初始账户数据
a 1000
b 1000
----------
#a: 开启事务并进行对账户数据进行修改,但并没有提交
start transaction;
update account set money=money-100 where name=a;
update account set money=money+100 where name=b;
----------
#b: 开启事务查询账户
start transaction;
select * from account;
#查询结果如下:
a : 900
b : 1100
----------
#a: 这是回滚数据
rollback;
----------
#b:再此查询
start transaction;
select* from account;
#查询结果如下
a: 1000
b: 1000
(二)、不可重复读:在一个事务内读取表中的某一行数据,多次读取结果不同. --- 行级别的问题
a: 1000 1000 1000
b: 银行职员
---------
b:start transaction;
select 活期存款 from account where name='a'; ---- 活期存款:1000
select 定期存款 from account where name='a'; ---- 定期存款:1000
select 固定资产 from account where name='a'; ---- 固定资产:1000
-------
a:
start transaction;
update accounset set 活期=活期-1000 where name='a';
commit;
-------
select 活期+定期+固定 from account where name='a'; --- 总资产:2000
commit;
----------
(三)、幻读(虚读):一个事务读取到了另一个事务插入的数据(已提交),导致前后读取不一致 --- 表级别的问题
a: 1000
b: 1000
d: 银行业务人员
-----------
d:
start transaction;
select sum(money) from account; --- 2000 元
select count(name) from account; --- 2 个
------
c:
start transaction;
insert into account values(c,4000);
commit;
------
select sum(money)/count(name) from account; --- 平均:2000元/个
commit;
------------
四、数据库的四个隔离级别
- Read uncommitted:如果将数据库设定为此隔离级别,数据库将会有脏读、不可重复度、幻读的问题。
- Read committed:如果将数据库设定为此隔离级别,数据库可以防止脏读,但有不可重复度、幻读的问题。
- Repeatable read:如果将数据库设定为此隔离级别,数据库可以防止脏读、不可重复度,但是不能防止幻读。
- Serializable:将数据库串行化,可以避免脏读、不可重复读、幻读。
比较:
- 安全性来说:Serializable>Repeatable read>Read committed>Read uncommitted
- 效率来说:Serializable<Repeatable read<Read committed<Read uncommitted
- 通常来说,一般的应用都会选择Repeatable read或Read committed作为数据库隔离级别来使用。
- 真正使用数据的时候,根据自己使用数据库的需求,综合分析对安全性和对效率的要求,选择一个隔离级别使数据库运行在这个隔离级别上.
在MySQL中设置事务的隔离级别
MySQL
默认的数据库隔离级别为:REPEATABLE-READ
Oracle
默认下就是read committed
个隔离级别如何查询当前数据库的隔离级别?
MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)。我们可以通过
SELECT @@tx_isolation
;命令来查看,MySQL 8.0 该命令改为SELECT @@transaction_isolation;
mysql> SELECT @@transaction_isolation; +-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ |
+-------------------------+
1 row in set (0.00 sec)
这里需要注意的是:与 SQL 标准不同的地方在于InnoDB 存储引擎在 REPEATABLE-READ(可重读)事务隔离级别下使用的是Next-Key Lock 锁算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server)是不同的。所以说InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读) 已经可以完全保证事务的隔离性要求,即达到了 SQL标准的SERIALIZABLE(可串行化)隔离级别。
如何设置当前数据库的隔离级别?
SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL [READ UNCOMMITTED|READ COMMITTED|REPEATABLE READ|SERIALIZABLE]
此种方式设置的隔离级别只对当前连接起作用。
set transaction isolation level read uncommitted; set session transaction isolation level read uncommitted;
此种方式设置的隔离级别是设置数据库默认的隔离级别
set global transaction isolation level read uncommitted;
五、数据库中的锁机制:
- 共享锁:在非Serializable隔离级别做查询不加任何锁,而在Serializable隔离级别下做的查询加共享锁,
- 共享锁的特点:共享锁和共享锁可以共存,但是共享锁和排他锁不能共存
排他锁:在所有隔离级别下进行增删改的操作都会加排他锁,
- 排他锁的特点:和任意其他锁都不能共存
- 在非串行化下,所有的查询都不加锁,所有的修改操作都会加排他锁。
- 在串行化下,所有的查询都加共享锁,所有的修改都加排他锁。
六、更新丢失
- 如果多个线程操作,基于同一个查询结构对表中的记录进行修改,那么后修改的记录将会覆盖前面修改的记录,前面的修改就丢失掉了,这就叫做更新丢失。
- Serializable可以防止更新丢失问题的发生。其他的三个隔离级别都有可能发生更新丢失问题。
Serializable虽然可以防止更新丢失,但是效率太低,通常数据库不会用这个隔离级别,所以我们需要其他的机制来防止更新丢失:
两个线程基于同一个查询结果进行修改,后修改的人会将先修改人的修改覆盖掉.
乐观锁和悲观锁不是数据库中真正存在的锁,只是人们在解决更新丢失时的不同的解决方案,体现的是人们看待事务的态度。
悲观锁:
- 悲观锁悲观的认为每一次操作都会造成更新丢失问题,在每次查询时就加上排他锁
select * from xxx for update
- 隔离级别不设置为Serializable,防止效率过低。
- 在查询时手动加上排他锁。
- 如果数据库中的数据查询比较多而更新比较少的话,悲观锁将会导致效率低下。
乐观锁:
- 乐观锁会乐观的认为每次查询都不会造成更新丢失.利用一个版本字段进行控制
- 在表中增加一个version字段,在更新数据库记录是将version加一,从而在修改数据时通过检查版本号是否改变判断出当前更新基于的查询是否已经是过时的版本。
- 如果数据库中数据的修改比较多,更新失败的次数会比较多,程序需要多次重复执行更新操作。
查询非常多,修改非常少,使用乐观锁
修改非常多,查询非常少,使用悲观锁
七、并发事务所带来的的问题
上面都讲过了,这里总结一下:
脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
不可重复读(Unrepeatableread): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
丢失修改(Lost to modify): 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。