springboot中操作MySQL事務


使用JDBC來操作數據庫

1、JDBC

沒有JDBC:

有了JDBC:

JDBC是Sun公司為了簡化和統一java連接數據庫的一套規范接口,定義的一套API

JDBC和驅動之間的關系:是實現類的關系。而每個數據庫廠商都會提供對應的驅動,也就是實現類的jar包來進行操作數據庫。

JDBC是規范,而響應的驅動是每個數據庫廠商提供對JDBC規范的實現,每個廠商的實現方式不同。但是用JDBC規范就可以統一的來對數據庫進行相同的操作,屏蔽了底層的實現細節,簡化開發人員對每一個數據庫的開發。

2、快速入門

准備數據庫表:

create database jdbc;
create table user(
	id int primary key auto_increment,
	username varchar(20),
	password varchar(20),
	nickname varchar(20)
);

INSERT INTO `USER` VALUES(null,'zs','123456','老張');
INSERT INTO `USER` VALUES(null,'ls','123456','老李');
INSERT INTO `USER` VALUES(null,'wangwu','123','東方不敗');

編寫JDBC代碼:

public class JDBCTest {
    public static void main(String[] args) throws SQLException {
        //注冊驅動
        DriverManager.registerDriver(new Driver());
        String url = "jdbc:mysql://localhost:3306/jdbc";
        String user = "root";
        String password = "root";
        //獲得連接。根據ulr、username、password來連接指定的數據庫
        Connection connection = DriverManager.getConnection(url, user, password);
        //創建執行sql語句對象
        Statement statement = connection.createStatement();
        //執行sql,處理結果
        String sql = "select * from user";
        ResultSet resultSet = statement.executeQuery(sql);
        while (resultSet.next()) {
            System.out.println(resultSet.getObject(1));
            System.out.println(resultSet.getObject(2));
            System.out.println(resultSet.getObject(3));
            System.out.println(resultSet.getObject(4));
        }
        //關閉資源
        if(resultSet != null){
            resultSet.close();
        }
        if(statement  != null){
            statement .close();
        }
        if(connection != null){
            connection.close();
        }
    }
}

3、API中的詳細說明:

0、注冊驅動(將對使用的數據庫的JDBC的實現注冊到內存中);

1、從驅動管理器中獲取得到對應的連接。(java程序和數據庫的連接,操作很重);

2、編寫SQL;

3、獲取得到連接之后,需要從連接中獲取得到執行數據庫中SQL的執行器對象;

4、執行器對象執行完成之后需要得到對應的響應結果;

3.1、DriverManager

利用這個類中的靜態代碼塊注冊驅動:

static {
	try {
		java.sql.DriverManager.registerDriver(new Driver());
	} catch (SQLException E) {
		throw new RuntimeException("Can't register driver!");
	}
}

翻閱源碼發現,通過API的方式注冊驅動,Driver會new兩次,所有推薦這種寫法:

Class.forName("com.mysql.jdbc.Driver");  //當前就理解成 可以讓com.mysql.jdbc.Driver里面的靜態代碼塊執行

3.2、Connection

接口的實現在數據庫驅動中。所有與數據庫交互都是基於連接對象的。

這個類提供了兩個方法:

createStatement() ;創建執行sql語句對象

prepareStatement(String sql) ;創建預編譯執行sql語句的對象

3.3、Statement接口

接口的實現在數據庫驅動中. 用來操作sql語句(增刪改查),並返回相應結果對象

ResultSet  executeQuery(String sql) 根據查詢語句返回結果集。只能執行**select**語句。

int executeUpdate(String sql) 根據執行的DML(insert update delete)語句,返回受影響的行數。

boolean execute(String sql)  此方法可以執行任意sql語句。返回boolean值. 【了解】

		true:  執行select有查詢的結果

		false:  執行insert, delete,update, 執行select沒有查詢的結果

所以最常用的就是上面的兩個方法:

ResultSet  executeQuery(String sql) 根據查詢語句返回結果集。只能執行**select**語句。

int executeUpdate(String sql) 根據執行的DML(insert update delete)語句,返回受影響的行數。

3.3.1、PreparedStatement

看一下名字的,准備SQL的執行。那么是干嘛的呢?下面來通過一個案例來進行演示:

public class JDBCTest2 {
    public static void main(String[] args) throws SQLException, ClassNotFoundException {

        //1.獲得用戶輸入的用戶名和密碼
        Scanner scanner = new Scanner(System.in);
        System.out.println("請輸入用戶名:");
        String username = scanner.nextLine();
        System.out.println("請輸入密碼:");
        String password = scanner.nextLine();

        //1.獲得用戶輸入的用戶名和密碼
        String systemUsername = "root";
        String systemPassword = "root";
        //2.通過Jdbc,根據用戶名和密碼查詢數據庫,封裝成User對象
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbc",systemUsername,systemPassword);
        Statement statement = connection.createStatement();
        String sql = "SELECT * FROM user WHERE username = '" + username + "' AND password = '" + password + "'";
        ResultSet resultSet = statement.executeQuery(sql);
        User user = null;
        while (resultSet.next()) {
            user = new User(
                    resultSet.getInt("id"),
                    resultSet.getString("username"),
                    resultSet.getString("password"),
                    resultSet.getString("nickname")
            );
        }
        //關閉資源
        if (resultSet != null) {
            resultSet.close();
        }
        if (statement != null) {
            statement.close();
        }
        if (connection != null) {
            connection.close();
        }
        //3. 判斷是否登錄成功(說白了就是判斷user是否為null)
        if (user != null) {
            //3.1 不為null, 打印 '登錄成功'
            System.out.println("登錄成功!歡迎回來:" + user.getNickname());
        } else {
            //3.2 為null, 打印 '登錄失敗'
            System.err.println("登錄失敗!");
        }
    }
}

輸入一個數據庫中的賬號即可,即可顯示登錄成功。

但是這里有一個漏洞!下面來進行演示一下:

在輸入密碼的時候:在密碼后面追加

請輸入用戶名:
zs
請輸入密碼:
fkjdslkfd' or '' = '

可以查看控制台,發現登錄成功了。這種是一個問題,業界稱之為SQL注入

利用preparedStatement來進行解決問題。preparedStatement的作用是預編譯SQL語句對象, 是Statement對象的子接口。

而且preparedStatnment的特點是:

  • 性能要比Statement高
  • 會把sql語句先編譯,格式固定好,
  • sql語句中的參數會發生變化,過濾掉用戶輸入的關鍵字(eg: or)

使用:

- connection.prepareStatement(String sql) ;創建prepareStatement對象

- sql表示預編譯的sql語句,如果sql語句有參數通過?來占位
    
SELECT * FROM user WHERE username = ? AND password = ?    

將我們輸入的值用?來進行替代,用戶輸入的值作為真正的值。那么如何來進行設置參數:

prepareStatement.set類型(int i,Object obj);參數1 i 指的就是問號的索引(指第幾個問號,從1開始),參數2就是值

eg: setString(1,"zs"); setString(2,"123456");

Demo:

//創建預編譯的SQL語句對象(SQL參數需要使用?占位)
String sql = "SELECT * FROM user WHERE username  = ? AND password = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//設置參數, 執行(還是executeQuery()和executeQUpdate(), 但是不需要再傳入SQL語句, 上面已經傳入了)
preparedStatement.setString(1,username);
preparedStatement.setString(2,password);
ResultSet resultSet = preparedStatement.executeQuery();

這樣子再來進行操作的時候,發現可以來預防SQL注入問題。那么以后可以來使用這種方式來進行使用。

3.4、ResultSet接口

  1. 封裝結果集,查詢結果表的對象;

    提供一個游標,默認游標指向結果集第一行之前。

    調用一次next(),游標向下移動一行。

    提供一些get方法。

  2. ResultSet接口常用API

    • boolean next();將光標從當前位置向下移動一行(記錄)
    • int getInt(int colIndex)以int形式獲取ResultSet結果集當前行指定列號值
    • int getInt(String colLabel)以int形式獲取ResultSet結果集當前行指定列名值
    • float getFloat(int colIndex)以float形式獲取ResultSet結果集當前行指定列號值
    • float getFloat(String colLabel)以float形式獲取ResultSet結果集當前行指定列名值
    • String getString(int colIndex)以String 形式獲取ResultSet結果集當前行指定列號值
    • String getString(String colLabel)以String形式獲取ResultSet結果集當前行指定列名值
    • Date getDate(int columnIndex); 以Date 形式獲取ResultSet結果集當前行指定列號值
    • Date getDate(String columnName);以Date形式獲取ResultSet結果集當前行指定列名值
    • void close()關閉ResultSet 對象(關閉當前查詢的結果)

通過一個例子來查看一下封裝過程:

public class JDBCTest1 {
    public static void main(String[] args) throws SQLException {
        //注冊驅動
        DriverManager.registerDriver(new Driver());
        String url = "jdbc:mysql://localhost:3306/jdbc";
        String user = "root";
        String password = "root";
        //獲得連接。根據ulr、username、password來連接指定的數據庫
        Connection connection = DriverManager.getConnection(url, user, password);
        //創建執行sql語句對象
        Statement statement = connection.createStatement();
        //執行sql,處理結果
        String sql = "select * from user";
        ResultSet resultSet = statement.executeQuery(sql);
        List<User> userList = new ArrayList<>();
        while (resultSet.next()) {
            User user1 = new User();
            // 將xxxx類型的數據轉換成yyy類型的數據。決定了使用還能哪一種getYY來進行使用
            user1.setId(resultSet.getInt("id"));
            user1.setUsername(resultSet.getString("username"));
            user1.setPassword(resultSet.getString("password"));
            user1.setNickname(resultSet.getString("nickname"));
            userList.add(user1);
        }
        userList.forEach(System.out::println);
        //關閉資源
        if(resultSet != null){
            resultSet.close();
        }
        if(statement  != null){
            statement .close();
        }
        if(connection != null){
            connection.close();
        }
    }
}

問題:

0、對於上面的

1、java代碼和SQL耦合性太高。這個勢必要解決,索性框架已經做的很好了,比如說mybatis、mybatis-plus

2、對於查詢結果和執行SQL語句的對象的關閉來說,這個效率比較小。但是對於數據庫連接來說,這個是一個很重的操作。

所以需要解決獲取得到數據庫連接這個重量級別的操作方式。

4、數據源的配置

為什么需要數據庫連接池?

Connection對象在JDBC使用的時候就會去創建一個對象,使用結束以后就會將這個對象給銷毀了(close).每次創建和銷毀對象都是耗時操作.需要使用連接池對其進行優化. 數據庫的連接建立,開銷比較大,所以在一開始就用一個池子來裝若干個連接對象。

​ 程序初始化的時候,初始化多個連接,將多個連接放入到池(集合)中.每次獲取的時候,都可以直接從連接池中進行獲取.使用結束以后,將連接歸還到池中.

  1. 開始先使用集合存儲若干個連接對象到集合中
  2. 如果有程序需要使用連接,就從連接池中獲取
  3. 使用完畢后,要記得歸還而不是關閉連接
  4. 如果連接池中的連接對象已經使用完畢,再來一個新的程序想要連接,那么可以創建一個新的連接給它。使用完畢之后,可以歸還到連接池里面,也可以選擇不歸還。

現在主流的數據庫連接池是Druid數據庫連接池,那么接下來來進行演示一下:

下面將會在springboot項目中來演示數據源的配置。

首先去查看官方對jdbc操作的支持:

https://docs.spring.io/spring-boot/docs/2.4.13/reference/html/using-spring-boot.html#using-boot-starter

可以看到存在着對應的jdbc的starter

創建好項目之后,找到自動配置類:

@Configuration(
    proxyBeanMethods = false
)
// 要有數據庫連接池的配置類才會生效和數據庫的類型配置類
@ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class})
// 響應式
@ConditionalOnMissingBean(
    type = {"io.r2dbc.spi.ConnectionFactory"}
)
// 綁定的配置類
@EnableConfigurationProperties({DataSourceProperties.class})
// 配置信息
@Import({DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class})
public class DataSourceAutoConfiguration {

這里的自動配置類中的屬性就代表着我們可以在配置文件中寫上哪些:

@ConfigurationProperties(
    prefix = "spring.datasource"
)
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
    private ClassLoader classLoader;
    private String name;
    private boolean generateUniqueName = true;
    private Class<? extends DataSource> type;
    private String driverClassName;
    private String url;
    private String username;
    private String password;
    private String jndiName;
    private DataSourceInitializationMode initializationMode;
    private String platform;
    private List<String> schema;
    private String schemaUsername;
    private String schemaPassword;
    private List<String> data;
    private String dataUsername;
    private String dataPassword;
    private boolean continueOnError;
    private String separator;
    private Charset sqlScriptEncoding;
    private EmbeddedDatabaseConnection embeddedDatabaseConnection;
    private DataSourceProperties.Xa xa;
    private String uniqueName;

接着往下看:

    @Configuration(
        proxyBeanMethods = false
    )
    @Conditional({DataSourceAutoConfiguration.PooledDataSourceCondition.class})
    @ConditionalOnMissingBean({DataSource.class, XADataSource.class})
    @Import({Hikari.class, Tomcat.class, Dbcp2.class, OracleUcp.class, Generic.class, DataSourceJmxConfiguration.class})
    protected static class PooledDataSourceConfiguration {
        protected PooledDataSourceConfiguration() {
        }
    }

如果沒有配置數據庫連接池,那么將會配置@Import中的連接池。而在springboot項目中默認使用的是Hikari的數據庫連接池。

那么來在數據庫中來進行配置。但是如果導入了依賴,沒有來進行配置,那么這將會來報錯:

Consider the following:
	If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
	If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).

導入基本配置:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/jdbc?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

接下來使用spring提供的jdbcTemplate組件來操作一下數據庫:

那么查看一下jdbcTemplate的自動配置:

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnMissingBean({JdbcOperations.class})
class JdbcTemplateConfiguration {
    JdbcTemplateConfiguration() {
    }

    @Bean
    @Primary
    JdbcTemplate jdbcTemplate(DataSource dataSource, JdbcProperties properties) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        Template template = properties.getTemplate();
        jdbcTemplate.setFetchSize(template.getFetchSize());
        jdbcTemplate.setMaxRows(template.getMaxRows());
        if (template.getQueryTimeout() != null) {
            jdbcTemplate.setQueryTimeout((int)template.getQueryTimeout().getSeconds());
        }

        return jdbcTemplate;
    }
}

可以配置的信息非常少,而且已經將其放置到了Bean容器中去;

配置Druid官方文檔:https://github.com/alibaba/druid/wiki

5、事務

什么是事務?

百度百科解釋如下:

在關系數據庫中,一個事務可以是一條SQL語句,一組SQL語句或整個程序。

我覺得事務是開啟事務、操作數據庫的SQL語句和提交事務(回滾事務)的一系列的組合。

對應的SQL語句代碼Demo如下所示:

try{
	connection.setAutoCommit(false); //開啟事務
	...操作數據庫
	connection.commit(); //提交事務
}catch(Exection e){
	connection.rollback(); //回滾事務
}finally{
	...釋放資源
}

那么先來進行演示一下,然后進行詳細說明:

數據庫表准備:

create table account(
    id int primary key auto_increment,
    name varchar(20),
    money double
);

insert into account values (null,'zs',1000);
insert into account values (null,'ls',1000);
insert into account values (null,'ww',1000);

需求:zs給ls轉100, 使用事務進行控制

public class JDBCTest3 {
    public static void main(String[] args) throws SQLException, ClassNotFoundException {
        //1.獲得用戶輸入的用戶名和密碼
        String systemUsername = "root";
        String systemPassword = "root";
        //2.通過Jdbc,根據用戶名和密碼查詢數據庫,封裝成User對象
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbc",systemUsername,systemPassword);
        // 前提邏輯是首先判斷轉賬人的金額是否應該大於等於要轉賬的金額。但是這些不做這些復雜的操作。
        String subMoney = "update account set money=money-200 where name = 'zs'";
        String addMoney = "update account set money=money+200 where name = 'ls'";
        PreparedStatement preparedStatement1 = connection.prepareStatement(subMoney);
        PreparedStatement preparedStatement2 = connection.prepareStatement(addMoney);
        preparedStatement1.execute();
        preparedStatement2.execute();
        System.out.println("執行成功");
        //關閉資源
        if (preparedStatement1 != null&&preparedStatement2 != null) {
            preparedStatement1.close();
            preparedStatement2.close();
        }
        if (connection != null) {
            connection.close();
        }
    }
}

這里執行是正常的。

但是我現在想要模擬一下在對操作的過程中如果出現了異常,那么導致的結果是什么?

        preparedStatement1.execute();
        int i = 1 / 0;
        preparedStatement2.execute();

在中間來添加一行代碼來進行實現。這個時候再去查詢數據庫的數據:

+----+------+-------+
| id | name | money |
+----+------+-------+
|  1 | zs   |   800 |
|  2 | ls   |  1000 |
|  3 | ww   |  1000 |
+----+------+-------+

可以看到zs扣款成功,但是ls卻並沒有得到轉賬的錢。所以這里的操作不合乎邏輯。如果在操作中出現了問題,那么即使zs扣款成功了,但是因為ls沒有加上錢,那么zs的錢應該還原。而不是像現在這種方式。

那么針對這種操作,JDBC也是考慮到了,那么看一下jdbc提供的規范。

Connection中與事務有關的方法 說明
setAutoCommit(boolean autoCommit) 參數是true或false 如果設置為false,表示關閉自動提交,相當於開啟事務; 類似sql里面的 start transaction;
void commit() 提交事務; 類似sql里面的 commit;
void rollback() 回滾事務; 類似sql里面的 rollback;

這個是從connection中獲取得到的對事物的操作:

那么將上面的操作應用到java代碼里面來。

/**
 * zs給ls轉100, 使用事務進行控制
 * <p>
 * +----+------+-------+
 * | id | name | money |
 * +----+------+-------+
 * |  1 | zs   |  1000 |
 * |  2 | ls   |  1000 |
 * |  3 | ww   |  1000 |
 * +----+------+-------+
 */
public class JDBCTest3Pro {
    public static void main(String[] args)  {


        //1.獲得用戶輸入的用戶名和密碼
        String systemUsername = "root";
        String systemPassword = "root";
        //2.通過Jdbc,根據用戶名和密碼查詢數據庫,封裝成User對象
        Connection connection = null;
        PreparedStatement preparedStatement1 = null;
        PreparedStatement preparedStatement2 = null;
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
            connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbc", systemUsername, systemPassword);
            // 前提邏輯是首先判斷轉賬人的金額是否應該大於等於要轉賬的金額。但是這些不做這些復雜的操作。
            String subMoney = "update account set money=money-200 where name = 'zs'";
            String addMoney = "update account set money=money+200 where name = 'ls'";
            preparedStatement1 = connection.prepareStatement(subMoney);
            preparedStatement2 = connection.prepareStatement(addMoney);
            connection.setAutoCommit(false);
            preparedStatement1.execute();
            int i = 1 / 0;
            preparedStatement2.execute();
            // 如果SQL執行正常,那么應該進行回滾
            connection.commit();
        } catch (Exception throwables) {
            // 如果出現異常了,那么應該將對應的SQL操作進行回滾
            // TODO:回滾和提交使用的是同一個數據庫連接。如果是兩個連接,那么這么來進行操作的話也是沒有關系的
            try {
                connection.rollback();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            throwables.printStackTrace();
        }finally {
            //關閉資源
            if (preparedStatement1 != null && preparedStatement2 != null) {
                try {
                    preparedStatement1.close();
                    preparedStatement2.close();
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                }

            }
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                }
            }
        }

    }
}

這里需要注意的是:對事物的操作需要提供同一個數據庫連接。在try中將可能出現異常的代碼try起來,然后如果出現了catch住,然后最終將資源關閉住。因為在以后的connection使用過程中要注意一個事務中是否使用的是同一個連接。這個至關重要。

那么這里通過一個案例來演示一下springboot中的事務。

鏈接地址:https://mp.weixin.qq.com/s/3d4yeg3CIuFEa-4gkelObQ

@Service
public class ServiceOne{
    // 設置一把可重入的公平鎖
    private Lock lock = new ReentrantLock(true);
    
    @Transactional(rollbackFor = Exception.class)
    public Result  func(long seckillId, long userId) {
        lock.lock();
        // 執行數據庫操作——查詢商品庫存數量
        // 如果 庫存數量 滿足要求 執行數據庫操作——減少庫存數量——模擬賣出貨物操作
        lock.unlock();
    }
}

首先看一下MySQL數據庫的隔離機制:

對於MySQL來說,默認的隔離級別是可重復讀。那么這里可能存在的問題是幻讀,也就是兩個查詢結果條數可能不同。

然后針對上面的代碼來說,主要是兩個問題:

1、顯然事務的開啟一定是在 lock 之后的?

2、lock.unlock()方法的執行是在事務提交之前還是之后?

那么針對於上面的兩個問題,首先來定位一下事務的開啟時機。簡單的看一下spring的事務處理:

org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin

看一下下面圈起來的地方,對應的中文翻譯是:把數據庫連接切換為手動提交。

把連接的 AutoCommit 參數從 ture 修改為 false。

那么這個時候的事務還沒有開啟,只是事務處於就緒狀態而已。啟動和就緒還是有一點點差異的,就緒是啟動之前的步驟。

那么查看一下MySQL事務的選項:

MySQL默認采用自動提交(AUTOCOMMIT)模式,不是顯示的開啟一個事務,每個查詢都被當作一個事務執行提交操作。

在當前連接中,可以通過設置AUTOCOMMIT變量來開啟或者禁用自動提交功能。

mysql> show variables like  'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set, 1 warning (0.00 sec)
    
1或者ON表示開啟;0或者OFF表示禁用。
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.11 sec)
當 autocommit = 0 時,所有的查詢都在一個事務中,直到顯示的執行 commit 進行提交或者 rollback 進行回滾,該事務才最終結束,同時開啟了另一個事務。    

參考博文:https://www.cnblogs.com/jiangxiaobo/p/11648943.html

但是我常用的方式是直接begin然后進行commit或者是rollback操作;

但是這里有個問題:一旦開始進行begin操作的時候,如果多條SQL執行成功,而個別執行失敗,在進行commit的時候,將會把執行成功的插入到庫中去,所以一旦有這種情況的發生,那么應該執行手動執行rollback操作;

那么總結一下事務的啟動有哪些方式呢?

  • 第一種:使用啟動事務的語句,這種是顯式的啟動事務。比如 begin 或 start transaction 語句。與之配套的提交語句是 commit,回滾語句是 rollback。
  • 第二種:autocommit 的值默認是 1,含義是事務的自動提交是開啟的。如果我們執行 set autocommit=0,這個命令會將這個線程的自動提交關掉。意味着如果你只執行一個 select 語句,這個事務就啟動了,而且並不會自動提交。這個事務持續存在直到你主動執行 commit 或 rollback 語句,或者斷開連接。

很顯然,在 Spring 里面采用的是第二種方式。

而上面的代碼 con.setAutoCommit(false) 只是把這個鏈接的自動提交關掉。

事務真正啟動的時機是什么時候呢?

前面說的 begin/start transaction 命令並不是一個事務的起點,在執行到它們之后的第一個操作 InnoDB 表的語句,事務才算是真正啟動。

如果你想要馬上啟動一個事務,可以使用 start transaction with consistent snapshot 這個命令。需要注意的是這個命令在讀已提交的隔離級別(RC)下是沒意義的,和直接使用 start transaction 一個效果。

回到在前面的問題:什么時候才會執行第一個 SQL 語句?就是在 lock 代碼之后。所以,顯然事務的開啟一定是在 lock 之后的。

這一個簡單的“顯然”,先給大家鋪墊一下。接下來,給大家上個動圖看一眼,更加直觀。首先說一下這個 SQL:

select * from information_schema.innodb_trx;

不多解釋,你只要知道這是查詢當前數據庫有哪些事務正在執行的語句就行。

你就注意看下面的動圖,是不是第 27 行查詢語句執行完成之后,查詢事務的語句才能查出數據,說明事務這才真正的開啟:

那么我在本地數據庫操作一下,可以看到相同的效果;

那么可以總結出來begin的時候,事務並非真正的開啟,而是當SQL真正執行的時候事務開始真正發揮作用。

最后,我們把目光轉移到這個方法的注釋上:

寫這么長一段注釋,意思就是給你說,這個參數我們默認是 ture,原因就是在某些 JDBC 的驅動中,切換為自動提交是一個很重的操作。

那么在哪設置的為 true 呢?沒看到代碼,我一般是不死心的。所以,一起去看一眼。setAutoCommit 這個方法有好幾個實現類,我也不知道具體會走哪一個,所以在接口上打一個斷點:

java.sql.Connection#setAutoCommit

那么idea會自動執行到對應的實現類中的setAutoCommit方法上來:

所以,我是怎么知道在這個地方打斷點的呢?答案就是調用棧。先給大家看一下我的代碼:

啥也先不管,上來就先在 26 行,方法入口處打上斷點,跑起來:

誒,你看這個調用棧,我框起來的這個地方:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM