分布式锁的应用场景与分布式锁实现(一):传统锁处理并发及传统锁的问题

news/2024/11/8 14:49:22/

分布式锁

代码已同步至GitCode:https://gitcode.net/ruozhuliufeng/distributed-project.git

​ 在应用开发中,特别是Web工程开发,通常都是并发编程,不是多进程就是多线程。这种场景下极其容易出现线程并发性问题,此时不得不使用锁来解决问题。在多线程高并发场景下,为了保证资源的线程安全问题,jdk为我们提供了synchronized关键字和ReentrantLock可重入锁,但是它们只能提供一个工程内的线程安全。在分布式集群、微服务、云原生横行的当下,如何保证不同进程、不同服务、不同机器的线程安全问题,JDK并没有给我们提供既有的解决方案。此时,我们就必须借助于相关技术手动实现了。目前主流的实现有以下方式:

  • 基于MySQL关系型实现
  • 基于Redis非关系型数据实现
  • 基于Zookeeper/etcd实现

问题引入

从减库存说起

​ 多线程并发安全问题最典型的代表就是超卖现象。

​ 库存存在并发量较大情况下,很容易发生超卖现象,一旦发生超卖现象,就会出现多成交了订单而发不了货的情况。

场景:

​ 商品S库存余量为5时,用户A与用户B同时来购买一个商品,此时查询库存数都为5,库存充足则开始减库存:

​ 用户A: update db_stock set stock=stock-1 where id = 1

​ 用户B: update db_stock set stock=stock-1 where id = 1

​ 在并发情况下,更新后的结果可能是4,而实际的最终库存量应该是3才对。

环境准备

  • 数据库:MySQL 5.7
  • JAVA版本:1.8
  • 工程构建工具:Maven
  • 框架:SpringBoot、SpringMVC、MyBatis-Plus、SpringDataRedis
  • 开发工具:IDEA
  • 缓存服务:Redis
  • 负载均衡工具:Nginx
  • 接口与压测工具:Jmeter

创建基础数据表

  • 创建数据库表:db_stock
CREATE TABLE `db_stock` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`product_code` varchar(255) DEFAULT NULL COMMENT '商品编号',`stock_code` varchar(255) DEFAULT NULL COMMENT '仓库编号',`count` int(11) DEFAULT NULL COMMENT '库存量',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
  • 新增测试数据
INSERT INTO `distributed_lock`.`db_stock` (`id`, `product_code`, `stock_code`, `count`) VALUES (1, '1001', '001', 5000);

创建分布式锁demo工程

  • 使用IDEA新建SpringBoot项目,本次测试项目名:distributed-lock
  • 更新pom.xml文件,新增相关依赖
    <dependencies><!-- Spring --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><!--springboot默认使用内置tomcat,需要手动排除然后引入undertow(各方面性能更好,更稳定) --><exclusions><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-tomcat</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-undertow</artifactId></dependency><!-- MySQL --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.28</version></dependency><!-- MyBatis Plus --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.2</version></dependency><!-- Redis --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency></dependencies>
  • 创建application.yml文件,配置项目信息
server:# 端口port: 8001
spring:# 数据库datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/distributed_lock?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghaiusername: rootpassword: root# Redis配置redis:host: localhostdatabase: 0port: 6379
  • 启动类新增Mapper包扫描
@SpringBootApplication
@MapperScan("tech.msop.distributed.lock.mapper")
public class DistributedLockApplication {public static void main(String[] args) {SpringApplication.run(DistributedLockApplication.class, args);}
}
  • 新增实体类:StockEntity
/*** 库存信息实体*/
@Data
@TableName("db_stock")
public class StockEntity {/*** 主键ID*/@TableId(value = "id", type = IdType.AUTO)private Integer id;/*** 商品编号*/private String productCode;/*** 仓库编号*/private String stockCode;/*** 库存量*/private Integer count=5000;
}
  • 新增Mapper接口:StockMapper
public interface StockMapper extends BaseMapper<StockEntity> {
}
  • 新增Service服务:StockService

    • IStockService
    public interface IStockService extends IService<StockEntity> {
    }
    
    • StockServiceImpl
    @Service
    public class StockServiceImpl extends ServiceImpl<StockMapper, StockEntity>implements IStockService {
    }
    
  • 新增控制器:StockController

@RequestMapping("/stock")
@RestController
@RequiredArgsConstructor
public class StockController {private final IStockService stockService;}
  • 基础项目结构如下:

image-20230526141726828

简单实现减库存

  • 修改StockController
package tech.msop.distributed.lock.controller;import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.msop.distributed.lock.service.IStockService;/*** 库存 控制器*/
@RequestMapping("/stock")
@RestController
@RequiredArgsConstructor
public class StockController {private final IStockService stockService;/*** 减库存* @return*/@GetMapping("/check/lock")public String checkAndLock(){stockService.checkAndLock();return "验证库存并锁库存成功";}/*** 库存重置*/@GetMapping("/reset")public void reset(){stockService.reset();}}
  • 修改StockService
package tech.msop.distributed.lock.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import tech.msop.distributed.lock.constants.StockConstant;
import tech.msop.distributed.lock.entity.StockEntity;
import tech.msop.distributed.lock.mapper.StockMapper;
import tech.msop.distributed.lock.service.IStockService;/*** 库存服务实现类*/
@Service
@Slf4j
public class StockServiceImpl extends ServiceImpl<StockMapper, StockEntity>implements IStockService {private StockEntity stock = new StockEntity();/*** 减库存*/@Overridepublic void checkAndLock() {stock.setCount(stock.getCount() - 1);log.info("库存余量:{}",stock.getCount());
//        // 先查询库存是否充足
//        StockEntity stock = this.getById(1);
//        // 再减1个库存
//        if (stock != null && stock.getCount() >0 ){
//            stock.setCount(stock.getCount() - 1);
//            this.updateById(stock);
//        }}/*** 重置库存数量*/@Overridepublic void reset() {this.baseMapper.reset(StockConstant.DEFAULT_STOCK_COUNT);}
}
  • 修改StockMapper
package tech.msop.distributed.lock.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import tech.msop.distributed.lock.entity.StockEntity;public interface StockMapper extends BaseMapper<StockEntity> {void reset(@Param("count") Integer defaultStockCount);
}
  • 修改StockMapper.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="tech.msop.distributed.lock.mapper.StockMapper"><update id="reset">update db_stockset count = #{count}</update>
</mapper>
  • 接口调用并测试

image-20230526143845817

  • 查看控制台

image-20230526153856480

​ 使用接口一次一次调用时,每访问一次,库存量减1,没有任何问题。

简单演示超卖现象

​ 使用Jmeter压力测试工具,高并发下压测一下。恢复库存数为5000,添加线程组:并发100循环50次,即5000次请求。

image-20230526145321738

image-20230526145423703

​ 给线程组添加HTTP Request请求

image-20230526145503242

​ 添加测试接口与请求路径

image-20230526145553926

​ 选择想要的测试报表,这里选择聚和报告:

image-20230526145703422

​ 启动测试,查看压力测试报告

image-20230526154103065

  • Label 取样器别名,如果勾选Include Group Name,则会添加线程组的名称作为前缀
  • # Samples 取样器运行测试
  • Average 请求(事务)的平均响应时间
  • Median 中位数
  • 90%Line 90%用户响应时间
  • 95%Line 95%用户响应时间
  • 99%Line 99%用户响应时间
  • Min 最小响应时间
  • Max 最大响应时间
  • Error 错误率
  • Throughput 吞吐率
  • Received KB/sec 每秒收到的千字节
  • Sent KB/sec 每秒发送的千字节

​ 测试结果:请求总数5000次,平均请求时间54ms,中位数(50%)请求在26ms内完成的,错误率0%,每秒钟平均吞吐率1396.3次。

​ 查看数据库剩余库存数:461

image-20230526154206692

​ 此时如果还有人来下单,就会出现超卖现象(别人购买成功,而无货可发)。

传统锁处理

JVM本地锁处理

使用JVM锁:synchronized关键字
package tech.msop.distributed.lock.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import tech.msop.distributed.lock.constants.StockConstant;
import tech.msop.distributed.lock.entity.StockEntity;
import tech.msop.distributed.lock.mapper.StockMapper;
import tech.msop.distributed.lock.service.IStockService;/*** 库存服务实现类*/
@Service
@Slf4j
public class StockServiceImpl extends ServiceImpl<StockMapper, StockEntity>implements IStockService {private StockEntity stock = new StockEntity();/*** 减库存*/@Overridepublic synchronized void checkAndLock() {stock.setCount(stock.getCount() - 1);log.info("库存余量:{}",stock.getCount());}
}

​ Jmeter压测测试报告:

image-20230526154726384

​ 库存余量:0

image-20230526154740075

使用JVM锁:ReetrantLock
package tech.msop.distributed.lock.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import tech.msop.distributed.lock.constants.StockConstant;
import tech.msop.distributed.lock.entity.StockEntity;
import tech.msop.distributed.lock.mapper.StockMapper;
import tech.msop.distributed.lock.service.IStockService;import java.util.concurrent.locks.ReentrantLock;/*** 库存服务实现类*/
@Service
@Slf4j
public class StockServiceImpl extends ServiceImpl<StockMapper, StockEntity>implements IStockService {private StockEntity stock = new StockEntity();private ReentrantLock lock = new ReentrantLock();/*** 减库存*/@Overridepublic synchronized void checkAndLock() {lock.lock();try{stock.setCount(stock.getCount() - 1);log.info("库存余量:{}",stock.getCount());}finally {lock.unlock();}}
}

​ Jmeter压测测试报告:

image-20230526155231872

​ 库存余量:0

image-20230526155250238

原理

​ 添加了synchronized关键字后,StockService就具备了对象锁,由于添加了独占的排他锁,同一时刻只有一个请求能够获取到锁,并减库存。此时,所有请求只会one-by-one执行下去,也就不会发生超卖现象。

image-20230526155531198

JVM本地锁失效场景之:多例模式

​ Service添加多例模式注解,并进行压力测试

package tech.msop.distributed.lock.service.impl;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Service;
import tech.msop.distributed.lock.constants.StockConstant;
import tech.msop.distributed.lock.entity.StockEntity;
import tech.msop.distributed.lock.mapper.StockMapper;
import tech.msop.distributed.lock.service.IStockService;import java.util.concurrent.locks.ReentrantLock;/*** 库存服务实现类 <br/>* 注意:@Scope的proxyMode,若为Spring原生,使用的是JDK代理,proxyMode应为INTERFACES,<br/>* SpringBoot 2.x起,使用的是CGlib代理,proxyMode为TARGET_CLASS*/
@Service
@Slf4j
@Scope(value = "prototype",proxyMode = ScopedProxyMode.TARGET_CLASS)
public class StockServiceImpl extends ServiceImpl<StockMapper, StockEntity>implements IStockService {/*** 减库存*/@Overridepublic synchronized void checkAndLock() {try {// 先查询库存是否充足StockEntity stock = this.getOne(new QueryWrapper<StockEntity>().eq("product_code", "1001"));// 再减1个库存if (stock != null && stock.getCount() > 0) {stock.setCount(stock.getCount() - 1);this.updateById(stock);}} finally {}}/*** 重置库存数量*/@Overridepublic void reset() {this.baseMapper.reset(StockConstant.DEFAULT_STOCK_COUNT);}
}

​ 查看数据库余量:4846

image-20230526161327499

​ JVM本地锁已失效

JVM本地锁失效场景之:事务

更新库存余量为5000

​ 请求方法添加事务注解,并进行压力测试

package tech.msop.distributed.lock.service.impl;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tech.msop.distributed.lock.constants.StockConstant;
import tech.msop.distributed.lock.entity.StockEntity;
import tech.msop.distributed.lock.mapper.StockMapper;
import tech.msop.distributed.lock.service.IStockService;import java.util.concurrent.locks.ReentrantLock;/*** 库存服务实现类*/
@Service
@Slf4j
public class StockServiceImpl extends ServiceImpl<StockMapper, StockEntity>implements IStockService {/*** 减库存* 添加事务注解*/@Override@Transactionalpublic synchronized void checkAndLock() {try {// 先查询库存是否充足StockEntity stock = this.getOne(new QueryWrapper<StockEntity>().eq("product_code", "1001"));// 再减1个库存if (stock != null && stock.getCount() > 0) {stock.setCount(stock.getCount() - 1);this.updateById(stock);}} finally {}}/*** 重置库存数量*/@Overridepublic void reset() {this.baseMapper.reset(StockConstant.DEFAULT_STOCK_COUNT);}
}

​ 查看数据库库存余量:14

image-20230526161909083

​ JVM本地锁已失效

JVM本地锁失效场景之:集群部署

修改库存余量为5000

​ 复制启动类,并命名为DistributedLockApplication2,修改启动类的端口号为8002

image-20230526162704582

​ 启动复制的服务:

image-20230526162731725

​ 编辑Nginx的配置文件nginx.conf文件,实现负载均衡

worker_processes  1;events {worker_connections  1024;
}http {default_type  application/octet-stream;log_format  main  '$remote_addr - $remote_user [$time_local] "$request" ''$status $body_bytes_sent "$http_referer" ''"$http_user_agent" "$http_x_forwarded_for"';access_log  D:/Program/Nginx/access.log  main;sendfile        on;#tcp_nopush     on;keepalive_timeout  65;#gzip  on;upstream distributedLock{server localhost:8001;server localhost:8002;}server{listen  80;server_name localhost;location / {proxy_pass http://distributedLock;}}include D:/Program/Nginx/conf/conf.d/*.conf;}

​ 启动Nginx,修改Jmeter的HTTP请求,端口修改为80,并再次进行压力测试,查看数据库余量:2012

image-20230526163601519

​ JVM本地锁机制已失效

单SQL语句处理

在更新数量时进行判断

可以解决JVM本地锁失效的场景

​ 更新服务代码:

package tech.msop.distributed.lock.service.impl;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tech.msop.distributed.lock.constants.StockConstant;
import tech.msop.distributed.lock.entity.StockEntity;
import tech.msop.distributed.lock.mapper.StockMapper;
import tech.msop.distributed.lock.service.IStockService;import java.util.concurrent.locks.ReentrantLock;/*** 库存服务实现类 <br/>* 注意:@Scope的proxyMode,若为Spring原生,使用的是JDK代理,proxyMode应为INTERFACES,<br/>* SpringBoot 2.x起,使用的是CGlib代理,proxyMode为TARGET_CLASS*/
@Service
@Slf4j
public class StockServiceImpl extends ServiceImpl<StockMapper, StockEntity>implements IStockService {/*** 减库存*/@Overridepublic void checkAndLock() {try {// 1.先查询库存是否充足
//            StockEntity stock = this.getOne(new QueryWrapper<StockEntity>().eq("product_code", "1001"));
//            // 2.判断库存余量
//            if (stock != null && stock.getCount() > 0) {
//                stock.setCount(stock.getCount() - 1);
//                // 3.更新到数据库
//                this.updateById(stock);
//            }// update insert delete写操作本身就会加锁// 使用一条SQL语句完成减库存操作// update db_stock set count = count - 1 where product_code = '1001' and count >=1this.baseMapper.updateStock(1,"1001");} finally {}}/*** 重置库存数量*/@Overridepublic void reset() {this.baseMapper.reset(StockConstant.DEFAULT_STOCK_COUNT);}
}

​ 更新Mapper代码:

package tech.msop.distributed.lock.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import tech.msop.distributed.lock.entity.StockEntity;public interface StockMapper extends BaseMapper<StockEntity> {void reset(@Param("count") Integer defaultStockCount);@Update("update db_stock set count = count - #{count} where product_code = #{productCode} and count >= #{count} ")void updateStock(@Param("count") int count,@Param("productCode") String productCode);
}

​ 进行压力测试,并查看数据库余量:0

image-20230526164754771

存在的问题
  • 锁范围的问题
  • 同一个商品可能有多条库存记录
  • 无法记录库存变化前后的状态

MySQL悲观锁

select … for update

在MySQL的InnoDB中,预设的Transaction isolation level为REPEATABLE READ(可重读)

​ 在SELECT的读取锁定主要分为两种方式:

  • SELECT … LOCK IN SHARE MODE (共享锁)
  • SELECT … FOR UPDATE (悲观锁)

​ 这两种方式在事务(Transaction)进行当中SELECT到同一个数据库时,都必须等待其他事务数据被提交(Commit)后才会执行。

​ 而主要的不同在于LOCK IN SHARE MODE在有一方事务要UPDATE同一个表单时很容易造成死锁。

​ 简单来说,如果SELECT后若要UPDATE同一个表单,最好使用 SELECT …. FOR UPDATE

代码实现

​ 新增数据库数据:

image-20230526165048260

​ 修改服务类:

package tech.msop.distributed.lock.service.impl;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import tech.msop.distributed.lock.constants.StockConstant;
import tech.msop.distributed.lock.entity.StockEntity;
import tech.msop.distributed.lock.mapper.StockMapper;
import tech.msop.distributed.lock.service.IStockService;import java.util.List;
import java.util.concurrent.locks.ReentrantLock;/*** 库存服务实现类 <br/>* 注意:@Scope的proxyMode,若为Spring原生,使用的是JDK代理,proxyMode应为INTERFACES,<br/>* SpringBoot 2.x起,使用的是CGlib代理,proxyMode为TARGET_CLASS*/
@Service
@Slf4j
public class StockServiceImpl extends ServiceImpl<StockMapper, StockEntity>implements IStockService {/*** 减库存*/@Override@Transactionalpublic void checkAndLock() {// 1. 查询库存信息并锁定库存信息List<StockEntity> list = this.baseMapper.queryStock("1001");//      这里取第一个库存if (CollectionUtils.isEmpty(list)) {return;}StockEntity stock = list.get(0);// 2. 判断库存是否充足if (stock != null && stock.getCount() > 0) {stock.setCount(stock.getCount() - 1);// 3.更新到数据库this.updateById(stock);}}public void checkAndLock2() {try {// 1.先查询库存是否充足
//            StockEntity stock = this.getOne(new QueryWrapper<StockEntity>().eq("product_code", "1001"));
//            // 2.判断库存余量
//            if (stock != null && stock.getCount() > 0) {
//                stock.setCount(stock.getCount() - 1);
//                // 3.更新到数据库
//                this.updateById(stock);
//            }// update insert delete写操作本身就会加锁// 使用一条SQL语句完成减库存操作// update db_stock set count = count - 1 where product_code = '1001' and count >=1this.baseMapper.updateStock(1, "1001");} finally {}}/*** 重置库存数量*/@Overridepublic void reset() {this.baseMapper.reset(StockConstant.DEFAULT_STOCK_COUNT);}
}

​ 修改Mapper文件:

package tech.msop.distributed.lock.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import tech.msop.distributed.lock.entity.StockEntity;import java.util.List;public interface StockMapper extends BaseMapper<StockEntity> {void reset(@Param("count") Integer defaultStockCount);@Update("update db_stock set count = count - #{count} where product_code = #{productCode} and count >= #{count} ")void updateStock(@Param("count") int count,@Param("productCode") String productCode);@Select("select * from db_stock where product_code = #{productCode} for update")List<StockEntity> queryStock(@Param("productCode") String productCode);
}

​ 进行压力测试并查询数据库余量:0

image-20230526171152482

MySQL悲观锁中使用行级锁
  • 锁的查询或者更新条件必须是索引字段
  • 查询或者更新条件必须是具体值(如=、in,但like、!=条件均不可以,悲观锁仍是表级锁)
优缺点
  • 优点:
    • 解决同一个商品有多条库存记录同时更新的问题
    • 可以记录库存变化前后的状态
  • 缺点:
    • 性能问题
    • 死锁问题:对多条数据加锁时,加锁顺序要一致
    • 库存操作要统一:select … for update 普通的select

MySQL乐观锁

借助时间戳/version版本号/CAS机制实现

  • CAS:Compare And Swap(Set),比较并交换
    • 变量K 旧值A 新值B
    • 如用户更新密码,输入旧密码 A 与新密码 B,根据用户名 K 判断用户密码与旧密码是否一致,若一致,更新为新密码,否则放弃本次修改
  • 每次更新时,更新库存时同时更新新的时间戳/版本号,并判断时间戳/版本号是否与查询时的数据一致

​ 数据库表新增字段version

ALTER TABLE `distributed_lock`.`db_stock` 
ADD COLUMN `version` int(11) NULL DEFAULT 0 COMMENT '版本号' AFTER `count`;

​ 实体类同步新增字段:version

   /*** 版本号*/private Integer version;

​ 改造Service服务

package tech.msop.distributed.lock.service.impl;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import tech.msop.distributed.lock.constants.StockConstant;
import tech.msop.distributed.lock.entity.StockEntity;
import tech.msop.distributed.lock.mapper.StockMapper;
import tech.msop.distributed.lock.service.IStockService;import java.util.List;
import java.util.concurrent.locks.ReentrantLock;/*** 库存服务实现类 <br/>* 注意:@Scope的proxyMode,若为Spring原生,使用的是JDK代理,proxyMode应为INTERFACES,<br/>* SpringBoot 2.x起,使用的是CGlib代理,proxyMode为TARGET_CLASS*/
@Service
@Slf4j
public class StockServiceImpl extends ServiceImpl<StockMapper, StockEntity>implements IStockService {/*** 减库存* 乐观锁不要使用事务注解*/@Override
//    @Transactionalpublic void checkAndLock() {// 1. 查询库存信息List<StockEntity> list = this.list(new QueryWrapper<StockEntity>().eq("product_code","1001"));//      这里取第一个库存if (CollectionUtils.isEmpty(list)) {return;}StockEntity stock = list.get(0);// 2. 判断库存是否充足if (stock != null && stock.getCount() > 0) {// 3.更新到数据库stock.setCount(stock.getCount() - 1);// 更新版本号,在原版本号的基础上加1Integer version = stock.getVersion();stock.setVersion(version + 1);// 判断是否更新成功,更新失败则递归调用,直至保证更新成功// true 表示更新行数不为null且大于等于1,false 表示更新失败boolean result = this.update(stock,new UpdateWrapper<StockEntity>().eq("id",stock.getId()).eq("version",version));if (!result){// 避免栈内存溢出try{Thread.sleep(20);}catch (InterruptedException e){e.printStackTrace();}this.checkAndLock();}}}}

​ 使用Jmeter进行压力测试,并查询数据库余量:0

image-20230527134958119

注意:

  • 若需要递归调用确保数据更新成功,不要使用事务注解
    • MDL(更新、删除、新增)语句会自动加锁,重复调用可能会导致阻塞
  • 若需要递归调用确保数据更新成功,需要线程休眠一段时间,避免栈内存溢出
缺点
  • 高并发情况下,性能极低
  • ABA问题
    • 用户1查询数据X=A
    • 用户2更新数据X=B
    • 用户3更新数据X=C
    • 用户4更新数据X=A
    • 用户1更新数据时判断X是否等于A,若相同,更新X=S
      • 虽然X仍然等于A,但数据变更过
  • 读写分离情况下导致乐观锁不可靠
    • 写数据到主服务器,从服务器读取数据

MySQL锁总结

  • 性能:单SQL>悲观锁>JVM锁>乐观锁
  • 如果追求极致性能、业务场景简单并且不需要记录数据前后变化的情况下
    • 优先选择:单SQL
  • 如果写并发量较低(多读),争论不是很激烈的情况:
    • 优先选择:乐观锁
  • 如果写并发量较高,一般会经常冲突,此时选择乐观锁的话,会导致业务代码不间断的重试
    • 优先选择:悲观锁
  • 不推荐JVM本地锁

Redis乐观锁

更新Redis中的库存

​ 在Redis中新增库存:

$ set stock 5000

​ 更新StockService服务

package tech.msop.distributed.lock.service.impl;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import tech.msop.distributed.lock.constants.StockConstant;
import tech.msop.distributed.lock.entity.StockEntity;
import tech.msop.distributed.lock.mapper.StockMapper;
import tech.msop.distributed.lock.service.IStockService;import java.util.List;
import java.util.concurrent.locks.ReentrantLock;/*** 库存服务实现类 <br/>*/
@Service
@Slf4j
public class StockServiceImpl extends ServiceImpl<StockMapper, StockEntity>implements IStockService {@Autowiredprivate StringRedisTemplate redisTemplate;/*** 减库存*/@Overridepublic void checkAndLock() {// 1. 查询库存信息String stock = redisTemplate.opsForValue().get("stock");// 2. 判断库存是否充足if (stock != null && stock.length() != 0){Integer st = Integer.valueOf(stock);if (st > 0){// 3.更新到数据库redisTemplate.opsForValue().set("stock",String.valueOf(--st));}}}public void checkAndLock2() {try {// 1.先查询库存是否充足
//            StockEntity stock = this.getOne(new QueryWrapper<StockEntity>().eq("product_code", "1001"));
//            // 2.判断库存余量
//            if (stock != null && stock.getCount() > 0) {
//                stock.setCount(stock.getCount() - 1);
//                // 3.更新到数据库
//                this.updateById(stock);
//            }// update insert delete写操作本身就会加锁// 使用一条SQL语句完成减库存操作// update db_stock set count = count - 1 where product_code = '1001' and count >=1this.baseMapper.updateStock(1, "1001");} finally {}}/*** 重置库存数量*/@Overridepublic void reset() {this.baseMapper.reset(StockConstant.DEFAULT_STOCK_COUNT);}
}

​ 使用Jmeter进行压力测试,并查询库存余量

image-20230527141527673

Redis乐观锁

watch:可以监控一个或多个key的值,如果在事务执行(exec)之前,key的值发生拜年话,则取消事务执行

multi:开启事务

exec:执行事务

​ 利用Redis监听+事务

$ watch stock
$ multi
$ set stock 5000
$ exec

​ 如果执行过程中,stock的值没有被其他链接改变,则执行成功

image-20230527141934683

​ 如果执行过程中stock的值被改变,则执行失败

image-20230527142107031

​ 更新StockService

 /*** 减库存*/@Overridepublic void checkAndLock() {redisTemplate.execute(new SessionCallback<Object>() {@Overridepublic <K, V> Object execute(@NotNull RedisOperations<K, V> operations) throws DataAccessException {// watchoperations.watch((K) "stock");// 1. 查询库存信息String stock = (String) operations.opsForValue().get("stock");// 2. 判断库存是否充足if (stock != null && stock.length() != 0){Integer st = Integer.valueOf(stock);if (st > 0){// multioperations.multi();// 3.更新到数据库operations.opsForValue().set((K) "stock", (V) String.valueOf(--st));// exec 执行事务List<Object> exec = operations.exec();// 如果执行事务的返回结果集为空,则代表减库存失败,重试if (exec ==null || exec.size() == 0){try {Thread.sleep(40);} catch (InterruptedException e) {throw new RuntimeException(e);}checkAndLock();}return exec;}}return null;}});}

​ 使用Jmeter进行压力测试并查询库存余量:0

image-20230527143816559

缺点
  • 性能问题
  • 由于运行机器的性能问题,可能导致连接数不够用

分布式锁

跨进程、跨服务、跨服务器

分布式锁的应用场景:

  • 超卖现象(NoSQL)
  • 缓存击穿

分布式锁的实现方式:

  • 基于Redis实现
  • 基于Zookeeper/etcd实现
  • 基于MySQL实现

http://www.ppmy.cn/news/142400.html

相关文章

动态规划-硬币排成线

动态规划-硬币排成线 1 描述2 样例2.1 样例 1:2.2 样例 2:2.3 样例 3: 3 算法解题思路及实现3.1 算法解题分析3.1.1 确定状态3.1.2 转移方程3.1.3 初始条件和边界情况3.1.4 计算顺序 3.2 算法实现3.2.1 动态规划常规实现3.2.2 动态规划滚动数组 该题是lintcode的第394题&#x…

python-pandas按各种时间统计

使用到的库 pandas、matplotlib、numpy 使用到的函数 df.resample(“H”).sum() 参数 B business day frequency C custom business day frequency (experimental) D calendar day frequency W weekly frequency M month end frequency BM business month end frequency CBM…

内核 panic

/* * 2023/5/31 11:45 qing 38度 */ /* * Unable to handle kernel NULL pointer dereference at virtual address 00000000 */ 非法地址访问出错,使用了空指针 /* * BUG: scheduling while atomic: fpv_cams/605/0x00010001 */ 试图在不应该休眠的地…

国产给力啊啊啊----国产MCU-CW32F030开发学习

国产MCU-CW32F030开发学习 1. 相关资料下载 1.1 武汉芯源半导体 武汉芯源半导体官网 武汉芯源半导体 武汉芯源 21ic 电子论坛 21ic 电子论坛 1.2 CW32F030系列资料 CW32F030技术文档 • 内核&#xff1a;ARM Cortex-M0 – 最高主频64MHz • 工作温度&#xff1a;-40℃ 至…

安卓MVI架构模式常见问题:View层接收不到新的StateFlow状态

1、检查ui层是否正确监听了Flow 如果监听操作封装在ui层的基类&#xff0c;如BaseActivity&#xff0c;的initView方法中&#xff0c;那么实现类在重新父类的initView方法时&#xff0c;必须调用super.initView() 2、检查Ui层当前的ViewModel和发送新ui状态的ViewModel是否是…

uni-app使用echarts绘制数据可视化图

先打开项目 然后选择 使用命令行窗口打开所在目录(U) 在弹出的终端中输入指令来引入依赖 npm install echarts然后 我们 打开echarts的官网 点击这里这个 示例 找一个自己喜欢的案例点进去 我这里就用一个最简单的吧 代码看着不会乱 将他这个 option中的内容复制出来 然后…

Qt 帮助项目

Qt帮助项目收集生成压缩帮助文件所需的所有数据。除了诸如目录&#xff0c;索引关键字和帮助文档之类的实际帮助数据外&#xff0c;它还包含一些其他信息&#xff0c;例如用于标识帮助文件的名称空间。一个帮助项目代表一个文档集。 Qt帮助项目文件格式 文件格式是基于XML的。…

Error running Android Debugger (8600): Unable to open debugger port (localhost:8600): java.net.Conne

adb kill-server adb start-server 在AS底部找到Terminal 命令输入框&#xff0c;依次输入上面两个命令