MySQL的乐观锁、悲观锁机制及实现

目录

乐观锁

概述

实现

乐观锁的重试机制

使用场景

悲观锁

概述

实现

使用场景


乐观锁

乐观锁的实现参考了这篇文章,里面还将了乐观锁的时间戳实现方式:

跳转

概述

乐观锁是一种并发控制策略,它假设多个事务不会发生冲突,在执行操作时不加锁,非常乐观,只需每次进行提交时利用标识进行对比,确认其他事务没修改过便可提交。

实现

使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。

  • 即为数据增加一个版本标识,一般是给数据库表增加一个数字类型的 "version" 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一 。当我们提交更新的时候 ,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对 ,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。用下面的一张图来说明:

  • 如上图所示,如果更新操作顺序执行,则数据的版本(version)依次递增,不会产生冲突。但是如果发生有不同的业务操作对同一版本的数据进行修改,那么,先提交的操作(图中B)会把数据version更新为2,当A在B之后提交更新时发现数据的version已经被修改了,那么A的更新操作会失败

  • 假设我们有以下两张表(商品库存表和库存变动历史表),包含一个 version 字段用于乐观锁:

  • 对应实体类:
    *

    java 复制代码
    package com.example.entity;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    
    /**
     * 实体类:库存
     */
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class Inventory {
    
        /**
         * 产品ID
         */
        private int productId;
    
        /**
         * 库存数量
         */
        private int stockQuantity;
    
        /**
         * 版本号
         */
        private int version;
    
    }
    java 复制代码
    package com.example.entity;
    
    import com.example.entity.Inventory;
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import java.sql.Timestamp;
    
    /**
     * 实体类:库存变动历史
     */
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class StockChangeHistory {
    
        /**
         * 变动ID
         */
        private Integer changeId;
    
        /**
         * 产品ID
         */
        private Integer productId;
    
        /**
         * 变动时间
         */
        private Timestamp changeTime;
    
        /**
         * 变动前库存数量
         */
        private int previousStockQuantity;
    
        /**
         * 变动后库存数量
         */
        private int newStockQuantity;
    
        /**
         * 变动数量
         */
        private int changeAmount;
    
    }
  • 使用 MyBatis 进行数据库操作的 Mapper 接口:
    *

    java 复制代码
    @Mapper
    public interface InventoryMapper {
    
        /**
         * 根据id更新商品,使用了乐观锁
         * @param inventory
         * @return
         */
        @Update("UPDATE inventory SET stock_quantity = #{stockQuantity}, version = version + 1 WHERE product_id = #{productId} AND version = #{version}")
        int updateWithVersion(Inventory inventory);
    
        /**
         * 根据id查询出商品,mapper文件脑补就行,也不重要
         * @param productId
         * @return
         */
        Inventory findById(int productId);
    }
    java 复制代码
    @Mapper
    public interface StockChangeHistoryMapper {
        void insert(StockChangeHistory stockChangeHistory);
    }
  • 在 Service 层中实现乐观锁的逻辑(Service接口自己脑补):
    *

    java 复制代码
    @Service
    public class StockServiceImpl implements StockService {
    
        @Autowired
        private InventoryMapper inventoryMapper;
    
        @Autowired
        private StockChangeHistoryMapper stockChangeHistoryMapper;
    
        /**
         * 使用乐观锁来减库存
         * @param productId
         */
        @Override
        @Transactional(isolation = Isolation.REPEATABLE_READ)
        public void decreaseStockQuantity(int productId) {
            //根据商品id查询出商品
            Inventory inventory = inventoryMapper.findById(productId);
            if (inventory != null && inventory.getStockQuantity() > 0) {
                //保存更新的库存
                int oldStockQuantity = inventory.getStockQuantity();
                //计算新的库存并设置:库存-1
                int newStockQuantity = inventory.getStockQuantity() - 1;
                inventory.setStockQuantity(newStockQuantity);
                //执行减库存操作
                int updatedRows = inventoryMapper.updateWithVersion(inventory);
                //updateWithVersion方法使用了带有版本号的更新语句,并返回受影响的行数。如果更新行数为0,则表示更新失败,即乐观锁冲突,此时可以抛出自定义的异常
                if (updatedRows == 0) {
                    throw new OptimisticLockingException("Failed to update stock quantity due to optimistic locking conflict.");
                }
                //添加变动记录到 库存变动历史表 中
                saveStockChangeHistory(productId, oldStockQuantity, newStockQuantity, -1);
            }
        }
    
    
    
        private void saveStockChangeHistory(int productId, int previousStockQuantity, int newStockQuantity, int changeAmount) {
            //创建一个库存变动历史对象
            StockChangeHistory stockChangeHistory = new StockChangeHistory();
            //设置变动的商品id
            stockChangeHistory.setProductId(productId);
            //变动时间
            stockChangeHistory.setChangeTime(new Timestamp(System.currentTimeMillis()));
            //设置变动前的库存
            stockChangeHistory.setPreviousStockQuantity(previousStockQuantity);
            //设置变动后的库存
            stockChangeHistory.setNewStockQuantity(newStockQuantity);
            //设置变动量
            stockChangeHistory.setChangeAmount(changeAmount);
            //插入到 库存变动历史表 中
            stockChangeHistoryMapper.insert(stockChangeHistory);
        }
    }
  • 最后,在 Controller 层中暴露更新库存的接口:
    *

    java 复制代码
    @RestController
    @RequestMapping("/stock")
    public class StockController {
    
        @Autowired
        private StockService stockService;
    
        /**
         * 使用乐观锁来减库存
         * @param productId
         * @return
         */
        @GetMapping("/decrease/{productId}")
        public ResponseEntity<String> decreaseStockQuantity(@PathVariable("productId") int productId) {
            stockService.decreaseStockQuantity(productId);
            return ResponseEntity.ok("Stock quantity decreased successfully.");
        }
    }
  • 其实就是编写了这样一条sql语句:

    • 根据id和版本号更新数据库对应用户信息,同时版本号加1
      *

      sql 复制代码
      -- 假设有一张用户表 users,包含 id、name 和 version 字段
      -- 读取数据
      SELECT id, name, version FROM users WHERE id = 1;
      
      -- 更新数据时检查版本号
      UPDATE users
      SET name = 'new_name', version = version + 1
      WHERE id = 1 AND version = current_version;

乐观锁的重试机制

通过乐观锁的重试机制,在保证数据一致性的前提下,可以解决由于版本冲突导致的放弃更新问题。

乐观锁冲突重试机制,重试3次:

参考这篇文章,也详细了:跳转

使用场景

乐观锁适合并发冲突少,读多写少的场景,不用通过加锁只需通过比较字段版本号(或时间戳)是否发生改变的形式,无锁操作,吞吐量较高。

悲观锁

概述

悲观锁认为每次操作都会发生冲突,非常悲观。它会在任何可能发生冲突的地方进行加锁,其他操作想修改都需要等它执行完后释放锁,再通过争抢到锁而进行操作。

实现

使用悲观锁来解决并发冲突的问题,可以在查询库存时使用SELECT ... FOR UPDATE语句来获取悲观锁。这样可以确保在事务中对查询结果加锁,避免其他事务对查询结果进行修改。

mapper层的代码:

java 复制代码
@Mapper
public interface InventoryMapper {


    /**
     * 加悲观锁的根据id查询商品
     * @param productId
     * @return
     */
    @Select("SELECT * FROM inventory WHERE product_id = #{productId} FOR UPDATE")
    Inventory findByIdForUpdate(int productId);

}

在上面的代码中,findByIdForUpdate 方法使用了 SELECT ... FOR UPDATE 语句来查询库存,并获取悲观锁。

通过使用 FOR UPDATE 子句,查询结果会被锁定,直到事务结束。这样可以确保在事务中对查询结果加锁,避免其他事务对查询结果进行修改。

Service代码:

java 复制代码
@Service
public class StockServiceImpl implements StockService {

    @Autowired
    private InventoryMapper inventoryMapper;

    @Autowired
    private StockChangeHistoryMapper stockChangeHistoryMapper;


    /**
     * 加悲观锁的根据id查询商品
     * @param
     */
    @Override
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public Inventory getInventoryForUpdate(int productId) {
        Inventory inventory = inventoryMapper.findByIdForUpdate(productId);
        return inventory;
    }
}

Controller层:

java 复制代码
package com.example.controller;

import com.example.entity.Inventory;
import com.example.service.StockService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/stock")
public class StockController {

    @Autowired
    private StockService stockService;

    /**
     * 加悲观锁的根据id查询商品
     * @param productId
     * @return
     */
    @GetMapping("/inventory/{productId}")
    public ResponseEntity<Inventory> getInventory(@PathVariable int productId) {
        Inventory inventory = stockService.getInventoryForUpdate(productId);
        return ResponseEntity.ok(inventory);
    }
}

总结:悲观锁的sql语句:

java 复制代码
-- 读取数据并加锁
SELECT id, name FROM users WHERE id = 1 FOR UPDATE;

-- 执行更新操作
UPDATE users SET name = 'new_name' WHERE id = 1;

使用场景

悲观锁适合并发冲突多,写多读少的场景。通过每次加锁的形式来确保数据的安全性,吞吐量较低.

相关推荐
尘浮生7 分钟前
Java项目实战II基于Java+Spring Boot+MySQL的植物健康系统(开发文档+源码+数据库)
java·开发语言·数据库·spring boot·mysql·maven·intellij-idea
YUNBEE_chen8 分钟前
ansible批量安装postgresql软件
数据库·postgresql·ansible
会洗碗的CV工程师31 分钟前
828华为云征文——使用Flexus云服务器X实例CentOS镜像下创建MySQL服务器教程
运维·服务器·mysql·centos·华为云
A乐神32 分钟前
Django 对数据库的增删改查
数据库·django·sqlite
bug菌¹38 分钟前
lettuce连接哨兵redis,主从切换异常,如何解决??
数据库·spring boot·redis·bootstrap
噼里啪啦啦.43 分钟前
阻塞队列(多线程)
java·开发语言·数据库
抹茶生活2 小时前
Face++API调用
java·开发语言·数据库
Aries2632 小时前
Redisson使用详解:一个强大的Redis Java客户端
java·数据库·redis
计算机程序设计开发2 小时前
计算机毕业设计非遗项目网站 登录注册搜索 评论留言资讯 前后台管理/springboot/javaWEB/J2EE/MYSQL数据库/vue前后分离小程序
数据库·vue.js·spring boot·java-ee·课程设计·计算机毕设·计算机毕业设计
码农褚2 小时前
CentOS中使用Docker运行mysql并挂载本地目录
mysql·docker·centos