参考资料:MySQL事务的实现原理
事务的隔离级别
读未提交:一个事务可以看到其他事务未提交的修改。允许脏读
读已提交:一个事务能看到其他事务已经提交的修改。允许不可重复读和幻读
可重复读:保证同一个事务多次读取的数据是一致的。MySQL innoDB默认隔离级别,不会出现幻读
串行化:并发事务之间是串行化。读取需要获取共享锁,更新需要获取排它锁————最高隔离级别
- 脏读:一个事务读到了另一个事务未提交的数据
- 不可重复读:在事务A中先后两次读取同一个数据,两次读取的结果不一样
- 幻读:在事务A中按照某个条件先后两次查询数据库,两次查询结果的条数不同(InnoDB实现的RR通过
next-key lock
机制避免了幻读现象)
MySQL 默认隔离级别 RR(可重复读)解决脏读、不可重复读、幻读等问题,使用的是MVCC:
MVCC全称Multi-Version Concurrency Control,即多版本的并发控制协议,在同一时刻,不同的事务读取到的数据可能是不同的(即多版本)。
MVCC最大的优点是读不加锁,因此读写不冲突,并发性能好。
InnoDB实现的RR,通过锁机制、数据的隐藏列、undo log和类next-key lock,实现了一定程度的隔离性,可以满足大多数场景的需要。
脏读、不可重复读和幻读
首先来看并发情况下,读操作可能存在的三类问题:
脏读:当前事务(A)中可以读到其他事务(B)未提交
的数据(脏数据),这种现象是脏读。
举例如下(以账户余额表为例)
不可重复读:在事务A中先后两次读取同一个数据,两次读取的结果不一样,这种现象称为不可重复读。
脏读与不可重复读的区别在于:前者读到的是其他事务未提交的数据,后者读到的是其他事务已提交
的数据。
举例如下:
幻读:在事务A中按照某个条件先后两次查询数据库,两次查询结果的条数不同,这种现象称为幻读。
不可重复读与幻读的区别可以通俗的理解为:前者是数据变了,后者是数据的行数
变了。
举例如下:
1.1 模拟转账
生活当中转账是转账方账户扣钱,收账方账户加钱。我们用数据库操作来模拟现实转账。
1.1.1 数据库模拟转账
1 2 3 4 5 6 7 8 9 10 11 12
| # 模拟转账 CREATE TABLE account( id INT, money DOUBLE )CHARSET = utf8; SELECT * FROM account; INSERT INTO account(id, money) VALUES(1, 20000); INSERT INTO account(id, money) VALUES(2, 2000);
# 账户1 转给 账户2 金额 5000,账户1 -5000,账户2 +5000 UPDATE account SET money = money - 5000 WHERE id = 1; # 可能出现错误,需要回滚 UPDATE account SET money = money + 5000 WHERE id = 2;
|
1.1.2 模拟转账错误
1 2 3 4 5 6 7
| # A 账户转账给 B 账户 1000 元。 # A 账户减 1000 元 UPDATE account SET MONEY = MONEY-1000 WHERE id=1; # 断电、异常、出错...
# B 账户加 1000 元 UPDATE account SET MONEY = MONEY+1000 WHERE id=2;
|
- 上述代码在减操作后过程中出现了
异常或出错
,会发现,减钱仍旧是成功的,而加钱失败了!
- 注意:每条 SQL 语句都是一个独立的操作,
一个操作执行完对数据库是永久性的影响
。
1.2 事务的概念
事务,是一个原子操作,是一个最小执行单元,可以由一个或多个SQL语句组成
。在同一个事务当中,所有的SQL语句都成功执行时,整个事务成功,有一个SQL语句执行失败,整个事务都执行失败。
1.3 事务的起始
开始:上一个事务结束后的第一条增删改的语句,即事务的开始。
结束:
1). 提交:
a. 显示提交:COMMIT
;
b. 隐式提交:一条创建/删除的语句,正常退出(客户端退出连接);
2). 回滚:
a. 显示回滚:ROLLBACK
;
b. 隐式回滚:非正常退出(断电/宕机)执行了创建/删除语句,但是失败了,会为这个无效的语句执行回滚。
1.4 事务的原理
数据库会为每一个客户端都维护一个空间独立的缓存区
(回滚段),一个事务中所有的增删改语句的执行结果都会缓存在回滚段
中,只有当事务中所有 SQL 语句均正常结束
(commit),才会将回滚段中的数据同步到数据库。否则无论因为哪种原因失败,整个事务将回滚(rollback)。
1.5 事务的特性(ACID)
表示一个事务内的所有操作是一个整体,要么全部成功,要么全部失败。
表示一个事务内有一个操作失败时,所有的更改过的数据都必须回滚到修改前状态。
事务查看数据操作时数据所处的状态,要么是另一并发事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看中间状态的数据。
持久性事务完成之后,它对于系统的影响是永久性的。
1.6 事务应用
应用环境:基于增删改语句的操作结果(均返回操作后受影响的行数)可通过程序逻辑手动控制事务提交或回滚。
1.6.1 事务完成转账
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| # 账户1 给 账户2 转账(开启事务方式任选其一) # 1.开启事务 方式一: START TRANSACTION; # 1.开启事务 方式二: (autoCommit = 0 关闭自动提交,1 开启自动提交) SET autoCommit = 0;
UPDATE account SET money = money - 1000 WHERE id = 1; UPDATE account SET money = money + 1000 WHERE id = 2;
# 2.事务内数据操作语句 UPDATE ACCOUNT SET MONEY = MONEY - 1000 WHERE ID = 1; UPDATE ACCOUNT SET MONEY = MONEY + 1000 WHERE ID = 2; # 3.事务内语句都成功了,执行 COMMIT; COMMIT; # 4.事务内如果出现错误,执行 ROLLBACK; ROLLBACK;
|
- 注意:开启事务后,执行的语句均属于
当前事务,成功再执行 COMIIT,失败要进行 ROLLBACK
。
1.7 多线程事务回滚
使用 sqlSession 控制手动提交事务
:
1.7.1 事务回滚示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| @Resource SqlContext sqlContext;
@Override public void saveThread(List<EmployeeDO> employeeDOList) throws SQLException { SqlSession sqlSession = sqlContext.getSqlSession(); Connection connection = sqlSession.getConnection(); try { connection.setAutoCommit(false); EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class); employeeMapper.delete(null); ExecutorService service = ExecutorConfig.getThreadPool(); List<Callable<Integer>> callableList = new ArrayList<>(); List<List<EmployeeDO>> lists=averageAssign(employeeDOList, 5); AtomicBoolean atomicBoolean = new AtomicBoolean(true); for (int i =0;i<lists.size();i++){ if (i==lists.size()-1){ atomicBoolean.set(false); } List<EmployeeDO> list = lists.get(i); Callable<Integer> callable = () -> { if (!atomicBoolean.get()){ throw new ServiceException("001","出现异常"); } return employeeMapper.saveBatch(list); }; callableList.add(callable); } List<Future<Integer>> futures = service.invokeAll(callableList); for (Future<Integer> future:futures) { if (future.get()<=0){ connection.rollback(); return; } } connection.commit(); System.out.println("添加完毕"); }catch (Exception e){ connection.rollback(); log.info("error",e); throw new ServiceException("002","出现异常"); }finally { connection.close(); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <insert id="saveBatch" parameterType="List"> INSERT INTO employee (employee_id,age,employee_name,birth_date,gender,id_number,creat_time,update_time,status) values <foreach collection="list" item="item" index="index" separator=","> ( #{item.employeeId}, #{item.age}, #{item.employeeName}, #{item.birthDate}, #{item.gender}, #{item.idNumber}, #{item.creatTime}, #{item.updateTime}, #{item.status} ) </foreach> </insert>
|
测试结果:抛出异常
数据库中的数据:删除操作的数据回滚了,数据库中的数据依旧存在,说明事务成功了。
1.7.2 事务提交示例
成功执行操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| @Resource SqlContext sqlContext;
@Override public void saveThread(List<EmployeeDO> employeeDOList) throws SQLException { SqlSession sqlSession = sqlContext.getSqlSession(); Connection connection = sqlSession.getConnection(); try { connection.setAutoCommit(false); EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class); employeeMapper.delete(null); ExecutorService service = ExecutorConfig.getThreadPool(); List<Callable<Integer>> callableList = new ArrayList<>(); List<List<EmployeeDO>> lists=averageAssign(employeeDOList, 5); for (int i =0;i<lists.size();i++){ List<EmployeeDO> list = lists.get(i); Callable<Integer> callable = () -> employeeMapper.saveBatch(list); callableList.add(callable); } List<Future<Integer>> futures = service.invokeAll(callableList); for (Future<Integer> future:futures) { if (future.get()<=0){ connection.rollback(); return; } } connection.commit(); System.out.println("添加完毕"); }catch (Exception e){ connection.rollback(); log.info("error",e); throw new ServiceException("002","出现异常"); } }
|
测试结果:
数据库中数据:删除的删除了,添加的添加成功了,测试成功。