事务
程序员文章站
2022-05-09 15:58:21
...
事务
事务就是一个事情。组成这个事情可能有多个单元,要求这些单元,要么全部成功,要么全都不成功。
- 汇款包括两个单元组成:从一个账户中取出来,向另外一个账户存进去。
- 在开发中有事物的存在,可以保证数据的完整性。
事务的操作
MySQL下事务的操作
方式1适合一个事务中有多条SQL语句
- start transaction 开启事务
- rollback 事务回滚
- commit 事务提交
开启事务
create database remit;
use remit;
create table account(
id int primary key auto_increment,
name varchar(20),
money double
);
insert into account values(null,'aaa',1000);
insert into account values(null,'bbb',1000);
insert into account values(null,'ccc',1000);
-- 开启事务,开启事务后可以在该过程中写任意SQL语句
start transaction;
--select语句并不能对表中的数据进行改变,此时开启事务没有意义
--
update account set money = money -500 where id = 1;
-- 此时重启数据库,会发现,表中的数据并没有改变
事务回滚
start transaction;
update account set monet =1;
--表中的money为1
select * from account;
rollback;
--表中的money又恢复
select * from account;
事务提交
start transaction;
update account set money = money -500 where id = 1;
commit;
--此时重启数据库,id=1的money已经变成500
方式2适合一个事务中有单条SQL语句
- show variables like ‘%commit%’;可以查看当前autocommit 值
- 在MySQL数据库中它的默认值是“on”代表自动事务
- 自动事务的意义就是:执行任意一条SQL语句都会自动提交事务
- 关闭自动提交 set autocommit = off 或者 set autocommit=0
- 如果设置autocommit为off,意味着以后每条SQL语句都会处于一个事务中,相当于每条SQL执行前都执行start transaction
- 测试:将autocommit的值设置成off
- set autocommit = off 关闭自动事务
- 必须手动commit才可以将事务提交
- 注意:
- MySQL默认autocommit=on
- Oracle默认的autocommit=off
jdbc下事务的操作
java.sql.connection中有以下方法
- setAutocommit(boolean flag);
- 如果flag=false,它就相当于 start transaction;
- rollBack()
- 事务回滚
- commit()
- 事务提交
效果测试
public class TransactionTest1{
public static void main(String args[]){
//修改id=2的人的money=500
String sql = "update account set money = 500 where id = 2";
Connection con = JdbcUtils.getConnection();
//开启事务,相当于 start transaction
con.setAutocommit(false);
Statement st= con.cresteStatement();
st.executeUpdate(sql);
//事务回滚
con.rollback();
//事务提交
con.commit();
st.close();
con.close();
}
}
--查询数据库中表的数据
select * from account ;
-- 表的数据没有变化
开发实例事务操作,异常不能抛
在实际的开发中,出现异常后,在catch中进行回滚,在finally中进行事务的提交,以及释放资源操作
public class TransactionTest2{
public static void main(String args[]){
//修改id=2的人的money=500
String sql = "update account set money = 500 where id = 2";
Connection con = null;
Statement st = null;
try{
Connection con = JdbcUtils.getConnection();
//开启事务,相当于 start transaction
con.setAutocommit(false);
Statement st= con.cresteStatement();
st.executeUpdate(sql);
}catch(SQLException e){
e.printStackTrace();
//事务回滚
con.rollback();
}finally{
try{
//事务提交
con.commit();
st.close();
con.close();
}catch(SQLException e){
e.printStackTrace();
}
}
}
}
事务特性ACID(重点)
- 原子性(Atomicity)
- 指事务是一个不可分割的工作单位,事务中的操作都发生,要么都不发生
- 一致性(consistency)
- 事务前后数据的完整性必须保持一致
- 隔离性(Isolation)
- 是指多个用户并发访问数据库时,一个用户的事务不能被其他用户的事务所干扰,多个并发事务之间数据要相互隔离
- 持久性(Durability)
- 一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响
如果不考虑事务的隔离性,会出现的问题
- 脏读:一个事务读取到另一个事务的未提交数据
- 不可重复读:在一个事务里,两次读取的数据不一致(读提交数据)(update)
- 例如:第一次读 账户金额是200,第二次读账户金额成100了
- 虚读或者幻读:两次读取的数据不一致(读提交数据)(insert)
- 例如:第一次读有两条记录,第二次读成三条或四条记录
- 丢失更新:两个事务对同一条记录操作,后提交的事务将先提交的刘改记录覆盖了
- 对于以上问题,可以通过设置隔离级别来解决
事务的隔离级别
分类
- Serializable:可避免脏读、不可重复读、虚读情况的发生。(串行化)(会锁表)
- Repeatable read :可避免脏读和不可重复读情况的发生。(可重复读)不可以避免虚读
- Read committed :可避免脏读情况发生。(读已提交)
- Read uncommitted:最低级别,以上情况均无法保证。(读未提交)
- 开发中用第二个和第三个比较多
设置隔离级别
- MySQL中设置
- 查看当前事务的隔离级别
- select @@tx_isolation
- MySQL中默认的事务隔离级别是 :Repeatable read
- Oracle中默认的事务隔离级别是:Read committed
- 设置事务隔离级别
- set session transaction isolation level
- 例如:set session transaction isolation level Read uncommitted
- jdbc中设置
- 使用java.sql.connection接口中提供的方法
- setTransactionIsolation(int level) throw SQLException
- 参数level可以取下面值
- Connection.TRANSACTION_READ_UNCOMMITTED
- Connection.TRANSACTION_READ_COMMITTED
- Connection.TRANSACTION_REPEATABLE_READ
- Connection.TRANSACTION_SERIALIZABLE
- (注意:不能使用Connection.TRANSACTION_NONE,因为它指定了不受支持的事务)
脏读演示
一个事务读取另一个事务的未提交数据
- 设置A,B事务(开启两个MySQL窗口)隔离级别分别为 Read uncommitted
- set session transaction isolation level read uncommitted;
- 在A事务中
- start transaction ;
- update account set money = money -500 where name = ‘aaa’;
- update account set money = money + 500 where name = ‘bbb’;
mysql> select * from account;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | aaa | 500 |
| 2 | bbb | 1500 |
| 3 | ccc | 1000 |
+----+------+-------+
3 rows in set (0.00 sec)- 在B事务中
- start transaction;
- select * from account;
mysql> select * from account;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | aaa | 500 |
| 2 | bbb | 1500 |
| 3 | ccc | 1000 |
+----+------+-------+
3 rows in set (0.00 sec)- 这时,B事务读取到了A事务未提交的数据,发现表中的数据已经修改,就出现的脏读
- 此时,在A事务中
- rollback;
- commit;
- 在B事务中
- select * from account;
mysql> select * from account;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | aaa | 1000 |
| 2 | bbb | 1000 |
| 3 | ccc | 1000 |
+----+------+-------+
3 rows in set (0.00 sec)- 此时,在B事务中查询表的数据,发现数据已经恢复原样。也出现了两次查询结果不一致的问题,即不可重复读。
解决脏读问题
将事务的隔离级别设置为 read committed 来解决脏读
- 设置A,B事务隔离级别为 Read committed
- set session transaction isolation level read committed;
- 在A事务中
- start transaction ;
- update account set money = money -500 where name = ‘aaa’;
- update account set money = money + 500 where name = ‘bbb’;
- 在B事务中
- start transaction;
- select * from account;
mysql> select * from account;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | aaa | 1000 |
| 2 | bbb | 1000 |
| 3 | ccc | 1000 |
+----+------+-------+
3 rows in set (0.00 sec)- 此时在B事务中,读取信息时,是不能读取到A 事务未提交的数据的,这也就解决了脏读。
- 在A事务中,提交数据
- commit;
- 在B事务中,读取数据
- select * from account;
mysql> select * from account;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | aaa | 500 |
| 2 | bbb | 1500 |
| 3 | ccc | 1000 |
+----+------+-------+
3 rows in set (0.00 sec)- 此时,在B事务中,两次读取信息的结果不同,表明还存在不可重复读的问题。
解决不可重复读
将事务的隔离级别设置成Repeatable read 来解决不可重复读
- 设置A、B事务隔离级别为 Repeatable read ;
- set session transaction isolation level Repeatable read;
- 在A事务中
- start transaction ;
- update account set money = money -500 where name = ‘aaa’;
- update account set money = money + 500 where name = ‘bbb’;
- 在B事务中
- start transaction;
- select * from account;
mysql> select * from account;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | aaa | 1000 |
| 2 | bbb | 1000 |
| 3 | ccc | 1000 |
+----+------+-------+
3 rows in set (0.00 sec)- 在A事务中,提交数据
- commit;
- 在B事务中,读取数据
- select * from account;
mysql> select * from account;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | aaa | 1000 |
| 2 | bbb | 1000 |
| 3 | ccc | 1000 |
+----+------+-------+
3 rows in set (0.00 sec)- A 事务提交后,B事务载查询,两次的查询结果一致,解决了重复读的问题
- 在B事务中
- commit;
- select * from account;
mysql> select * from account;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | aaa | 500 |
| 2 | bbb | 1500 |
| 3 | ccc | 1000 |
+----+------+-------+
3 rows in set (0.00 sec)
设置事务隔离级别 Serializable,可以解决所有的问题
- set session transactio isolation level Serializable;
- 如果设置成这种隔离级别,那么会出现锁表,也就是说,一个事务载对表进行操作时,其他的事务操作不了。
案例:转账汇款
- 问题1:service调用了dao中两个方法完成了一个业务操作,如果其中一个方法执行失败怎么办?
- 需要业务控制
- 问题2:怎样进行事务控制
- 我们在service层进行事务的开启,回滚以及提交操作
- 问题3:进行事务操作需要使用Connection对象,那么,怎样保证,在service中的与dao中所使用的是同一个Connection
- 在service层创建出Connection对象,将这个对象传递给dao层
- 注意:Connection 对象使用完成后,在service层的finally中关闭
- 而每一个PreparedStatement他们在dao层的方法中用完就关闭
在设置dao 层时
public interface AccountDao{
public void accountOut(String accountOut,double money)throws Exception;
public void accountIn(String accountIn,double money)throws Exception;
}
那么,我们自己去实现这个接口时,怎样去吃力,同一个Connection对象问题?
使用ThreadLocal
- ThreadLocal 可以理解为一个Map集合
Map<Thread,Object>
- set 方法是向ThreadLocal中存储数据,那么当前的key值就是当前线程对象
- get方法是从ThreadLocal中获取数据,它是根据当前线程对象来获取值。
- 如果我们在同一个线程中,只要在任意的一个位置存储了数据,在其他的位置,就可以获取到这个数据。
丢失更新
多个事务对同一条记录进行操作,但这些事务彼此之间不知道其他的事务进行的修改,后提交的事务将先提交的事务操作覆盖了
丢失更新问题的解决
- 悲观锁(Pessimistic Locking)
- (假设丢失更新一定会发生的)–利用数据库内部锁机制,管理事务
- 提供的锁机制有两种:
- 共享锁
- select * from table lock in share mode (读锁、共享锁)
- 排它锁
- select * from table for update(写锁、排他锁)
- update语句默认添加排它锁
- 允许一张数据表中数据记录,添加多个共享锁,添加共享锁记录,对于其他事务可读不可写的
- 一张数据表中数据记录,只能添加一个排他锁,在添加排它锁的数据,不能再添加其他共享锁和排它锁。对于其他事务是可读不可写的
- 乐观锁(Optimistic Locking)
- (假设丢失更新不会发生)—采用程序中添加版本字段解决丢失更新问题
乐观锁演示
采用记录的版本字段,来判断记录是否修改过—timestamp(timestamp可以自动更新)
create table product(
id int,
name varchar(20),
updatetime timestamp
);
insert into product values(1,'冰箱',null);
update product set name ='洗衣机' where id = 1;
连接池
推荐阅读
-
重新学习MySQL数据库6:浅谈MySQL的中事务与锁
-
解决Mysql收缩事务日志和日志文件过大无法收缩问题
-
mysql的存储过程、游标 、事务实例详解
-
数据库事务的四种隔离模式
-
Android SQLite事务处理结合Listview列表显示功能示例
-
阿里分布式事务seata入门(采坑)
-
Android开发教程之Fragment定义、创建与使用方法详解【包含Activity通讯,事务执行等】
-
pymssql默认关闭自动模式开启事务行为浅析
-
html5 Web SQL Database 之事务处理函数transaction与executeSQL解析
-
html5 Web SQL Database 之事务处理函数transaction与executeSQL解析