目录
乐观锁
乐观锁的实现参考了这篇文章,里面还将了乐观锁的时间戳实现方式:
概述
乐观锁是一种并发控制策略,它假设多个事务不会发生冲突,在执行操作时不加锁,非常乐观,只需每次进行提交时利用标识进行对比,确认其他事务没修改过便可提交。
实现
使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。
-
即为数据增加一个版本标识,一般是给数据库表增加一个数字类型的 "version" 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一 。当我们提交更新的时候 ,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对 ,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。用下面的一张图来说明:
-
如上图所示,如果更新操作顺序执行,则数据的版本(version)依次递增,不会产生冲突。但是如果发生有不同的业务操作对同一版本的数据进行修改,那么,先提交的操作(图中B)会把数据version更新为2,当A在B之后提交更新时发现数据的version已经被修改了,那么A的更新操作会失败
-
假设我们有以下两张表(商品库存表和库存变动历史表),包含一个
version
字段用于乐观锁: -
对应实体类:
*javapackage 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; }
javapackage 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;
使用场景
悲观锁适合并发冲突多,写多读少的场景。通过每次加锁的形式来确保数据的安全性,吞吐量较低.