Spring 事务介绍(一)之 数据库的事务的基本特性
Spring 事务介绍(一)之 数据库的事务的基本特性
数据库的事务的基本特性
事务是区分文件存储系统和Nosql数据库重要特性之一,其存在的意义是为了保证即时在并发的情况下,也能正确的执行crud操作,怎样才能算是正确的?这时提出了事务需要保证的四个特性ACID:
- A:原子性(atomicity)
事务中各项操作,要么全做要么不做,任何一项操作的失败都会导致整个事务的失败;
- C:一致性(consistency)
事务结束后系统状态是一致的;
- I:隔离性(isolation)
并发执行的事务彼此无法看到对方的中间状态;
- D:持久性(durability)
事务完成后所做的改动都会被持久化,即使发生灾难性的失败;
在高并发的情况下,要完全保证其ACID是非常困难的,除非把所有的事务串行化执行,但是后果就是性能大打折扣。很多时候我们有些业务对事务的要求是不一样的,所有数据库中设计了四种隔离级别,供用户基于业务进行选择。
隔离级别 |
脏读(Dirty Read) |
不可重复读(NonRepeatable Read) |
幻读(Phantom read) |
---|---|---|---|
读未提交(Read Uncommitted) |
可能 |
可能 |
可能 |
读已提交(Read Committed) |
不可能 |
可能 |
可能 |
可重复读(Repeatable Read) |
不可能 |
不可能 |
可能 |
可串行化(Serializable) |
不可能 |
不可能 |
不可能 |
- 脏读:
一个事务读取到另一个事务未提交的更新数据。
- 不可重复读:
在同一事务中,多次读取同一数据返回的结果有所不同,换句话说,后面读取可以读到另一个事务已提交的更新数据,相反,“可重复读”在同一事务中多次读取数据时,能够保证所读数据一样,也就是后续读取不能读取到另一事务所提交的更新数据。
- 幻读
查询表中一条数据如果不存在就插入一条,并发的时候却发现,里面居然有两条相同的数据,导致插入失败,这就是幻读的问题。
幻读在mysql中,在默认的可重复读的隔离级别下,由mvcc(多版本并发控制)引起的,其中间隙锁可以避免幻读的问题,但是间隙锁会引起锁等待问题。
MVCC:
MVCC是通过保存数据在某个时间点的快照来实现的. 不同存储引擎的MVCC. 不同存储引擎的MVCC实现是不同的,典型的有乐观并发控制和悲观并发控制.
间隙锁:
当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。
几种隔离级别的代码demo:
ReadUncommittedTest.java
package com.demo.spring;
import java.sql.*;
/**
* com.demo.spring
*
* @author Zyy
* @date 2019/2/13 22:55
*
* Connection.TRANSACTION_READ_UNCOMMITTED
* 允许读取未提交事务,会出现脏读,不可重复读,幻读的问题
*/
public class ReadUncommittedTest {
private static String jdbcUrl = "jdbc:mysql://192.168.5.104:3306/spring";
private static String userName = "root";
private static String password = "root";
private static Object lock = new Object();
public static void main(String[] args) throws InterruptedException, SQLException, ClassNotFoundException {
Thread t1 = run(new Runnable() {
public void run() {
insert("001", "test", 100);
}
});
Thread t2 = run(new Runnable() {
public void run() {
try {
Thread.sleep(500);
Connection conn = openConnection();
// 将参数升级成 Connection.TRANSACTION_READ_COMMITTED 即可解决脏读的问题
conn.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
select("test", conn);
} catch (Exception e) {
e.printStackTrace();
}
}
});
t1.join();
}
public static Thread run(Runnable runnable) {
Thread thread = new Thread(runnable);
thread.start();
return thread;
}
public static Connection openConnection() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(jdbcUrl, userName, password);
return conn;
}
static {
try {
Connection connection = openConnection();
deleteAccount(connection);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void insert(String accountName, String name, int money) {
try {
Connection conn = openConnection();
PreparedStatement prepare = conn.
prepareStatement("insert into account (accountname,user,money) values (?,?,?)");
prepare.setString(1, accountName);
prepare.setString(2, name);
prepare.setInt(3, money);
prepare.executeUpdate();
System.out.println("执行插入成功");
conn.close();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void select(String name, Connection conn) {
try {
PreparedStatement prepare = conn.
prepareStatement("select * from account where user = ?");
prepare.setString(1, name);
ResultSet resultSet = prepare.executeQuery();
System.out.println("执行查询");
while (resultSet.next()) {
for (int i = 1; i <= 4; i++) {
System.out.print(resultSet.getString(i) + " ");
}
System.out.println();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void deleteAccount(Connection conn) {
try {
PreparedStatement prepare = conn.prepareStatement("delete from account");
prepare.executeUpdate();
System.out.println("执行删除");
} catch (SQLException e) {
e.printStackTrace();
}
}
}
执行结果:
执行插入成功
执行查询
141 001 test 100
出现脏读问题,读取到未提交的插入数据。
ReadCommittedTest.java
package com.demo.spring;
import java.sql.*;
/**
* com.demo.spring
*
* @author Zyy
* @date 2019/2/13 22:32
*
* Connection.TRANSACTION_READ_COMMITTED
* 允许读取已提交事务,会出现不可重复读,幻读的问题
*/
public class ReadCommittedTest {
private static String jdbcUrl = "jdbc:mysql://192.168.5.104:3306/spring";
private static String userName = "root";
private static String password = "root";
private static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = run(new Runnable() {
public void run() {
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
insert("001", "test", 100);
}
}
});
Thread t2 = run(new Runnable() {
public void run() {
try {
Connection connection = openConnection();
connection.setAutoCommit(false);
// 将参数升级成 Connection.TRANSACTION_REPEATABLE_READ 即可解决不可重复读问题
connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
// 第一次读取不到
select("test", connection);
// 释放锁
synchronized (lock) {
lock.notify();
}
// 第二次读取到(数据不一至)
Thread.sleep(500);
select("test", connection);
connection.close();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.join();
t2.join();
}
public static Thread run(Runnable runnable) {
Thread thread = new Thread(runnable);
thread.start();
return thread;
}
public static Connection openConnection() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(jdbcUrl, userName, password);
return conn;
}
static {
try {
Connection connection = openConnection();
//deleteAccount(connection);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void insert(String accountName, String name, int money) {
try {
Connection conn = openConnection();
PreparedStatement prepare = conn.
prepareStatement("insert into account (accountname,user,money) values (?,?,?)");
prepare.setString(1, accountName);
prepare.setString(2, name);
prepare.setInt(3, money);
prepare.executeUpdate();
System.out.println("执行插入成功");
conn.close();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void select(String name, Connection conn) {
try {
PreparedStatement prepare = conn.
prepareStatement("select * from account where user = ?");
prepare.setString(1, name);
ResultSet resultSet = prepare.executeQuery();
System.out.println("执行查询");
while (resultSet.next()) {
for (int i = 1; i <= 4; i++) {
System.out.print(resultSet.getString(i) + " ");
}
System.out.println();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void deleteAccount(Connection conn) {
try {
PreparedStatement prepare = conn.prepareStatement("delete from account");
prepare.executeUpdate();
System.out.println("执行删除");
} catch (SQLException e) {
e.printStackTrace();
}
}
}
执行结果
执行查询
141 001 test 100
142 001 test 100
143 001 test 100
执行插入成功
执行查询
141 001 test 100
142 001 test 100
143 001 test 100
144 001 test 100
出现不可重复读的问题,两次读取结果不一致。
ReadRepeatableTest.java
package com.demo.spring;
import java.sql.*;
/**
* com.demo.spring
*
* @author Zyy
* @date 2019/2/13 23:15
*
* Connection.TRANSACTION_REPEATABLE_READ
* 可重复读 ,在一个事务中同一SQL语句 无论执行多少次都会得到相同的结果
* 会出现幻读的问题
*/
public class ReadRepeatableTest {
private static String jdbcUrl = "jdbc:mysql://192.168.5.104:3306/spring";
private static String userName = "root";
private static String password = "root";
private static Object lock = new Object();
public static void main(String[] args) throws InterruptedException, SQLException, ClassNotFoundException {
Thread t1 = run(new Runnable() {
public void run() {
try {
synchronized (lock) {
lock.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
update("test");
}
});
Thread t2 = run(new Runnable() {
public void run() {
try {
Connection conn = openConnection();
conn.setAutoCommit(false);
conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
// 第一次读取 读取到的数据为未修改前的数据
select("test", conn);
// 释放锁
synchronized (lock) {
lock.notify();
}
// 第二次读取 TRANSACTION_REPEATABLE_READ级别,读取到的数据也为未修改前的数据 两次读取数据一至
// 设置id为主键 如果此时t1做插入(id=1),t2按主键查询(id=1)
// 因为此时为TRANSACTION_REPEATABLE_READ级别 ,所以查询为空,然后进行插入(id=1)
// 此时会出现主键冲突的异常,这种情况为幻读,有兴趣的可以尝试一下
Thread.sleep(500);
select("test", conn);
conn.close();
} catch (Exception e) {
e.printStackTrace();
}
}
});
t1.join();
}
public static Thread run(Runnable runnable) {
Thread thread = new Thread(runnable);
thread.start();
return thread;
}
public static Connection openConnection() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(jdbcUrl, userName, password);
return conn;
}
static {
try {
Connection connection = openConnection();
//deleteAccount(connection);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void insert(String accountName, String name, int money) {
try {
Connection conn = openConnection();
PreparedStatement prepare = conn.
prepareStatement("insert into account (accountname,user,money) values (?,?,?)");
prepare.setString(1, accountName);
prepare.setString(2, name);
prepare.setInt(3, money);
prepare.executeUpdate();
System.out.println("执行插入成功");
conn.close();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void deleteAccount(Connection conn) {
try {
PreparedStatement prepare = conn.prepareStatement("delete from account");
prepare.executeUpdate();
System.out.println("执行删除成功");
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void update(String user) {
try {
Connection conn = openConnection();
PreparedStatement prepare = conn.
prepareStatement("update account set money = money + 1 where user = ?");
prepare.setString(1, user);
prepare.executeUpdate();
conn.close();
System.out.println("执行修改成功");
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void select(String name, Connection conn) {
try {
PreparedStatement prepare = conn.
prepareStatement("select * from account where user = ?");
prepare.setString(1, name);
ResultSet resultSet = prepare.executeQuery();
System.out.println("执行查询");
while (resultSet.next()) {
for (int i = 1; i <= 4; i++) {
System.out.print(resultSet.getString(i) + " ");
}
System.out.println();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
执行结果:
执行查询
141 001 test 100
142 001 test 100
143 001 test 100
144 001 test 100
执行修改成功
执行查询
141 001 test 100
142 001 test 100
143 001 test 100
144 001 test 100
两次查询结果一致,已解决了不可重复读的问题,可是会出现幻读的问题。
幻读场景描述:
设置id为主键,在两个同时进行的事务中,如果此时事务t1做插入(id=1),事务t2按主键查询(id=1)因为此时为TRANSACTION_REPEATABLE_READ级别 ,所以查询为空,然后进行插入(id=1) 此时会出现主键冲突的异常,这种情况主要是由MVCC导致的,t2查询的数据因为没有改动所以是之前保留的查询数据,为快照版本,但实际上数据库已经新增了一条,此时进行插入,就抛出主键冲突异常了,明明查询没有数据然后进行插入,可是会出现插入失败的情况,这种场景就是幻读。
数据库默认隔离级别:
Oracle:读已提交(Read Committed)
Mysql:可重复读(Repeatable Read)
另外,mysql执行一条查询语句默认是一个独立的事务,所以看上去效果与读已提交一样。
Mysql:
查看当前会话隔离级别
select @@tx_isolation;
查看系统当前隔离级别
select @@global.tx_isolation;
设置当前会话隔离级别
set session transaction isolatin level repeatable read;
设置系统当前隔离级别
set global transaction isolation level repeatable read;
Oracle
查看系统默认事务隔离级别,也是当前会话隔离级别
#首先创建一个事务
declare
trans_id Varchar2(100);
begin
trans_id := dbms_transaction.local_transaction_id( TRUE );
end;
#查看事务隔离级别
SELECT s.sid, s.serial#,
CASE BITAND(t.flag, POWER(2, 28))
WHEN 0 THEN 'READ COMMITTED'
ELSE 'SERIALIZABLE'
END AS isolation_level
FROM v$transaction t
JOIN v$session s ON t.addr = s.taddr AND s.sid = sys_context('USERENV', 'SID');
github : https://github.com/zhaoyybalabala/spring-test
欢迎留言交流:)
- C#执行XSL转换
- javascript:算法笔记
- spring boot 登录注册 demo (一)
- linux学习:CentOS、Mac上SSH的设置以及SceureCRT中的文件上传下载
- 中关村成为北京“高精尖”产业发展主阵地
- jquery-barcode:js实现的条码打印
- 页面json 格式化+颜色高亮
- Python 里 and、or 的计算规则
- Python中赋值、浅拷贝与深拷贝
- git 简易使用说明
- 开发篇-MySQL分区(一)
- Establishing SSL connection without server's identity verification is not recommended. According to
- Django-认证系统
- javascript:双链表-插入排序
- java教程
- Java快速入门
- Java 开发环境配置
- Java基本语法
- Java 对象和类
- Java 基本数据类型
- Java 变量类型
- Java 修饰符
- Java 运算符
- Java 循环结构
- Java 分支结构
- Java Number类
- Java Character类
- Java String类
- Java StringBuffer和StringBuilder类
- Java 数组
- Java 日期时间
- Java 正则表达式
- Java 方法
- Java 流(Stream)、文件(File)和IO
- Java 异常处理
- Java 继承
- Java 重写(Override)与重载(Overload)
- Java 多态
- Java 抽象类
- Java 封装
- Java 接口
- Java 包(package)
- Java 数据结构
- Java 集合框架
- Java 泛型
- Java 序列化
- Java 网络编程
- Java 发送邮件
- Java 多线程编程
- Java Applet基础
- Java 文档注释
- 进程监控工具 Procmon有Linux版本了
- 你凭什么说Spring会导致MyBatis的一级缓存失效!
- 团体程序设计天梯赛-练习集 L1-042 日期格式化
- 官方工具|MySQL Router 高可用原理与实战
- 团体程序设计天梯赛-练习集 L1-030 一帮一
- 团体程序设计天梯赛-练习集 L1-035 情人节
- 团体程序设计天梯赛-练习集 L1-038 新世界
- 团体程序设计天梯赛-练习集 L1-040 最佳情侣身高差
- 团体程序设计天梯赛-练习集 L1-041 寻找250
- 十年磨一剑!腾讯QQ Linux版 2.0.0 Beta重磅发布!
- 团体程序设计天梯赛-练习集 L1-045 宇宙无敌大招呼
- 团体程序设计天梯赛-练习集 L1-047 装睡
- 团体程序设计天梯赛-练习集 L1-052 2018我们要赢
- 团体程序设计天梯赛-练习集 L1-053 电子汪
- 团体程序设计天梯赛-练习集 L1-056 猜数字