概述

全英文:Java DataBase Connectivity,即Java数据库连接,用Java语言去连接数据库。JDBC本质上是Sun公司制定的一套接口(Interface),接口都有调用者和实现者,而面向接口调用、面向接口写实现类都属于面向接口编程。而面向接口编程的好处就是解耦合,即降低程序的耦合度,提高程序的扩展力。多态就是非常典型的面向抽象编程。image.png
我们访问数据库有好几种方式,比如通过命令行、Navicat客户端以及直接通过Java对象,这样的访问架构被称为C/S架构,即客户端/服务器架构。所以,通过Java对象和命令行方式去操作数据库本质上是没有区别的。但是,如果Java对象直接去操作数据库,那么不同数据库就会有不同的实现方式,会给用户切换操作数据库时带来巨大的麻烦,因此,Sun公司开发了一套接口,这套接口就是一套规范,各数据库厂商要遵循这套规范实现各自厂商的接口,而用户在通过Java对象去操作数据库时只需要导入响应数据库的驱动,通过这样中间的方式间接操作数据库,这样中间的方式实现其实就是JDBC。

模拟JDBC的实现

SUN公司

  1. package jdbc.jdbcinterface;
  2. //Sun公司负责制定这套JDBC接口,其实现类被称为驱动
  3. public interface JDBC {
  4. /*
  5. 连接数据库的方法
  6. */
  7. void getConnection();
  8. }

数据库厂家之MySQL

  1. package jdbc.jdbcinterface;
  2. public class MySQL implements JDBC {
  3. @Override
  4. public void getConnection() {
  5. //这段代码理应涉及到底层MySQL的实现原理
  6. System.out.println("连接MySQL数据库成功!");
  7. }
  8. }

数据库厂家之Oracle

  1. package jdbc.jdbcinterface;
  2. public class Oracle implements JDBC {
  3. @Override
  4. public void getConnection() {
  5. //这段代码理应涉及到底层Oracle的实现原理
  6. System.out.println("连接Oracle数据库成功!");
  7. }
  8. }
  1. **数据库厂家之SQLServer**
  1. package jdbc.jdbcinterface;
  2. public class SQLserver implements JDBC {
  3. @Override
  4. public void getConnection() {
  5. //这段代码理应涉及到底层SQLserver的实现原理
  6. System.out.println("连接SQLserver数据库成功!");
  7. }
  8. }

Java程序员作为调用者身份调用

  1. package jdbc.jdbcinterface;
  2. import java.util.ResourceBundle;
  3. /*
  4. Java程序员角色
  5. 不需要关心具体是哪个品牌的数据库,只需要面向JDBC接口写代码。
  6. 面向接口编程、面向抽象编程,不要面向具体编程
  7. */
  8. public class JavaProgrammer {
  9. public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
  10. JDBC jdbc = new MySQL();
  11. //创建对象可以通过反射机制
  12. ResourceBundle bundle = ResourceBundle.getBundle("jdbc");
  13. String className = bundle.getString("className");
  14. Class c = Class.forName(className);
  15. JDBC jdbc1 = (JDBC)c.newInstance();
  16. //以下代码都是面向接口编程,不需要改动
  17. jdbc1.getConnection(); //连接Oracle数据库成功!
  18. }
  19. }
  1. **jdbc.properties文件内容**
  1. className=Oracle

JDBC开发前的准备工作

文本编辑器方式:先从官网下载对应的驱动jar包,然后将其配置到环境变量classpath中,供类加载器加载。注意:要在环境变量中配:classpath=.;jar包路径
IDEA方式

JDBC编程6步(要求背会)

1、注册驱动

告诉Java程序,即将要连接的是哪个品牌的数据库

2、获取连接

表示JVM进程和数据库进程之间的通道打开了,这属于进程的通信,重量级的,使用完要记得关闭

3、获取数据库操作对象

专门执行SQL语句的对象

4、执行SQL语句

DQL、DML等

(5、处理查询结果集)

这是建立在第四步的基础上的

6、释放资源

Java和数据库属于进程间的通信,开启后要关闭。

  1. package jdbc.test;
  2. import java.sql.*;
  3. public class JDBCTest01 {
  4. public static void main(String[] args) {
  5. //注册驱动(这里更为普遍的方式是通过类加载,触发静态代码块的执行,并可以写到属性配置文件中)
  6. DriverManager.registerDriver(new com.oracle.jdbc.Driver());
  7. //获取连接
  8. Connection conn = DriverManager.getConnection("url", "username", "password");
  9. //获取数据库操作对象
  10. Statement statement = conn.createStatement();
  11. //执行SQL语句
  12. int count = statement.executeLargeUpdate("需要执行的SQL语句");
  13. System.out.println(count == 1 ? "执行成功" : "执行失败");
  14. //若是查询,则有处理的结果集
  15. while(rs.next()){
  16. //光标指向的行有数据
  17. //取数据
  18. //getString()方法的特点是:不管数据库中的数据类型是什么,都以String的形式取出
  19. //除了以String类型取出外,还能以特定的类型取出
  20. //JDBC所有下标从1开始,不是从0开始
  21. String empno = rs.getString("empno");
  22. String ename = rs.getString("ename");
  23. String sal = rs.getString("sal");
  24. System.out.println(empno+","+ename+","+sal);
  25. }
  26. //释放资源
  27. statement.close();
  28. conn.close();
  29. }
  30. }

附:工具

1、Navicat for MySQL
表已经建好了,用于便捷化操作。开发阶段使用。
2、PowerDesigner
物理建模之用,系统架构师使用。设计阶段使用。

简单项目

1、需求

模拟用户登录功能的实现

2、业务描述

程序运行的时候,提供了一个输入的入口,可以让用户输入用户名和密码,用户输入用户名和密码后提交信息,Java程序收集到用户信息,Java程序连接数据库验证用户名和密码是否合法。
合法:显示登录成功
不合法:显示登录失败

3、数据的准备

在实际开发中,表的设计会使用专门的建模工具,这里安装PowerDesigner,即PD工具来进行数据库表的设计,请参见user-login.sql脚本。

4、功能实现

  1. package ustc.java.jdbc;
  2. import java.sql.*;
  3. import java.util.HashMap;
  4. import java.util.Map;
  5. import java.util.Scanner;
  6. /*
  7. 模拟实现用户登录功能
  8. */
  9. public class JDBCTest06 {
  10. public static void main(String[] args) {
  11. // 初始化界面
  12. Map<String,String> userLoginInfo = initUI();
  13. // 验证用户名和密码
  14. boolean loginSuccess = login(userLoginInfo);
  15. // 输出最后结果
  16. System.out.println(loginSuccess ? "登录成功" : "登录失败");
  17. }
  18. /**
  19. * 用户登录
  20. * @param userLoginInfo 用户登录信息
  21. * @return true表示登录成功,false表示登录失败
  22. */
  23. private static boolean login(Map<String, String> userLoginInfo) {
  24. boolean loginSuccess = false;
  25. Connection conn = null;
  26. Statement stmt = null;
  27. ResultSet rs = null;
  28. try {
  29. // 1、注册驱动
  30. Class.forName("com.mysql.jdbc.Driver");
  31. // 2、获取连接
  32. conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydatabase","root","146");
  33. // 3、获取数据库操作对象
  34. stmt = conn.createStatement();
  35. // 4、执行sql语句
  36. String sql = "select * from t_user where userName = '"+ userLoginInfo.get("userName")+ "' and userPassword = '" + userLoginInfo.get("userPassword")+ "'";
  37. rs = stmt.executeQuery(sql);
  38. // 5、处理结果集
  39. if(rs.next()) {
  40. loginSuccess = true;
  41. }
  42. } catch (ClassNotFoundException e) {
  43. e.printStackTrace();
  44. } catch (SQLException throwables) {
  45. throwables.printStackTrace();
  46. } finally {
  47. // 6、释放资源
  48. if (rs != null) {
  49. try {
  50. rs.close();
  51. } catch (SQLException throwables) {
  52. throwables.printStackTrace();
  53. }
  54. }
  55. if (stmt != null) {
  56. try {
  57. stmt.close();
  58. } catch (SQLException throwables) {
  59. throwables.printStackTrace();
  60. }
  61. }
  62. if (conn != null) {
  63. try {
  64. conn.close();
  65. } catch (SQLException throwables) {
  66. throwables.printStackTrace();
  67. }
  68. }
  69. }
  70. return loginSuccess;
  71. }
  72. /**
  73. * 初试化界面
  74. * @return 用户输入的用户名和密码等登录信息
  75. */
  76. private static Map<String, String> initUI() {
  77. Scanner s = new Scanner(System.in);
  78. System.out.print("请输入用户:");
  79. String userName = s.nextLine();
  80. System.out.print("请输入密码:");
  81. String userPassword = s.nextLine();
  82. Map<String,String> userLoginInfo = new HashMap<>();
  83. userLoginInfo.put("userName",userName);
  84. userLoginInfo.put("userPassword",userPassword);
  85. return userLoginInfo;
  86. }
  87. }

5、隐患

当前程序存在的问题是:
用户名:fdsa
密码:fdsa or 1=1
登陆成功
这种现象被称为SQL注入。导致SQL注入的根本原因是用户信息中含有SQL语句的关键字,并且这些关键字参与SQL语句的编译过程,以致SQL语句的愿意被扭曲,进而达到SQL注入。

6、思路和解决办法

思路

只要用户提供的信息不参与SQL语句的编译过程,问题就解决了,即使用户提供的信息中含有SQL语句的关键字,但没有参与编译,不起作用。想要用户信息不参与SQL语句的编译,那么必须使用java.sql.PreparedStatement,PreparedStatement接口继承了java.sql.Statement,PreparedStatement是属于预编译的数据库操作对象,PreparedStatement的原理是:预先对SQL语句的框架进行编译,然后给SQL语句传“值”。

解决办法

  1. package ustc.java.jdbc;
  2. import java.sql.*;
  3. import java.util.HashMap;
  4. import java.util.Map;
  5. import java.util.Scanner;
  6. public class JDBCTest07 {
  7. public static void main(String[] args) {
  8. // 初始化界面
  9. Map<String,String> userLoginInfo = initUI();
  10. // 验证用户名和密码
  11. boolean loginSuccess = login(userLoginInfo);
  12. // 输出最后结果
  13. System.out.println(loginSuccess ? "登录成功" : "登录失败");
  14. }
  15. /**
  16. * 用户登录
  17. * @param userLoginInfo 用户登录信息
  18. * @return true表示登录成功,false表示登录失败
  19. */
  20. private static boolean login(Map<String, String> userLoginInfo) {
  21. boolean loginSuccess = false;
  22. Connection conn = null;
  23. PreparedStatement ps = null;
  24. ResultSet rs = null;
  25. try {
  26. // 1、注册驱动
  27. Class.forName("com.mysql.jdbc.Driver");
  28. // 2、获取连接
  29. conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydatabase","root","146");
  30. // 3、获取预编译的数据库操作对象
  31. // sql语句的框架中,一个?,表示一个占位符,一个?将来接收一个值。注意:?不要用单引号括起来
  32. String sql = "select * from t_user where userName = ? and userPassword = ?";
  33. // 程序执行到此处,会发送sql语句框架给DBMS,DBMS对sql语句框架进行预编译。
  34. ps = conn.prepareStatement(sql);
  35. // 给占位符?传值,第一个?的下标是1,第二个?的下标是2(JDBC中下标都从1开始)
  36. ps.setString(1,userLoginInfo.get("userName"));
  37. ps.setString(2,userLoginInfo.get("userPassword"));
  38. // 4、执行sql语句
  39. rs = ps.executeQuery();
  40. // 5、处理结果集
  41. if(rs.next()) {
  42. loginSuccess = true;
  43. }
  44. } catch (ClassNotFoundException e) {
  45. e.printStackTrace();
  46. } catch (SQLException throwables) {
  47. throwables.printStackTrace();
  48. } finally {
  49. // 6、释放资源
  50. if (rs != null) {
  51. try {
  52. rs.close();
  53. } catch (SQLException throwables) {
  54. throwables.printStackTrace();
  55. }
  56. }
  57. if (ps != null) {
  58. try {
  59. ps.close();
  60. } catch (SQLException throwables) {
  61. throwables.printStackTrace();
  62. }
  63. }
  64. if (conn != null) {
  65. try {
  66. conn.close();
  67. } catch (SQLException throwables) {
  68. throwables.printStackTrace();
  69. }
  70. }
  71. }
  72. return loginSuccess;
  73. }
  74. /**
  75. * 初试化界面
  76. * @return 用户输入的用户名和密码等登录信息
  77. */
  78. private static Map<String, String> initUI() {
  79. Scanner s = new Scanner(System.in);
  80. System.out.print("请输入用户:");
  81. String userName = s.nextLine();
  82. System.out.print("请输入密码:");
  83. String userPassword = s.nextLine();
  84. Map<String,String> userLoginInfo = new HashMap<>();
  85. userLoginInfo.put("userName",userName);
  86. userLoginInfo.put("userPassword",userPassword);
  87. return userLoginInfo;
  88. }
  89. }

7、Statement和PreparedStatement的区别

简述

  • Statement存在SQL注入的问题,而PreparedStatement解决了这一问题;
  • Statement由于是拼接的方式,所以总是编译一次执行一次;而PreparedStatement是编译一次执行N次,PreparedStatement也会在编译器做类型检查,更安全一些

综上所述,PreparedStatement使用得更为普遍。而使用Statement的场景一般是需要SQL注入,比如在页面显示方式,即升序还是降序或是其他方式的时候,只能采取Statement方式。体会如下例:

代码1

  1. package ustc.java.jdbc;
  2. import java.sql.*;
  3. import java.util.Scanner;
  4. public class JDBCTest08 {
  5. public static void main(String[] args) {
  6. Scanner s = new Scanner(System.in);
  7. System.out.println("请输入desc或者asc");
  8. String keyWords = s.nextLine();
  9. Connection conn = null;
  10. PreparedStatement ps = null;
  11. ResultSet rs = null;
  12. try {
  13. Class.forName("com.mysql.jdbc.Driver");
  14. conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydatabase","root","146");
  15. String sql = "select ename from emp order by ename ?";
  16. ps = conn.prepareStatement(sql);
  17. ps.setString(1,keyWords);
  18. rs = ps.executeQuery();
  19. while(rs.next()){
  20. System.out.println(rs.getString("ename"));
  21. }
  22. } catch (ClassNotFoundException e) {
  23. e.printStackTrace();
  24. } catch (SQLException throwables) {
  25. throwables.printStackTrace();
  26. } finally {
  27. if (rs != null) {
  28. try {
  29. rs.close();
  30. } catch (SQLException throwables) {
  31. throwables.printStackTrace();
  32. }
  33. }
  34. if (ps != null) {
  35. try {
  36. rs.close();
  37. } catch (SQLException throwables) {
  38. throwables.printStackTrace();
  39. }
  40. }
  41. if (conn != null) {
  42. try {
  43. rs.close();
  44. } catch (SQLException throwables) {
  45. throwables.printStackTrace();
  46. }
  47. }
  48. }
  49. }
  50. }

代码2

  1. package ustc.java.jdbc;
  2. import java.sql.*;
  3. import java.util.Scanner;
  4. public class JDBCTest09 {
  5. public static void main(String[] args) {
  6. Scanner s = new Scanner(System.in);
  7. System.out.println("请输入desc或者asc");
  8. String keyWords = s.nextLine();
  9. Connection conn = null;
  10. Statement stmt = null;
  11. ResultSet rs = null;
  12. try {
  13. Class.forName("com.mysql.jdbc.Driver");
  14. conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydatabase","root","146");
  15. stmt = conn.createStatement();
  16. String sql = "select ename from emp order by ename " + keyWords;
  17. rs = stmt.executeQuery(sql);
  18. while(rs.next()){
  19. System.out.println(rs.getString("ename"));
  20. }
  21. } catch (ClassNotFoundException e) {
  22. e.printStackTrace();
  23. } catch (SQLException throwables) {
  24. throwables.printStackTrace();
  25. } finally {
  26. if (rs != null) {
  27. try {
  28. rs.close();
  29. } catch (SQLException throwables) {
  30. throwables.printStackTrace();
  31. }
  32. }
  33. if (stmt != null) {
  34. try {
  35. rs.close();
  36. } catch (SQLException throwables) {
  37. throwables.printStackTrace();
  38. }
  39. }
  40. if (conn != null) {
  41. try {
  42. rs.close();
  43. } catch (SQLException throwables) {
  44. throwables.printStackTrace();
  45. }
  46. }
  47. }
  48. }
  49. }

体会PreparedStatement

  1. package ustc.java.jdbc;
  2. import java.sql.Connection;
  3. import java.sql.DriverManager;
  4. import java.sql.PreparedStatement;
  5. import java.sql.SQLException;
  6. /*
  7. 使用PreparedStatement完成insert、update、delete
  8. */
  9. public class JDBCTest10 {
  10. public static void main(String[] args) {
  11. Connection conn = null;
  12. PreparedStatement ps = null;
  13. try {
  14. // 1、注册驱动
  15. Class.forName("com.mysql.jdbc.Driver");
  16. // 2、获取连接
  17. conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydatabase","root","146");
  18. // 3、获取预编译的数据库操作对象
  19. /*String sql = "insert into dept(deptno,dname,loc) values(?,?,?)";
  20. ps = conn.prepareStatement(sql);
  21. ps.setInt(1,60);
  22. ps.setString(2,"研发部");
  23. ps.setString(3,"北京");*/
  24. /*String sql = "update dept set dname = ?, loc = ? where deptno = ?";
  25. ps = conn.prepareStatement(sql);
  26. ps.setString(1,"销售二部");
  27. ps.setString(2,"西安");
  28. ps.setInt(3,60);*/
  29. String sql = "delete from dept where deptno = ?";
  30. ps = conn.prepareStatement(sql);
  31. ps.setInt(1,60);
  32. // 4、执行sql语句
  33. int count = ps.executeUpdate();
  34. System.out.println(count == 1? "修改成功" : "修改失败");
  35. } catch (ClassNotFoundException e) {
  36. e.printStackTrace();
  37. } catch (SQLException throwables) {
  38. throwables.printStackTrace();
  39. } finally {
  40. // 6、释放资源
  41. if (ps != null) {
  42. try {
  43. ps.close();
  44. } catch (SQLException throwables) {
  45. throwables.printStackTrace();
  46. }
  47. }
  48. if (conn != null) {
  49. try {
  50. conn.close();
  51. } catch (SQLException throwables) {
  52. throwables.printStackTrace();
  53. }
  54. }
  55. }
  56. }
  57. }

乐观锁、悲观锁

行级锁:以下语句查出来的显示表示,结果是不能被修改的。也称悲观锁。

  1. select ename,job,sal fromemp where job = 'MANGER' for update;

image.png

数据库事务

事务就是一种原子性的操作,一组事务中的所有操作,要么全部执行成功,要么全部执行失败。Mysql数据库事务默认开启自动提交,即执行完一句SQL就向数据库提交一句。
下面采用伪代码说明整个提交过程:

  1. //模拟账户A给账户B转账10000元,一开始A、B都是10000元
  2. Connection connection = JDBCUtils.getConnection();
  3. PreparedStatement preparedStatement = connection.preparedStatement();
  4. try{
  5. preparedStatement.executeUpdate('A的金额减10000');//标记代码1
  6. }catchException e){
  7. //.....
  8. }
  9. int 1/0;//标记代码2
  10. preparedStatement.executeUpdate('B的金额加10000');
  11. //关闭资源
  12. .....
  1. 从以上代码可以知道,当标记代码1 出现异常且标记代码2 被注释的时候,B 的账户变为20000A的账户还是10000;当标记代码1 正常而标记代码2 未被注释的时候,B的账户为10000元而A的账户已经变为0元,所以,这种操作是非常危险的。因此,在实际操作中不能默认数据库的自动提交,而应开启事务手动提交。<br />数据库事务的特性简单概括为ACID
  • A 原子性,一气呵成。要么全部执行成功,要么全部执行失败。
  • C 一致性,其实是数据前后的一致性,比如转账案例中转账前后A+B总金额肯定是一样的
  • I 隔离性。由于转账案例的双方可能出现并发操作,所以双方对数据库同一数据的操作可能会显示不同的内容。所以隔离性就是指并发操作的程度。
  • D 持久性。一经更改,长期有效。除非磁盘被烧了。

原子性、一致性和持久性都好理解,而隔离性会因为数据库隔离级别的不同对同一数据的操作双方产生巨大的差别。隔离级别由弱到强依次为:读未提交、读已提交、可重复读、串行化。数据库的默认隔离级别是可重复读。
这里还是以A、B初始都是10000元,A给B转账10000元,C可以查看A、B账户变化,以cmd命令行执行为例,说明隔离级别的内容。

读未提交的过程

  1. select @@ tx_isolation; //查看数据库的默认隔离级别,这里默认repeatable read
  2. set global transaction isolation level read uncommitted;
  3. exit
窗口1 窗口2(注意两个窗口同步操作)
select * from account;(A、B账户都是10000) select * from account;(A、B账户都是10000)
update account set money = 0 where name = A;
select * from Account;(A:0;B:10000) select * from Account; (账户A:0;账户B:10000)
rollback;
select * from account;(A、B账户都是10000) select * from account;(A、B账户都是10000)

窗口2读到了窗口1还未提交的数据,这是不允许的,这就是脏读现象。

读已提交的过程

  1. select @@ tx_isolation; //查看数据库的隔离级别,这里默认read uncommitted
  2. set global transaction isolation level read committed;
  3. exit
窗口1 窗口2(注意两个窗口同步操作)
select * from account;(A、B账户都是10000) select * from account;(A、B账户都是10000)
update account set money = 0 where name = A;
select * from Account;(A:0;B:10000) select * from Account; (A:10000;B:10000)
commit;
select * from account;(A:0;B:10000) select * from account;(A:0;B:10000)

窗口1修改了数据但是还未提交的时候,窗口2是读不到窗口1 修改的数据,本就该如此,所以读已提交隔离级别解决了脏读现象。但是产生了新的问题:不可重复读,即窗口2读取同一份数据前后2次是不一样的,那么该信谁的呢?这个不一定,所以这是个问题。

可重复读的过程

重置原场景:A、B账户都是10000元,A给B转账10000元

  1. select @@ tx_isolation; //查看数据库的隔离级别,这里默认read committed
  2. set global transaction isolation level repeatable read;
  3. exit
窗口1 窗口2(注意两个窗口同步操作)
select * from account;(A、B账户都是10000) select * from account;(A、B账户都是10000)
update account set money = 0 where name = A;
select * from Account;(A:0;B:10000) select * from Account; (A:10000;B:10000)
commit;
select * from account;(A:0;B:10000) select * from account;(A:10000;B:10000)
insert into Account values (3,’D’,40000);
select * from account;(A:0;B:10000;D:40000) select * from account;(A:0;B:10000;D:40000)
insert into Account values (4,’E’,50000);
select * from account;(A:0;B:10000;D:40000;E:50000) select * from account;(A:0;B:10000;D:40000)

不管窗口1修改A的数据是提交了还是没提交,在窗口2看来都是没有变化的,所以可重复读隔离级别解决了脏读、不可重复读的问题。但是引来的新的问题,即当窗口1新插入或删除数据的时候,窗口2有时候能读到增删的数据,有时候又看不到,这就是虚幻读的问题。顾名思义,就是读的数据虚无缥缈,恍如幻觉,有时候能读到有时候又读不到。

可串行化的过程

重置原场景:A、B账户都是10000元,A给B转账10000元

  1. select @@ tx_isolation; //查看数据库的隔离级别,这里默认repeatable read
  2. set global transaction isolation level serializable;
  3. exit
窗口1 窗口2(注意两个窗口同步操作)
select * from account;(A、B账户都是10000) select * from account;(A、B账户都是10000)
update account set money = 0 where name = A; 这里我想执行select * from Account;但输入不了
commit;
select * from account;(A:0;B:10000) select * from Account; (A:10000;B:10000)
连接数据库、开启事务等语句略 连接数据库、开启事务等语句略
update account set money = 20000 where name = B; 这里我想执行select * from Account;但输入不了
select * from account ;(A:0;B:20000) select * from account ;(A:0;B:20000)
commit;

窗口A修改数据时,窗口B是不能做任何操作,哪怕是查看也是不行的。只有窗口1提交之后窗口2才能操作。所谓可串行化,就是将并发的事务转变为单线程,所以绝对安全,没有任何问题。效率就不行了,一般不常用。

总结

隔离级别 脏读 不可重复读 虚幻读
读未提交
读已提交 ×
可重复读 × ×
可串行化 × × ×