并发情况下数据校验-基于数据库实现

并发情况下数据校验-基于数据库实现

  • 数据库行锁
    • 原理
    • 实际操作
      • 数据准备
      • 开启事务,更新数据
  • 项目实战
    • 项目配置
    • 多线程测试

在开发过程中,我们会遇到校验数据的唯一性,数据更新之后是否超过设置的阈值等等。并发情况下数据校验常见方式有使用分布式锁,数据库行锁等。本章介绍并发情况下使用数据库进行数据校验,常见的场景有:

  1. 金额扣减
  2. 抽奖奖品数量扣减
  3. 库存扣减

数据库行锁

原理

mysql数据库锁
悲观锁&乐观锁
Mysql 如何解决并发更新同一行数据
MySql MVCC 详解

实际操作

mysql 的默认引擎 InnoDB 支持行锁的,本节使用 Mysql 数据库来说明数据库行锁
在这里插入图片描述

数据准备

  1. 创建金额表
create table t_amount (
id int primary key auto_increment,
total_amount decimal(10,2) not null default 0,
used_amount decimal(10,2) not null default 0
) charset = utf8mb4;
  1. 插入测试数据
insert into t_amount(total_amount, used_amount) values(100, 0);
  1. 查询数据表
mysql> select * from t_amount;
+----+--------------+-------------+
| id | total_amount | used_amount |
+----+--------------+-------------+
|  1 |       100.00 |        0.00 |
+----+--------------+-------------+
1 row in set (0.00 sec)

开启事务,更新数据

  1. 打开2个终端,开启事务
start transaction;

在这里插入图片描述
start transaction 语句有啥作用?可以使用 help start transaction 命令查看

mysql> help start transaction
Name: 'START TRANSACTION'
Description:
Syntax:
START TRANSACTION[transaction_characteristic [, transaction_characteristic] ...]transaction_characteristic:WITH CONSISTENT SNAPSHOT| READ WRITE| READ ONLYBEGIN [WORK]
COMMIT [WORK] [AND [NO] CHAIN] [[NO] RELEASE]
ROLLBACK [WORK] [AND [NO] CHAIN] [[NO] RELEASE]
SET autocommit = {0 | 1}These statements provide control over use of transactions:o START TRANSACTION or BEGIN start a new transaction.o COMMIT commits the current transaction, making its changes permanent.o ROLLBACK rolls back the current transaction, canceling its changes.o SET autocommit disables or enables the default autocommit mode forthe current session.By default, MySQL runs with autocommit mode enabled. This means that as
soon as you execute a statement that updates (modifies) a table, MySQL
stores the update on disk to make it permanent. The change cannot be
rolled back.To disable autocommit mode implicitly for a single series of
statements, use the START TRANSACTION statement:START TRANSACTION;
SELECT @A:=SUM(salary) FROM table1 WHERE type=1;
UPDATE table2 SET summary=@A WHERE type=1;
COMMIT;With START TRANSACTION, autocommit remains disabled until you end the
transaction with COMMIT or ROLLBACK. The autocommit mode then reverts
to its previous state.START TRANSACTION permits several modifiers that control transaction
characteristics. To specify multiple modifiers, separate them by
commas.o The WITH CONSISTENT SNAPSHOT modifier starts a consistent read forstorage engines that are capable of it. This applies only to InnoDB.The effect is the same as issuing a START TRANSACTION followed by aSELECT from any InnoDB table. Seehttp://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.html.The WITH CONSISTENT SNAPSHOT modifier does not change the currenttransaction isolation level, so it provides a consistent snapshotonly if the current isolation level is one that permits a consistentread. The only isolation level that permits a consistent read isREPEATABLE READ. For all other isolation levels, the WITH CONSISTENTSNAPSHOT clause is ignored. A warning is generated when the WITHCONSISTENT SNAPSHOT clause is ignored.o The READ WRITE and READ ONLY modifiers set the transaction accessmode. They permit or prohibit changes to tables used in thetransaction. The READ ONLY restriction prevents the transaction frommodifying or locking both transactional and nontransactional tablesthat are visible to other transactions; the transaction can stillmodify or lock temporary tables.MySQL enables extra optimizations for queries on InnoDB tables whenthe transaction is known to be read-only. Specifying READ ONLYensures these optimizations are applied in cases where the read-onlystatus cannot be determined automatically. Seehttp://dev.mysql.com/doc/refman/8.0/en/innodb-performance-ro-txn.htmlfor more information.If no access mode is specified, the default mode applies. Unless thedefault has been changed, it is read/write. It is not permitted tospecify both READ WRITE and READ ONLY in the same statement.In read-only mode, it remains possible to change tables created withthe TEMPORARY keyword using DML statements. Changes made with DDLstatements are not permitted, just as with permanent tables.For additional information about transaction access mode, includingways to change the default mode, see [HELP ISOLATION].If the read_only system variable is enabled, explicitly starting atransaction with START TRANSACTION READ WRITE requires theCONNECTION_ADMIN or SUPER privilege.*Important*:Many APIs used for writing MySQL client applications (such as JDBC)
provide their own methods for starting transactions that can (and
sometimes should) be used instead of sending a START TRANSACTION
statement from the client. See
http://dev.mysql.com/doc/refman/8.0/en/connectors-apis.html, or the
documentation for your API, for more information.To disable autocommit mode explicitly, use the following statement:SET autocommit=0;After disabling autocommit mode by setting the autocommit variable to
zero, changes to transaction-safe tables (such as those for InnoDB or
NDB (http://dev.mysql.com/doc/refman/5.7/en/mysql-cluster.html)) are
not made permanent immediately. You must use COMMIT to store your
changes to disk or ROLLBACK to ignore the changes.autocommit is a session variable and must be set for each session. To
disable autocommit mode for each new connection, see the description of
the autocommit system variable at
http://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html.BEGIN and BEGIN WORK are supported as aliases of START TRANSACTION for
initiating a transaction. START TRANSACTION is standard SQL syntax, is
the recommended way to start an ad-hoc transaction, and permits
modifiers that BEGIN does not.The BEGIN statement differs from the use of the BEGIN keyword that
starts a BEGIN ... END compound statement. The latter does not begin a
transaction. See [HELP BEGIN END].*Note*:Within all stored programs (stored procedures and functions, triggers,
and events), the parser treats BEGIN [WORK] as the beginning of a BEGIN
... END block. Begin a transaction in this context with START
TRANSACTION instead.The optional WORK keyword is supported for COMMIT and ROLLBACK, as are
the CHAIN and RELEASE clauses. CHAIN and RELEASE can be used for
additional control over transaction completion. The value of the
completion_type system variable determines the default completion
behavior. See
http://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html.The AND CHAIN clause causes a new transaction to begin as soon as the
current one ends, and the new transaction has the same isolation level
as the just-terminated transaction. The new transaction also uses the
same access mode (READ WRITE or READ ONLY) as the just-terminated
transaction. The RELEASE clause causes the server to disconnect the
current client session after terminating the current transaction.
Including the NO keyword suppresses CHAIN or RELEASE completion, which
can be useful if the completion_type system variable is set to cause
chaining or release completion by default.URL: http://dev.mysql.com/doc/refman/8.0/en/commit.html

默认情况下, mysql 会自动自提交事务,执行 start transaction 不会自动提交事务,需要执行 commit 才会提交事务
在这里插入图片描述

  1. 执行更新数据SQL语句
    终端1执行更新已使用金额(used_amount)语句,终端1不提交事务,终端2也执行同样的sql语句
update t_amount set used_amount = used_amount + 2.11 where id = 1;

在这里插入图片描述
可以看到终端2的更新卡在这里不动了,此时这行数据已经被终端1锁住了,等待一段时间之后,发现终端2报错了
在这里插入图片描述

ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

锁等待时间超时,重新开始事务。mysql 锁的超时时间是可以设置的,参考博客mysql修改数据库锁超时时间。

mysql> show variables like 'innodb_lock_wait_timeout';
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| innodb_lock_wait_timeout | 50    |
+--------------------------+-------+
1 row in set, 1 warning (0.20 sec)

查询出锁等待超时时间为50秒。再次在终端2中执行更新sql,在行锁等待时间范围内,提交终端1的事务,此时终端2获取到行锁,可以更新数据了
在这里插入图片描述
3. 校验超过总金额SQL语句
该步骤模拟实际操作中,多个线程并发更新使用金额(used_amount),但是需要保证使用金额小于总金额(total_amount)。使用2个终端,分别开启事务,更新使用金额,在where条件之后校验是否小于等于总金额

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)mysql> update t_amount set used_amount = used_amount + 50 where id = 1 and used_amount + 50 <= total_amount;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

终端1执行commit; 之后,可以发现终端2在获取到行锁之后,更新的数据为0条Rows matched: 0 Changed: 0 Warnings: 0, 在业务代码中可以根据jdbc返回的update结果来确定是否更新成功了
在这里插入图片描述
查看此时的金额数据,发现使用金额小于总金额√
在这里插入图片描述

项目实战

在实际的项目使用多线程来测试并发情况下的数据校验,本节使用MyBatis框架来更新数据。

项目配置

  1. mybatis 配置文件 mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!--Copyright 2009-2017 the original author or authors.Licensed under the Apache License, Version 2.0 (the "License");you may not use this file except in compliance with the License.You may obtain a copy of the License athttp://www.apache.org/licenses/LICENSE-2.0Unless required by applicable law or agreed to in writing, softwaredistributed under the License is distributed on an "AS IS" BASIS,WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.See the License for the specific language governing permissions andlimitations under the License.-->
<!DOCTYPE configurationPUBLIC "-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration><!-- autoMappingBehavior should be set in each test case --><properties resource="templates/db.properties"/><environments default="development"><environment id="development"><transactionManager type="JDBC"><property name="" value=""/></transactionManager><dataSource type="POOLED"><property name="driver" value="${driverClassName}"/><property name="url" value="${jdbcUrl}"/><property name="username" value="${username}"/><property name="password" value="${password}"/></dataSource></environment></environments><mappers><mapper resource="mapper/AmountMapper.xml"/></mappers></configuration>
  1. 数据库配置文件 db.properties
driverClassName=com.mysql.cj.jdbc.Driver
jdbcUrl=jdbc:mysql://localhost:3306/bootdo?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
username=chengdu
password=chengdu
  1. 相关代码文件
    AmountMapper.xml 文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.scd.mapper.AmountMapper"><update id="updateUsedAmount">update t_amount set used_amount = used_amount + #{usedAmount}where id = 1 and used_amount + #{usedAmount} <![CDATA[<=]]> total_amount</update>
</mapper>

AmountMapper.java 文件

package com.scd.mapper;import java.math.BigDecimal;public interface AmountMapper {int updateUsedAmount(BigDecimal updateUsedAmount);
}

多线程测试

测试类 AmountTest.java

package com.scd.amount;import com.scd.mapper.AmountMapper;
import com.scd.sql.SqlTest;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.io.Reader;
import java.math.BigDecimal;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;public class AmountTest {private static final Logger LOGGER = LoggerFactory.getLogger(SqlTest.class);private SqlSessionFactory sqlSessionFactory;@Beforepublic void setUp() throws Exception {String resource = "templates/mybatis-config.xml";Reader reader = Resources.getResourceAsReader(resource);sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);}@Testpublic void testUpdateUsedAmount() {try (SqlSession sqlSession = sqlSessionFactory.openSession()) {AmountMapper amountMapper = sqlSession.getMapper(AmountMapper.class);int updated = amountMapper.updateUsedAmount(new BigDecimal("50.22"));if (updated == 0) {LOGGER.info("当前使用金额大于总金额");} else {LOGGER.info("更新成功");}}}@Testpublic void testMultiThreadUpdate() {ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 1,TimeUnit.HOURS,new ArrayBlockingQueue<>(100));CountDownLatch countDownLatch = new CountDownLatch(5);for (int i = 0; i < 5; i++) {threadPoolExecutor.execute(() -> {try (SqlSession sqlSession = sqlSessionFactory.openSession(false)) {AmountMapper amountMapper = sqlSession.getMapper(AmountMapper.class);int updated = amountMapper.updateUsedAmount(new BigDecimal("20"));sqlSession.commit();if (updated == 0) {LOGGER.info("当前使用金额大于总金额, 执行完成时间 " + System.currentTimeMillis());} else {LOGGER.info("更新成功, 执行完成时间 " + System.currentTimeMillis());}} finally {countDownLatch.countDown();}});}try {countDownLatch.await();} catch (InterruptedException e) {e.printStackTrace();}}
}

执行 testMultiThreadUpdate 方法,可以看到有部分线程更新成功,有些线程由于超过总金额未更新成功

17:47:43.540 [main] DEBUG org.apache.ibatis.logging.LogFactory - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
17:47:43.568 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections.
17:47:43.568 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections.
17:47:43.568 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections.
17:47:43.568 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections.
17:47:43.732 [pool-1-thread-1] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
17:47:43.732 [pool-1-thread-3] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
17:47:43.732 [pool-1-thread-2] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
17:47:43.732 [pool-1-thread-5] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
17:47:43.732 [pool-1-thread-4] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
17:47:44.772 [pool-1-thread-1] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 1212136459.
17:47:44.772 [pool-1-thread-1] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@483fbc0b]
17:47:44.776 [pool-1-thread-1] DEBUG com.scd.mapper.AmountMapper.updateUsedAmount - ==>  Preparing: update t_amount set used_amount = used_amount + ? where id = 1 and used_amount + ? <= total_amount
17:47:44.792 [pool-1-thread-4] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 538640972.
17:47:44.792 [pool-1-thread-4] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@201b024c]
17:47:44.792 [pool-1-thread-4] DEBUG com.scd.mapper.AmountMapper.updateUsedAmount - ==>  Preparing: update t_amount set used_amount = used_amount + ? where id = 1 and used_amount + ? <= total_amount
17:47:44.804 [pool-1-thread-4] DEBUG com.scd.mapper.AmountMapper.updateUsedAmount - ==> Parameters: 20(BigDecimal), 20(BigDecimal)
17:47:44.804 [pool-1-thread-1] DEBUG com.scd.mapper.AmountMapper.updateUsedAmount - ==> Parameters: 20(BigDecimal), 20(BigDecimal)
17:47:44.812 [pool-1-thread-4] DEBUG com.scd.mapper.AmountMapper.updateUsedAmount - <==    Updates: 1
17:47:44.812 [pool-1-thread-4] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@201b024c]
17:47:44.816 [pool-1-thread-5] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 149901742.
17:47:44.816 [pool-1-thread-5] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@8ef51ae]
17:47:44.816 [pool-1-thread-5] DEBUG com.scd.mapper.AmountMapper.updateUsedAmount - ==>  Preparing: update t_amount set used_amount = used_amount + ? where id = 1 and used_amount + ? <= total_amount
17:47:44.816 [pool-1-thread-5] DEBUG com.scd.mapper.AmountMapper.updateUsedAmount - ==> Parameters: 20(BigDecimal), 20(BigDecimal)
17:47:44.824 [pool-1-thread-2] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 529041956.
17:47:44.824 [pool-1-thread-2] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1f888a24]
17:47:44.824 [pool-1-thread-2] DEBUG com.scd.mapper.AmountMapper.updateUsedAmount - ==>  Preparing: update t_amount set used_amount = used_amount + ? where id = 1 and used_amount + ? <= total_amount
17:47:44.828 [pool-1-thread-2] DEBUG com.scd.mapper.AmountMapper.updateUsedAmount - ==> Parameters: 20(BigDecimal), 20(BigDecimal)
17:47:44.836 [pool-1-thread-3] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 1100293226.
17:47:44.836 [pool-1-thread-3] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4195246a]
17:47:44.836 [pool-1-thread-3] DEBUG com.scd.mapper.AmountMapper.updateUsedAmount - ==>  Preparing: update t_amount set used_amount = used_amount + ? where id = 1 and used_amount + ? <= total_amount
17:47:44.836 [pool-1-thread-3] DEBUG com.scd.mapper.AmountMapper.updateUsedAmount - ==> Parameters: 20(BigDecimal), 20(BigDecimal)
17:47:44.967 [pool-1-thread-4] INFO com.scd.sql.SqlTest - 更新成功, 执行完成时间 1708854464967
17:47:44.967 [pool-1-thread-1] DEBUG com.scd.mapper.AmountMapper.updateUsedAmount - <==    Updates: 1
17:47:44.967 [pool-1-thread-4] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@201b024c]
17:47:44.967 [pool-1-thread-1] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@483fbc0b]
17:47:44.967 [pool-1-thread-4] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@201b024c]
17:47:44.967 [pool-1-thread-4] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 538640972 to pool.
17:47:45.033 [pool-1-thread-5] DEBUG com.scd.mapper.AmountMapper.updateUsedAmount - <==    Updates: 0
17:47:45.033 [pool-1-thread-1] INFO com.scd.sql.SqlTest - 更新成功, 执行完成时间 1708854465033
17:47:45.033 [pool-1-thread-1] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@483fbc0b]
17:47:45.033 [pool-1-thread-5] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@8ef51ae]
17:47:45.033 [pool-1-thread-1] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@483fbc0b]
17:47:45.033 [pool-1-thread-2] DEBUG com.scd.mapper.AmountMapper.updateUsedAmount - <==    Updates: 0
17:47:45.033 [pool-1-thread-1] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 1212136459 to pool.
17:47:45.033 [pool-1-thread-5] INFO com.scd.sql.SqlTest - 当前使用金额大于总金额, 执行完成时间 1708854465033
17:47:45.033 [pool-1-thread-2] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1f888a24]
17:47:45.033 [pool-1-thread-5] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@8ef51ae]
17:47:45.033 [pool-1-thread-5] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@8ef51ae]
17:47:45.033 [pool-1-thread-2] INFO com.scd.sql.SqlTest - 当前使用金额大于总金额, 执行完成时间 1708854465033
17:47:45.033 [pool-1-thread-5] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 149901742 to pool.
17:47:45.033 [pool-1-thread-3] DEBUG com.scd.mapper.AmountMapper.updateUsedAmount - <==    Updates: 0
17:47:45.033 [pool-1-thread-3] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4195246a]
17:47:45.033 [pool-1-thread-2] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1f888a24]
17:47:45.033 [pool-1-thread-3] INFO com.scd.sql.SqlTest - 当前使用金额大于总金额, 执行完成时间 1708854465033
17:47:45.033 [pool-1-thread-3] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4195246a]
17:47:45.033 [pool-1-thread-2] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1f888a24]
17:47:45.033 [pool-1-thread-2] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 529041956 to pool.
17:47:45.037 [pool-1-thread-3] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4195246a]
17:47:45.037 [pool-1-thread-3] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 1100293226 to pool.

查询数据表t_amount
在这里插入图片描述
未执行多线程测试方法之前 used_amount 的金额为 54.22, 执行完成之后,通过运行日志可以确定有2个线程执行成功了,使用金额变成了94.22

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/493560.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

谁有权开除在编教师岗位

当晨光熹微&#xff0c;大多数人还在被窝中沉睡时&#xff0c;老师们已经开始了一天的工作。备课、上课、批改作业&#xff0c;还要处理学生间的各种矛盾&#xff0c;关注每一个孩子的成长。这份工作&#xff0c;看似平凡&#xff0c;实则责任重大。这样一份承载着无数家庭希望…

FMM 笔记:FMM(colab上执行)【官方案例解读】

在colab上运行&#xff0c;所以如何在colab上安装fmm&#xff0c;可见FMM 笔记&#xff1a;在colab上执行FMM-CSDN博客 fmm见&#xff1a;论文笔记&#xff1a;Fast map matching, an algorithm integrating hidden Markov model with precomputation_ubodt(upper bounded ori…

快让Live2D小可爱住进你的网站吧

文章目录 一、效果请欣赏二、教程1.下载项目工程2.本地自行修复测试3. 测试 一、效果请欣赏 二、教程 1.下载项目工程 github地址 可以根据工程的readme来使用demo测试&#xff0c;demo中需要修改 autoload.js api的cdnPath或者apiPath&#xff0c;否则加载不出来人物图片 api…

【算法】BP神经网络(BP,Back Propagation)

参考资料&#xff1a;来自于老哥数学建模课程。 BP神经网络的背景 1986年&#xff0c;Rumelhart等提出了误差反向传播神经网络&#xff0c;简称BP网络&#xff08;Back Propagation&#xff09;&#xff0c;该网络是一种单向传播的多层前向网络。误差反向传播的学习算法简称B…

冯诺依曼体系结构 与 操作系统

一、冯诺依曼体系结构 深入理解冯诺依曼体系结构 计算机的出现就是为了解决实际问题, 所以把问题交给计算机&#xff0c;计算机经过处理&#xff0c;得到一个结果反馈给我们&#xff0c;所以这中间就必然涉及到了输入设备&#xff0c;中央处理器(包括运算器和控制器)和输出设备…

微软Azure OpenAI的 GPT 接口使用小结

直接使用OpenAI的 GPT服务&#xff0c;在国内环境使用上会一些相关问题&#xff0c;微软提供了OpenAI的服务&#xff0c;基本上可以满足的相关的需要。下面提供一些简单的使用操作&#xff0c;来让你快速使用到 GPT 的服务。 前提&#xff1a;注册Azure的账户&#xff0c;并绑…

VS2022调试技巧(一)

什么是bug&#xff1f; 在1945年&#xff0c;美国科学家Grace Hopper在进行计算机编程时&#xff0c;发现一只小虫子钻进了一个真空管&#xff0c;导致计算机无法正常工作。她取出虫子后&#xff0c;计算机恢复了正常&#xff0c;由此&#xff0c;她首次将“Bug”这个词用来描…

3 easy 26. 删除有序数组中的重复项

双指针&#xff1a; //给你一个 非严格递增排列 的数组 nums &#xff0c;请你 原地 删除重复出现的元素&#xff0c;使每个元素 只出现一次 &#xff0c;返回删除后数组的新长度。元素的 相对顺序 应该保持 //一致 。然后返回 nums 中唯一元素的个数。 // // 考虑 nums 的唯…

顶顶通呼叫中心中间件-如何使处于机器人话术中的通话手动转接到坐席分机上

文章目录 前言联系我们实现步骤freeswitch命令转接api接口转接 前言 本文讲解呼叫中心中间件如何手动转接通话。 场景&#xff1a;利用自动外呼进入机器人&#xff0c;在通话过程中&#xff0c;转接到坐席分机上。 联系我们 有意向了解呼叫中心中间件的用户&#xff0c;可以点…

闪测影像|闪测仪,一键自动批量测量尺寸

在现代化工业中&#xff0c;闪测仪只需一键即可快速批量测量尺寸&#xff0c;为产品尺寸控制和质量管理提供重要保障。 工作原理 机器视觉系统的优势是高精度、重复性的进行运作&#xff0c;并能提供清晰的图像。整个系统由光源、镜头、相机、图像采集卡、图像处理软件等组件…

HTTP 与HTTPS笔记

HTTP 80 HTTP是一个在计算机世界里专门在【两点】之间【传输】文字、图片、音频、视频等【超文本】数据的约定和规范。 HTTP状态码 1xx 提示信息&#xff0c;表示目前是协议处理的中间状态&#xff0c;还需要后续的操作&#xff1b;2xx 200 204 026 成功3xx 重定向&#xff…

2024国际生物发酵展览会全面揭秘-西尼尔过程控制

参展企业检查 西尼尔&#xff08;南京&#xff09;过程控制有限公司成立于2007年&#xff0c;坐落于美丽的六朝古都南京&#xff0c;占地面积20000平方米&#xff0c;现有员工130人&#xff0c;其中70%为本科及以上学历&#xff0c;高级、中级专业技术人员占比30%以上。 公司为…