MySQL数据库update A set num=num+ ? where id=?并发执行情况下存在线程安全问题吗?

探讨SQL语句执行的原子性及线程安全性,通过实例演示如何利用MySQL排它锁解决并发更新问题。

首先,先说结论:这个SQL语句执行可以看做是原子操作,没有线程安全问题

使用代码演示一下:

1.创建一个xu_hao表,为了简单,只有id和num两个字段

CREATE TABLE `xu_hao`  (
  `id` int(11) NOT NULL,
  `num` int(11) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

2.插入一条初始化数据:

INSERT INTO `xu_hao`(`id`, `num`) VALUES (1, 0);

3.创建一个spring boot工程,项目结构如下:
在这里插入图片描述
4.导入maven依赖:

	<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.2</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.22</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

5.编写application.yml配置文件,测试项目,简单配置即可:

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/learn_work?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    
logging:
  level:
      root: warn
      com.learn.spmysqltest.mapper: trace

6.创建一个XuHaoMapper:

public interface XuHaoMapper {
    /**
     * 自增+1的方式更新num
     *
     * @param id
     * @return
     */
    @Update("update xu_hao set num = num + 1 where id = #{id}")
    int updateXuHaoByIncrease(int id) throws Exception;

     /**
     * 通过id直接更新num
     *
     * @param num
     * @param id
     * @return
     * @throws Exception
     */
    @Update("update xu_hao set num = #{num} where id = #{id}")
    int updateXuHao(int num, int id) throws Exception;

    /**
     * 通过id查询num
     *
     * @param id
     * @return
     * @throws Exception
     */
    @Select("select num from xu_hao where id = #{id} ")
    int selectXuHao(int id) throws Exception;
}

7.创建XuHaoService:

public interface XuHaoService {

    /**
     * 自增+1的方式更新num
     *
     * @param id
     * @return
     */
    int updateXuHaoByIncrease(int id) throws Exception;

    /**
     * 直接更新num
     *
     * @param id
     * @throws Exception
     */
    int updateXuHao(int id) throws Exception;

    /**
     * 通过id查询num
     *
     * @param id
     * @return
     * @throws Exception
     */
    int getXuHao(int id) throws Exception;

}

8.创建XuHaoServiceImpl:

@Service
public class XuHaoServiceImpl implements XuHaoService {

    @Autowired
    private XuHaoMapper xuHaoMapper;

    @Override
    @Transactional
    public int updateXuHaoByIncrease(int id) throws Exception {
        return xuHaoMapper.updateXuHaoByIncrease(id);
    }

    @Override
    @Transactional
    public int updateXuHao(int id) throws Exception {
        int dbNum = xuHaoMapper.selectXuHao(id);
        dbNum++; // num + 1
        return xuHaoMapper.updateXuHao(dbNum, id);
    }

    @Override
    @Transactional(readOnly = true)
    public int getXuHao(int id) throws Exception {
        return xuHaoMapper.selectXuHao(id);
    }
}

9.开启事务支持:

@SpringBootApplication
@EnableTransactionManagement // 开启事务
@MapperScan("com.learn.spmysqltest.mapper")
public class SpMysqlTestApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpMysqlTestApplication.class, args);
    }
}

由于单元测试多线程可能会有问题,为了简单,编写一个XuHaoController进行测试:

@RestController
@RequestMapping("/xuhao")
public class XuHaoController {

    @Autowired
    private XuHaoService xuHaoService;

    /**
     * 测试 update xu_hao set num = num + 1 where id = 1
     *
     * @return
     */
    @GetMapping("/updateadd1")
    public String updateByIncrease() {
        int cupNum = Runtime.getRuntime().availableProcessors(); // 获取可用的处理器个数
        CountDownLatch latch = new CountDownLatch(cupNum);
        for (int i = 0; i < cupNum; i++) {
            new Thread(() -> {
                try {
                    latch.await();
                    xuHaoService.updateXuHaoByIncrease(1);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
            latch.countDown();
        }
        return "cupNum=" + cupNum + ",预期num=" + cupNum;
    }

    /**
     * num = num + 1
     * 测试 update xu_hao set num = num  where id = 1
     *
     * @return
     */
    @GetMapping("/update")
    public String update() {
        int cupNum = Runtime.getRuntime().availableProcessors(); // 获取可用的处理器个数
        CountDownLatch latch = new CountDownLatch(cupNum);
        for (int i = 0; i < cupNum; i++) {
            new Thread(() -> {
                try {
                    latch.await();
                    xuHaoService.updateXuHao(1);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
            latch.countDown();
        }
        return "cupNum=" + cupNum + ",预期num=" + cupNum;
    }
}

进入正式测试,访问:http://127.0.0.1:8080/xuhao/updateadd1,运行结果:
在这里插入图片描述
数据库:
在这里插入图片描述
可以看到,我电脑处理器个数是6个,我用CountDownLatch模拟6个线程同时执行数据库更新操作,预期值和处理器个数一致,并没有出现线程安全问题。

简单解释一下:每次执行update的时候,会持有id=1这行记录的排它锁,后面的update需要持有这个记录的排它锁释放之后,才能对他进行修改,也就是说,后面的update操作会阻塞,直到前一个update提交成功或者发生异常回滚事务了,他才会获得这个锁,从而对数据进行修改。(可以网上查询更加详细的解答)

如果我们是查询num后,在代码里面进行加1操作,再更新回数据库呢?把数据库中的num重新置为0,访问:http://127.0.0.1:8080/xuhao/update,数据库结果:
在这里插入图片描述
预期是6,现在却是1,很明显有线程安全问题,有其他的解决方法吗?

其实有了上面的启示,我们很容易想到,使用MySQL数据库的排它锁来解决(select … for update),在XuHaoMapper中增加一个函数:

/**
     * 通过id查询num
     *
     * @param id
     * @return
     * @throws Exception
     */
    @Select("select num from xu_hao where id = #{id} for update ")
    int selectXuHao2(int id) throws Exception;

修改XuHaoServiceImpl中的updateXuHao方法:

	@Override
    @Transactional
    public int updateXuHao(int id) throws Exception {
        // int dbNum = xuHaoMapper.selectXuHao(id);
        int dbNum = xuHaoMapper.selectXuHao2(id);
        dbNum++; // num + 1
        return xuHaoMapper.updateXuHao(dbNum, id);
    }

重新将数据库中num置为0,并访问http://127.0.0.1:8080/xuhao/update,数据库结果:
在这里插入图片描述
可以看到,数据库结果与预期结果一致,此时也没有并发访问问题,原因与上面相似,for update是一种行级锁,又叫排它锁,一旦用户对某个行施加了行级加锁,则该用户可以查询也可以更新被加锁的数据行,其它用户只能查询但不能更新被加锁的数据行.如果其它用户想更新该表中的数据行,则必须获取该行级锁.行锁永远是独占方式锁。for update 需要事务提交或回滚了才会释放锁,所以在spring管理的事务方法中,使用这种方式,也不会出现并发访问问题。

注意:select … for update ,如果查询条件带有主键,会锁行数据,如果没有,会锁表,另外并发访问下可能会造成死锁!

<think>好的,我现在要解决的问题是在MySQL主从架构中使用分布式锁时如何处理锁状态的同步延迟问题。首先,我需要回顾一下用户之前提供的引用资料,以及之前的回答,看看有哪些相关的信息可以利用。 根据引用[1]和[3],MySQL主从同步延迟的主要原因包括主库的高负载、大事务、单线程复制等。主库的binlog是顺序写入,效率高,而从库的SQL线程需要重放这些日志,但DML操作可能是随机的,导致复制延迟。这会影响分布式锁的状态同步,比如在主库上获取了锁,但从库还没有同步,其他客户端可能读取到旧的锁状态,从而引发并发问题。 用户之前的回答中提到,使用MySQL实现分布式锁时,需要注意主从同步延迟的问题,并提到了设置`WAIT_FOR_EXECUTED_GTID_SET`来强制同步。但具体如何操作还需要进一步探讨。另外,引用[4]和[5]提到了通过调整主从复制的参数、使用读写分离中间件或更高效的集群架构如PXC来减少延迟。 现在,用户的问题是,在MySQL主从架构中实现分布式锁时,如何解决锁状态同步延迟的问题。需要结合已有的信息,提出具体的解决方案。 首先,可能的解决方案包括: 1. 强制读主库:在获取锁或检查锁状态时,强制从主库读取数据,避免读取从库的延迟数据。这可以通过在查询中添加提示或者使用中间件路由来实现。 2. 使用半同步复制:确保至少一个从库已经接收到binlog后再返回提交成功,减少数据丢失的风险,虽然可能增加延迟,但提高了一致性。 3. GTID同步控制:通过等待GTID同步,确保操作在从库上已经应用后再进行后续步骤,例如使用`WAIT_FOR_EXECUTED_GTID_SET`函数。 4. 减少主从延迟本身:优化主库和从库的性能,比如拆分大事务、优化查询、调整复制线程等,从而降低同步延迟的可能性。 5. 采用集群架构:如PXC或Galera Cluster,使用多主同步复制,避免单点延迟问题。 接下来需要详细分析每个方案的可行性、优缺点以及具体实施步骤。 比如,强制读主库虽然简单,但可能增加主库的负载,尤其是在高并发场景下。而半同步复制需要在MySQL配置中启用,并且可能影响主库的写入性能。使用GTID需要MySQL版本支持,并且应用程序需要相应的处理逻辑。优化主从延迟需要系统性的调整,可能涉及多个方面。集群架构如PXC可能需要较大的架构调整,但能提供更好的同步性能。 此外,需要考虑分布式锁的实现细节,比如锁的获取、释放、续期等操作如何与主从同步机制结合,避免因延迟导致锁状态不一致。例如,在释放锁时,如果从库尚未同步,其他客户端可能误判锁仍被持有,从而等待或重试,影响系统性能。 可能需要结合多种方法,例如在关键操作时强制读主库,同时优化复制延迟,并使用GTID控制来确保一致性。此外,应用层需要有重试机制和超时处理,以应对可能的延迟情况。 需要验证这些方案是否在用户提供的引用中有依据,例如引用[4]提到了通过改变master的日志位置来同步,引用[5]提到了使用读写分离和中间件,以及PXC集群。 总结下来,解决方案应涵盖以下几个方面: - 强制读取主库以获取最新锁状态 - 使用同步复制机制(如半同步复制) - 应用GTID控制确保数据同步 - 优化主从复制性能减少延迟 - 采用高一致性集群架构 每个方案需要具体步骤和配置示例,比如SQL语句、参数设置等,同时指出其优缺点和适用场景。最后,可能需要比较不同方案的适用性,帮助用户根据实际情况选择合适的方法。</think>#### 使用MySQL主从架构实现分布式锁时解决锁状态同步延迟的6种核心方案 --- ### 一、强制读写分离控制 **核心思路**:分布式锁的**写操作强制走主库**,**读操作根据场景选择主/从库**,避免因从库延迟导致锁状态误判。 1. **关键操作强制读主库** - 在获取锁、释放锁、续期等关键操作中,通过`/*+ FORCE_MASTER */`提示强制路由到主库: ```sql SELECT /*+ FORCE_MASTER */ * FROM distributed_lock WHERE lock_key = 'order_lock' FOR UPDATE; ``` - 通过中间件(如ProxySQL)配置规则,将`distributed_lock`表的查询自动路由到主库[^5]。 2. **业务逻辑补偿机制** - 在非关键操作(如锁状态监控)允许读从库,但需配合**版本号校验**: ```sql -- 从库查询 SELECT lock_version FROM distributed_lock WHERE lock_key = 'order_lock'; -- 主库校验 UPDATE distributed_lock SET expire_time = NOW() + 30s WHERE lock_key = 'order_lock' AND lock_version = {queried_version}; ``` --- ### 二、GTID同步控制 **核心思路**:通过全局事务标识(GTID)确保操作在从库完成同步后再响应客户端[^4]。 1. **强制同步写入** 在关键事务提交后,使用`WAIT_FOR_EXECUTED_GTID_SET`等待操作同步到指定数量的从库: ```sql COMMIT; SELECT WAIT_FOR_EXECUTED_GTID_SET( (SELECT @@global.gtid_executed), -- 等待当前GTID同步 TIMEOUT 5 -- 超时时间 ); ``` *适用场景*:对一致性要求极高的金融交易锁 2. **GTID状态校验** 客户端获取锁后记录主库GTID,其他节点需等待该GTID在从库应用后才允许操作: ```sql -- 客户端A获取锁时记录GTID SET @lock_gtid = @@global.gtid_executed; -- 客户端B操作前校验 SELECT WAIT_UNTIL_SQL_THREAD_AFTER_GTIDS(@lock_gtid, TIMEOUT 3); ``` --- ### 三、主从架构优化 **核心思路**:**减少主从延迟本身**,从根本上降低锁状态同步延迟概率[^2][^3]。 1. **大事务拆分** - 将长事务拆分为多个短事务,例如锁续期操作改为小批量更新: ```sql -- 原事务(风险高) UPDATE distributed_lock SET count = count+1 WHERE lock_key = 'order_lock'; -- 优化后(拆分) BEGIN; SELECT count FROM distributed_lock WHERE lock_key = 'order_lock' FOR UPDATE; UPDATE distributed_lock SET count = {new_value} WHERE lock_key = 'order_lock'; COMMIT; ``` 2. **并行复制配置** 启用MySQL多线程复制(需5.7+版本),提升从库同步速度: ```sql STOP SLAVE; SET GLOBAL slave_parallel_workers = 8; -- 根据CPU核数调整 SET GLOBAL slave_parallel_type = LOGICAL_CLOCK; START SLAVE; ``` 3. **硬件与参数调优** - 主从库使用SSD磁盘,设置`sync_binlog=1`和`innodb_flush_log_at_trx_commit=1`保障日志可靠性 - 调整`binlog_group_commit_sync_delay`平衡吞吐量与延迟 --- ### 四、同步复制方案 **核心思路**:使用半同步复制或全同步集群,确保数据写入多个节点后才返回成功。 1. **半同步复制(Semisynchronous Replication)** 配置至少一个从库ACK后才响应客户端: ```sql -- 主库配置 INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so'; SET GLOBAL rpl_semi_sync_master_enabled = 1; -- 从库配置 INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so'; SET GLOBAL rpl_semi_sync_slave_enabled = 1; ``` *效果*:锁状态写入主库和至少一个从库后才确认,降低数据丢失风险 2. **全同步集群方案** 采用Percona XtraDB Cluster(PXC)或MariaDB Galera Cluster,实现多主实时同步: ```sql -- 任一节点写入后立即同步到其他节点 INSERT INTO distributed_lock(...) VALUES (...); ``` *优势*:彻底解决主从延迟问题,但网络开销增加30%-50% --- ### 五、锁设计增强 **核心思路**:在锁机制中**内置延迟容忍设计**,降低同步延迟的影响。 1. **双重时间窗口校验** 设置`安全时间窗口`,在锁过期后额外保留缓冲期: ```sql CREATE TABLE distributed_lock ( ... safe_expire_time DATETIME -- = expire_time + 主从延迟阈值 ); -- 获取锁时计算 SET safe_expire_time = NOW() + 30s + 5s; -- 假设最大延迟5秒 ``` 2. **动态心跳检测** 客户端定期更新`last_heartbeat`字段,结合主从延迟动态调整锁有效期: ```sql UPDATE distributed_lock SET last_heartbeat = NOW() WHERE lock_key = 'order_lock' AND last_heartbeat > NOW() - INTERVAL 10 SECOND; ``` --- ### 六、监控与熔断 **核心思路**:建立实时监控体系,主从延迟超标时触发熔断机制。 1. **延迟监控** 通过`SHOW SLAVE STATUS`监控`Seconds_Behind_Master`: ```sql SHOW SLAVE STATUS\G -- 关键指标 Seconds_Behind_Master: 0 -- 延迟秒数 Last_IO_Error: Last_SQL_Error: ``` 2. **自动熔断规则** - 当主从延迟超过阈值(如3秒)时,自动将分布式锁服务切换为单主库模式 - 使用Hystrix等框架实现熔断降级: ```java // 伪代码示例 @HystrixCommand(fallbackMethod = "fallbackAcquireLock") public boolean acquireLock(String lockKey) { // 正常获取锁逻辑 } public boolean fallbackAcquireLock(String lockKey) { // 降级为本地锁或排队机制 } ``` --- #### 方案选型对比表 | 方案 | 一致性保障 | 实现复杂度 | 性能影响 | 适用场景 | |---------------------|------------|------------|----------|--------------------------| | 强制读主库 | ★★★★ | ★★ | 中 | 所有主从架构 | | GTID同步控制 | ★★★★★ | ★★★★ | 较高 | 金融等高一致性场景 | | 主从架构优化 | ★★ | ★★★ | 低 | 长期运行的业务系统 | | 同步复制方案 | ★★★★★ | ★★★★★ | 高 | 新建高可用架构 | | 锁设计增强 | ★★★ | ★★ | 低 | 中度一致性要求的业务 | | 监控与熔断 | ★★ | ★★★ | 可变 | 作为辅助方案配合其他使用 | --- #### 实施建议 1. **混合部署方案** - 核心业务:GTID同步控制 + 半同步复制 - 非核心业务:强制读主库 + 动态心跳检测 - 全局配置:主从延迟监控与自动熔断 2. **压测验证** 使用sysbench模拟高并发锁操作,验证不同方案下的事务成功率: ```bash sysbench --test=oltp_lock --mysql-host=master_ip --mysql-port=3306 \ --mysql-user=test --mysql-password=pass --mysql-db=test \ --num-threads=64 --max-requests=100000 run ``` 3. **灰度发布** 通过数据库中间件(如ProxySQL)分批次切换流量,观察延迟指标变化。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Coder-文小白

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

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

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

打赏作者

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

抵扣说明:

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

余额充值