1.mysql事务
事务(Transaction)是数据库系统中一系列操作的一个逻辑单元,所有操作要么全部成功要么全部失败;目的是为了保证在并发情况下能正确的执行crud操作;
1.1 mysql事务命令
-- 查询mysql事务隔离级别(mysql版本 8.0 以前)
SELECT @@tx_isolation;
-- 设置mysql事务隔离级别为读未提交(mysql版本 8.0 以前)
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- 设置mysql事务隔离级别为读已提交(mysql版本 8.0 以前)
set session transaction isolation level read committed;
-- 设置mysql事务隔离级别为可重复读(mysql版本 8.0 以前)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 设置mysql事务隔离级别为串行化(mysql版本 8.0 以前)
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- 开启事务
START TRANSACTION;
-- 提交
COMMIT;
-- 回滚
ROLLBACK;
1.2 事务的四个特性ACID
Atomicity(原子性):原子操作,对数据的修改,要么全部成功,要么全部失败。
Consistent(一致性):在事务开始和完成时,数据都必须保持一致状态。银行转账例子,转出100,另一方在事务完成后一定收到100。
Isolation(隔离性):数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的”独立“环境执行。
Durable(持久性):事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。
1.3 事务的隔离级别
read-uncommitted < read-committed < REPEATABLE-READ < SERIALIZABLE(后面比前面严格),不同隔离级别解决并发下的不同问题(脏写、脏读、不可重读、幻读),同时也会存在其它问题。
CREATE TABLE `user_info` (
`user_id` int(11) DEFAULT NULL,
`user_name` varchar(50) DEFAULT NULL,
`user_state` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO user_info(user_id,user_name,user_state)VALUE('1','user1',0);
脏写(更新丢失):查了很多资料都说是事务A和事务B同时更新一条记录,一个事务回滚导致另一个事务的改动丢失;不过试验了很多次,都没出现,InnoDB引擎下第一个事务已经把要操作的行锁住了,后面的事务无法获取锁,也不能对该行进行修改,自然不会出现脏写。
目前有两种猜测:1、高并发下,由于数据库的bug,导致出现了两个事务对同一行进行操作,因而出现脏写;2、会不会是两个线程对同一行数据进行操作,一个线程通过代码回滚了另一个线程的数据,导致出现脏写;后面继续研究。
脏读:一个事务读到了另一个事务未提交的修改数据。事务1
-- 设置mysql事务隔离级别为读未提交(mysql版本 8.0 以前)
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- 查询mysql事务隔离级别(mysql版本 8.0 以前)
SELECT @@tx_isolation;
-- 开启事务
START TRANSACTION;
UPDATE user_info SET user_name='tx1' WHERE user_id=1;
-- 在回滚前执行事务2
ROLLBACK;
事务2可以读到事务1回滚前的修改。
-- 设置mysql事务隔离级别为读未提交(mysql版本 8.0 以前)
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- 查询mysql事务隔离级别(mysql版本 8.0 以前)
SELECT @@tx_isolation;
-- 开启事务
START TRANSACTION;
SELECT * FROM user_info;
避免脏读的方法:设置事务隔离级别为read-committed以上。
不可重读:事务在读取数据后,再次读取以前读过的数据,发现第二次读出的数据已经发生了改变。
事务1
-- 设置mysql事务隔离级别为读未提交(mysql版本 8.0 以前)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 查询mysql事务隔离级别(mysql版本 8.0 以前)
SELECT @@tx_isolation;
-- 开启事务
START TRANSACTION;
-- 第一次读取user_name
SELECT user_name FROM user_info
-- 等另一个事务修改提交后执行,第二次读取到的user_name与第一次不一样
SELECT user_name FROM user_info
ROLLBACK;
事务2
-- 设置mysql事务隔离级别为读未提交(mysql版本 8.0 以前)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 查询mysql事务隔离级别(mysql版本 8.0 以前)
SELECT @@tx_isolation;
-- 开启事务
START TRANSACTION;
UPDATE user_info SET user_name='tx2' WHERE user_id=1;
COMMIT;
避免不可重复读的方法:设置隔离级别为可重复读以上。
幻读:事务在前后两次查询同一个范围数据时,后一次看到了前一次没看到的数据行。
事务1
-- 设置mysql事务隔离级别为可重复读(mysql版本 8.0 以前)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 查询mysql事务隔离级别(mysql版本 8.0 以前)
SELECT @@tx_isolation;
-- 开启事务
START TRANSACTION;
SELECT * FROM user_info WHERE user_id < 10;
-- 另一个事务先执行,再次查询,未查询到user_id为3的记录,接着插入id为3的记录,提交后会发现出现2条id为3的记录
SELECT * FROM user_info WHERE user_id < 10; -- 在该事务内对小于10的记录进行操作,未完全隔离,导致另一个事务可以在该范围内插入数据,造成前面未查询到的幻象
INSERT INTO user_info(user_id, user_name, user_state)VALUE(3, 'user3', 0);
COMMIT;
事务2
-- 设置mysql事务隔离级别为可重复读(mysql版本 8.0 以前)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 查询mysql事务隔离级别(mysql版本 8.0 以前)
SELECT @@tx_isolation;
-- 开启事务
START TRANSACTION;
INSERT INTO user_info(user_id, user_name, user_state)VALUE(3, 'user3', 0);
-- 在回滚前执行事务2
COMMIT;
造成幻读的原因:快照读(查询)和当前读(插入及更新)一起使用。快照读的是某个版本的数据,而插入和更新操作的是最新数据,从而导致快照读有时会漏读最新数据,导致没有看到某个数据的幻象,在这个基础上继续执行更新就有可能出错。
解决方法:提升事务级别为串行化;或使用select * for update。
2.java事务
2.1 jdbc事务
业务操作为同一个数据库连接对象,使用的是底层数据库事务。
-- 关闭自动提交
con.setAutoCommit(false);
-- 提交
con.commit();
-- 回滚
con.rollbac();
2.2 spring事务
2.2.1 spring声明式事务
注解(@Transaction)或xml指定,使用方便,缺点为粒度大(方法级别以上)。注意在声明式事务中也可以手动控制回滚的。
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
2.2.2 spring编程式事务
将事务管理代码嵌入嵌入到业务代码中,来控制事务的提交和回滚。
@Autowired
private PlatformTransactionManager transactionManager;
public void testTransaction() {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// .... 业务代码
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
}
}