什么是事务?java
要么所有都要执行,要么就都不执行。mysql
事务所具备的四种特性git
原子性,一致性,隔离性,持久性sql
原子性 数据库
我的理解,就是事务执行不可分割,要么所有完成,要么所有拉倒不干。并发
一致性 异步
关于一致性这个概念咱们来举个例子说明吧,假设张三给李四转了100元,那么须要先从张三那边扣除100,而后李四那边增长100,这个转帐的过程对于其余事务而言是没法看到的,这种状态始终都在保持一致,这个过程咱们称之为一致性。ide
隔离性 函数
并发访问数据库时,一个用户的事务不被其余事务所干扰,各并发事务之间数据是独立的;工具
持久性
一个事务被提交以后。它对数据库中数据的改变是持久的,即便数据库发生故障也不该该对其有任何影响。
为何会出现事务的隔离级别?
咱们都知道,数据库都是有相应的事物隔离级别。之因此须要分红不一样级别的事务,这个是由于在并发的场景下,读取数据可能会有出现脏读,不可重复读以及幻读的状况,所以须要设置相应的事物隔离级别。
为了方便理解,咱们将使用java程序代码来演示并发读取数据时候会产生的相应场景:
环境准备:
jdk8
mysql数据
创建测试使用表:
CREATE TABLE `money` ( `id` int(11) NOT NULL AUTO_INCREMENT, `money` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
一个方便于操做mysql的简单JdbcUtil工具类:
import java.io.IOException; import java.sql.*; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Properties; /** * Jdbc操做数据库工具类 * * @author idea * @version 1.0 */ public class JdbcUtil { public static final String DRIVER; public static final String URL; public static final String USERNAME; public static final String PASSWORD; private static Properties prop = null; private static PreparedStatement ps = null; /** * 加载配置文件中的信息 */ static { prop = new Properties(); try { prop.load(JdbcUtil.class.getClassLoader().getResourceAsStream("db.properties")); } catch (IOException e) { e.printStackTrace(); } DRIVER = prop.getProperty("driver"); URL = prop.getProperty("url"); USERNAME = prop.getProperty("username"); PASSWORD = prop.getProperty("password"); } /** * 获取链接 * * @return void * @author blindeagle */ public static Connection getConnection() { try { Class.forName(DRIVER); Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD); return conn; } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SQLException e) { e.printStackTrace(); } return null; } /** * 数据转换为list类型 * * @param rs * @return * @throws SQLException */ public static List convertList(ResultSet rs) throws SQLException { List list = new ArrayList(); //获取键名 ResultSetMetaData md = rs.getMetaData(); //获取行的数量 int columnCount = md.getColumnCount(); while (rs.next()) { //声明Map HashMap<String,Object> rowData = new HashMap(); for (int i = 1; i <= columnCount; i++) { //获取键名及值 rowData.put(md.getColumnName(i), rs.getObject(i)); } list.add(rowData); } return list; } }
脏读
所谓的脏读是指读取到没有提交的数据信息。
模拟场景:两个线程a,b同时访问数据库进行操做,a线程须要插入数据到库里面,可是没有提交事务,这个时候b线程须要读取数据库的信息,将a里面所要插入的数据(可是没有提交)给读取了进来,形成了脏读现象。
代码以下所示:
import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; /** * @author idea * @date 2019/7/2 * @Version V1.0 */ public class DirtyReadDemo { public static final String READ_SQL = "SELECT * FROM money"; public static final String WRITE_SQL = "INSERT INTO `money` (`id`, `money`) VALUES ('3', '350')"; public Object lock = new Object(); /** * 脏读模拟(注意:须要设置表的存储引擎为innodb类型) */ public static void dirtyRead() { try { Connection conn = JdbcUtil.getConnection(); conn.setAutoCommit(false); PreparedStatement writePs = conn.prepareStatement(WRITE_SQL); writePs.executeUpdate(); System.out.println("执行写取数据操做----"); Thread.sleep(500); //须要保证链接不一样 Connection readConn = JdbcUtil.getConnection(); //注意这里面须要保证提交的事物等级为:未提交读 readConn.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED); PreparedStatement readPs = readConn.prepareStatement(READ_SQL); ResultSet rs = readPs.executeQuery(); System.out.println("执行读取数据操做----"); List list = JdbcUtil.convertList(rs); for (Object o : list) { System.out.println(o); } readConn.close(); } catch (SQLException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { dirtyRead(); } }
因为这个案例里面的事物隔离级别知识设置在了TRANSACTION_READ_UNCOMMITTED层级,所以对于没有提交事务的数据也会被读取进来。形成了脏数据读取的状况。
所以程序运行以后的结果以下:
为了预防脏读的状况发生,咱们一般须要提高事务的隔离级别,从原先的TRANSACTION_READ_UNCOMMITTED提高到TRANSACTION_READ_COMMITTED,这个时候咱们再来运行一下程序,会发现原先有的脏数据读取消失了:
不可重复读
所谓的不可重复读,个人理解是,多个线程a,b同时读取数据库里面的数据,a线程负责插入数据,b线程负责写入数据,b线程里面有两次读取数据库的操做,分别是select1和select2,因为事务的隔离级别设置在了TRANSACTION_READ_COMMITTED,因此当select1执行了以后,a线程插入了新的数据,再去执行select2操做的时候会读取出新的数据信息,致使出现了不可重复读问题。
演示代码:
import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; /** * 不可重复读案例 * @author idea * @date 2019/7/2 * @Version V1.0 */ public class NotRepeatReadDemo { public static final String READ_SQL = "SELECT * FROM money"; public static final String WRITE_SQL = "INSERT INTO `money` (`id`, `money`) VALUES ('3', '350')"; public Object lock = new Object(); /** * 不可重复读模拟 */ public void notRepeatRead() { Thread writeThread = new Thread(new Runnable() { @Override public void run() { try (Connection conn = JdbcUtil.getConnection();) { //堵塞等待唤醒 synchronized (lock) { lock.wait(); } conn.setAutoCommit(true); PreparedStatement ps = conn.prepareStatement(WRITE_SQL); ps.executeUpdate(); System.out.println("执行写取数据操做----"); ps.close(); } catch (SQLException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread readThread = new Thread(new Runnable() { @Override public void run() { try { Connection readConn = JdbcUtil.getConnection(); readConn.setAutoCommit(false); readConn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); PreparedStatement readPs = readConn.prepareStatement(READ_SQL); ResultSet rs = readPs.executeQuery(); System.out.println("执行读取数据操做1----"); List list = JdbcUtil.convertList(rs); for (Object obj : list) { System.out.println(obj); } synchronized (lock){ lock.notify(); } Thread.sleep(1000); ResultSet rs2 = readPs.executeQuery(); System.out.println("执行读取数据操做2----"); List list2 = JdbcUtil.convertList(rs2); for (Object obj : list2) { System.out.println(obj); } readConn.commit(); readConn.close(); } catch (SQLException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } }); writeThread.start(); readThread.start(); } public static void main(String[] args) { NotRepeatReadDemo notRepeatReadDemo=new NotRepeatReadDemo(); notRepeatReadDemo.notRepeatRead(); } }
在设置了TRANSACTION_READ_COMMITTED隔离级别的状况下,上述程序的运行结果为:
为了不这种状况的发生,须要保证在同一个事务里面,屡次重复读取的数据都是一致的,所以须要将事务的隔离级别从TRANSACTION_READ_COMMITTED提高到TRANSACTION_REPEATABLE_READ级别,这种状况下,上述程序的运行结果为:
幻读
官方文档对于幻读的定义以下:
The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if a SELECT is executed twice, but returns a row the second time that was not returned the first time, the row is a “phantom” row.
读到上一次没有返回的记录,看起来是幻影通常。
幻读与不可重复读相似。它发生在一个事务(T1)读取了几行数据,接着另外一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些本来不存在的记录,就好像发生了幻觉同样,因此称为幻读。为了解决这种状况,能够选择将事务的隔离级别提高到TRANSACTION_SERIALIZABLE。
什么是TRANSACTION_SERIALIZABLE?
TRANSACTION_SERIALIZABLE是当前事务隔离级别中最高等级的设置,能够彻底服从ACID的规则,经过加入行锁的方式(innodb存储引擎中)来防止出现数据并发致使的数据不一致性问题。为了方便理解,能够看看下方的程序:
import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.util.concurrent.CountDownLatch; /** * @author idea * @date 2019/7/2 * @Version V1.0 */ public class FantasyReadDemo { public static final String READ_SQL = "SELECT * FROM money"; public static final String UPDATE_SQL = "UPDATE `money` SET `money` = ? WHERE `id` = 3;n"; public CountDownLatch countDownLatch=new CountDownLatch(2); public void readAndUpdate1() { try (Connection conn = JdbcUtil.getConnection();) { conn.setAutoCommit(false); PreparedStatement ps = conn.prepareStatement(READ_SQL); conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); ResultSet rs = ps.executeQuery(); rs.next(); int currentMoney = (int) rs.getObject(2); System.out.println("执行写取数据操做----" + currentMoney); //堵塞等待唤醒 countDownLatch.countDown(); PreparedStatement writePs = conn.prepareStatement(UPDATE_SQL); writePs.setInt(1, currentMoney - 1); writePs.execute(); conn.commit(); writePs.close(); ps.close(); System.out.println("执行写操做结束---1"); } catch (Exception e) { e.printStackTrace(); readAndUpdate1(); } } public void readAndUpdate2() { try (Connection conn = JdbcUtil.getConnection();) { conn.setAutoCommit(false); PreparedStatement ps = conn.prepareStatement(READ_SQL); conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); ResultSet rs = ps.executeQuery(); rs.next(); int currentMoney = (int) rs.getObject(2); System.out.println("执行写取数据操做----" + currentMoney); //堵塞唤醒 countDownLatch.countDown(); PreparedStatement writePs = conn.prepareStatement(UPDATE_SQL); writePs.setInt(1, currentMoney - 1); writePs.execute(); conn.commit(); writePs.close(); ps.close(); System.out.println("执行写操做结束---2"); } catch (Exception e) { //使用串行化事务级别可以较好的保证数据的一致性,可串行化事务 serializable 是事务的最高级别,在每一个读数据上加上锁 //innodb里面是加入了行锁,所以出现了异常的时候,只须要从新执行一遍事务便可。 e.printStackTrace(); readAndUpdate2(); } } public void fantasyRead() { Thread thread1 = new Thread(new Runnable() { @Override public void run() { readAndUpdate1(); } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { readAndUpdate2(); } }); try { thread1.start(); // Thread.sleep(500); thread2.start(); } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { FantasyReadDemo fantasyReadDemo = new FantasyReadDemo(); fantasyReadDemo.fantasyRead(); } }
这里面将事务的隔离级别设置到了TRANSACTION_SERIALIZABLE,可是在运行过程当中为了保证数据的一致性,串行化级别的事物会给相应的行数据加入行锁,所以在执行的过程当中会抛出下面的相关异常:
com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.mysql.jdbc.Util.handleNewInstance(Util.java:377) .......
这里为了方便演示,在抛出异常的时候从新再次执行了一遍事务的方法,从而完成屡次事务并发执行。
可是实际应用场景中,咱们对于这种并发状态形成的问题都会交给业务层面加入锁来解决冲突,所以TRANSACTION_SERIALIZABLE隔离级别通常在应用场景中比较少见。
七种事务的传播机制
事务的七种传播机制分别为:
REQUIRED(默认) 默认的事务传播机制,若是当前不支持事务,那么就建立一个新的事务。
SUPPORTS 表示支持当前的事务,若是当前没有事务,则不会单首创建事务
以上的这两种事务传播机制比较好理解,接下来的几种事务传播机制就比上边的这几类稍微复杂一些了。
REQUIRES_NEW
定义: 建立一个新事务,若是当前事务已经存在,把当前事务挂起。
为了更好的理解REQUIRES_NEW的含义,咱们经过下边的这个实例来进一步理解:
有这么一个业务场景,须要往数据插入一个account帐户信息,而后同时再插入一条userAccount的流水信息。(只是模拟场景,因此对象的命名有点简陋)
直接来看代码实现,内容以下所示:
/** * @author idea * @data 2019/7/6 */ @Service public class AccountService { @Autowired private AccountDao accountDao; @Autowired private UserAccountService userAccountService; /** * 外层定义事务, userAccountService.saveOne单独定义事务 * * @param accountId * @param money */ @Transactional(propagation = Propagation.REQUIRED) public void saveOne(Integer accountId, Double money) { accountDao.insert(new Account(accountId, money)); userAccountService.saveOne("idea", 1001); //这里模拟抛出异常 int j=1/0; } }
再来看userAccountService.saveOne函数:
/** * @author idea * @data 2019/7/6 */ @Service public class UserAccountService { @Autowired private UserAccountDao userAccountDao; /** * @param username * @param accountId */ @Transactional(propagation = Propagation.REQUIRES_NEW) public void saveOne(String username,Integer accountId){ userAccountDao.insert(new UserAccount(username,accountId)); } }
执行程序的时候,AccountService.saveOne里面的 userAccountService.saveOne函数为单独定义的一个事务,并且传播属性为REQUIRES_NEW。所以在执行外层函数的时候,即便后边抛出了异常,也并不会影响到内部 userAccountService.saveOne的函数执行。
REQUIRES_NEW 老是新启一个事务,这个传播机制适用于不受父方法事物影响的操做,好比某些业务场景下须要记录业务日志,用于异步反查,那么无论主体业务逻辑是否完成,日志都须要记录下来,不能由于主体业务逻辑报错而丢失日志;可是自己是一个单独的事物,会受到回滚的影响,也就是说 userAccountService.saveOne里面要是抛了异常,子事务内容一块儿回滚。
NOT_SUPPORTED
定义:无事务执行,若是当前事务不存在,把已存在的当前事务挂起。
仍是接上边的代码来进行试验:
帐户的转帐操做:
userAccountService内部的saveOne操做:
在执行的过程当中,userAccountService.saveOne抛出了异常,可是因为该方法申明的事物传播属性为NOT_SUPPORTED级别,所以当子事务内部抛出异常的时候,子事务自己不会回滚,并且也不会影响父类事务的执行。
NOT_SUPPORTED能够用于发送提示消息,站内信、短信、邮件提示等。不属于而且不该当影响主体业务逻辑,即便发送失败也不该该对主体业务逻辑回滚,而且执行过程当中,若是父事务出现了异常,进行回滚,也不会影响子类的事务。
NESTED
定义:嵌套事务,若是当前事务存在,那么在嵌套的事务中执行。若是当前事务不存在,则表现跟REQUIRED同样。
关于Nested的定义,我我的感受网上写的比较含糊,因此本身经过搭建Demo来强化理解,仍是原来的例子,假设说父类事务执行的过程当中抛出了异常以下,那么子类也要跟着回滚:
当父事务出现了异常以后,进行回滚,子事务也会被牵扯进来一块儿回滚。
MANDATORY
定义:MANDATORY单词中文翻译为强制,支持使用当前事务,若是当前事务不存在,则抛出Exception。
这个比较好理解
当子方法定义了事务,且事务的传播属性为MANDATORY级别的时候,若是父方法没有定义事务操做的话,就会抛出异常。(此时的子方法会将数据记录到数据库里面)
NEVER
定义:当前若是存在事务则抛出异常
在执行userAccountService.saveOne函数的时候,发现父类的方法定义了事务,所以会抛出异常信息,而且userAccountService.saveOne会回滚。
传播属性小结:
PROPAGATION_NOT_SUPPORTED
不会受到父类事务影响而回滚,本身也不会影响父类函数,出现异常后会自动回滚。
PROPAGATION_REQUIRES_NEW
不会受到父类事务影响而回滚,本身也不会影响父类函数,出现异常后会自动回滚。
NESTED
会受到父类事务影响而回滚,出现异常后自身也回滚。若是不但愿影响父类函数,那么能够经过使用try catch来控制操做。
MANDATORY
强制使用当期的事物,若是当前的父类方法没有事务,那么在处理数据的时候就会抛出异常
NEVER
当前若是存在事务则抛出异常
REQUIRED(默认) 默认的事务传播机制,若是当前不支持事务,那么就建立一个新的事务。
SUPPORTS 表示支持当前的事务,若是当前没有事务,则不会单首创建事务
本文的所有相关代码都已经上传到gitee上边了,欢迎感兴趣的朋友前往进行代码下载:
https://gitee.com/IdeaHome_admin/wfw