一、数据库视角
SQL Server 2008 R2 事务与隔离级别实例讲解 笔记
1、事务是数据库的工作单元,可视为一个原子操作,要么成功,要么什么也不曾发生
事务操作的三种命令:
a、BEGIN TRANSACTION
b、COMMIT TRANSACTION
c、ROLLBACK TRANSACTION
2、隔离级别是针对事务,事务隔离级别规定了读操作(从而间接影响写操作),具体通过行级锁来实现(共享锁、排他锁)
四种基本隔离级别:
a、READ UNCOMMITTED NOLOCK,脏读
b、READ COMMITTED (Default) HOLDLOCK,不可重复读 共享锁存在于语句级别,查询语句执行完会立即释放
c、REPEATABLE READ HOLDLOCK,幻读 共享锁存在于事务级别,一旦获取,只有事务结束后才会释放
d、SERIALIZABLE HOLDLOCK,不存在上述问题 共享锁存在于事务级别,针对查询范围内的记录,查询范围内insert会阻塞
另外两种隔离级别
e、SNAPSHOT 在整个事务中,读取快照,不会阻塞,不存在上述三种问题,但写操作会报错
f、READ COMMITTED SNAPSHOT 在整个事务中,读取快照或者已提交的数据,不会阻塞,允许写操作,存在不可重复读、幻读问题
3、锁的粒度默认为行,无论何种隔离级别,写操作获取的排他锁,在事务结束时才释放
注:该笔记也参考:a、 SQLserver锁和事务隔离级别 b、SQL Server 事务隔离级别详解
注:mysql默认的隔离级别为REPEATABLE READ,可在 /etc/my.cnf 下添加配置 transaction_isolation=read-committed 更改默认配置
二、Java视角
java事务学习笔记总结 笔记
1、Java对事物的支持体现在三方面
a、自动提交模式(Auto-commit mode)
1)当auto-commit为true时,当每个独立SQL操作的执行完毕,事务立即自动提交,也就是说每个SQL操作都是一个事务。
2)当auto-commit为false时,每个事务都必须显示调用commit方法进行提交,或者显示调用rollback方法进行回滚。auto-commit默认为true。
b、事务隔离级别(Transaction Isolation Levels)
JDBC定义了五种事务隔离级别:
1)TRANSACTION_NONE JDBC驱动不支持事务
2)TRANSACTION_READ_UNCOMMITTED 允许脏读、不可重复读和幻读。
3)TRANSACTION_READ_COMMITTED 禁止脏读,但允许不可重复读和幻读。
4)TRANSACTION_REPEATABLE_READ 禁止脏读和不可重复读,单运行幻读。
5)TRANSACTION_SERIALIZABLE 禁止脏读、不可重复读和幻读。
经验证,sql server及mysql均不支持隔离级别(1)
c、保存点(SavePoint)
JDBC定义了SavePoint接口,提供在一个更细粒度的事务控制机制。当设置了一个保存点后,可以rollback到该保存点处的状态,而不是rollback整个事务。Connection接口的setSavepoint和releaseSavepoint方法可以设置和释放保存点。
2、事务的分类
a、显示事务:以begin transaction显示开始,以commit或rollback结束。
b、隐式事务:数据库连接首次执行任一sql语句会自动启动一个事物,当提交或回滚时结束事物,隐式事务须显式以commit或rollback结束
auto-commit为false时, Java事务采用隐式事务,每个事务须显式以commit或rollback结束,若省略,则自动添加rollback结束
3、JDBC规范定义了事务的各种行为,但是每个数据库厂商对事务的支持程度可能各不相同,为此,JDBC提供了DatabaseMetaData接口,提供了一系列JDBC特性支持情况的获取方法,如:
a、DatabaseMetaData.supportsTransactionIsolationLevel 判断对事务隔离级别的支持情况
b、DatabaseMetaData.supportsSavepoints 判断对保存点的支持情况
4、mysql在TRANSACTION_READ_COMMITTED 隔离级别上的行为与SQL Server存在差异
当事务1对一个记录更新而尚未提交,之后,事务2读取同一条记录,此时,
1)SQL Server会阻塞,等待事务1提交
2)mysql正常运行,读取事务1更新之前的记录快照,但如果事务2继续更新同一条记录,则会阻断, 与SQL Server处在隔离级别READ COMMITTED SNAPSHOT 相同
测试代码
Task1
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package cn.matt.jdbc; import java.sql.*; public class Task1 implements Runnable { @Override public void run() { // String url = // "jdbc:sqlserver://10.102.16.63:1433;databaseName=Wind_test;user=sa;password=ABcd1234"; String url = "jdbc:mysql://10.102.16.64:3306/test?useUnicode=true&characterEncoding=utf-8"; Connection conn = null; PreparedStatement pstmt = null; try { // conn = DriverManager.getConnection(url); conn = DriverManager.getConnection(url, "sa", "abcd1234"); conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); conn.setAutoCommit(false); String sql = "update UserInfo set username = ? where id = ?"; pstmt = conn.prepareStatement(sql); pstmt.setInt(2, 5); pstmt.setString(1, "Hellol13"); pstmt.execute(); conn.commit(); } catch (SQLException e) { e.printStackTrace(); try { conn.rollback(); } catch (SQLException e1) { e1.printStackTrace(); } } finally { if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } } }
Task2
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package cn.matt.jdbc; import java.sql.*; public class Task2 implements Runnable { @Override public void run() { // String url = // "jdbc:sqlserver://10.102.16.63:1433;databaseName=Wind_test;user=sa;password=ABcd1234"; String url = "jdbc:mysql://10.102.16.64:3306/test?useUnicode=true&characterEncoding=utf-8"; Connection conn = null; Statement stmt = null; PreparedStatement pstmt = null; ResultSet rs = null; try { // conn = DriverManager.getConnection(url); conn = DriverManager.getConnection(url, "sa", "abcd1234"); conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); conn.setAutoCommit(false); stmt = conn.createStatement(); rs = stmt.executeQuery("select * from UserInfo"); String sql = "update UserInfo set username = ? where id = ?"; pstmt = conn.prepareStatement(sql); pstmt.setInt(2, 5); pstmt.setString(1, "Hellol133"); pstmt.execute(); rs = stmt.executeQuery("select * from UserInfo"); conn.commit(); } catch (SQLException e) { e.printStackTrace(); try { conn.rollback(); } catch (SQLException e1) { e1.printStackTrace(); } } finally { if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } } }
TransTest
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package cn.matt.jdbc; public class TransTest { public static void main(String[] args) { new Thread(new Task1()).start(); new Thread(new Task2()).start(); } }
5、考虑一种场景:客户A的银行账户有1万元,某一天,A从ATM提取了6000元,与此同时,A的妻子在商场买了一款包,从同一个账户支付了6000元,此时账户余额为-2000;由于支付的操作是先查询账户余额,后插入交易流水,当两笔交易同时发生时,查到的余额均大于支付金额,校验成功,使得上述场景有可能发生。
隔离级别TRANSACTION_READ_COMMITTED 模式下,解决上述并发问题的方法(乐观锁,将查询和插入操作作为一个事物处理):
在查询账户余额时(第一次查询)保存交易流水的最新时间戳,在执行插入操作时,先查询流水的最新时间戳(第二次查询),并与第一次查询对比,若相等,则插入成功;对于更新操作,第二次查询为update中的where子句
注意:第二次查询与之后的插入须为原子操作,实现方式:
insert into A
select a, b, c from dual
where exists(select 1 from dual where '...' = (select max(...) from A))
注意:TRANSACTION_READ_COMMITTED及TRANSACTION_REPEATABLE_READ隔离级别下,乐观锁逻辑上依然存在并发可能性,在SERIALIZABLE隔离级别下,无并发可能
并发解决方案:
1、 a、前端不可重复提交操作,b、后端做一个队列,串行对数据库的更改操作
2、隔离级别设为SERIALIZABLE(下下策)