当前位置: 首页 > news >正文

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

目录

乐观锁

概述

实现

乐观锁的重试机制

使用场景

悲观锁

概述

实现

使用场景


乐观锁

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

跳转

概述


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

实现

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

  • 即为数据增加一个版本标识,一般是给数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。用下面的一张图来说明:
  • 如上图所示,如果更新操作顺序执行,则数据的版本(version)依次递增,不会产生冲突。但是如果发生有不同的业务操作对同一版本的数据进行修改,那么,先提交的操作(图中B)会把数据version更新为2,当A在B之后提交更新时发现数据的version已经被修改了,那么A的更新操作会失败

  • 假设我们有以下两张表(商品库存表和库存变动历史表),包含一个 version 字段用于乐观锁:
  • 对应实体类:
    • 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;}
    • 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 接口:
    • @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);
      }
    • @Mapper
      public interface StockChangeHistoryMapper {void insert(StockChangeHistory stockChangeHistory);
      }
  • 在 Service 层中实现乐观锁的逻辑(Service接口自己脑补):
    • @Service
      public class StockServiceImpl implements StockService {@Autowiredprivate InventoryMapper inventoryMapper;@Autowiredprivate 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();//计算新的库存并设置:库存-1int 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();//设置变动的商品idstockChangeHistory.setProductId(productId);//变动时间stockChangeHistory.setChangeTime(new Timestamp(System.currentTimeMillis()));//设置变动前的库存stockChangeHistory.setPreviousStockQuantity(previousStockQuantity);//设置变动后的库存stockChangeHistory.setNewStockQuantity(newStockQuantity);//设置变动量stockChangeHistory.setChangeAmount(changeAmount);//插入到 库存变动历史表 中stockChangeHistoryMapper.insert(stockChangeHistory);}
      }
  • 最后,在 Controller 层中暴露更新库存的接口:
    • @RestController
      @RequestMapping("/stock")
      public class StockController {@Autowiredprivate 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
      • -- 假设有一张用户表 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层的代码:

@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代码:

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

Controller层:

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 {@Autowiredprivate 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语句:

-- 读取数据并加锁
SELECT id, name FROM users WHERE id = 1 FOR UPDATE;-- 执行更新操作
UPDATE users SET name = 'new_name' WHERE id = 1;

使用场景

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


http://www.mrgr.cn/news/35708.html

相关文章:

  • My_SQL day3
  • .NET 9中的record类型:不可变数据结构的介绍与应用场景分析
  • Java:JVM
  • from sklearn.preprocessing import Imputer.处理缺失数据的工具
  • 【JAVA毕业设计】基于Vue和SpringBoot的宠物咖啡馆平台
  • 深度解析 Linux 系统下的 top 命令
  • 葡萄城亮相2024全球产品经理大会,共探创新之旅
  • 公司用什么软件监控电脑?分享6个常见的电脑监控软件,赶紧Get吧!
  • mac-m1安装nvm,docker,miniconda
  • 电商时代,品牌控价的新征程
  • 9.创新与未来:ChatGPT的新功能和趋势【9/10】
  • 牛客小白月赛101(下)
  • 人工智能与网络安全
  • 网络编程,端口号,网络字节序,udp
  • Logback 基本概念
  • Python 解析 html
  • 如何玩转生成式RAG工具LangChain,轻松构建应用程序?看这一篇就够了!
  • 2024最新Linux发行版,Kali Linux迎来劲敌,零基础入门到精通,收藏这一篇就够了
  • 网络资源模板--Android Studio 零食工坊(商城)
  • 【Kubernetes】常见面试题汇总(三十七)
  • OpenCVSharp中的图像金字塔详解
  • 问:JAVA中唤醒阻塞的线程有哪些?
  • SysML图例-核聚变
  • SAM核心代码注释总结
  • AJAX(简介以及一些用法)
  • 微信小程序开发