专栏首页程序猿的大杂烩脏读,不可重复读,幻读

脏读,不可重复读,幻读

MySQL事务隔离级别:

在介绍脏读,不可重复读,幻读现象之前,我们先来了解MySQL的事务隔离级别,因为脏读,不可重复读,幻读等现象都是由数据库里的事务隔离级别来决定是否可能发生的。

在MySQL里共有四个隔离级别,分别是:Read uncommttied(可以读取未提交数据)、Read committed(可以读取已提交数据)、Repeatable read(可重复读)、Serializable(可串行化)。

在MySQL数据库里,默认的事务隔离级别是Repeatable read(可重复读)。

使用select @@tx_isolation; 命令可以查看MySQL默认的事务隔离级别:

每个事务隔离级别会导致的数据现象:

但是这里有一点需要注意的是数据库的默认引擎是InnoDB在使用InnoDB引擎下,即便设定的事务隔离级别是Repeatable read,也不会出现数据幻读现象。

  -原因:MySQL InnoDB存储引擎,实现的是基于多版本的并发控制协议——MVCC (Multi-Version Concurrency Control) 所以在Repeatable Read (RR)隔离级别下不存在幻读。

脏读现象:

在默认的事务隔离级别下,我们是无法读取到未提交的数据的,在能够读取到未提交数据的事务隔离级别下,才会出现脏读现象。脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是脏数据(Dirty Data),依据脏数据所做的操作可能是不正确的。

简而言之会出现脏读现象就是因为用户能够读取到未提交到数据里的数据,也即是无效的数据,然后对这些无效的脏数据进行了操作,所以这些操作都是无效或者错误的。

用言语来描述可能有点抽象、不好理解,下面我们打开两个MySQL客户端,来进行脏读现象的实验:

1.使用SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; 命令将两个MySQL客户端的事务隔离级别设定为Read uncommttied级别:

2.现在我们使用其中一个用户,往表格里插入一条数据,但是不执行commit命令,会发现另一个用户也能读取到这个未提交的数据:

这就是脏读现象,此现象称之为脏读因为读取出来的是无效数据,无效数据就等于是垃圾数据垃圾就当然就是脏的所以才叫脏读,而且如果我们以这个脏数据作为某些参数的话,必然会出现错误。

不可重复读现象:

在一个事务内,多次读同一个数据。在这个事务还没有结束时,另一个事务也访问该同一数据。那么,在第一个事务的两次读数据之间。由于第二个事务的修改,那么第一个事务读到的数据可能不一样,这样就发生了在一个事务内两次读到的数据是不一样的,因此称为不可重复读,即原始读取不可重复。

下面我们通过实验来看看不可重复读现象:

1.使用SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;命令将两个MySQL客户端的事务隔离级别设定为Read committed,这是因为要避免出现脏读现象:

2.现在我们使用其中一个用户,修改表格里的一条数据,但是不执行commit命令,会发现另一个用户不能读取到这个未提交的数据:

3.但是用户B执行commit命令后就不一样了:

不可重复读现象主要是指,在一个事务结束前(执行commit或rollback前),进行两次或多次读取同一个数据会出现不同的结果,所以称为不可重复读,因为重复读取就会出现这种数据不一致的情况。

幻读现象:

幻读是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,比如这种修改涉及到表中的“全部数据行”。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入“一行新数据”。那么,以后就会发生操作第一个事务的用户发现表中还存在没有修改的数据行,就好象发生了幻觉一样.一般解决幻读的方法是增加范围锁RangeS,锁定检索范围为只读,这样就避免了幻读。

幻读实际上和不可重复读有一点类似,都是第二次或多次查询的时候发现数据发生了变化,但是幻读侧重在表格里数据的数量上的变化,而且也是在事务生命周期内的查询上发生的变化,所以有一点要注意的是:严格意义上只有当用户A在事务生命周期内多次查询数据时数据发生变化,才能算得上是不可重复读或幻读现象,如果用户A在一个事务结束后接着在另一个新的事务里查询后发现数据发生了变化,那么这就不算是不可重复读或者幻读。

下面我们通过实验来看看幻读现象:

  1.因为实际上在InnoDB存储引擎里的Repeatable read级别,已经解决了幻读现象,所以我们不需要更改隔离级别,仍旧使用Read committed级别即可:

2.现在我们使用其中一个用户,往表格了里表格里插入一条数据,但是不执行commit命令,同样的会发现另一个用户不能读取到这个未提交的数据:

3.当用户B commit之后用户A再查询就会发现多了一行数据:

然后用户B把这条数据删除了:

所以幻读称之为幻读的原因就是这,在一个事务生命周期内的查询上发生的表格数据数量上的变化,一下多了几行数据,一下又少了几行数据,跟活在梦一样,分分钟上下几百万。

不可重复读和幻读的区别:

不可重复读强调的是每次读取的是相同位置的数据,且该数据在另一个事务下被修改。注重的是修改。这个位置指的是哪一行、哪一个字段的数据。

幻读强调的是第二次读比第一次读取时,内容多了或者少了几行,注重的是新增和删除。

Serializable级别:

完全串行化的读,每次读都需要获得表级共享锁,读写相互会相互互斥,这样可以更好的解决数据一致性的问题,但是同样会大大的降低数据库的实际吞吐性能。所以该隔离级别因为损耗太大,一般很少在开发中使用,在此就不介绍了。

幻读的实际应用例题:

以上介绍的那些现象并不是数据库的BUG或者一些问题什么的,实际上有些业务需求就是需要这些数据现象来完成。例如幻读现象,在车票、电影票锁座等方面都有幻读的应用例子。

例如假设在购买车票的时候,你一开始明明查询只有三张票,但是一会再查一次就发现多了五张票,这就是幻读的现象。因为别人查询到这几张票的时候这几张票处于锁定状态,所以你就查询不到,如果对方放弃购买的话,这些票又重新回到出售界面了,所以你第二次查询的才会发现多了几张票,这就是幻读在实际生活中的一个应用例子。

现在我们编写一个简单的票务系统来演示幻读的应用:

图形界面代码示例:

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.sql.SQLException;
import java.util.Vector;
 
import javax.swing.JButton;
import javax.swing.JComboBox;
 
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
 
public class PiaoWuSystem extends JFrame {
 
private JTable table;
private JComboBox comboBox;
 
/**
 * Launch the application
 * 
 * @param args
 */
public static void main(String args[]) {
EventQueue.invokeLater(new Runnable() {
public void run() {
try {
PiaoWuSystem frame = new PiaoWuSystem();
frame.setVisible(true);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
 
public Vector<String> cols = new Vector();
public Vector<Vector<String>> rows = new Vector();
public final JLabel label_2 = new JLabel();
 
PiaoWuDB piaoWuDB;
 
/**
 * Create the frame
 */
/**
 * 
 */
int time = 100;
 
public PiaoWuSystem() {
super();
 
Thread thread = new Thread(new Runnable() {
 
public void run() {
while (true) {
label_2.setText("有效时间: " + time + " 秒");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
time--;
if (time == 0) {
break;
}
}
try {
piaoWuDB.rollback();
rows.clear();
table.updateUI();
} catch (Exception e) {
// TODO: handle exception
}
 
}
});
thread.start();
 
setResizable(false);
setBounds(100, 100, 750, 389);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
final JLabel label = new JLabel();
label.setFont(new Font("微软雅黑", Font.BOLD, 24));
label.setHorizontalAlignment(SwingConstants.CENTER);
label.setText("凯哥学堂-票务实验");
getContentPane().add(label, BorderLayout.NORTH);
 
final JPanel panel = new JPanel();
panel.setLayout(null);
getContentPane().add(panel, BorderLayout.CENTER);
 
final JLabel label_1 = new JLabel();
label_1.setText("卧铺数量:");
label_1.setBounds(10, 14, 66, 18);
panel.add(label_1);
 
comboBox = new JComboBox();
comboBox.addItem("1");
comboBox.addItem("2");
comboBox.addItem("3");
comboBox.addItem("4");
comboBox.addItem("5");
comboBox.setBounds(82, 10, 66, 27);
panel.add(comboBox);
 
final JButton button = new JButton();
button.addActionListener(new ActionListener() {
public void actionPerformed(final ActionEvent e) {
if (piaoWuDB != null) {
try {
piaoWuDB.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
time=30;
try {
piaoWuDB = new PiaoWuDB();
piaoWuDB.openTran();
Vector<Vector<String>> r = piaoWuDB.chaxun(Integer.parseInt(comboBox.getSelectedItem().toString()));
rows.clear();
rows.addAll(r);
table.updateUI();
} catch (Exception e2) {
e2.printStackTrace();
}
 
}
});
button.setText("查询票务");
button.setBounds(628, 9, 106, 28);
panel.add(button);
 
label_2.setForeground(new Color(255, 0, 0));
label_2.setFont(new Font("微软雅黑", Font.BOLD, 15));
label_2.setHorizontalAlignment(SwingConstants.CENTER);
label_2.setText("有效时间:");
label_2.setBounds(464, 10, 158, 27);
panel.add(label_2);
 
final JScrollPane scrollPane = new JScrollPane();
scrollPane.setBounds(10, 52, 724, 234);
panel.add(scrollPane);
 
cols.add("编号");
cols.add("类型");
cols.add("铺位");
 
table = new JTable(rows, cols);
scrollPane.setViewportView(table);
 
final JButton button_1 = new JButton();
button_1.addActionListener(new ActionListener() {
public void actionPerformed(final ActionEvent e) {
 
try {
piaoWuDB.rollback();
} catch (SQLException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
rows.clear();
table.updateUI();
 
}
});
button_1.setText("取消查询");
button_1.setBounds(628, 292, 106, 28);
panel.add(button_1);
 
final JButton button_2 = new JButton();
button_2.addActionListener(new ActionListener() {
public void actionPerformed(final ActionEvent e) {
try {
piaoWuDB.commit();
} catch (SQLException e1) {
e1.printStackTrace();
}
rows.clear();
table.updateUI();
}
});
button_2.setText("确认购买");
button_2.setBounds(516, 292, 106, 28);
panel.add(button_2);
//
}
}

JDBC代码示例:

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Vector;
 
import org.zero01.DBManager.DBManager;
 
public class PiaoWuDB {
 
private Connection conn = null;
 
// 开启事务
public void openTran() throws SQLException {
 
conn = DBManager.getDBManager();
conn.setAutoCommit(false); 
// 设置事务隔离等级
conn.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
 
}
 
public Vector<Vector<String>> chaxun(int num) throws SQLException {
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("select * from piaowu where state=1 limit 0," + num);
Vector<Vector<String>> rows = new Vector<Vector<String>>();
while (rs.next()) {
st = conn.createStatement();
int rowNum = st.executeUpdate("UPDATE PIAOWU SET STATE=0 WHERE PID=" + rs.getInt(1) + " AND STATE=1");
if (rowNum >= 1) {
Vector<String> row = new Vector();
row.add(rs.getInt(1)+"");
row.add(rs.getString(2));
row.add(rs.getString(3));
rows.add(row);
}
}
return rows;
}
 
public void commit() throws SQLException {
conn.commit();
conn.close();
}
 
public void rollback() throws SQLException {
conn.rollback();
conn.close();
}
}

运行结果:

  用户B想买五张票,但是查询的时候用户B只能查到编号为6、7、8、9的四张车票,因为其他票都被用户A锁定了:

然后第二次查询的时候用户B发现能够查到五张票了,这是因为用户A放弃了购买,这些票又重新回到出售界面了,这就是幻读的实际应用例子:

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 基于MHA搭建MySQL Replication集群高可用架构

    MHA是Master High Availability的缩写,它是目前MySQL高可用方面的一个相对成熟的解决方案,其核心是使用perl语言编写的一组脚本,是...

    端碗吹水
  • 同一浏览器下sessionid互相覆盖的问题

    在一台机器上安装多个Tomcat,端口不一样,这里姑且分别称为tomcat1 和 tomcat2,在两个不同的Tomcat上部署了A和B两个项目,两个项目的代码...

    端碗吹水
  • 当删库时如何避免跑路

    删库跑路也是个老梗了,可见在运维数据库的过程中误删除数据,或者开发的代码有bug,造成数据的误删除屡见不鲜。不过现在也有许多用于恢复或预防误删除的方案,例如SQ...

    端碗吹水
  • Tokudb安装测试初探

    TokuDB 是一个高性能、支持MVCC的MySQL 和 MariaDB 的存储引擎。TokuDB 的主要特点是数据压缩功能出色,对高写压力的支持,由美国...

    用户1278550
  • linux常用命令之压缩打包用法选项DEMO注意选项DEMO用法选项用法选项DEMO

    DF df – report file system disk space usage 查看文件系统的使用清空 用法 df [-hi] [path]选项-h h...

    用户1174983
  • UI5 control inheritance implementation question

    问题1:为什么new 一个JerryButton时,line 35会触发? var oJerryButton = new JerryButton({ appl...

    Jerry Wang
  • 神经网络图的简介(基本概念,DeepWalk以及GraphSage算法)

    近来,图神经网络(GNN)在各个领域广受关注,比如社交网络,知识图谱,推荐系统以及生命科学。GNN在对图节点之间依赖关系进行建模的强大功能使得与图分析相关的研究...

    AI研习社
  • Django实战-提供数据库事务功能

    Django网络应用开发的5项基础核心技术包括模型(Model)的设计,URL 的设计与配置,View(视图)的编写,Template(模板)的设计和Form(...

    小团子
  • 激萌相机Faceu用.net域名获5000万美金融资

    曾获美图、光速中国、IDG资本等的多轮融资的激萌相机 Faceu 已完成5000万美金C轮融资。

    躲在树上的域小名
  • 一文俯瞰Elasticsearch核心原理

    Lucene是一种高性能、可伸缩的信息搜索(IR)库,在2000年开源,最初由鼎鼎大名的Doug Cutting开发,是基于Java实现的高性能的开源项目。Lu...

    暴走大数据

扫码关注云+社区

领取腾讯云代金券