分布式锁
代码已同步至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.Driver
    url: jdbc:mysql://localhost:3306/distributed_lock?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: root
  # Redis配置
  redis:
    host: localhost
    database: 0
    port: 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;
}
- 基础项目结构如下:

简单实现减库存
- 修改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();
    /**
     * 减库存
     */
    @Override
    public 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);
//        }
    }
    /**
     * 重置库存数量
     */
    @Override
    public 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_stock
        set count = #{count}
    </update>
</mapper>
- 接口调用并测试

- 查看控制台

 使用接口一次一次调用时,每访问一次,库存量减1,没有任何问题。
简单演示超卖现象
 使用Jmeter压力测试工具,高并发下压测一下。恢复库存数为5000,添加线程组:并发100循环50次,即5000次请求。


 给线程组添加HTTP Request请求

 添加测试接口与请求路径

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

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

- 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

 此时如果还有人来下单,就会出现超卖现象(别人购买成功,而无货可发)。
传统锁处理
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();
    /**
     * 减库存
     */
    @Override
    public synchronized void checkAndLock() {
        stock.setCount(stock.getCount() - 1);
        log.info("库存余量:{}",stock.getCount());
    }
}
 Jmeter压测测试报告:

 库存余量:0

使用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();
    /**
     * 减库存
     */
    @Override
    public synchronized void checkAndLock() {
        lock.lock();
        try{
            stock.setCount(stock.getCount() - 1);
            log.info("库存余量:{}",stock.getCount());
        }finally {
            lock.unlock();
        }
    }
}
 Jmeter压测测试报告:

 库存余量:0

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

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 {
    /**
     * 减库存
     */
    @Override
    public 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 {
        }
    }
    /**
     * 重置库存数量
     */
    @Override
    public void reset() {
        this.baseMapper.reset(StockConstant.DEFAULT_STOCK_COUNT);
    }
}
 查看数据库余量:4846

 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
    @Transactional
    public 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 {
        }
    }
    /**
     * 重置库存数量
     */
    @Override
    public void reset() {
        this.baseMapper.reset(StockConstant.DEFAULT_STOCK_COUNT);
    }
}
 查看数据库库存余量:14

 JVM本地锁已失效
JVM本地锁失效场景之:集群部署
修改库存余量为5000
 复制启动类,并命名为DistributedLockApplication2,修改启动类的端口号为8002

 启动复制的服务:

 编辑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

 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 {
    /**
     * 减库存
     */
    @Override
    public 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 >=1
            this.baseMapper.updateStock(1,"1001");
        } finally {
        }
    }
    /**
     * 重置库存数量
     */
    @Override
    public 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

存在的问题
- 锁范围的问题
- 同一个商品可能有多条库存记录
- 无法记录库存变化前后的状态
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
代码实现
 新增数据库数据:

 修改服务类:
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
    @Transactional
    public 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 >=1
            this.baseMapper.updateStock(1, "1001");
        } finally {
        }
    }
    /**
     * 重置库存数量
     */
    @Override
    public 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

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
//    @Transactional
    public 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);
            // 更新版本号,在原版本号的基础上加1
            Integer 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

注意:
- 若需要递归调用确保数据更新成功,不要使用事务注解
- 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 {
    @Autowired
    private StringRedisTemplate redisTemplate;
    /**
     * 减库存
     */
    @Override
    public 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 >=1
            this.baseMapper.updateStock(1, "1001");
        } finally {
        }
    }
    /**
     * 重置库存数量
     */
    @Override
    public void reset() {
        this.baseMapper.reset(StockConstant.DEFAULT_STOCK_COUNT);
    }
}
 使用Jmeter进行压力测试,并查询库存余量

Redis乐观锁
watch:可以监控一个或多个key的值,如果在事务执行(exec)之前,key的值发生拜年话,则取消事务执行
multi:开启事务
exec:执行事务
 利用Redis监听+事务
$ watch stock
$ multi
$ set stock 5000
$ exec
 如果执行过程中,stock的值没有被其他链接改变,则执行成功

 如果执行过程中stock的值被改变,则执行失败
 
 更新StockService
 /**
     * 减库存
     */
    @Override
    public void checkAndLock() {
        redisTemplate.execute(new SessionCallback<Object>() {
            @Override
            public <K, V> Object execute(@NotNull RedisOperations<K, V> operations) throws DataAccessException {
                // watch
                operations.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){
                        // multi
                        operations.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

缺点
- 性能问题
- 由于运行机器的性能问题,可能导致连接数不够用
分布式锁
跨进程、跨服务、跨服务器
分布式锁的应用场景:
- 超卖现象(NoSQL)
- 缓存击穿
分布式锁的实现方式:
- 基于Redis实现
- 基于Zookeeper/etcd实现
- 基于MySQL实现



















