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

Redis架构实战:高并发情况下并发扣减库存 相信大家从网上学习项目大部分人第一个项目都是电商,生活中时时刻刻也会用到电商APP,例如淘宝,京东等。做技术的人都知道,电商的业务逻辑简单,但是大部分电商都会涉及到高并发高可用,对并发和对数据的处理要求是很高的。这里我今天就讲一下高并发情况下是如何扣减库存的? 我们对扣减库存所需要关注的技术点如下: 当前剩余的数量大于等于当前需要扣减的数量,不允许超卖 对于同一个数据的数量存在用户并发扣减,需要保证并发的一致性 需要保证可用性和性能,性能至少是秒级 一次的扣减包含多个目标数量 当次扣减有多个数量 阅读详情

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

  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 ONLY

BEGIN [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 for
  the 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 for
  storage engines that are capable of it. This applies only to InnoDB.
  The effect is the same as issuing a START TRANSACTION followed by a
  SELECT from any InnoDB table. See
  http://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.html.
  The WITH CONSISTENT SNAPSHOT modifier does not change the current
  transaction isolation level, so it provides a consistent snapshot
  only if the current isolation level is one that permits a consistent
  read. The only isolation level that permits a consistent read is
  REPEATABLE READ. For all other isolation levels, the WITH CONSISTENT
  SNAPSHOT clause is ignored. A warning is generated when the WITH
  CONSISTENT SNAPSHOT clause is ignored.

o The READ WRITE and READ ONLY modifiers set the transaction access
  mode. They permit or prohibit changes to tables used in the
  transaction. The READ ONLY restriction prevents the transaction from
  modifying or locking both transactional and nontransactional tables
  that are visible to other transactions; the transaction can still
  modify or lock temporary tables.

  MySQL enables extra optimizations for queries on InnoDB tables when
  the transaction is known to be read-only. Specifying READ ONLY
  ensures these optimizations are applied in cases where the read-only
  status cannot be determined automatically. See
  http://dev.mysql.com/doc/refman/8.0/en/innodb-performance-ro-txn.html
  for more information.

  If no access mode is specified, the default mode applies. Unless the
  default has been changed, it is read/write. It is not permitted to
  specify both READ WRITE and READ ONLY in the same statement.

  In read-only mode, it remains possible to change tables created with
  the TEMPORARY keyword using DML statements. Changes made with DDL
  statements are not permitted, just as with permanent tables.

  For additional information about transaction access mode, including
  ways to change the default mode, see [HELP ISOLATION].

  If the read_only system variable is enabled, explicitly starting a
  transaction with START TRANSACTION READ WRITE requires the
  CONNECTION_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 at

          http://www.apache.org/licenses/LICENSE-2.0

       Unless required by applicable law or agreed to in writing, software
       distributed 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 and
       limitations under the License.

-->
<!DOCTYPE configuration
        PUBLIC "-//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;

    @Before
    public void setUp() throws Exception {
        String resource = "templates/mybatis-config.xml";
        Reader reader = Resources.getResourceAsReader(resource);
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
    }

    @Test
    public 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("更新成功");
            }
        }
    }

    @Test
    public 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

KingbaseES数据库迁移实战:利用dblink实现新旧库数据平滑比对与校验 本文详细介绍了KingbaseES数据库迁移过程中如何利用dblink技术实现新旧库数据的平滑比对与校验。从环境配置到实战技巧,涵盖KDB_Database_link和PostgreSQL风格dblink的配置方法,以及表结构验证、数据量核对等关键步骤,为DBA提供了一套完整的跨库数据校验解决方案。 阅读详情

相关推荐

Java应用中的数据校验

翻译:吴嘉俊 ,叩丁狼高级讲师。   [译者注:这篇文章是开源项目CUBA Platform的作者,在这篇文章中,作者阐述了CUBA平台中关于数据校验的设计思想和使用方式,可以作为大家在设计数据校验方面一个比较好的参考。] 我接触到的很多项目中,对数据校验这方面内容都没有一个很明确的策略。这些团队常常面对即将临近的交付期压力,不明确的项目续期,所以根本没有太多时间来规划和实现项目中的校验策略...

wolfcode_cn的博客 5770

MySQL并发测试

MySQL并发测试   一 测试说明 本次主要对比MySQL官方社区版mysql5.6.34和onesql5.6.34。测试模式为从100并发到4000并发,每次增加100并发,单表1000万数据,每次总请求数100万requests。测试参数如下: --oltp-table-size=10000000  //表的记录数1000万 --num-threads=$i   //并发数

Vovis的博客 5475

后端接口幂等性校验(数据库唯一索引 + 自定义注解和拦截器的token校验)

最近工作中需要实现一个防止重复提交的任务,其本质就是需要保证接口的幂等性。接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。 采用的方案包括了两部分,一部分核心的业务逻辑使用token校验,另一部分使用数据库的唯一索引来防止重复更改数据库。 先来说说使用数据库唯一索引的方案,使用数据库唯一索引的方案可以防止数据库表中在唯一索引字段相同的情况下去...

LO_YUN的博客 2599

数据库并发测试

【代码】数据库并发测试。

jiang_shang的博客 254

SpringBoot 数据校验与表单处理:从入门到精通(万字长文)

在现代Web应用开发中,数据验证是保证系统安全性和数据完整性的第一道防线。没有经过验证的用户输入可能导致各种安全问题,如SQL注入、XSS攻击,或者简单的业务逻辑错误。 数据验证的主要目的包括: 确保数据的完整性和准确性 防止恶意输入导致的安全问题 提供清晰的错误反馈改善用户体验 保证业务规则的执行 SpringBoot提供了强大的数据验证机制,主要通过Java Bean Validation API(JSR-380)实现,该规范目前最新的实现是Hibernate Validator。

本博客聚焦 YOLOv11 全流程落地,涵盖架构优化、数据集处理、训练技巧与多场景部署,兼及国产数据库、Java 开发与 AI 模型应用,内容兼顾理论与工程实践,为开发者提供系统干货,助力高效提升技术能力。 2054

项目实战第二讲:高并发下如何保障缓存和数据库的一致性

项目实战第二讲:如何优雅地清理缓存

Jet_qi的博客 1145

Jmeter--数据库测试+web测试

Jmeter负载测试

LiUh__N的博客 1748

基于fMRI数据计算脑脊液(CSF)与全脑BOLD信号的时间耦合分析

BOLD和CSF信号有多相似CSF信号要延迟多少时间才能最匹配BOLD信号BOLD-CSF的互相关公式lag:时间延迟采样点数BOLD信号CSF信号延迟时间点对应的相关系数。

Rose9614的博客 2980

go-sqlmock:Go 数据库测试的轻量级方案

go-sqlmock是Go语言中一个轻量级的数据库测试工具,通过模拟SQL驱动行为,避免了搭建真实测试数据库的繁琐流程。它基于标准库database/sql接口实现,支持并发、上下文相关mock、参数匹配等功能,无需修改业务代码即可测试SQL执行逻辑。开发者可以声明预期SQL和返回结果,在执行后验证期望是否满足。项目提供自定义SQL匹配策略和参数匹配等高级功能,适用于TDD流程中的数据库层测试。虽然项目已进入维护状态,但在Go社区获得广泛认可,是解决数据库测试痛点的实用方案。

psidock562的博客 320

CUDA 12.1环境下的动态库链接修复:从libcusparse.so.12的undefined symbol错误到libnvJitLink.so.12的正确加载

本文详细解析了在CUDA 12.1环境下,PyTorch因libcusparse.so.12找不到libnvJitLink.so.12中的__nvJitLinkAddData_12_1符号而报错的根本原因。通过诊断动态链接过程,提供了创建符号链接、修改环境变量等三种解决方案,并深入探讨了nvJitLink组件的作用及环境配置的最佳实践,帮助开发者彻底解决此类动态库链接问题。

c8d9e0f1的博客 205

【49】软考软件设计师——冲刺复盘:高频易错点与应试策略|复盘清单+应试技巧

本文是《软件设计师·50讲通关|从零基础到工程师职称》专栏第49篇,属于**模块六:冲刺与模拟**终篇,聚焦考前最后阶段的“精准复盘+应试提分”。全文围绕“高频易错点清零、答题时间优化、应急处理落地”三大核心,梳理9大知识领域的典型易错点(附避坑技巧)、优化版答题时间分配(含取舍策略)、考中全场景应急方案,配套可直接使用的“复盘清单”和“应试技巧手册”,帮助考生在最后阶段查漏补缺、固化得分逻辑,避免因低级错误丢分,以“稳、准、快”的状态应对考试。下一篇第50篇(终章)将进行全专栏知识体系回顾、考试策略总结

专注于人工智能、软件开发、工控自动化、工厂数字化及智能化等领域,希望和大家共同进步! 439

聊一聊缓存和数据库不一致性问题的产生及主流解决方案以及扩展的思考

1、无论选择下列那种方式先更新缓存,再更新数据库先更新数据库,再更新缓存先删除缓存,再更新数据库先更新数据库,再删除缓存如果是在多服务或是并发情况下,其实都有可能产生数据不一致性。不过在这四种选择中,平常都会优先考虑后两种方式。并且市面上对于这后两种选择,也已经有一些解决方案。在谈解决方案之前,我们先看看需要解决的问题我们要如何保证这两段代码一起执行成功?【先删除缓存,再更新数据库】在读写并发时,会产生一个缓存旧数据,而数据库是新数据的问题,这该如何解决呢?

Java技术攻略的博客 415

「JS逆向三百例」JS逆向HIKVISION-视频监控的前端密码加密

本文详细分析了HIKVISION-海康威视监控前端登录过程中密码的加密机制,通过逆向工程解析了其 JavaScript 加密逻辑,重点研究了HIKVISION-海康威视监控的前端密码SHA256加密算法的实现。基于逆向分析结果,使用 Python 模拟了HIKVISION-海康威视监控的登录流程,最终实现了自动化登录功能。本文旨在为安全研究人员提供技术参考,帮助理解前端加密机制,并强调技术研究的合法性和合规性。

布啦啦 1096

一起写个数据库 —— 4. 日志文件与恢复策略

本文原载于我的博客:https://ziyang.moe/article/mydb4.html 本章涉及代码都在 https://github.com/CN-GuoZiyang/MYDB/tree/master/src/main/java/top/guoziyang/mydb/backend/dm/logger 和 https://github.com/CN-GuoZiyang/MYDB/blob/master/src/main/java/top/guoziyang/mydb/backend/dm/Reco

Ziyang的博客 2768

VT-Refine:基于仿真微调的视觉-触觉反馈双臂装配强化学习

本研究提出VT-Refine框架,通过融合真实世界示范、高保真触觉仿真与强化学习,解决高精度双臂装配任务。方法分为两阶段:首先利用少量人类示范数据预训练视觉-触觉扩散策略;随后在数字孪生仿真环境中进行强化学习微调。关键创新包括:开发GPU加速的触觉仿真模块精确模拟压阻式传感器;采用基于点云的多模态表征实现无缝的跨领域迁移。实验表明,该方法在五个双臂装配任务中显著提升性能,触觉反馈使成功率提高40%。研究首次实现仿真环境中的大规模双臂视觉-触觉强化学习及sim-to-real迁移,为接触密集型任务提供了新解决

2506_90492529的博客 1795

STM32CubeIDE下载程序报错:Failed to execute MI command: load G:\\Cube_IDE_EXP\\00_FIRST\\Debug\\00...如何解决??

🏆本文收录于 《全栈 Bug 调优(实战版)》 专栏。专栏聚焦真实项目中的各类疑难 Bug,从成因剖析 → 排查路径 → 解决方案 → 预防优化全链路拆解,形成一套可复用、可沉淀的实战知识体系。无论你是初入职场的开发者,还是负责复杂项目的资深工程师,都可以在这里构建一套属于自己的「问题诊断与性能调优」方法论,助你稳步进阶、放大技术价值 。

**My Coding Family** 2593

EasyExcel 并发读取文件字段并进行校验,数据写入到新文件,批量插入数据到数据库

背景 公司要求做一个数据统计类的功能,大概就是由每个地方采集数据用excel进行导入,根据地方大小每个文件的数量可能在10万-100万之间不等,导入时要求对每个字段进行校验,可能存在的规则有非空判断,时间格式判断,长度判断,手机号这类正则表达式类校验,文件有多个sheet,每个sheet对应一个表,对应的数量不确定但是不会重复表,有个别sheet需要前面sheet的字段判断。 如果数据有错的,要求在后面新加一列告诉用户错误的原因,正确的数据也要输出一遍(不理解客户的想法。。。),未来有可能会开新shee

入门菜鸟程序员的成长 3832

东北大学计算机预推免(双非友好)

东北大学,个人觉得在985里面对双非比较友好,而且学校的计算机专业实力也比较强,上岸难度不算很大,可以重点关注一下。学校复试审核的时间比较早,拿到复试资格之后,一定要留意报名系统首页中计算机学院的信息,学校也会在全国各地不同时间安排面试,可以根据自己的情况选择一个合适的时间前去面试,并且有对应面试场次的QQ群,信息会在群里面发布。面试成绩会在预推免系统中直接公布,但是是否拟录取不会公布,需要考生自行判断,今年计算机学院录取线定在了86分左右,最终递补下来,好像录取到了84分左右。

D1379951481的博客 3120

深度解析大云海山数据库(He3DB)FileCache 机制

FileCache 作为 He3DB 数据库 的高性能二级缓存方案,通过 “本地缓存 + 智能淘汰 + 一致性保障”,有效突破了远程数据访问和磁盘 IO 的性能瓶颈。数据根据不同的访问规则,被标记为冷数据,热数据和温数据,存储在相应位置,当filecache 温数据层在实践中配置合理的大小,保证较高的命中率时,将会大大提高数据访问性能,在读多写少的场景中将会有较好的性能表现。

weixin_43544348的博客 1021

基于AUTOSAR的汽车电子项目--电动助力转向系统

基于AUTOSAR的汽车电子项目开发通常涵盖广泛的领域,例如动力系统控制、车身电子、信息娱乐系统、高级驾驶辅助系统(ADAS)等。以下是一个简化的示例项目,展示了如何在AUTOSAR环境下开发一个电动助力转向系统(Electric Power Steering, EPS)的控制软件

MHD0815的博客 2126

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

EasyExcel并发读取文件字段并进行校验数据写入到新文件批量插入数据库Demo:高效数据处理利器

EasyExcel并发读取文件字段并进行校验数据写入到新文件批量插入数据库Demo:高效数据处理利器 去发现同类优质开源项目:https://gitcode.com/ 项目介绍 EasyExcel 并发读取文件字段并进行校验,数据写入到新文件,批量插入数据库 Demo 是一个基于 Java 和 EasyExcel 的开源项目。该项目提供了一个示例代码,旨在帮助开发者理解和掌握如何高效地处理 Exc...

gitblog_06752的博客 1046

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

数据仓库迁移中的数据一致性校验实践

数据一致性校验是数据仓库迁移中的关键技术环节,涉及结构、内容和语义三个维度的验证。其核心原理是通过行数比对、聚合指标分析和Checksum校验等多层防御机制,确保源端与目标端数据的比特级一致。在PB级数据迁移场景下,高效的校验方案能显著降低业务风险,尤其适用于金融、电商等对数据准确性要求严格的领域。以MaxCompute和Hadoop集群迁移为例,通过分布式计算优化和自动化校验平台建设,可将传统耗时数天的校验过程压缩到小时级,同时支持智能差异定位和修复建议。

weixin_34268579的博客 605

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

发消息不入库可以吗?不依赖数据库_高并发情况下 队列的100%可靠消费

不依赖数据库_高并发情况下 队列的100%可靠消费 发送消息前要不要先入DB呢? 传统的方式一般都要先DB后发送消息,但是如果系统并发量超高时,如果减少一次DB,性能将提高很多。 但是如果不落库,那么怎么能够保证消息的100%可靠呢? 传统的发消息架构 ​ 高并发情况下的消息队列100%架构 这个架构也是我们线上正在使用的架构。 采用中间服务异步补偿的方式来保证消息的可靠,100%消费。 详细流程如下 等到业务数据事务提交之后,上游短 发送一条消息 ,并且同时发送一条校验的延迟消息(

qq_38324542的博客 642

数据库三级选择填空常考题目总结

数据仓库与数据挖掘 联机事务处理(OLTP)环境:用于支持企业基本业务应用的环境。 数据仓库:面向主题的、集成的、非易失的、随时间变化的数据集合,用来支持管理人员的决策。 数据仓库特性:面向主题、集成、不可更新、时间特性。 典型数据仓库的数据体系结构:操作型数据、操作型数据存储、数据仓库、数据集市、个体层数据。 数据仓库的数据体系中的功能可分为数据处理、数据管理、数据应用三层。 系统中...

老张的博客 7028
上一篇: spring-security 过滤器 (三)
下一篇: spring-security 项目实战(一)个人健康档案
Chengdu.S
博客等级 码龄9年 282粉丝 86原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Chengdu.S

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值