前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >脏读,不可重复读,幻读

脏读,不可重复读,幻读

作者头像
端碗吹水
发布2020-09-23 10:39:59
1.6K0
发布2020-09-23 10:39:59
举报

MySQL事务隔离级别:

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

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

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

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

7a778ac5fa67928f2bf7a84d78efbdac.png
7a778ac5fa67928f2bf7a84d78efbdac.png

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

33490579427db9abae844a358a812ba8.png
33490579427db9abae844a358a812ba8.png

但是这里有一点需要注意的是数据库的默认引擎是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级别:

c18d106a5842ea2062d38c6372f69a1a.png
c18d106a5842ea2062d38c6372f69a1a.png

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

81a9f0142877c4623aa5caa7eeb8206d.png
81a9f0142877c4623aa5caa7eeb8206d.png
84109b2d50c369e1adb8063fd53727a7.png
84109b2d50c369e1adb8063fd53727a7.png
39a4bfbde76604d81e3353a721042365.png
39a4bfbde76604d81e3353a721042365.png

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

不可重复读现象:

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

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

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

6da4899693a44620e36ec93235b0cc57.png
6da4899693a44620e36ec93235b0cc57.png

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

4cf262daa35f431e989ce62e798b6f9a.png
4cf262daa35f431e989ce62e798b6f9a.png

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

42112ce431b4822b677d1fdb91d3361a.png
42112ce431b4822b677d1fdb91d3361a.png

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

幻读现象:

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

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

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

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

4877328757096b089369ad6ca257ff20.png
4877328757096b089369ad6ca257ff20.png

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

aceb82708085d7b459294af79e46806f.png
aceb82708085d7b459294af79e46806f.png

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

3b424b7dd3905944f00751091cb46f3e.png
3b424b7dd3905944f00751091cb46f3e.png

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

c3d775b229e066d9be8cc2fe01474217.png
c3d775b229e066d9be8cc2fe01474217.png

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

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

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

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

Serializable级别:

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

幻读的实际应用例题:

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

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

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

图形界面代码示例:

代码语言:javascript
复制
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代码示例:

代码语言:javascript
复制
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锁定了:

60af1643bd11e4f51da16f3b9daa1694.png
60af1643bd11e4f51da16f3b9daa1694.png

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

ffe0fd92e6a796f0bc9e68bfd959b452.png
ffe0fd92e6a796f0bc9e68bfd959b452.png
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017-10-28 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 SQL Server
腾讯云数据库 SQL Server (TencentDB for SQL Server)是业界最常用的商用数据库之一,对基于 Windows 架构的应用程序具有完美的支持。TencentDB for SQL Server 拥有微软正版授权,可持续为用户提供最新的功能,避免未授权使用软件的风险。具有即开即用、稳定可靠、安全运行、弹性扩缩等特点。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档