Web开发|11|数据库技术

一、数据库连接池

1、DriverManager.getConnection() 数据库连接过程解读

JDBC 与数据库建立连接的过程:JDBC 向数据库服务器发送连接请求,数据库随机返回一个密码种子,JDBC收到随机的密码种子后通过特定的加密算法生成一个加密密码,并把这个加密密码发送给数据库服务器,数据库服务验证这个加密密码正确后,通知 JDBC 数据库连接成功。这个过程即如下图所示:

Web开发|11|数据库技术_第1张图片
JDBC 获取数据库连接的过程

JDBC 与数据库建立连接,有什么隐含的风险?

JDBC 与数据库服务器建立连接,最大的风险特点是时间开销较大。如果客户端每一次请求数据库时都新建一个新的连接,这无疑将是巨大的时间开销。那么并发的客户端请求过大时,将有可能导致数据库服务器当机。

JDBC 与数据库服务器进行连接,除了时间开销大以外,数据库服务端能够同时处理的并发请求数也是有限的,如果超出了这个最大连接数限制,数据库就会抛出 TooManyConnections 异常。

Web开发|11|数据库技术_第2张图片
超出数据库最大连接数限制

鉴于上述隐含的数据库连接风险,我们该如何解决问题?

一方面数据库服务器能并发承担的最大连接数是有限的,另一方面与数据库的连接需要较大的时间开销。鉴于上述两个原因,我们引用了“数据库连接池”来解决这一问题,以规避数据库连接潜在的风险。

Web开发|11|数据库技术_第3张图片
数据库连接池

数据库连接池能有效地复用数据库连接,从而大大地减少了数据库连接的新建次数,自然地节省了数据库连接的时间。数据库连接池,以保证了 JDBC 程序有序地可控地获取数据库连接。

Web开发|11|数据库技术_第4张图片
有序的数据库连接

2、到底什么是数据库连接池?

数据库连接池本质上就是一组 Java JAR 包,它控制着 Java 应用程序与数据库的连接。当数据库请求较多时,连接池会自动地创建新的数据库连接;当数据库请求较少时,连接池会自动地销毁并释放数据库连接;当数据库请求过多时,连接池会采用排队等待机制。从而保证了Java应用程序有序地获取数据库连接,进一步避免了并发请求数超过数据库的最大连接限制数。数据库连接池不仅保证数据库服务器并发请求的安全,同时有效提升了数据库请求的时间效率。

Web开发|11|数据库技术_第5张图片
什么数据库连接池?

通常我们在 Java 项目中,使用的数据库连接池技术是由 Apache 开源的 DBCP 项目。

3、什么是 DBCP ?如何使用它来创建数据库连接池?

DBCP 是 Apache 开源的 Java 数据库连接池项目,也是 Tomcat 所使用的数据库连接池组件,是目前应用最为广泛的连接池项目之一。它包含如下图所示的三个 Java JAR 包。

Web开发|11|数据库技术_第6张图片
什么是 DBCP ?

在哪里可以下载 DBCP 项目?
可以在 Apache 官网上下载 DBCP 项目包,如下图所示:

Web开发|11|数据库技术_第7张图片
下载 DBCP 项目

如何创建数据库连接池?

DBCP 使用 BasicDataSource 类来创建数据库连接池对象,BasicDataSource 对象用于管理数据库连接,其底层仍然是使用 JDBC 来连接和操作数据库的。创建数据库连接池对象的四个必要参数如下图所示:

Web开发|11|数据库技术_第8张图片
使用 DBCP 创建数据库连接池对象

使用 DBCP 创建数据库连接池,示例代码如下:

// TestDbPool.java
package cn.geekxia;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

import org.apache.commons.dbcp2.BasicDataSource;

public class TestDbPool {
    public static BasicDataSource ds = null;
    
    public final static String DRIVER_NAME = "com.mysql.jdbc.Driver";
    public final static DB_URL = "jdbc:mysql://localhost/world";
    public final static String USER_NAME = "root";
    public final static String PASS_WORD = "123456";
    
    public static void dbpoolInit() {
        // 创建数据库连接池对象
        ds = new BasicDataSource();
        // 设置数据库连接池的四个必须参数:驱动名称、数据库地址、用户名和密码
        ds.setDriverClassName(DRIVER_NAME);
        ds.setUrl(DB_URL);
        ds.setUsername(USER_NAME);
        ds.setPassword(PASS_WORD);
    }
    public void testConnection() {
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;
        
        try {
            // 从数据库连接池中取出一个现有的数据库连接对象
            conn = ds.getConnection();
            stmt = conn.createStatement();
            rs = stmt.executeQuery("select * from world");
            while(rs.next()) {
                System.out.println(rs.getString("name"));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 这里的 conn.close() 不是关闭数据库连接,而是把数据库连接对象归还给数据库连接池
            try {
                if (conn != null) conn.close();
                if (stmt != null) stmt.close();
                if (rs != null) rs.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }       
        }
    }
    public static void main(String[] args) {
        dbpoolInit();
        new TestDbPool().testConnection();
    }
}

上述代码中,使用了数据库连接池对象的 getConnection() 方法从连接池中取出一个现有的数据库连接对象:


Web开发|11|数据库技术_第9张图片
从数据库连接池中取出一个数据库连接对象

在用完数据库连接对象后,使用 conn.close() 方法,把数据库连接对象归还给连接池。这里需要注意的是“归还”,而不是“销毁”。DBCP 实际上对 Connection 类的 close() 进行了重写,把 JDBC 中的“销毁”逻辑重写成了“归还”逻辑,即把使用完的数据库连接丢回到数据库连接池中,以供其它数据库请求复用。


Web开发|11|数据库技术_第10张图片
把用完的数据库连接对象归还到数据库连接池中

4、如何对数据库连接池进行更加高级的设置?

使用 BasicDataSource 类可以为数据库连接池设置相关参数,以提升数据库访问的效率、优化数据库性能。

Web开发|11|数据库技术_第11张图片
BasicDataSource 类

BasicDataSource 类的常用方法,如下

  1. setInitialSize() 设置数据库连接池初始化时所需要新建的数据库连接对象的数量,即应用程序启动时预设多少个数据库连接对象。它能够有效解决第一次访问数据库相对较慢的问题。
  1. setMaxTotal() 设置数据库连接池中最多可以同时存在多少数据库连接对象,如果并发访问数超出这个设定值,则启用排队等待机制。它主要的作用是为保护数据库服务器,避免并发访问过多导致数据库服务器当机。
  1. setMaxWaitMillis() 设置数据库连接允许的最大等待时长,如果排队等待时长超出了这个设定值,则会报异常。
  1. setMaxIdle() 设置连接池中数据库连接对象的最大空闲数,如果连接池中空闲的数据库连接对象的数量超出了这个设定的值,则多余的数据库连接对象会被销毁。
  1. setMinIdle() 设置连接池中数据库连接对象的最小空闲数,如果连接池中空闲的数据库连接对象的数量低于了这个设定的值,则连接池会自动创建新的数据库连接对象,以保证最小空闲数。
    注:为了避免数据库连接池频繁地创建和销毁数据库连接对象,我们通常建议把 setMaxIdle() 和 setMinIdle() 设置成相同的数值。
  1. setTestWhiteIdle(true) 用于开启 DBCP 定期检查,以检查并保证连接池中的数据库连接对象都是有效的。因为 MySQL 数据库会默认关闭掉空闲时长超过 8 个小时的数据库连接,被 MySQL 服务器关闭掉的数据库连接,在连接池中是不同步的,因此需要开启 DBCP 检查以清除被 MySQL 服务器关闭掉的数据库连接对象,否则有可能导致连接池把失效的数据库连接对象分派给客户端请求。
  1. setMinEvictableIdleTimeMillis() 用于设定销毁数据库连接对象所需要的最小空闲时间。即,如果数据库连接对象的空闲时间超出了该设定的值,则它将会被连接池销毁。
  1. setTimeBetweenEvictionRunsMillis() 用于设定 DBCP 检查的周期,即每隔多长时间执行一次 DBCP 检查,以确保连接池中的数据库连接对象始终都是有效的。通常这个设定值要小于 8 小时才有意义,因为 MySQL 默

小结:DBCP 数据库连接池,一方面可以对数据库连接对象进行复用,以提升数据库访问的效率;另一方面可以对数据库访问进行限流,以保证后端数据库服务器被并发访问时不会当机


二、SQL 注入与防范

1、什么是 SQL 注入?

在 Web 应用架构模式下,客户端是无法直接访问后端数据库的,它们必须通过发送 HTTP 请求到后端服务器,再由后端服务器使用 SQL 来访问数据库。它们若想直接获取到数据库数据,唯一的途径就是利用业务程序的漏洞、伪装自己的请求、欺骗业务程序,从而达到直接获取数据库数据的目的。

Web开发|11|数据库技术_第12张图片
Web 架构

所谓 SQL 注入,即用户通过表单输入或者 URL 参数携带 SQL 命令,欺骗后端应用程序,以达到破坏原有 SQL 语句的目的,从而导致后端数据库信息泄露的行为。

Web开发|11|数据库技术_第13张图片
什么是 SQL 注入?

2、SQL 注入的问题根源是什么?

问题的根源是 SQL 语句本质上是动态拼接而成的,由用户输入改变了原有 SQL 语句的语义。

Web开发|11|数据库技术_第14张图片
SQL 注入的问题根源

3、那么该如何防范 SQL 注入呢?

使用 JDBC 的 Connection 对象的 prepareStatement(sql) 方法,实现参数化格式化的 SQL,以保证 SQL 语句在接收用户参数之前、之后的 SQL 语义是一致的,进而有效避免 SQL 语句因用户输入参数的不同而发生语义变化

Web开发|11|数据库技术_第15张图片
PreparedStatement 格式化 SQL 参数

PreparedStatement 实际上是继承自 Statement,PreparedStatement 实现了 Statement 所有的方法,它最大的优势是它提供了 SQL 参数化的解决方案。

prepareStatement(sql) 方法接收一个格式化的 SQL 语句,其中使用 ? 问号作为用户输入参数的占位符。有了参数化格式化的 SQL 后,我们就可以使用 PreparedStatement 对象的 setInt() / setString() / setBoolean() 等方法来填充用户输入的参数了,如下图:

Web开发|11|数据库技术_第16张图片
填充用户输入的参数

使用 PreparedStatement 对象实现“SQL注入与防范”,示例代码如下:

// TestSQL.java
public class Login {
    static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";
    static final String DB_URL = "jdbc:mysql://localhost/db_name";
    static final String USERNAME = "root";
    static final String PASSWORD = "123456";
    
    public static User login(String username, String password) throws ClassNotFountException {
        User user = null;

        Connection conn = null;
        PreparedStatement ptmt = null;
        ResultSet rs = null;

        // 1、装载驱动程序
        Class.forName(JDBC_DRIVER);
        // 2、建立数据库连接
        try {
            conn = DriverManager.getConnection(DB_URL, USERNAME, PASSWORD);
            // 3、格式化 SQL 语句
            ptmt = conn.prepareStatement("select * from user where username = ? and password = ?");
            // 向格式化 SQL 语句中传入用户参数
            ptmt.setString(1, username);
            ptmt.setString(2, password);
            // 执行 SQL 语句
            rs = ptmt.executeQuery();
            // 4、获取 SQL 执行结果
            while (rs.next()) {
                user = new User();
                user.setUsername(rs.getString("username"));
                user.setSex(rs.getBoolean("sex"));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 5、关闭资源,清理环境
            try {
                if (conn != null) conn.close();
                if (ptmt != null) conn.close();
                if (rs != null) conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return user;
    }
    public static void main(String[] args) throws ClassNotFoundException {
        System.out.println(login("geekxia';--", "123456")); // 登录失败
        System.out.println(login("geekxia", "123456")); // 登录成功
    }
}

4、关于 SQL 注入与防范的其它注意事项

1、把风险操作权限尽量避免给予应用程序,比如 drop table 这样的权限就不要给予应用程序。同时需要注意,我们应该尽可能小地把数据库权限给予用户和应用程序,这种数据库权限范围越小越好。

Web开发|11|数据库技术_第17张图片
最小化数据库权限

2、在任何时候,都不应该把数据库异常直接暴露给用户。因为数据库异常中包含着数据库相关的信息,用户可以根据这些信息进行推理和判断,这是很危险的。因此,对数据库异常进行合理封装是非常有必要的。

Web开发|11|数据库技术_第18张图片
封装数据库异常

3、在数据库中,密码不能使用明文保存,一定要进行加密再保存。密码加密后,即使数据库中数据被窃取,也不会导致密码暴露。这是 SQL 注入与防范的最后一道防线。MySQL 提供了 AES_ENCRYPT / AES_DECRYPT 这两个函数对密码进行加密和解密。

Web开发|11|数据库技术_第19张图片
对密码进行加密


三、数据库事务

1、以银行转账为例,初探“事务”的四个主要特征:这个任务的目标是,由张三给李四转账 100 元。

Web开发|11|数据库技术_第20张图片
转账任务

特性1:原子性,以这个转账任务为例,从张三账户减去100,给李四账户增加100,这两个动作是不可分隔的,要么同时成功,要么同时失败,即原子性。

Web开发|11|数据库技术_第21张图片
特性1-原子性

特性2:一致性,以转账任务为例,转账前后金钱的总额是不变的,即一致性。

Web开发|11|数据库技术_第22张图片
特性2-一致性

特性3:隔离性,即在一个事务发生时,不能受到其它并发事务的影响。

Web开发|11|数据库技术_第23张图片
特性3-隔离性

特性4:持久性,即一个事务执行成功后,它的结果是永久生效的。以转账任务为例,转账任务一旦执行完成,它将不受其它任何故障的影响,即这个任务是永久生效的。

Web开发|11|数据库技术_第24张图片
特性4-持久性

2、到底什么是“事务”?

Web开发|11|数据库技术_第25张图片
什么是事务?

如果一个任务同时符合“原子性”“一致性”“隔离性”“持久性”这 4 个特征,我们就可以使用事务来实现。

3、JDBC 中如何实现数据库事务控制?
JDBC 的 Connection 对象提供了事务控制相关的 API,其介绍如下:

setAutoCommit(false) 用于开启一个数据库事务。JDBC 默认是不开启事务的,即执行非事务的数据库操作。
commit() 用于提交事务。把事务开启后的所有数据库操作一并提交,当执行完 commit() 操作后,数据库中的数据状态才会发生变化。
rollback() 用于回滚事务。把上一次 commit() 所提交的事务,回滚到事务执行前的数据库状态。

Web开发|11|数据库技术_第26张图片
JDBC 事务控制

JDBC事务控制,示例代码如下:

import java.sql.*;

public class TestTransaction {
    public static BasicDataSource ds = null;
    static final String DRIVER_NAME = "com.mysql.jdbc.Driver";
    static final String DB_URL = "jdbc:mysql://localhost/db_name";
    static final String USERNAME = "root";
    static final String PASSWORD = "123456";

    public static void transferAccount() throws ClassNotFoundException {
        Connection conn = null;
        PreparedStatement ptmt = null;
        try {
            conn = ds.getConnection();
            // 开启事务
            conn.setAutoCommit(false);
            ptmt = conn.prepareStatement("update user set account = ? where username = ?");
            // 把张三的账户余额更新为 0
            ptmt.setInt(1, 0);
            ptmt.setString(2, "ZhangSan");
            ptmt.execute();
            // 把李四的账户余额
            ptmt.setInt(1, 100);
            ptmt.setString(2, "LiSi");
            ptmt.execute();
            // 提交事务
            // 只有提交事务后,数据库中的数据状态才会发生变化
            conn.commit();
        } catch (SQLException e) {
            // 如果发生数据库异常,就回滚事务
            if (conn != null) {
                try {
                    conn.rollback();
                } catch (SQLException e1) {
                    e1.printStackTrace();
                }
            }
            e.printStackTrace();
        } finally {
            try {
                if (conn != null) conn.close();
                if (ptmt != null) conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        new TestTransaction().transferAccount();
    }
}

上述代码的核心逻辑,使用 conn.setAutoCommit(false) 开启事务,使用 conn.commit() 提交事务。当有数据库异常发生时,使用 conn.rollback() 回滚事务。

4、给事务设置断点

使用 Connection 对象的 setSavePoint() 方法,可以给事务设置断点。当事务执行过程遇到异常时,我们可以回滚到指定的断点处,然后再执行其它数据库相关操作。

Web开发|11|数据库技术_第27张图片
给事务设置断点

使用 setSavePoint() 给事务设置断点,异常发生时回滚到指定断点处。示例代码如下:

public static BasicDataSource ds = null;

public static void testSavePoint() {
    Connection conn = null;
    PreparedStatement ptmt = null;
    Savepoint sp = null;
    try {
        conn = ds.getConnection();
        // 开启事务
        conn.setAutoCommit(false);
        ptmt = conn.prepareStatement("update user set account = ? where username = ?");
        ptmt.setInt(1, 0);
        ptmt.setString(2, "ZhangSan");
        ptmt.execute();
        sp = conn.setSavePoint();
        ptmt.setInt(1, 100);
        ptmt.setString(2, "LiSi");
        ptmt.execute();
        // 手动抛出一个异常
        throw new SQLException();
    } catch (SQLException e) {
        if (conn != null) {
            try {
                // 回滚到指定断点处
                conn.rollback(sp);
                // 从断点处继续执行其它数据库操作
                ptmt.setInt(1, 0);
                ptmt.setString(2, "WangWu");
                ptmt.execute();
                // 提交事务
                ptmt.commit();
            } catch (SQLException e) {
                e.printStackTrace();
            }

        }
        e.printStackTrace();
    } finally {
        try {
            if (conn != null) conn.close();
            if (ptmt != null) conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

5、事务并发执行 与 事务隔离级别

数据库事务的四大特征,其中原子性、一致性、持久性是比较容易理解的,难点在于“隔离性”。“隔离性”之所以相对难于理解,是因为它伴随着数据库的并发访问与操作。下面我们继续以银行转账为例,来了解一下事务在并发执行过程中可能遇到的一些场景。

Web开发|11|数据库技术_第28张图片
事务并发执行

场景1:脏读

如果一个事务读取了另一个事务尚未提交更新的数据,即被称为“脏读”。

Web开发|11|数据库技术_第29张图片
脏读

场景2:不可重复读

在同一个事务中,两次读取同一条记录,其读取结果不一致,即被称为“不可重复读”。

Web开发|11|数据库技术_第30张图片
不可重复读

场景3:幻读

在同一个事务中,两次读取的结果所包含的行记录数不一致,即被称为“幻读”。“幻读”和“不可重复读”的区别在于,前者读取的是行记录数,后者读取的是行记录的值。

Web开发|11|数据库技术_第31张图片
幻读

基于上述 3 种并发事务场景,就有了如下的 4 种事务隔离级别。

四种事务隔离级别

级别1:读未提交(read uncommitted),该级别下允许出现“脏读”的情况,即可以出现“一个事务读取到另一个事务尚未提交更新的数据”的情况。

级别2:读提交(read committed),该级别下不允许出现“脏读”,但允许出现“不可重复读”的情况,即在同一事务中可以出现“两次读取同一记录的结果可以不一致”的情况。

级别3:重复读(repeatable read),该级别下不允许出现“不可重复读”,但允许出现“幻读”,即在同一事务中可以出现“两次读取的结果所包含的行记录数不一致”的情况。

级别4:串行化(serializable),它是事务隔离的最高级别,它不允许出现“幻读”。它的事务并发控制最为严格,所有事务都是串行执行的,它的缺点是它会导致数据库的性能较差。

Web开发|11|数据库技术_第32张图片
4 种事务隔离级别

MySQL 默认的事务隔离级别是“重复读”级别(repeatable read)。对数据库来讲,事务隔离级别越高,数据库的性能将越差。对编程开发者来讲,事务隔离级别越高,编程的难度就越低。

那么,在 JDBC 中如何设置事务的隔离级别呢?

Web开发|11|数据库技术_第33张图片
设置事务隔离级别

6、什么是“死锁”?有什么特征?

Web开发|11|数据库技术_第34张图片
什么是死锁?

死锁一定是发生在多个事务之间的,单个事务是不会发生死锁的。死锁一定是因为多个事务并发竞争“锁资源”而导致的。死锁是因为“行加锁”顺序冲突而导致的。死锁通常需要外部干预来解决和处理。

7、死锁情景再现
上述我们学习了事务的 ACID 特性,尤其是“隔离性”。为了避免事务之间的相互影响,数据库通常会使用行加锁的方式对事务进行隔离。如果我们对数据库加锁不太了解的话,难免会遇到“死锁”的问题,下面我就来还原一下死锁情景,分析一下死锁发生的根本原因。

继续以银行转账任务为例,如下图所示,事务一和事务二并发执行。


Web开发|11|数据库技术_第35张图片
事务并发执行

MySQL 是以“行加锁”的方式来避免多个事务同时操作同一行记录的
事务一和事务二并发执行。事务一要修改 ID=1 行记录的“account”字段,因此事务一持有 ID=1 行记录的“行锁”。事务二修改 ID=1 和 ID=2 的行记录的“Gorp”字段,事务二持有 ID=2 行记录的“行锁”。此时此刻,两个事务分别持有一个行锁,并且等待另一个行锁,从而导致两个事务相互等待,这种相互等待的状态,即是“死锁”。

Web开发|11|数据库技术_第36张图片
两个事务相互等待

8、哪些必要条件和场景下会发生“死锁”现象?

场景1:互斥场景
并发执行的事务为了进行必要的隔离以保证执行正确,则在事务执行过程中需要对修改的数据库记录进行“持锁”,从而保证多个事务对相同数据库记录串行修改,这就是“互斥”场景。但是对于大型并发系统来讲,这种“互斥”是无法避免的,因此在这种场景下会发生“死锁”。

场景2:请求与保持
一个事务已经持有了某个资源锁,仍在等待另一个资源锁,这就“请求与保持”场景。在这样的情景下,如果有多个事务彼此等待着对方所持有的资源锁,“死锁”也就发生了。在大型应用中,这种场景是很常见的。

场景3:不剥夺
已经获得锁资源的事务,在未执行完毕之前,是不能被强制剥夺锁资源的;有且仅当事务执行完毕后,才能由事务自己去释放锁资源,这就是“不剥夺”场景。
基于这种“不剥夺”的特征,当实际发生死锁时,我们通常的解决方案是破坏这个“不剥夺”的条件以达到解除死锁的目的,具体的做法是数据库系统通过一定的死锁检测机制发现死锁,并强制回滚代价相对较小的事务,释放掉这个代价相对较小的事务所持有的资源锁。(这种解决方案,可以类比成十字路口堵车,交警会要求车辆较少的一方倒车后退以让出空间,从而达到疏通道路的目的。)

场景4:环路等待
如果多个事务交叉等待另一个事务所持有的资源锁,从而形成了环形等待状态时,这就是“环路等待”场景,即事务一等待事务二的资源锁,事务二等待事务三的资源锁,事务三等待事务N 的资源锁,事务N 等待事务一的资源锁。
当发生这种“环路等待”死锁时,我们的解决方案是按照一定的顺序获取资源锁,以破坏这个“环路”条件,通过分析死锁事务之间的锁竞争关系,调整 SQL 顺序,从而达到消除死锁的目的。按序获取锁资源,是我们最常用的解决方案。

9、MySQL 中有哪几种类型的锁?分别有什么特点?

MySQL 中有两种锁,分别是排它锁和共享锁。

排它锁,所谓排它锁,即它和任何其它锁都是冲突的。如果给一个事务添加了排它锁,那么其它任何事务都必须等待这个事务完成后才能再获取到这个锁资源。

共享锁,所谓共享锁,即多个事务可以共享一个锁资源。如果事务一获取了一个共享锁,事务二如果也将其设置为共享锁,则事务二可以直接获取这个锁资源而无须等待;如果事务二将其设置为排它锁,则事务二必须等待。

Web开发|11|数据库技术_第37张图片
排它锁、共享锁

从上表中可以看出,如果一个事务持有“排它锁”,则其它所有事务都不能获取这个资源锁,从而保证了这个事务顺利地执行,即避免发生死锁现象。
如果一个事务持有“共享锁”,则其它持有“共享锁”的事务可以同时获取到这个资源锁;其它持有“排它锁”的事务则必须等待。

有哪两种加锁方式?

MySQL 数据库支持两种加锁方式,分别是外部加锁和内部加锁。外部加锁,是由应用程序显示地指定 SQL 语句进行加锁,这种方式的加锁相对容易分析。内部加锁,是由数据库系统内部自动地隐式地加锁。
学习要求是,对外部加锁要能够熟练掌握,对内部加锁仅作了解即可。更多关于 MySQL 加锁机制和原理,可以参考相关专业书籍。

Web开发|11|数据库技术_第38张图片
Snipaste_2018-09-07_11-19-08.png

关于加锁方式,举例说明:

Web开发|11|数据库技术_第39张图片
“内部加锁”方式
上图 SQL 语句属于“内部加锁”方式,Update 数据库操作,数据库给行记录添加的是“排它锁”。

Web开发|11|数据库技术_第40张图片
“外部加锁”方式

上图 SQL 语句属于“外部加锁”方式,"in share mode"显示地给行记录添加了“共享锁”。

哪些 SQL 语句需要持有锁?哪些 SQL 语句不需要持有锁?
在 MySQL 数据库中,所有的 select 读都是“快照读”,因为“快照读”是不需要加锁的,所以所有的 select 读操作都是不需要加锁的。“快照读”具有以下特征:

Web开发|11|数据库技术_第41张图片
快照读 不需要加锁

以下数据库操作,都属于“当前读”,是需要加锁的。其中前两条 SQL 语句属于“外部加锁”方式,后面三条 SQL 语句属于“内部加锁”方式。
Web开发|11|数据库技术_第42张图片
当前读 需要加锁

10、如何捕获“死锁”发生的原因呢?

MySQL 数据库会自动地检测死锁,并强制回滚代价更小的事务。MySQL 会自动地帮助我们解除这种“死锁”现象,无须我们做任何处理。但是,我需要在死锁解除以后去分析死锁发生的原因,以找出导致死锁的 SQL 语句并做相应修改(修改 SQL 语句的执行顺序,或者修改 SQL 语句的加锁方式等),从而避免死锁再次发生。那么该如何捕获导致死锁发生的 SQL 语句呢?

Web开发|11|数据库技术_第43张图片
查看“死锁”发生的原因

如上图,在命令行中我们使用 show engine innodb status 命令就可以在死锁发生后查看本次导致死锁发生的原因,可以查看到导致死锁发生的 SQL 语句,可以查看到 MySQL 系统强制回滚了哪个事务。根据这些有效信息,我们就可以修复应用程序中事务的 SQL 语句及其执行顺序,以避免以后再次发生死锁。


本节完!!!

你可能感兴趣的:(Web开发|11|数据库技术)