事务是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。
组成一个事务的多个数据库操作是一个不可分割的原子单元,这些原子单元不可能在进行分割,事务中的所有操作执行成功整个事务才可以提交,如果某个操作失败,那么执行成功的操作也必须撤销,让数据库恢复到原始状态
源于数据的并发操作。两个事务拥有独立的数据空间,彼此之间互不干扰。一个事务的状态不会影响到另一个事务的执行。该特性需要事务隔离级别支持。也就是说我们可以通过设置不同的隔离级别来达到对应的隔离要求
是事务的根本,其他的3个特性都是为该特性服务的。只要事务提交成功,那么数据应该和对应的业务规则保持一致,数据不会被破坏
一旦提交事务提交成功后,事务中涉及的数据都会保存到数据库中
数据库采用重执行日志来保证原子性,一致性和持久性。重执行日志记录了数据库变化的每一个动作。数据库在一个事务中执行一部分操作后发生错误退出,数据库即可根据重执行日志撤销已经执行的操作。此外,当事务提交成功后,数据库出现故障宕机,导致事务涉及的数据没有及时保存到数据库中,当数据库再次重启时,数据库可以根据重执行日志,及时将提交成功但为更新到数据库中的数据更新到数据库中
一个事务读取到了另一个事务没有提交的数据,此时基于该数据的操作都是不被认可的。
时间 |
事务A |
事务B |
T1 |
开始事务 |
|
T2 |
开始事务 |
|
T3 |
查询余额1000 |
|
T4 |
取出500,余额为500 |
|
T5 |
查询余额1000(事务B未提交,事务A获取的数据为脏读) |
|
T6 |
提交事务 |
|
T7 |
存入200,余额为1200 |
|
T8 |
提交事务 |
简单的说就是同一个事务多次读取数据后发现每次读取的数据都不一样。读取过程中,其他的事务更新或者删除数据。也就是说在重新读取前,事务已经提交并将数据更新到数据库中。读取到了已经提交的更改数据。重点强调的是数据更新(删除/更新)
脏读和不可重复读的区别:
脏读:事务没有提交
不可重复读:分为如下两个阶段:1.第一次读时,事务未提交,此时的表现和脏读一致 2.第n次读时,事务完成并提交即重新读取前,事务结束并提交。
时间 |
事务A |
事务B |
T1 |
开始事务 |
|
T2 |
开始事务 |
|
T3 |
查询余额1000 |
|
T4 |
查询余额1000 |
|
T5 |
取出100,余额为900 |
|
T6 |
提交事务 |
|
T7 |
查询余额900 |
一个事务读取到了另一个事务插入的数据,读取到了其他已经提交事务的新增数据。重点强调的是数据的新增
时间 |
事务A |
事务B |
T1 |
开始事务 |
|
T2 |
开始事务 |
|
T3 |
查询总余额1000 |
|
T4 |
新开户并存入100 |
|
T5 |
提交事务 |
|
T6 |
再次统计总查询余额1100(幻想读) |
事务B进入了事务A的可视范围,导致两次统计的总余额不一致。在幻想读中存在的新数据的插入操作
事务撤销时,把已经提交的事务的更新数据覆盖了,该类丢失可能造成很严重的后果。
时间 |
事务A |
事务B |
T1 |
开始事务 |
|
T2 |
开始事务 |
|
T3 |
查询总余额1000 |
|
T4 |
查询总余额1000 |
|
T5 |
存入100,余额为1100 |
|
T6 |
提交事务 |
|
T7 |
取出100,余额为900 |
|
T8 |
撤销事务 |
|
T9 |
恢复余额1000,更新丢失 |
事务A覆盖事务B已经提交的数据,导致事务B所做的操作丢失
时间 |
事务A |
事务B |
T1 |
开始事务 |
|
T2 |
开始事务 |
|
T3 |
查询总余额1000 |
|
T4 |
查询总余额1000 |
|
T5 |
取出100,余额为900 |
|
T6 |
提交事务 |
|
T7 |
存入100 |
|
T8 |
提交事务 |
|
T9 |
余额1100,更新丢失 |
数据库通过锁机制来解决并发带来的问题。按照锁定对象的不同可以将锁分为行锁定和表锁定,行锁定就是锁定某一行记录。表锁定就是锁定整张表。页锁定就是锁定相邻的一组记录。
按照并发事务的关系可以分为共享锁定和独占锁定,共享锁定会防止其他的独占锁定但允许其他共享锁定。独占锁定即防止其他的独占锁定也防止其他的共享锁定。
更改数据时,数据库必须在进行更改的行上施加行独占锁定,[INSERT | UPDATE | DELETE | SELECT] FOR UPDATE语句都会隐式的采用必要的行锁定。如下是ORACLE中常见的锁定机制
属于共享锁定也属于行锁。该共享锁定下可以对数据进行更改操作。但是防止其他会话获取独占性数据表锁定。允许行共享和行独占锁定,还允许进行数据表的共享或者采用共享行独占锁定。在oracle中用户可以通过LOCK TABLE IN ROW SHARE MODE显式获得行独占锁定。
属于独占锁定也属于行锁。通过[INSERT | UPDATE | DELETE]语句隐式获得或者通过LOCK TABLE IN ROW EXCLUSIVE MODE语句显示获得。该中锁定不允许其他回话获取共享锁定,共享行独占锁定以及独占锁定
属于共享锁定也属于表锁。通过LOCK TABLE IN SHARE MODE显式获得锁定。防止其他会话获得行独占锁定(INSERT,UPDATE,DELETE),表共享行独占锁定或者表独占锁定,允许多个表共享锁定以及行共享锁定。该锁定可以让会话具有对表事务级别的特性。当前事务没有被提交或者回滚前,该锁定不允许其他事务更新表中的任何数据
通过LOCK TABLE IN SHARE ROW EXCLUSIVE MODE显式获得该锁定。防止其他会话获取一个表共享,行独占或者表独占锁定。允许其他行共享锁定。类似于表共享锁定,只是一次只能对一张表放置一个表共享行独占锁定。事务A拥有该锁定,事务B可以执行查询操作,如果事务B需要进行更新操作,需要等待获得该锁
通过LOCK TABLE IN EXCLUSIVE MODE显式获得,防止其他会话对该表的其他任何锁定
就是每次获取数据时都认为其他事务会修改数据,所以每次在获取数据时都会将数据锁定。锁定后,其他的事务想要获取数据时就会背阻塞。直到当前事务提交或者回滚。通俗的讲其实就是在操作前先上锁。行锁,表锁,共享锁,排他锁都属于该悲观锁
锁定行。eg:select * from my_table where id = 1 for update.显式锁定id=1的行,也就是说如果要操作id=1的行需要等待当前事务提交或者回滚。
锁定表。select * from my_table for update
行锁锁定的表格中的某一行,表锁锁定的整张表,页锁定锁定的是相邻的一组记录
又被称为读锁。一个事务给操作该数据时候,其他事务只能读取该数据,不能进行修改
又被称为写锁,一个事务一旦采用该种方式锁定数据,就不允许其他的事务读写该数据
默认事务不会修改数据,所以也就不用上锁。只有在更新前判断其他事务是否在此期间修改了数据,如果修改,交给业务层处理,常用的方式是使用版本戳。
事务A读取数据并操作该数据此时数据的version=1
事务B读取数据并操作该数据此时数据的version=1.
事务A操作完成提交事务发现version=1,提交成功并将version修改为2.
此时事务B操作完成提交事务发现version=2,于之前的version=1不一致,
说明有其他事务操作了数据,此时需要通知业务逻辑处理。
通常情况下,读操作较多时,使用乐观锁,写操作较多时,使用悲观锁。
隔离级别 |
脏读 |
不可重复读 |
幻想读 |
第一类丢失 |
第二类丢失 |
READ UNCOMMITED |
允许 |
允许 |
允许 |
不允许 |
允许 |
READ COMMITED |
不允许 |
允许 |
允许 |
不允许 |
允许 |
REPEATABLE READ |
不允许 |
不允许 |
允许 |
不允许 |
不允许 |
SERIALIZABLE |
不允许 |
不允许 |
不允许 |
不允许 |
不允许 |
事务的隔离级别主要分4类。隔离级别越高数据库处理并发的性能越低。一般READ COMMITED是数据库默认的隔离级别
String URL="jdbc:mysql://127.0.0.1:3306/imooc?useUnicode=true&characterEncoding=utf-8"; String USER="root"; String PASSWORD="tiger"; Connection conn = null; Statement statement = null; try { //加载驱动 Class.forName("com.mysql.jdbc.Driver"); //获得连接 conn = DriverManager.getConnection(URL, USER, PASSWORD); //设置事务是否自提交,fasle禁止自动提交 conn.setAutoCommit(false); //设置事务隔离级别 conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); //执行数据库操作 statement = conn.createStatement(); statement.execute("select * from table"); statement.executeUpdate("UPDATE SQL Statement"); //提交事务 conn.commit(); } catch (Exception e) { e.printStackTrace(); } finally { try { //回滚事务 conn.rollback(); //关闭资源 statement.close(); conn.close(); } catch (SQLException e1) { e1.printStackTrace(); } }
采用传统的方式需要经过如下几个步骤
1.定义数据库URL以及用户名密码
2.获取驱动
3.获取数据库连接
4.关闭事务自动提交
5.设置事务隔离级别
6.执行数据库操作
7.没有异常提交事务
8.发生异常回滚事务
9.不管是否发生异常,都需要关闭资源
如果我们进行多次数据库的操作,希望在发生异常时我们依然将某些操作提交的数据库中,此时我们可以使用保存点来实现该功能
String URL="jdbc:mysql://127.0.0.1:3306/imooc?useUnicode=true&characterEncoding=utf-8"; String USER="root"; String PASSWORD="tiger"; Connection conn = null; Statement statement = null; Savepoint savepoint1 = null; Savepoint savepoint2 = null; try { //加载驱动 Class.forName("com.mysql.jdbc.Driver"); //获得连接 conn = DriverManager.getConnection(URL, USER, PASSWORD); //设置事务是否自提交,fasle禁止自动提交 conn.setAutoCommit(false); //设置事务隔离级别 conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); //执行数据库操作 statement = conn.createStatement(); statement.execute("select * from table"); //新增保存点 savepoint1 = conn.setSavepoint("savepoint1"); statement.execute("select * from table2"); //新增保存点 savepoint2 = conn.setSavepoint("savepoint2"); statement.executeUpdate("UPDATE SQL Statement"); //提交事务 conn.commit(); } catch (Exception e) { e.printStackTrace(); } finally { try { //回滚事务 conn.rollback(savepoint2); //关闭资源 statement.close(); conn.close(); } catch (SQLException e1) { e1.printStackTrace(); } }发生异常时回滚到savepoint2,也就是说凡是在 savepoint2之前的操作(两次查询)会背提交的数据库而更新操作则被回滚