分布式锁详解

分布式锁

案例搭建

需求背景:

​ 电商项目中,用户购买商品后,会对商品的库存进行扣减

需求实现:

​ 根据用户购买商品及购买商品数量,对商品库存进行指定数量的扣减

  1. 数据库脚本

    create table if not exists `good_stock`
    (
        `id`       bigint not null auto_increment,
        `goods_id` bigint not null,
        `stock`    int    not null,
        primary key (`id`)
    ) engine = InnoDB
      auto_increment = 1
      default charset = utf8mb4 comment ='商品库存表';
    insert into good_stock (goods_id, stock)values (1,1);
    
  2. 案例技术点

    • Spring Boot3.0
    • JDK17
    • Mybatis
    • Lombok
  3. 新建distribute-lock-stock项目

    image-20240710093102782

    image-20240710093210687

    pom.xml文件

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.0.6</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>3.0.0</version>
        </dependency>
    
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter-test</artifactId>
            <version>3.0.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
  4. 配置application.yml

    server:
      port: 9099
    spring:
      application:
        name: stock-application
      datasource:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://10.11.1.96:3306/shop?serverTimezone=GMT%2B8&autoReconnect=false&useUnicode=true&characterEncoding=UTF-8&characterSetResults=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=true
        username: root
        password: xxxxxx
    
  5. 创建mapper接口

    package com.ajie.stock.mapper;
    
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Param;
    import org.apache.ibatis.annotations.Select;
    import org.apache.ibatis.annotations.Update;
    
    /**
     * @author ajie
     * @description: TODO
     */
    @Mapper
    public interface StockMapper {
    
        /**
         * 根据商品id查询库存
         * @param goodsId
         * @return
         */
        @Select("select stock from good_stock where goods_id = #{goodsId}")
        Integer selectStockByGoodsId(@Param("goodsId") Long goodsId);
    
        /**
         * 根据商品id更新库存
         * @param goodsId
         * @param stock
         * @return
         */
        @Update("update good_stock set stock = #{stock} where goods_id = #{goodsId};")
        Integer updateStockByGoodsId(@Param("goodsId") Long goodsId, @Param("stock") Integer stock);
    }
    
  6. 编写service层代码

    StockService

    package com.ajie.stock.service;
    
    public interface StockService {
        String deductStock(Long goodsId, Integer stock);
    }
    

    StockServiceImpl

    package com.ajie.stock.service.impl;
    
    import com.ajie.stock.mapper.StockMapper;
    import com.ajie.stock.service.StockService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    /**
     * @author ajie
     * @description: TODO
     */
    @Service
    public class StockServiceImpl implements StockService {
        @Autowired
        private StockMapper stockMapper;
    
        @Override
        public String deductStock(Long goodsId, Integer count) {
            // 1.根据商品id查询库存
            Integer stock = stockMapper.selectStockByGoodsId(goodsId);
            // 2.判断库存数量是否足够
            if (stock < count) {
                return "库存不足";
            }
            // 3.库存数量足够,扣减库存
            stockMapper.updateStockByGoodsId(goodsId, stock - count);
            return "库存扣减成功";
        }
    }
    
  7. 编写controller层代码

    package com.ajie.stock.controller;
    
    import com.ajie.stock.service.StockService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * @author ajie
     * @description: TODO
     */
    @RestController
    @RequestMapping("stock")
    public class StockController {
    
        @Autowired
        private StockService stockService;
    
        @GetMapping("deductStock/{goodsId}/{count}")
        public String deductStock(@PathVariable Long goodsId,
                                  @PathVariable Integer count){
            return stockService.deductStock(goodsId, count);
        }
    }
    
  8. 启动项目进行测试

    在resources目录下新建.http文件stock.http

    image-20240710103226089

    image-20240710103244047

    自此项目案例搭建完毕,库存也成功扣减

jemeter使用

  1. 创建线程组

    image-20240710104406231

    image-20240710104442032

  2. 创建http请求

    image-20240710104524028

    image-20240710104627591

  3. 创建聚合报告

    image-20240710104723792

  4. 将数据库中的库存改为6000,因为一共会发送6000次请求。然后点击启动jemeter

    结果如下图所示

    image-20240710104923974

    image-20240710104935277

    从结果可以看出,库存量并没有按照预期的结果减为0。这里涉及到了并发操作共享资源的问题

jvm锁

使用synchronized

修改service类中的方法,加上synchronized关键字

@Override
public synchronized String deductStock(Long goodsId, Integer count) {
    // 1.根据商品id查询库存
    Integer stock = stockMapper.selectStockByGoodsId(goodsId);
    // 2.判断库存数量是否足够
    if (stock < count) {
        return "库存不足";
    }
    // 3.库存数量足够,扣减库存
    stockMapper.updateStockByGoodsId(goodsId, stock - count);
    return "库存扣减成功";
}

重新启动项目,修改数据库的库存为6000,使用jemeter测试,进行两次测试,记录第二次的执行性能

image-20240710110644044

image-20240710110655396

通过上面两个截图可以看到,通过加synchronized可以解决库存因为并发扣减异常的问题

使用ReentrantLock

  1. 修改service类中的方法

    public static final ReentrantLock LOCK = new ReentrantLock();
    
    @Override
    public String deductStock(Long goodsId, Integer count) {
        // 加锁
        LOCK.lock();
        try {
            // 1.根据商品id查询库存
            Integer stock = stockMapper.selectStockByGoodsId(goodsId);
            // 2.判断库存数量是否足够
            if (stock < count) {
                return "库存不足";
            }
            // 3.库存数量足够,扣减库存
            stockMapper.updateStockByGoodsId(goodsId, stock - count);
            return "库存扣减成功";
        } finally {
            // 解锁
            LOCK.unlock();
        }
    }
    
  2. 重新启动项目,修改数据库的库存为6000,使用jemeter测试,进行两次测试,记录第二次的执行性能

    image-20240710111537807

    image-20240710111549407

    通过上面两个截图可以看到,通过加ReentrantLock也可以解决库存因为并发扣减异常的问题

执行性能对比

三种执行方式的性能对比

## 没有任何限制的情况下
HTTP请求	6000	63	49	115	158	258	7	606	0.0	670.8407871198568	117.76291193817083	91.06139590787119

## 加synchronized锁
HTTP请求	6000	951	865	1811	2207	3019	14	4535	0.0	50.99959200326398	8.964772031823745	6.922796180130559

## 加ReentrantLock锁
HTTP请求	6000	952	876	1463	1704	1975	16	2254	0.0	52.028685148411824	9.145667311244265	7.062487534794183

可以看到加锁以后有明显的吞吐量下降

jvm锁失效场景

多例模式

在多例模式下,锁失效必须满足以下条件:

  • 使用synchronized锁
  • 类的代理使用CGLIB代理
  • synchronized锁的对象必须是某个类的实例对象,而不能是类对象

案例演示:

  1. 修改service代码

    package com.ajie.stock.service.impl;
    
    import com.ajie.stock.mapper.StockMapper;
    import com.ajie.stock.service.StockService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Scope;
    import org.springframework.context.annotation.ScopedProxyMode;
    import org.springframework.stereotype.Service;
    
    
    /**
     * @author ajie
     * @description: TODO
     */
    @Service
    // 使用scope注解,将Spring的bean对象变成多例的
    @Scope(value = "prototype",proxyMode = ScopedProxyMode.TARGET_CLASS)
    public class StockServiceImpl implements StockService {
        @Autowired
        private StockMapper stockMapper;
    
        @Override
        public synchronized String deductStock(Long goodsId, Integer count) {
            // 1.根据商品id查询库存
            Integer stock = stockMapper.selectStockByGoodsId(goodsId);
            // 2.判断库存数量是否足够
            if (stock < count) {
                return "库存不足";
            }
            // 3.库存数量足够,扣减库存
            stockMapper.updateStockByGoodsId(goodsId, stock - count);
            return "库存扣减成功";
        }
    }
    
  2. 修改数据中的库存,进行测试

    image-20240710114131445

    image-20240710114144442

    可以看出,使用多例模式锁失效了

锁失效原因分析:

  1. synchronized如果没有指定类对象作为锁对象,默认使用的是实例对象作为锁对象
  2. 多例模式下,每个线程执行方法用的实例对象都不一样

解决方法:

手动指定synchronized的锁对象为类对象

  1. 修改service代码

    package com.ajie.stock.service.impl;
    
    import com.ajie.stock.mapper.StockMapper;
    import com.ajie.stock.service.StockService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Scope;
    import org.springframework.context.annotation.ScopedProxyMode;
    import org.springframework.stereotype.Service;
    
    
    /**
     * @author ajie
     * @description: TODO
     */
    @Service
    @Scope(value = "prototype",proxyMode = ScopedProxyMode.TARGET_CLASS)
    public class StockServiceImpl implements StockService {
        @Autowired
        private StockMapper stockMapper;
    
        @Override
        public  String deductStock(Long goodsId, Integer count) {
            // 指定类对象作为锁对象,解决多例模式锁失效场景
            synchronized(StockServiceImpl.class){
                // 1.根据商品id查询库存
                Integer stock = stockMapper.selectStockByGoodsId(goodsId);
                // 2.判断库存数量是否足够
                if (stock < count) {
                    return "库存不足";
                }
                // 3.库存数量足够,扣减库存
                stockMapper.updateStockByGoodsId(goodsId, stock - count);
                return "库存扣减成功";
            }
        }
    }
    
  2. 查看结果

    image-20240710115235435

    image-20240710115244549

    锁失效现象已经得到解决

事务注解

事务注解场景下,对两种锁都会失效

案例演示:

  • 修改service代码

    package com.ajie.stock.service.impl;
    
    import com.ajie.stock.mapper.StockMapper;
    import com.ajie.stock.service.StockService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Scope;
    import org.springframework.context.annotation.ScopedProxyMode;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    
    /**
     * @author ajie
     * @description: TODO
     */
    @Service
    public class StockServiceImpl implements StockService {
        @Autowired
        private StockMapper stockMapper;
    
        // 加事务注解
        @Transactional
        @Override
        public synchronized String deductStock(Long goodsId, Integer count) {
            // 1.根据商品id查询库存
            Integer stock = stockMapper.selectStockByGoodsId(goodsId);
            // 2.判断库存数量是否足够
            if (stock < count) {
                return "库存不足";
            }
            // 3.库存数量足够,扣减库存
            stockMapper.updateStockByGoodsId(goodsId, stock - count);
            return "库存扣减成功";
        }
    }
    
  • 修改数据中的库存,进行测试

    image-20240710141645143

    image-20240710141654582

    可以看出,使用事务对代码进行控制,锁失效了

事务注解导致锁失效的原理图:

image-20240710142910005

发生这种现象的原因是事务的隔离级别,InnoDB引擎默认支持的是读未提交。

解决方法:

将加锁、解锁动作放在事务外面:

  1. service接口代码改造

    package com.ajie.stock.service;
    
    public interface StockService {
        String deductStock(Long goodsId, Integer stock);
    
        String deduct(Long goodsId, Integer count);
    
    }
    
  2. service实现类代码改造

    package com.ajie.stock.service.impl;
    
    import com.ajie.stock.mapper.StockMapper;
    import com.ajie.stock.service.StockService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    
    /**
     * @author ajie
     * @description: TODO
     */
    @Service
    //@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
    public class StockServiceImpl implements StockService {
        @Autowired
        private StockMapper stockMapper;
        @Autowired
        private StockService owner;
    
        @Override
        public synchronized String deductStock(Long goodsId, Integer count) {
            return owner.deduct(goodsId, count);
        }
    
        @Transactional
        public String deduct(Long goodsId, Integer count) {
            // 1.根据商品id查询库存
            Integer stock = stockMapper.selectStockByGoodsId(goodsId);
            // 2.判断库存数量是否足够
            if (stock < count) {
                return "库存不足";
            }
            // 3.库存数量足够,扣减库存
            stockMapper.updateStockByGoodsId(goodsId, stock - count);
            return "库存扣减成功";
        }
    }
    
  3. 解决依赖注入问题

    spring:
      main:
      	# 允许定义相同名称的bean,名称相同,后者覆盖前者
        allow-bean-definition-overriding: true
        # 解决循环依赖问题
        allow-circular-references: true
    
  4. 修改数据中的库存,进行测试

    image-20240710144054400

    image-20240710144103465

    锁失效现象已经得到解决

集群模式

案例演示:

  1. 搭建nginx反向代理

    配置文件

    upstream distribute{
     	server localhost:9098;
    	server localhost:9099;
    }
    server {
        listen       8000;
        listen  [::]:80;
        server_name  localhost;
    
        location /distribute{
    	proxy_pass http://distribute;
        }
    }
    
    
  2. 配置idea启动两个应用实例

    image-20240710145520231

    image-20240710145553150

  3. 修改jemeter

    image-20240710151142556

  4. 进行代码测试

    image-20240710151333742

    image-20240710151341931

失效原因:

​ 无论是synchronized锁还是ReentrantLock锁使用的锁对象都是内存中的数据。当启动多个进程以后,由于不同进程间的内存资源是不共享的,那么同一个进程里面的多个线程还是可以受到锁的控制,但是不同进程间的线程就不受控了

mysql锁

悲观锁

单条update语句
  1. mapper层代码改造

    package com.ajie.stock.mapper;
    
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Param;
    import org.apache.ibatis.annotations.Select;
    import org.apache.ibatis.annotations.Update;
    
    /**
     * @author ajie
     * @description: TODO
     */
    @Mapper
    public interface StockMapper {
    
        /**
         * 通过单条update语句实现扣减库存
         * @param goodsId
         * @param count
         * @return
         */
        @Update("update good_stock set stock = stock-#{count} where goods_id = #{goodsId} and stock>=#{count};")
        Integer updateStockByGoodsIdAndCount(@Param("goodsId") Long goodsId, @Param("count") Integer count);
    }
    
  2. service层代码改造

    package com.ajie.stock.service.impl;
    
    import com.ajie.stock.mapper.StockMapper;
    import com.ajie.stock.service.StockService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    
    /**
     * @author ajie
     * @description: TODO
     */
    @Service
    public class StockServiceImpl implements StockService {
        @Autowired
        private StockMapper stockMapper;
    
        public String deductStock(Long goodsId, Integer count) {
            Integer update = stockMapper.updateStockByGoodsIdAndCount(goodsId, count);
            if(update>0){
                return "库存扣减成功";
            }
            return "库存不足";
        }
    }
    
    
  3. 进行代码测试

    image-20240710152846954

    image-20240710152856831

    通过上面的结果可以看出,解决了由于并发带来的共享资源问题。同时吞吐量也是比较高的

单条update语句问题:

  • 易造成缩范围过大:update语句中,如果更新条件不是索引字段,会升级为表锁
  • 无法再程序中获取到扣减库存之前的库存值
  • 在复杂业务场景下无法满足业务需求
  • 死锁问题
for update
  1. mapper层代码改造

    package com.ajie.stock.mapper;
    
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Param;
    import org.apache.ibatis.annotations.Select;
    import org.apache.ibatis.annotations.Update;
    
    /**
     * @author ajie
     * @description: TODO
     */
    @Mapper
    public interface StockMapper {
    
        /**
         * 根据商品id更新库存
         * @param goodsId
         * @param stock
         * @return
         */
        @Update("update good_stock set stock = #{stock} where goods_id = #{goodsId};")
        Integer updateStockByGoodsId(@Param("goodsId") Long goodsId, @Param("stock") Integer stock);
    
        /**
         * 根据商品id查询库存
         * @param goodsId
         * @return
         */
        @Select("select stock from good_stock where goods_id = #{goodsId} for update")
        Integer selectStockByGoodsIdForUpdate(@Param("goodsId") Long goodsId);
    }
    
    
  2. service层代码改造

    package com.ajie.stock.service.impl;
    
    import com.ajie.stock.mapper.StockMapper;
    import com.ajie.stock.service.StockService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    
    /**
     * @author ajie
     * @description: TODO
     */
    @Service
    public class StockServiceImpl implements StockService {
        @Autowired
        private StockMapper stockMapper;
    
        @Transactional
        @Override
        public String deductStock(Long goodsId, Integer count) {
            // 1.根据商品id查询库存
            Integer stock = stockMapper.selectStockByGoodsIdForUpdate(goodsId);
            // 2.判断库存数量是否足够
            if (stock < count) {
                return "库存不足";
            }
            // 3.库存数量足够,扣减库存
            stockMapper.updateStockByGoodsId(goodsId, stock - count);
            return "库存扣减成功";
        }
    }
    
    

    使用for update会在事务开始之后对查询的数据加锁,知道事务提交以后才释放锁。所以这里一定要对整个方法加事务注解,要让整个方法逻辑执行完以后才能释放锁

  3. 进行代码测试

    image-20240710154428767

    image-20240710154437141

    通过上面的结果可以看出,实用for update加事务注解也能解决由于并发带来的共享资源问题。但是吞吐量比较低

for update语句问题:

  • 易造成缩范围过大(和update语句原理一样)
  • 性能较差
  • 死锁问题
  • select … for update 和普通的select语句读取到的内容不一致:前者可以读取到另一个会话未提交的数据,后者不能读取到

单条update和for update性能对比

## 单条update语句
HTTP请求	6000	55	47	80	114	155	7	824	0.0	812.6777732629013	141.26625355546525	119.0445956928078

## for update方式
HTTP请求	6000	765	778	906	952	1101	152	1447	0.0	64.59878769608423	11.229086142483393	9.462713041418588

乐观锁

​ 乐观锁总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,只在更新的时候会判断一下在此期间别人有没有去更新这个数据

版本号

实现方式:

  1. 给数据库表增加一列 version
  2. 读取数据的时候,将version字段一并读出
  3. 数据每更新一次,version字段加1
  4. 提交更新时,判断库中的version字段值和之前取出来的version比较
  5. 相同更新,不相同重试

案例实操:

  1. goods_stock表新增version字段

    image-20240710160500037

  2. 新增Stock实体类

    package com.ajie.stock.entity;
    
    import lombok.Data;
    
    /**
     * @author ajie
     * @description: TODO
     */
    @Data
    public class Stock {
        private Long id;
        private Long goodsId;
        private Integer stock;
        private Integer version;
    }
    
  3. 修改mapper层代码

    package com.ajie.stock.mapper;
    
    import com.ajie.stock.entity.Stock;
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Param;
    import org.apache.ibatis.annotations.Select;
    import org.apache.ibatis.annotations.Update;
    
    /**
     * @author ajie
     * @description: TODO
     */
    @Mapper
    public interface StockMapper {
    
        /**
         * 根据商品id查询库存和版本号
         * @param goodsId
         * @return
         */
        @Select("select stock,version from good_stock where goods_id = #{goodsId}")
        Stock selectStockAndVersionByGoodsId(@Param("goodsId") Long goodsId);
    
        /**
         * 根据商品id和版本号更新库存和版本号信息
         * @param goodsId
         * @param stock
         * @return
         */
        @Update("update good_stock set stock = #{stock},version=#{version}+1 where goods_id = #{goodsId} and version = #{version};")
        Integer updateStockByGoodsIdAndVersion(@Param("goodsId") Long goodsId,
                                               @Param("stock") Integer stock,
                                               @Param("version")Integer version);
    }
    
  4. 修改service代码

    package com.ajie.stock.service.impl;
    
    import com.ajie.stock.entity.Stock;
    import com.ajie.stock.mapper.StockMapper;
    import com.ajie.stock.service.StockService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    
    /**
     * @author ajie
     * @description: TODO
     */
    @Service
    public class StockServiceImpl implements StockService {
        @Autowired
        private StockMapper stockMapper;
        
        @Override
        public String deductStock(Long goodsId, Integer count) {
            Integer result = 0;
            while (result == 0){
                // 1.根据商品id查询库存
                Stock stock = stockMapper.selectStockAndVersionByGoodsId(goodsId);
                // 2.判断库存数量是否足够
                if (stock.getStock() < count) {
                    return "库存不足";
                }
                // 3.库存数量足够,扣减库存
                result = stockMapper.updateStockByGoodsIdAndVersion(goodsId,
                        stock.getStock() - count, stock.getVersion());
            }
            return "库存扣减成功";
        }
    }
    
  5. 进行代码测试

    image-20240710161650716

    image-20240710161703168

    通过上面的结果可以看到,使用乐观锁的方式解决了并发资源共享问题,同时版本号也增加到了6000

时间戳

实现方式:

  1. 给数据库表增加一列timestamp
  2. 读取数据的时候,将timestamp字段一并读出
  3. 数据没更新一次,timestamp取当前时间戳
  4. 提交更新时,判断库中的timestamp字段和之前取出来的timestamp比较
  5. 想通更新,不相同重试

实现方式和版本号一样,这里就不做演示了

乐观锁存在的问题:

  • 高并发写操作下性能很低:大量线程不断循环,有的循环几千次才能更新成功
  • ABA问题。一个线程修改后,又把数据修改回去,另一个线程读取的数据虽然没有变,但是已经是修改过的数据了

分布式锁

分布式锁是控制分布式系统之间同步访问共享资源的额一种方式

普通锁示意图:

image-20240710170640735

分布式锁示意图:

image-20240710170826470

实现方式:

  • Redis实现分布式锁
  • Zookeeper实现分布式锁
  • Mysql实现分布式锁
  • Etcd实现分布式锁

实现分布式锁注意的点

  • 互斥性
  • 可重入性
  • 锁超时、防死锁
  • 锁释放正确、防误删
  • 阻塞和非阻塞
  • 公平和非公平

创建抽象类AbstractLock 实现Lock接口,重写里面的方法

package com.ajie.stock.lock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * @author ajie
 * @description: TODO
 */
public class AbstractLock implements Lock {
    @Override
    public void lock() {
        
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    @Override
    public void unlock() {

    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

​ 使用抽象类实现接口的目的方便扩展。将来我们会实现Redis、Zookeeper、mysql、Etcd等各种分布式锁。这些分布式锁有一些共同的属性,还有一些共同的方法,这些方法是Lock接口所不具有的。如果不借助抽象类,那么每种锁的类里面都要写一次,不具备复用性。

Redis实现分布式锁

Redis的特点:

  • Redis是高性能的内存数据库,满足高并发的需求
  • Redis支持原子性操作,保证操作的原子性和一致性
  • Redis支持分布式部署,支持多节点间数据同步和复制,从而提高高可用性和容错性
简化版
  1. 添加Redis依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
  2. yml新增配置

    spring:
      data:
        redis:
          host: xxxxxxx
          # 端口,默认为6379
          port: 6379
          # 数据库索引
          database: 1
          # 密码
          password: xxxxx
          # 连接超时时间
          timeout: 60s
          lettuce:
            pool:
              # 连接池中的最小空闲连接
              min-idle: 0
              # 连接池中的最大空闲连接
              max-idle: 8
              # 连接池的最大数据库连接数
              max-active: 8
              # #连接池最大阻塞等待时间(使用负值表示没有限制)
              max-wait: -1ms
    
  3. 创建RedisLock集成AbstractLock类

    package com.ajie.stock.lock;
    
    import org.springframework.data.redis.core.StringRedisTemplate;
    
    import java.util.concurrent.TimeUnit;
    
    /**
     * @author ajie
     * @description: TODO
     */
    public class RedisLock extends AbstractLock {
        private StringRedisTemplate redisTemplate;
    
        public RedisLock(StringRedisTemplate redisTemplate, String lockName) {
            this.redisTemplate = redisTemplate;
            this.lockName = lockName;
        }
    
        @Override
        public void lock() {
            // 使用setnx指令进行加锁
            while (true) {
                Boolean result = redisTemplate.opsForValue().setIfAbsent(lockName, "1");
                if (result != null && result) {
                    break;
                }
                // 每次获取锁,如果没拿到,睡眠50毫秒
                try {
                    TimeUnit.MILLISECONDS.sleep(50);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    
        @Override
        public void unlock() {
            redisTemplate.delete(lockName);
        }
    }
    
  4. 改造service层方法

    package com.ajie.stock.service.impl;
    
    import com.ajie.stock.lock.AbstractLock;
    import com.ajie.stock.lock.RedisLock;
    import com.ajie.stock.mapper.StockMapper;
    import com.ajie.stock.service.StockService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.stereotype.Service;
    import org.springframework.util.StringUtils;
    
    
    /**
     * @author ajie
     * @description: TODO
     */
    @Service
    public class StockServiceImpl implements StockService {
        @Autowired
        private StockMapper stockMapper;
        @Autowired
        private StringRedisTemplate redisTemplate;
        
        @Override
        public String deductStock(Long goodsId, Integer count) {
            AbstractLock lock = null;
    
            try {
                lock = new RedisLock(redisTemplate, "lock" + goodsId);
                lock.lock();
                // 根据商品id查询库存
                String stock = redisTemplate.opsForValue().get("stock" + goodsId);
                // 判断商品是否存在
                if (StringUtils.isEmpty(stock)) {
                    return "商品不存在";
                }
                int lastLock = Integer.parseInt(stock);
                // 2.判断库存数量是否足够
                if (lastLock < count) {
                    return "库存不足";
                }
                redisTemplate.opsForValue().set("stock" + goodsId, String.valueOf(lastLock - count));
                return "库存扣减成功";
            } finally {
                if (lock != null) {
                    lock.unlock();
                }
            }
        }
    }
    
    
  5. 在Redis客户端设置库存为7000

    > set stock1 7000
    OK
    > get stock1
    7000
    
  6. 启动项目进行测试

    image-20240711094508588

    image-20240711094516389

    通过上面的结果可以看到Redis实现的分布式锁成功了。

简易版本的分布式锁解决的问题:

  • 互斥性

未解决的问题:

  • 可重入性
  • 锁超时、防死锁
  • 锁释放正确、防误删
  • 阻塞和非阻塞
  • 公平和非公平
增加过期时间

通过增加过期时间,解决锁超时、死锁问题

  1. 在抽象类AbstractLock中增加带有过期时间参数的lock方法

    package com.ajie.stock.lock;
    
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    
    /**
     * @author ajie
     * @description: TODO
     */
    public abstract class AbstractLock implements Lock {
        protected String lockName;
    
        @Override
        public Condition newCondition() {
            return null;
        }
    
        @Override
        public void unlock() {
    
        }
    
        @Override
        public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
            return false;
        }
    
        @Override
        public boolean tryLock() {
            return false;
        }
    
        @Override
        public void lockInterruptibly() throws InterruptedException {
    
        }
    
        @Override
        public void lock() {
    
        }
    	// 新增带有过期时间的lock方法
        public abstract void lock(Long expire, TimeUnit unit);
    }
    
  2. 修改RedisLock类的代码

    package com.ajie.stock.lock;
    
    import org.springframework.data.redis.core.StringRedisTemplate;
    
    import java.util.concurrent.TimeUnit;
    
    /**
     * @author ajie
     * @description: TODO
     */
    public class RedisLock extends AbstractLock {
        private StringRedisTemplate redisTemplate;
        // 设置默认的锁过期时间
        public static final long DEFAULT_EXPIRE = 30000;
    
        public RedisLock(StringRedisTemplate redisTemplate, String lockName) {
            this.redisTemplate = redisTemplate;
            this.lockName = lockName;
        }
    
        @Override
        public void lock() {
            lock(DEFAULT_EXPIRE,TimeUnit.MILLISECONDS);
        }
    
        @Override
        public void lock(Long expire, TimeUnit unit) {
            // 使用setnx指令进行加锁
            while (true) {
                // 使用带有过期时间的设置值的方法
                Boolean result = redisTemplate.opsForValue().setIfAbsent(lockName, "1",
                        expire, unit);
                if (result != null && result) {
                    break;
                }
                try {
                    TimeUnit.MILLISECONDS.sleep(50);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    
        @Override
        public void unlock() {
            redisTemplate.delete(lockName);
        }
    }
    
  3. 通过加过期时间,当应用宕机以后,锁在指定时间以后依然可以释放掉

增加UUID

通过增加UUID,解决锁误删问题

误删场景:

  1. 线程A获取到锁,但是由于业务逻辑在负载增加的时候,执行起来是比较耗时的,在业务逻辑还没执行完,锁过期时间到了。线程A获得的锁没有了
  2. 此时线程B就可以获取到锁,在线程B执行的过程中,线程A的逻辑执行完毕,开始删除锁
  3. 线程A的锁已经没了,此时删除的锁是B所持有的锁,这就是锁的误删

改造代码,增加UUID,修改RedisLock

package com.ajie.stock.lock;

import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.concurrent.TimeUnit;

/**
 * @author ajie
 * @description: TODO
 */
public class RedisLock extends AbstractLock {
    private StringRedisTemplate redisTemplate;
    public static final long DEFAULT_EXPIRE = 30000;
    public final String UUID;

    public RedisLock(StringRedisTemplate redisTemplate, String lockName) {
        this.redisTemplate = redisTemplate;
        this.lockName = lockName;
        // 增加UUID,解决锁误删问题
        this.UUID = java.util.UUID.randomUUID().toString().replaceAll("-", "");
    }

    @Override
    public void lock() {
        lock(DEFAULT_EXPIRE, TimeUnit.MILLISECONDS);
    }

    @Override
    public void lock(Long expire, TimeUnit unit) {
        // 使用setnx指令进行加锁
        while (true) {
            Boolean result = redisTemplate.opsForValue().setIfAbsent(lockName, UUID,
                    expire, unit);
            if (result != null && result) {
                break;
            }
            try {
                TimeUnit.MILLISECONDS.sleep(50);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    @Override
    public void unlock() {
        String uuid = redisTemplate.opsForValue().get(lockName);
		// 先判断是否是当前线程的锁
        if (uuid != null && uuid.equals(UUID)) {
            redisTemplate.delete(lockName);
        }
    }
}
初识Lua脚本

上面实现的分布式锁存在三个问题:

  1. 加锁逻辑使用了Redis的get、set指令,不是原子性的,中间会有穿插
  2. 释放锁的逻辑也是的,现获取值,再释放锁,这两步操作也不是原子性,也会出现指令的并发问题
  3. 对于公平和非公平锁的实现,需要复杂的Redis指令实现,这个也需要保证指令执行的原子性

基于上面的问题可以使用Lua脚本去实现。Redis支持Lua脚本,对于Lua脚本代码的执行是原子性操作的

Lua介绍

​ Lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放,其设计的目的是为了嵌入到程序中,从而为应用程序提供灵活的扩展和定制功能

Lua特性
  • 轻量级:它用标准C语言编写并以源代码形式开放,编译后仅仅100余K,可以很方便的嵌入到别的应用程序里
  • 可扩展:Lua提供了非常易于使用的扩展接口和定制:由宿主语言(通常是C或C++)提供这些功能,Lua可以使用他们,就像是本地内置的功能一样
Lua安装

前往github下载Windows版本的可执行文件

https://github.com/rjpcomputing/luaforwindows/releases/tag/v5.1.5-52

点击可执行文件,一直下一步就可以了

测试安装

  1. 在项目根目录创建test.lua文件

  2. 编写测试代码

    print('hello world')
    
  3. 在cmd窗口执行

    image-20240711141658757

数据类型
数据类型描述
nil这个最简单,只有nil属于该类,表示一个无效值(在条件表达式中相当于false)
boolean包含两个值:false和true
number表示双精度类型的实浮点数
string字符串由一对双引号或单引号来表示
function由C或lua编写的函数
tableLua中的表(table)其实是一个"关联数组",数组的索引可以是数字,字符串或表类型。在Lua中,table的创建是通过“构造表达式”来完成的,最简单构造表达式是{},用来创建一个空表
-- 数据类型
print(type("hello world")) -- string
print(type(1)) -- number
print(type(2.2))  -- number
print(type(print))  -- function
print(type(type))  -- function
print(type(nil)) -- nil
print(type(true))  -- boolean
print(type({"1","2"}))  -- table
变量

Lua变量有三种类型:全局变量、局部变量、表中的域

  • Lua中的变量全是全局变量,哪怕是语句块或是函数里,除非用local显示声明为局部变量
  • 局部变量的作用域为从声明位置开始到所在语句块结束
  • 变量的默认值均为nil
-- 变量
a = 5 -- 全局变量
print(a) -- 5
print(c) -- nil
local b = 4
print(b) -- 4

do
    local x = 1
    c = 7
    print(x)  -- 1
    print(c)  -- 7
end

print(x) -- nil
print(c) -- 7

-- 多变量赋值
a, b, c = 2, 3
print(a .. b) -- 23
print(c)  -- nil

a, b = 2, 3, 4
print(a .. b) -- 23

-- table赋值
tal = {key='a',key2='b'}
print(tal['key']) -- a
tal['key'] = 'c'
print(tal['key'] -- c
print(tal['key2']) -- b
循环
-- while循环
a = 1
while (a < 5) do
    print(a)
    a = a + 1
end

-- for循环
--[[
从exp1开始循环到exp2,exp3是步长
for var=exp1,epx2,exp3 do
end
]]
for i = 1, 5, 1 do
    print(i)
end

for i = 5, 1, -1 do
    print(i)
end
流程控制
-- 流程控制
a=9
if a<10 then
    print('小于10')
end

a=10
if a<10 then
    print('小于10')
else
    print('大于等于10')
end

a=10
if a<10 then
    print('小于10')
elseif a==10 then
    print('等于10')
else
    print('大于10')
end
函数
-- 函数
function xx(a)
    print(a)
end

xx(10)


xxx = function(result)
    print(result)
end
function sum(a,b,func1)
    sum = a+b
    func1(sum)
end

sum(10,20,xxx)
算术运算符
运算符描述示例
+加法10+20 输出结果 30
-减法20-10 输出结果 10
*乘法10*20 输出结果 200
/除法20/10 输出结果 2
%取余20%10 输出结果 0
^乘幂10^2 输出结果 100
-负号-10 输出结果 -10
//整除运算符(>=lua5.3)5//2 输出结果 2
-- 算术运算符
print(1 + 2)
print(2 - 1)
print(2 * 1)
print(-10)
print(10 / 6)
print(10 % 6)
print(10 // 6)
print(10 ^ 2)
关系运算符
运算符描述示例
==等于,检查左右两边的值是否相等,相等为true,不相等为false10==20为false
~=不等于,检查左右两边的值是否不相等,不相等为true,相等为false10~=20为true
>大于,如果左边的值大于右边的值,返回true,否则返回false10>20为false
<小于,如果左边的值小于右边的值,返回true,否则返回false10<20为true
>=大于等于,如果左边的值大于等于右边的值,返回true,否则返回false
<=小于等于,如果左边的值小于等于右边的值,返回true,否则返回false
逻辑运算符
运算符描述示例
and逻辑与操作符。若A为false,则返回A,否则返回B(A and B)
or逻辑或操作符。若A为true,则返回A,否则返回B(A or B)
not逻辑非操作符。与逻辑运算结果相反,如果条件结果为true,逻辑非为falsenot (A and B)
其它运算符
运算符描述示例
连接两个字符串a…b。其中a为"hello “,b为"world”。输出结果为"hello world"
#一元运算符,返回字符串长度或表的长度#"hello"的结果为5
Lua脚本在Redis中使用
  • eval执行Lua脚本

    EVAL script numkeys key [key...] arg [arg...]
    
    > eval "return redis.call('set',KEYS[1],ARGV[1])" 1 name xiaoming
    OK
    > keys *
    name
    stock1
    > get name
    xiaoming
    > eval "return redis.call('set',KEYS[1],ARGV[1])" 1 name xiaohong
    OK
    > get name
    xiaohong
    
  • 将脚本加载到Redis中

    当我们把Lua脚本加载到Redis中,这个脚本并不会立马执行,而是会缓存起来,并且会返回sha1校验和,后期我们可以通过EVALSHA来执行这个脚本

    > script load "return redis.call('set',KEYS[1],ARGV[1])"
    c686f316aaf1eb01d5a4de1b0b63cd233010e63d
    
  • 通过evalsha执行lua脚本

    > evalsha c686f316aaf1eb01d5a4de1b0b63cd233010e63d 1 name xiaozhang
    OK
    > get name
    xiaozhang
    
  • 判断脚本是否存在Redis中

    > script exists c686f316aaf1eb01d5a4de1b0b63cd233010e63d
    1
    
  • 将脚本从Redis中移除

    > script flush
    OK
    > script exists c686f316aaf1eb01d5a4de1b0b63cd233010e63d
    0
    
Lua脚本实现分布式锁

Redis客户端演示

  • 加锁的lua脚本

    -- 判断一下当前是否存在这把锁,如果锁存在,直接加锁失败,如果锁不存在,那么就set一把锁,并且给锁一个过期时间
    if (redis.call('exists', lockName) == 0) then
        redis.call('set', lockName, uuid)
        redis.call('pexpire', lockName, 30000)
        return 1
    else
        return 0
    end
    

    放到Redis客户端执行

    # 加锁成功
    > eval "if (redis.call('exists', KEYS[1]) == 0) then redis.call('set', KEYS[1], ARGV[1]) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end" 1 lockName uuid 30000
    1
    # 查看过期时间
    > ttl lockName
    15
    > ttl lockName
    13
    # 再次加锁失败
    > eval "if (redis.call('exists', KEYS[1]) == 0) then redis.call('set', KEYS[1], ARGV[1]) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end" 1 lockName uuid 30000
    0
    > ttl lockName
    8
    
  • 解锁的lua脚本

    -- 简易锁的释放锁lua
    -- 判断锁是否存在,如果不存在直接return,如果存在,那么我们需要判断UUID是否是当前线程的UUID,如果等于,那么就del,如果不等于就return
    if(redis.call('exists',lockName) == 0) then
        return 0
    elseif (redis.call('get', lockName) == uuid) then
        redis.call('del',lockName)
        return 1
    else
        return 0
    end
    

    放到Redis客户端执行

    # 没有锁,执行失败
    > eval "if(redis.call('exists',KEYS[1]) == 0) then return 0 elseif (redis.call('get', KEYS[1]) == ARGV[1]) then redis.call('del',KEYS[1]) return 1 else return 0 end" 1 lockName uuid
    0
    # 加锁脚本
    > eval "if (redis.call('exists', KEYS[1]) == 0) then redis.call('set', KEYS[1], ARGV[1]) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end" 1 lockName uuid 30000
    1
    # 查看锁的过期时间
    > ttl lockName
    27
    # 执行释放锁的脚本
    > eval "if(redis.call('exists',KEYS[1]) == 0) then return 0 elseif (redis.call('get', KEYS[1]) == ARGV[1]) then redis.call('del',KEYS[1]) return 1 else return 0 end" 1 lockName uuid
    1
    > ttl lockName
    -2
    

java代码改造

package com.ajie.stock.lock;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Collections;
import java.util.concurrent.TimeUnit;

/**
 * @author ajie
 * @description: TODO
 */
public class RedisLock extends AbstractLock {
    private StringRedisTemplate redisTemplate;
    public static final Long DEFAULT_EXPIRE = 30000L;
    public final String UUID;

    public RedisLock(StringRedisTemplate redisTemplate, String lockName) {
        this.redisTemplate = redisTemplate;
        this.lockName = lockName;
        this.UUID = java.util.UUID.randomUUID().toString().replaceAll("-", "");
    }

    @Override
    public void lock() {
        lock(DEFAULT_EXPIRE, TimeUnit.MILLISECONDS);
    }

    @Override
    public void lock(Long expire, TimeUnit unit) {
        // 加锁的lua脚本
        String lockScript = """
                if (redis.call('exists', KEYS[1]) == 0) then redis.call('set', KEYS[1], ARGV[1])
                 redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end
                """;
        // 使用setnx指令进行加锁
        while (true) {
            Long result = redisTemplate.execute(new DefaultRedisScript<>(lockScript, Long.class),
                    Collections.singletonList(lockName),
                    UUID, DEFAULT_EXPIRE.toString());
            if (result != null && result == 1) {
                break;
            }
            try {
                TimeUnit.MILLISECONDS.sleep(50);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    @Override
    public void unlock() {
        // 释放锁的lua脚本
        String unLockScript = """
                if(redis.call('exists',KEYS[1]) == 0) then return 0 elseif (redis.call('get',
                 KEYS[1]) == ARGV[1]) then redis.call('del',KEYS[1]) return 1 else return 0 end
                """;
        redisTemplate.execute(new DefaultRedisScript<>(unLockScript, Long.class),
                Collections.singletonList(lockName), UUID);
    }
}

Lua+hash实现可重入锁

实现原理:

  1. 使用Redis的hash结构。key为锁的名称,field为uuid,value为锁的加锁次数
  2. 没重入一次,加锁次数加一
  3. 当释放锁的时候对加锁次数减一
  4. 当加锁次数减到不大于0以后,就删除这个锁的key

Redis客户端演示

  • 加锁的lua脚本

    -- 可重入锁的加锁lua脚本
    -- 判断锁是否存在,如果不存在,直接加锁,给重入次数设置为1,设置过期时间
    -- 如果存在,对原有的锁重入次数加1,重新设置过期时间
    if (redis.call('exists', lockName) == 0) then
        redis.call('hincrby', lockName, uuid, 1)
        redis.call('pexpire', lockName, 30000)
        return 1
    end
    
    if (redis.call('hexists', lockName, uuid) == 1) then
        redis.call('hincrby', lockName, uuid, 1)
        redis.call('pexpire', lockName, 30000)
        return 1
    else
        return 0
    end
    

    redis命令行执行

    > eval "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 end if (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then redis.call('hincrby', KEYS[1], ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end" 1 lockName uuid 30000
    1
    > hget lockName uuid
    1
    > eval "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 end if (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then redis.call('hincrby', KEYS[1], ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end" 1 lockName uuid 30000
    1
    > eval "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 end if (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then redis.call('hincrby', KEYS[1], ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end" 1 lockName uuid 30000
    1
    > hget lockName uuid
    3
    
  • 释放锁的脚本

    -- 可重入锁的释放锁lua脚本
    -- 判断当前持有锁的线程是否是本线程,如果不是就不需要释放了
    -- 如果是,就对可重入次数-1,-1之后,判断是否大于0,如果大于0,重新设置过期时间,否则删除锁
    if (redis.call('hexists', lockName, uuid) == 0) then
        return 0
    end
    local lockCount = redis.call('hincrby', lockName, uuid, -1)
    if (lockCount > 0) then
        redis.call('pexpire', lockName, 30000)
    else
        redis.call('del', lockName)
    end
    return 1
    

    redis命令行执行

    > hget lockName uuid
    3
    > eval "if(redis.call('hexists',KEYS[1],ARGV[1])==0) then return 0; end local lockCount = redis.call('hincrby',KEYS[1],ARGV[1],-1) if(lockCount>0) then redis.call('pexpire',KEYS[1],ARGV[2]) else redis.call('del',KEYS[1]) end return 1" 1 lockName uuid 30000
    1
    > hget lockName uuid
    2
    > eval "if(redis.call('hexists',KEYS[1],ARGV[1])==0) then return 0; end local lockCount = redis.call('hincrby',KEYS[1],ARGV[1],-1) if(lockCount>0) then redis.call('pexpire',KEYS[1],ARGV[2]) else redis.call('del',KEYS[1]) end return 1" 1 lockName uuid 30000
    1
    > eval "if(redis.call('hexists',KEYS[1],ARGV[1])==0) then return 0; end local lockCount = redis.call('hincrby',KEYS[1],ARGV[1],-1) if(lockCount>0) then redis.call('pexpire',KEYS[1],ARGV[2]) else redis.call('del',KEYS[1]) end return 1" 1 lockName uuid 30000
    1
    > hget lockName uuid
    null
    

java代码改造

RedisLock

package com.ajie.stock.lock;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Collections;
import java.util.concurrent.TimeUnit;

/**
 * @author ajie
 * @description: TODO
 */
public class RedisLock extends AbstractLock {
    private StringRedisTemplate redisTemplate;
    public static final Long DEFAULT_EXPIRE = 30000L;
    public final String UUID;

    public RedisLock(StringRedisTemplate redisTemplate, String lockName) {
        this.redisTemplate = redisTemplate;
        this.lockName = lockName;
        this.UUID = java.util.UUID.randomUUID().toString().replaceAll("-", "");
    }

    @Override
    public void lock() {
        lock(DEFAULT_EXPIRE, TimeUnit.MILLISECONDS);
    }

    @Override
    public void lock(Long expire, TimeUnit unit) {
        // 加锁的lua脚本
        String lockScript = """
                if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1],
                 ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 end if
                  (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then redis.call('hincrby', KEYS[1],
                   ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end
                """;
        // 使用setnx指令进行加锁
        while (true) {
            Long result = redisTemplate.execute(new DefaultRedisScript<>(lockScript, Long.class),
                    Collections.singletonList(lockName),
                    UUID, DEFAULT_EXPIRE.toString());
            /*Boolean result = redisTemplate.opsForValue().setIfAbsent(lockName, UUID,
                    expire, unit);
            if (result != null && result) {
                break;
            }*/
            if (result != null && result == 1) {
                break;
            }
            try {
                TimeUnit.MILLISECONDS.sleep(50);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    @Override
    public void unlock() {
        // 释放锁的lua脚本
        String unLockScript = """
                if(redis.call('hexists',KEYS[1],ARGV[1])==0) then return 0; end local lockCount =
                 redis.call('hincrby',KEYS[1],ARGV[1],-1) if(lockCount>0) then redis.call('pexpire',
                 KEYS[1],ARGV[2]) else redis.call('del',KEYS[1]) end return 1
                """;
        redisTemplate.execute(new DefaultRedisScript<>(unLockScript, Long.class),
                Collections.singletonList(lockName), UUID);
        /*String uuid = redisTemplate.opsForValue().get(lockName);
        if (uuid != null && uuid.equals(UUID)) {
            redisTemplate.delete(lockName);
        }*/
    }
}

service实现类代码

package com.ajie.stock.service.impl;

import com.ajie.stock.lock.AbstractLock;
import com.ajie.stock.lock.RedisLock;
import com.ajie.stock.mapper.StockMapper;
import com.ajie.stock.service.StockService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;


/**
 * @author ajie
 * @description: TODO
 */
@Service
public class StockServiceImpl implements StockService {
    @Autowired
    private StockMapper stockMapper;
    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public String deductStock(Long goodsId, Integer count) {
        AbstractLock lock = null;

        try {
            lock = new RedisLock(redisTemplate, "lock" + goodsId);
            lock.lock();
            // 实现锁的重入效果
            lock(lock);
            // 根据商品id查询库存
            String stock = redisTemplate.opsForValue().get("stock" + goodsId);
            // 判断商品是否存在
            if (StringUtils.isEmpty(stock)) {
                return "商品不存在";
            }
            int lastLock = Integer.parseInt(stock);
            // 2.判断库存数量是否足够
            if (lastLock < count) {
                return "库存不足";
            }
            redisTemplate.opsForValue().set("stock" + goodsId, String.valueOf(lastLock - count));
            return "库存扣减成功";
        } finally {
            if (lock != null) {
                lock.unlock();
            }
        }
    }

    private void lock(AbstractLock lock) {
        lock.lock();
        lock.unlock();
    }
}
redis锁续期

为什么需要锁续期?

​ 由于前面对锁加了过期时间,有时候,一个线程执行业务逻辑确实需要花费比过期时间还要长的时间。如果在线程执行过程中就释放了锁。另一个线程就会获取到锁,同样会出现并发问题。由于正常业务执行导致的锁过期需要对锁的过期时间延长

实现方式:在同一个进程中开启异步线程,对锁进行时间重置的工作。当此进程宕机以后,也就不会再进行锁续期,锁也能正常释放掉了

lua脚本实现锁需求:

-- lua脚本实现锁自动续期
-- 判断当前持有锁的线程是否是本线程,如果是就进行续期,否则直接返回
if(redis.call('hexists',lockName,uuid) == 0) then
    return 0;
else
    redis.call('pexpire',lockName,30000)
end

redis客户端执行

# 没有锁,续期失败
> eval "if(redis.call('hexists',KEYS[1],ARGV[1]) == 0) then return 0; else redis.call('pexpire',KEYS[1],ARGV[2]) end" 1 lockName uuid 30000
0
# 加锁
> eval "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 end if (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then redis.call('hincrby', KEYS[1], ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end" 1 lockName uuid 30000
1
> ttl lockName
25
# 进行锁续期
> eval "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 end if (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then redis.call('hincrby', KEYS[1], ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end" 1 lockName uuid 30000
1
> ttl lockName
28

java代码改造

package com.ajie.stock.lock;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Collections;
import java.util.concurrent.TimeUnit;

/**
 * @author ajie
 * @description: TODO
 */
public class RedisLock extends AbstractLock {
    private StringRedisTemplate redisTemplate;
    public static final Long DEFAULT_EXPIRE = 30000L;
    public final String UUID;

    public RedisLock(StringRedisTemplate redisTemplate, String lockName) {
        this.redisTemplate = redisTemplate;
        this.lockName = lockName;
        this.UUID = java.util.UUID.randomUUID().toString().replaceAll("-", "");
    }

    @Override
    public void lock() {
        lock(DEFAULT_EXPIRE, TimeUnit.MILLISECONDS);
    }

    @Override
    public void lock(Long expire, TimeUnit unit) {
        // 加锁的lua脚本
        String lockScript = """
                if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1],
                 ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 end if
                  (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then redis.call('hincrby', KEYS[1],
                   ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end
                """;
        // 使用setnx指令进行加锁
        while (true) {
            Long result = redisTemplate.execute(new DefaultRedisScript<>(lockScript, Long.class),
                    Collections.singletonList(lockName),
                    UUID, DEFAULT_EXPIRE.toString());
            /*Boolean result = redisTemplate.opsForValue().setIfAbsent(lockName, UUID,
                    expire, unit);
            if (result != null && result) {
                break;
            }*/
            if (result != null && result == 1) {
                // 开启异步线程进行锁续期工作
                new Thread(()->{
                    while(true) {
                        String expireLua = """
                                if(redis.call('hexists',KEYS[1],ARGV[1]) == 0) then
                                    return 0;
                                else
                                    redis.call('pexpire',KEYS[1],ARGV[2])
                                end
                                """;
                        Long expireResult = redisTemplate.execute(new DefaultRedisScript<>(expireLua, Long.class),
                                Collections.singletonList(lockName),
                                UUID, DEFAULT_EXPIRE.toString());
                        if(expireResult != null && expireResult == 0){
                            break;
                        }
                        try {
                            // 为了减少和Redis的交互,当过期时间过了一半再续期
                            TimeUnit.MILLISECONDS.sleep(DEFAULT_EXPIRE/2);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }).start();
                break;
            }
            try {
                TimeUnit.MILLISECONDS.sleep(50);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    @Override
    public void unlock() {
        // 释放锁的lua脚本
        String unLockScript = """
                if(redis.call('hexists',KEYS[1],ARGV[1])==0) then return 0; end local lockCount =
                 redis.call('hincrby',KEYS[1],ARGV[1],-1) if(lockCount>0) then redis.call('pexpire',
                 KEYS[1],ARGV[2]) else redis.call('del',KEYS[1]) end return 1
                """;
        redisTemplate.execute(new DefaultRedisScript<>(unLockScript, Long.class),
                Collections.singletonList(lockName),
                UUID, DEFAULT_EXPIRE.toString());
        /*String uuid = redisTemplate.opsForValue().get(lockName);
        if (uuid != null && uuid.equals(UUID)) {
            redisTemplate.delete(lockName);
        }*/
    }
}
增加锁获取时间

​ 通过增加锁获取时间,解决锁一直阻塞等待的问题。通过指定锁的获取时间,在指定时间内,线程可以一直循环获取锁,当超过这个时间,线程就跳出循环,不再获取锁了

java代码改造

AbstractLock

package com.ajie.stock.lock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * @author ajie
 * @description: TODO
 */
public abstract class AbstractLock implements Lock {
    protected String lockName;

    @Override
    public Condition newCondition() {
        return null;
    }

    @Override
    public void unlock() {

    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }
    // 增加重载方法,支持设置过期时间
    protected boolean tryLock(long time,Long expire, TimeUnit unit) throws InterruptedException {
        return false;
    }

    @Override
    public boolean tryLock() {
        return false;
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public void lock() {

    }

    public abstract void lock(Long expire, TimeUnit unit);
}

RedisLock

package com.ajie.stock.lock;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Collections;
import java.util.concurrent.TimeUnit;

/**
 * @author ajie
 * @description: TODO
 */
public class RedisLock extends AbstractLock {
    private StringRedisTemplate redisTemplate;
    public static final Long DEFAULT_EXPIRE = 30000L;
    public final String UUID;

    public RedisLock(StringRedisTemplate redisTemplate, String lockName) {
        this.redisTemplate = redisTemplate;
        this.lockName = lockName;
        this.UUID = java.util.UUID.randomUUID().toString().replaceAll("-", "");
    }

    @Override
    public void lock() {
        lock(DEFAULT_EXPIRE, TimeUnit.MILLISECONDS);
    }

    @Override
    public void lock(Long expire, TimeUnit unit) {
        // 加锁的lua脚本
        String lockScript = """
                if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1],
                 ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 end if
                  (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then redis.call('hincrby', KEYS[1],
                   ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end
                """;
        // 使用setnx指令进行加锁
        while (true) {
            Long result = redisTemplate.execute(new DefaultRedisScript<>(lockScript, Long.class),
                    Collections.singletonList(lockName),
                    UUID, expire.toString());
            /*Boolean result = redisTemplate.opsForValue().setIfAbsent(lockName, UUID,
                    expire, unit);
            if (result != null && result) {
                break;
            }*/
            if (result != null && result == 1) {
                new Thread(() -> {
                    while (true) {
                        String expireLua = """
                                if(redis.call('hexists',KEYS[1],ARGV[1]) == 0) then
                                    return 0;
                                else
                                    redis.call('pexpire',KEYS[1],ARGV[2])
                                end
                                """;
                        Long expireResult = redisTemplate.execute(new DefaultRedisScript<>(expireLua, Long.class),
                                Collections.singletonList(lockName),
                                UUID, expire.toString());
                        if (expireResult != null && expireResult == 0) {
                            break;
                        }
                        try {
                            // 为了减少和Redis的交互,当过期时间过了一半再续期
                            TimeUnit.MILLISECONDS.sleep(expire / 2);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }).start();
                break;
            }
            try {
                TimeUnit.MILLISECONDS.sleep(50);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    private boolean tryLockInternal(Long expire, TimeUnit unit) {
        // 加锁的lua脚本
        String lockScript = """
                if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1],
                 ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 end if
                  (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then redis.call('hincrby', KEYS[1],
                   ARGV[1], 1) redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end
                """;
        // 使用setnx指令进行加锁
        Long result = redisTemplate.execute(new DefaultRedisScript<>(lockScript, Long.class),
                Collections.singletonList(lockName),
                UUID, expire.toString());
        if (result != null && result == 1) {
            new Thread(() -> {
                while (true) {
                    String expireLua = """
                            if(redis.call('hexists',KEYS[1],ARGV[1]) == 0) then
                                return 0;
                            else
                                redis.call('pexpire',KEYS[1],ARGV[2])
                            end
                            """;
                    Long expireResult = redisTemplate.execute(new DefaultRedisScript<>(expireLua, Long.class),
                            Collections.singletonList(lockName),
                            UUID, expire.toString());
                    if (expireResult != null && expireResult == 0) {
                        break;
                    }
                    try {
                        // 为了减少和Redis的交互,当过期时间过了一半再续期
                        TimeUnit.MILLISECONDS.sleep(DEFAULT_EXPIRE / 2);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }).start();
            return true;
        }
        try {
            TimeUnit.MILLISECONDS.sleep(50);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return tryLock(time, DEFAULT_EXPIRE, unit);
    }

    // 尝试在指定时间内获取锁
    @Override
    protected boolean tryLock(long time, Long expire, TimeUnit unit) throws InterruptedException {
        // 先记录一个开始时间,然后再记录当前时间,当当前时间-开始时间>time,跳出循环,否则就一直等待获取锁
        long startTime = System.currentTimeMillis();
        long currentTime = System.currentTimeMillis();
        boolean lockResult = false;
        while (currentTime - startTime <= time) {
            // 获取锁
            boolean result = tryLockInternal(expire, unit);
            if (result) {
                lockResult = true;
                break;
            }
            currentTime = System.currentTimeMillis();
        }
        return lockResult;
    }

    @Override
    public void unlock() {
        // 释放锁的lua脚本
        String unLockScript = """
                if(redis.call('hexists',KEYS[1],ARGV[1])==0) then return 0; end local lockCount =
                 redis.call('hincrby',KEYS[1],ARGV[1],-1) if(lockCount>0) then redis.call('pexpire',
                 KEYS[1],ARGV[2]) else redis.call('del',KEYS[1]) end return 1
                """;
        redisTemplate.execute(new DefaultRedisScript<>(unLockScript, Long.class),
                Collections.singletonList(lockName),
                UUID, DEFAULT_EXPIRE.toString());
    }
}

service代码

package com.ajie.stock.service.impl;

import com.ajie.stock.lock.AbstractLock;
import com.ajie.stock.lock.RedisLock;
import com.ajie.stock.mapper.StockMapper;
import com.ajie.stock.service.StockService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.concurrent.TimeUnit;


/**
 * @author ajie
 * @description: TODO
 */
@Service
public class StockServiceImpl implements StockService {
    @Autowired
    private StockMapper stockMapper;
    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public String deductStock(Long goodsId, Integer count) {
        AbstractLock lock = null;
        try {
            lock = new RedisLock(redisTemplate, "lock" + goodsId);
            // 尝试在指定时间内获取锁
            boolean result = lock.tryLock(5000, TimeUnit.MILLISECONDS);
            if (result) {
                try {
                    TimeUnit.MILLISECONDS.sleep(200000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                // 根据商品id查询库存
                String stock = redisTemplate.opsForValue().get("stock" + goodsId);
                // 判断商品是否存在
                if (StringUtils.isEmpty(stock)) {
                    return "商品不存在";
                }
                int lastLock = Integer.parseInt(stock);
                // 2.判断库存数量是否足够
                if (lastLock < count) {
                    return "库存不足";
                }
                redisTemplate.opsForValue().set("stock" + goodsId, String.valueOf(lastLock - count));
                return "库存扣减成功";
            }
            System.out.println("获取锁超时");
            return "系统繁忙";
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            if (lock != null) {
                lock.unlock();
            }
        }
    }
}

Redisson源码剖析

分布式锁示例
  1. 导入依赖

    <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson</artifactId>
        <version>3.32.0</version>
    </dependency>
    
  2. 创建配置类

    package com.ajie.stock.config;
    
    import org.redisson.Redisson;
    import org.redisson.api.RedissonClient;
    import org.redisson.config.Config;
    import org.springframework.boot.SpringBootConfiguration;
    import org.springframework.context.annotation.Bean;
    
    /**
     * @author ajie
     * @description: TODO
     */
    @SpringBootConfiguration
    public class RedissonConfig {
        @Bean
        public RedissonClient redissonClient() {
            Config config = new Config();
            config.useSingleServer()
                    .setAddress("redis://127.0.0.1:6379")
                    .setDatabase(1)
                    .setPassword("^Koron#3438$");
            return Redisson.create(config);
        }
    }
    
  3. 修改业务逻辑

    package com.ajie.stock.service.impl;
    
    import com.ajie.stock.lock.AbstractLock;
    import com.ajie.stock.mapper.StockMapper;
    import com.ajie.stock.service.StockService;
    import org.redisson.api.RLock;
    import org.redisson.api.RedissonClient;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.stereotype.Service;
    import org.springframework.util.StringUtils;
    
    
    /**
     * @author ajie
     * @description: TODO
     */
    @Service
    public class StockServiceImpl implements StockService {
        @Autowired
        private StockMapper stockMapper;
        @Autowired
        private StringRedisTemplate redisTemplate;
        @Autowired
        private RedissonClient redissonClient;
    
        @Override
        public String deductStock(Long goodsId, Integer count) {
            RLock lock = null;
            try {
                lock = redissonClient.getLock("lock"+goodsId);
                lock.lock();
                // 根据商品id查询库存
                String stock = redisTemplate.opsForValue().get("stock" + goodsId);
                // 判断商品是否存在
                if (StringUtils.isEmpty(stock)) {
                    return "商品不存在";
                }
                int lastLock = Integer.parseInt(stock);
                // 2.判断库存数量是否足够
                if (lastLock < count) {
                    return "库存不足";
                }
                redisTemplate.opsForValue().set("stock" + goodsId, String.valueOf(lastLock - count));
                return "库存扣减成功";
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                if (lock != null) {
                    lock.unlock();
                }
            }
        }
    }
    
源码导入

源码下载地址:https://github.com/redisson/redisson

选择3.20.0的版本,高版本需要jdk21

  1. 下载zip压缩包到指定文件夹,然后解压

  2. 使用idea打开,配置maven、jdk。导入依赖

  3. 进行单元测试,校验源码是否导入成功

    1. 修改BaseTest配置

          public static Config createConfig() {
      //        String redisAddress = System.getProperty("redisAddress");
      //        if (redisAddress == null) {
      //            redisAddress = "127.0.0.1:6379";
      //        }
              Config config = new Config();
      //        config.setCodec(new MsgPackJacksonCodec());
      //        config.useSentinelServers().setMasterName("mymaster").addSentinelAddress("127.0.0.1:26379", "127.0.0.1:26389");
      //        config.useClusterServers().addNodeAddress("127.0.0.1:7004", "127.0.0.1:7001", "127.0.0.1:7000");
              // 注释系统自带的配置设置
      /*        config.useSingleServer()
                      .setAddress(RedisRunner.getDefaultRedisServerBindAddressAndPort());*/
              // 设置自己的redis地址
              config.useSingleServer()
                      .setAddress("redis://localhost:6379")
                      .setDatabase(1)
                      .setPassword("xxxxxx");
      //        .setPassword("mypass1");
      //        config.useMasterSlaveConnection()
      //        .setMasterAddress("127.0.0.1:6379")
      //        .addSlaveAddress("127.0.0.1:6399")
      //        .addSlaveAddress("127.0.0.1:6389");
              return config;
          }
      
    2. 注释掉测试启动前的代码,否则每次执行都会清楚redis中的key

      @BeforeEach
      public void before() throws IOException, InterruptedException {
          /*if (flushBetweenTests()) {
                  redisson.getKeys().flushall();
              }*/
      }
      
    3. 修改RedissonRuntimeEnvironment类中的常量地址,根据自己的系统修改相应的地址

      private static final String MAC_PATH = "/usr/local/opt/redis/bin/redis-server";
      private static final String WINDOW_PATH = "D:\\develop\\redis\\redis-server.exe";
      
    4. 执行测试方法testIsLocked

可重入锁
  • 获取RedissonClient客户端源码:Redisson.create(config)

    // --------------1 Redisson
    public static RedissonClient create(Config config) {
        return new Redisson(config);
    }
    // --------------2 Redisson
    protected Redisson(Config config) {
        // 将配置对象存放在Redisson对象中
        this.config = config;
        // 重新拷贝一份
        Config configCopy = new Config(config);
    
        // 创建连接管理器
        connectionManager = ConfigSupport.createConnectionManager(configCopy);
        RedissonObjectBuilder objectBuilder = null;
        if (config.isReferenceEnabled()) {
            objectBuilder = new RedissonObjectBuilder(this);
        }
        // 创建命令行客户端,将连接器传进去
        commandExecutor = new CommandSyncService(connectionManager, objectBuilder);
        evictionScheduler = new EvictionScheduler(commandExecutor);
        writeBehindService = new WriteBehindService(commandExecutor);
    }
    
    // --------------3 ConfigSupport 根据配置对象中指定连接器创建
    public static ConnectionManager createConnectionManager(Config configCopy) {
        ServiceManager serviceManager = new ServiceManager(configCopy);
    
        ConnectionManager cm = null;
        if (configCopy.getMasterSlaveServersConfig() != null) {
            validate(configCopy.getMasterSlaveServersConfig());
            cm = new MasterSlaveConnectionManager(configCopy.getMasterSlaveServersConfig(), serviceManager);
        } else if (configCopy.getSingleServerConfig() != null) {
            validate(configCopy.getSingleServerConfig());
            // 我们指定的是单实例的
            cm = new SingleConnectionManager(configCopy.getSingleServerConfig(), serviceManager);
        } else if (configCopy.getSentinelServersConfig() != null) {
            validate(configCopy.getSentinelServersConfig());
            cm = new SentinelConnectionManager(configCopy.getSentinelServersConfig(), serviceManager);
        } else if (configCopy.getClusterServersConfig() != null) {
            validate(configCopy.getClusterServersConfig());
            cm = new ClusterConnectionManager(configCopy.getClusterServersConfig(), serviceManager);
        } else if (configCopy.getReplicatedServersConfig() != null) {
            validate(configCopy.getReplicatedServersConfig());
            cm = new ReplicatedConnectionManager(configCopy.getReplicatedServersConfig(), serviceManager);
        } else if (configCopy.getConnectionManager() != null) {
            cm = configCopy.getConnectionManager();
        }
    
        if (cm == null) {
            throw new IllegalArgumentException("server(s) address(es) not defined!");
        }
        // 获取连接
        cm.connect();
        return cm;
    }
    
    // --------------4 SingleConnectionManager
    public SingleConnectionManager(SingleServerConfig cfg, ServiceManager serviceManager) {
        // 调用 5-父类中的构造器
        super(create(cfg), serviceManager);
    }
    
    // --------------5 MasterSlaveConnectionManager
    public MasterSlaveConnectionManager(BaseMasterSlaveServersConfig<?> cfg, ServiceManager serviceManager) {
        this.serviceManager = serviceManager;
    
        if (cfg instanceof MasterSlaveServersConfig) {
            this.config = (MasterSlaveServersConfig) cfg;
            if (this.config.getSlaveAddresses().isEmpty()
                && (this.config.getReadMode() == ReadMode.SLAVE || this.config.getReadMode() == ReadMode.MASTER_SLAVE)) {
                throw new IllegalArgumentException("Slaves aren't defined. readMode can't be SLAVE or MASTER_SLAVE");
            }
        } else {
            this.config = create(cfg);
        }
    
        serviceManager.setConfig(this.config);
        // 这里是一个初始化timer的地方
        serviceManager.initTimer();
        subscribeService = new PublishSubscribeService(this);
    }
    
    // --------------6 ServiceManager
    public void initTimer() {
        int[] timeouts = new int[]{config.getRetryInterval(), config.getTimeout()};
        Arrays.sort(timeouts);
        int minTimeout = timeouts[0];
        if (minTimeout % 100 != 0) {
            minTimeout = (minTimeout % 100) / 2;
        } else if (minTimeout == 100) {
            minTimeout = 50;
        } else {
            minTimeout = 100;
        }
        // 这里是netty实现的时间轮,用于锁续期
        timer = new HashedWheelTimer(new DefaultThreadFactory("redisson-timer"), minTimeout, TimeUnit.MILLISECONDS, 1024, false);
    
        connectionWatcher = new IdleConnectionWatcher(group, config);
    }
    
  • 获取锁:redisson.getLock(“lock”)

    // --------------1 Redisson
    public RLock getLock(String name) {
        return new RedissonLock(commandExecutor, name);
    }
    
    // --------------2 RedissonLock
    public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
        super(commandExecutor, name);
        this.commandExecutor = commandExecutor;
        this.internalLockLeaseTime = commandExecutor.getServiceManager().getCfg().getLockWatchdogTimeout();
        this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub();
    }
    
    // --------------3 RedissonBaseLock
    public RedissonBaseLock(CommandAsyncExecutor commandExecutor, String name) {
        // 调用父类,将name进行记录
        super(commandExecutor, name);
        this.commandExecutor = commandExecutor;
        // 获取serviceManager里面设置的id:UUID.randomUUID().toString()
        this.id = commandExecutor.getServiceManager().getId();
        // 获取config中设置的锁过期时间,默认值:30 * 1000
        this.internalLockLeaseTime = commandExecutor.getServiceManager().getCfg().getLockWatchdogTimeout();
        // 应用的uuid拼上锁名称
        this.entryName = id + ":" + name;
    }
    
  • 加锁:lock.lock()

    // --------------1 RedissonLock
    public void lock() {
        try {
            lock(-1, null, false);
        } catch (InterruptedException e) {
            throw new IllegalStateException();
        }
    }
    
    // --------------2 RedissonLock
    // leaseTime:锁过期时间
    private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
        long threadId = Thread.currentThread().getId();
        // 尝试获取锁,-1代表锁一直阻塞获取
        Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
        // lock acquired
        // 获取到锁了,直接返回
        if (ttl == null) {
            return;
        }
    
        CompletableFuture<RedissonLockEntry> future = subscribe(threadId);
        pubSub.timeout(future);
        RedissonLockEntry entry;
        if (interruptibly) {
            entry = commandExecutor.getInterrupted(future);
        } else {
            entry = commandExecutor.get(future);
        }
    
        try {
            while (true) {
                ttl = tryAcquire(-1, leaseTime, unit, threadId);
                // lock acquired
                if (ttl == null) {
                    break;
                }
    
                // waiting for message
                if (ttl >= 0) {
                    try {
                        entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    } catch (InterruptedException e) {
                        if (interruptibly) {
                            throw e;
                        }
                        entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    }
                } else {
                    if (interruptibly) {
                        entry.getLatch().acquire();
                    } else {
                        entry.getLatch().acquireUninterruptibly();
                    }
                }
            }
        } finally {
            unsubscribe(entry, threadId);
        }
        //        get(lockAsync(leaseTime, unit));
    }
    
    // --------------3 RedissonLock
    private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));
    }
    
    // --------------4 RedissonLock
    private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        RFuture<Long> ttlRemainingFuture;
        // 判断是否设置了锁超时时间
        if (leaseTime > 0) {
            // 如果业务方传了所过期时间,就用业务方自己传的
            ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        } else {
            // 没有设置,如-1,走这个代码,使用默认是的锁过期时间,internalLockLeaseTime:30 * 1000
            // --------------5 RedissonLock
            ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
                                                   TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        }
        CompletionStage<Long> s = handleNoSync(threadId, ttlRemainingFuture);
        ttlRemainingFuture = new CompletableFutureWrapper<>(s);
    
        CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {
            // lock acquired
            // 如果==null,那么就获取锁成功,否则获取锁失败
            if (ttlRemaining == null) {
                if (leaseTime > 0) {
                    // 如果业务方自己传了锁过期时间,那么internalLockLeaseTime就改成业务方自己传的
                    internalLockLeaseTime = unit.toMillis(leaseTime);
                } else {
                    // 如果业务方没有传锁过期时间,默认锁过期时间为30s
                    // 下面这个逻辑就是加锁成功后,watchDog在后台自动续期的逻辑
                    // --------------6 RedissonBaseLock
                    scheduleExpirationRenewal(threadId);
                }
            }
            return ttlRemaining;
        });
        return new CompletableFutureWrapper<>(f);
    }
    
    // --------------5 RedissonLock
    <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
                              "if ((redis.call('exists', KEYS[1]) == 0) " +
                              "or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then " +
                              "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                              "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                              "return nil; " +
                              "end; " +
                              "return redis.call('pttl', KEYS[1]);",
                              // KEYS[1]=传进去的锁名称,ARGV[1]= 锁过期时间,ARGV[2]=id + ":" + threadId
                              // id为uuid
                              Collections.singletonList(getRawName()), unit.toMillis(leaseTime), 								getLockName(threadId));
    }
    // --------------6 RedissonBaseLock
    protected void scheduleExpirationRenewal(long threadId) {
        ExpirationEntry entry = new ExpirationEntry();
        // 享元模式,将当前线程需要续约对象放入的map中
        // map的key=id + ":" + name(uuid+":"+锁名称)
        // putIfAbsent:如果map中存在就返回已有的,如果不存在将entry放进去,返回null
        ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
        if (oldEntry != null) {
            // 不等于null,将当前线程加入到续期对象中
            oldEntry.addThreadId(threadId);
        } else {
            // 等于null,第一次加锁成功
            entry.addThreadId(threadId);
            try {
                // 续约过期时间
                // --------------7 RedissonBaseLock
                renewExpiration();
            } finally {
                if (Thread.currentThread().isInterrupted()) {
                    cancelExpirationRenewal(threadId);
                }
            }
        }
    }
    
    // --------------7 RedissonBaseLock
    private void renewExpiration() {
        // 从续约map中获取续约对象
        ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
        if (ee == null) {
            // 如果为空,则没有需要续约的对象,直接返回
            return;
        }
    
        Timeout task = commandExecutor.getServiceManager().newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
                // 再次进行判断,如果为空,则没有需要续约的对象,直接返回
                if (ent == null) {
                    return;
                }
                Long threadId = ent.getFirstThreadId();
                if (threadId == null) {
                    return;
                }
    
                // 如果续约对象不为空,续约的线程id也不为空,进行续约
                // --------------8 RedissonBaseLock
                CompletionStage<Boolean> future = renewExpirationAsync(threadId);
                future.whenComplete((res, e) -> {
                    if (e != null) {
                        log.error("Can't update lock {} expiration", getRawName(), e);
                        // 如果续约出现异常,从续约map中移除当前的续约对象
                        EXPIRATION_RENEWAL_MAP.remove(getEntryName());
                        return;
                    }
    
                    if (res) {
                        // reschedule itself
                        // 如果续约成功,那么下一个时间轮接着执行一次锁续约
                        renewExpiration();
                    } else {
                        // 如果续约失败,那么取消锁的续约操作
                        cancelExpirationRenewal(null);
                    }
                });
            }
            // 续约任务执行的时间间隔:30s/3=10s
        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    
        ee.setTimeout(task);
    }
    // --------------8 RedissonBaseLock
    protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
        return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                              "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                              "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                              "return 1; " +
                              "end; " +
                              "return 0;",
                              // KEYS[1]=传入的锁名称,ARGV[1]=锁过期时间,ARGV[2]=id + ":" + threadId
                              Collections.singletonList(getRawName()),
                              internalLockLeaseTime, getLockName(threadId));
    }
    
  • 释放锁:lock.unlock()

    // --------------1 RedissonBaseLock    
    public void unlock() {
        try {
            get(unlockAsync(Thread.currentThread().getId()));
        } catch (RedisException e) {
            if (e.getCause() instanceof IllegalMonitorStateException) {
                throw (IllegalMonitorStateException) e.getCause();
            } else {
                throw e;
            }
        }
    }
    
    // --------------2 RedissonBaseLock 
    public RFuture<Void> unlockAsync(long threadId) {
        RFuture<Boolean> future = unlockInnerAsync(threadId);
    
        CompletionStage<Void> f = future.handle((opStatus, e) -> {
            cancelExpirationRenewal(threadId);
    
            if (e != null) {
                throw new CompletionException(e);
            }
            if (opStatus == null) {
                IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
                                                                                      + id + " thread-id: " + threadId);
                throw new CompletionException(cause);
            }
    
            return null;
        });
    
        return new CompletableFutureWrapper<>(f);
    }
    
    // --------------3 RedissonLock 
    protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                              "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                              "return nil;" +
                              "end; " +
                              "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
                              "if (counter > 0) then " +
                              "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                              "return 0; " +
                              "else " +
                              "redis.call('del', KEYS[1]); " +
                              "redis.call('publish', KEYS[2], ARGV[1]); " +
                              "return 1; " +
                              "end; " +
                              "return nil;",
                              // KEYS[1]=锁名称,KEYS[2]="redisson_lock__channel" + ":{" + name + "}"
                              // ARGV[1]=0,ARGV[2]=internalLockLeaseTime,ARGV[3]=id + ":" + threadId
                              Arrays.asList(getRawName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, 						internalLockLeaseTime, getLockName(threadId));
    }
    
  • 阻塞获取锁

    // --------------1 RedissonLock
    private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
        long threadId = Thread.currentThread().getId();
        Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
        // lock acquired
        if (ttl == null) {
            return;
        }
        // 订阅释放锁的channel,等待锁持有者释放锁后pub的消息
        CompletableFuture<RedissonLockEntry> future = subscribe(threadId);
        pubSub.timeout(future);
        RedissonLockEntry entry;
        if (interruptibly) {
            entry = commandExecutor.getInterrupted(future);
        } else {
            entry = commandExecutor.get(future);
        }
    
        try {
            while (true) {
                // 再次尝试获取锁
                ttl = tryAcquire(-1, leaseTime, unit, threadId);
                // lock acquired
                if (ttl == null) {
                    // 获取锁成功,跳出循环
                    break;
                }
    
                // waiting for message
                if (ttl >= 0) {
                    try {
                        // 重要。通过Semaphore将没有获取锁的线程进行阻塞。阻塞时间为当前持有锁的剩余过期时间
                        // 这里的Semaphore设置的为0,目的是将获取锁失败的线程阻塞在这里。没有这一步,线程会
                        // 不断的循环,会造成CPU空转
                        entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    } catch (InterruptedException e) {
                        if (interruptibly) {
                            throw e;
                        }
                        entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    }
                } else {
                    if (interruptibly) {
                        entry.getLatch().acquire();
                    } else {
                        entry.getLatch().acquireUninterruptibly();
                    }
                }
            }
        } finally {
            // 循环结束,说明当前线程已经获取到锁,就取消消息订阅
            unsubscribe(entry, threadId);
        }
    }
    
  • 非阻塞获取锁

    // --------------1 RedissonLock
    @Override
    public boolean tryLock(long waitTime, TimeUnit unit) throws InterruptedException {
        return tryLock(waitTime, -1, unit);
    }
    // --------------2 RedissonLock
    @Override
    public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
        // 需要阻塞获取锁的时间
        long time = unit.toMillis(waitTime);
        // 当前时间
        long current = System.currentTimeMillis();
        long threadId = Thread.currentThread().getId();
        // 尝试获取锁,ttl为别的线程持有锁的剩余过期时间
        Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
        // lock acquired
        if (ttl == null) {
            // 如果为null,说明获取锁成功,直接返回
            return true;
        }
    	// 剩余阻塞时间。总的阻塞时间-(获取锁消耗的时间(当前时间-上一次记录的当前时间))
        time -= System.currentTimeMillis() - current;
        if (time <= 0) {
            // 剩余阻塞时间小于等于0,获取锁失败,进行返回
            acquireFailed(waitTime, unit, threadId);
            return false;
        }
    	// 重置上一次的当前时间变量
        current = System.currentTimeMillis();
        // 订阅锁释放的消息
        CompletableFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
        try {
            subscribeFuture.get(time, TimeUnit.MILLISECONDS);
        } catch (TimeoutException e) {
            if (!subscribeFuture.completeExceptionally(new RedisTimeoutException(
                "Unable to acquire subscription lock after " + time + "ms. " +
                "Try to increase 'subscriptionsPerConnection' and/or 'subscriptionConnectionPoolSize' parameters."))) {
                subscribeFuture.whenComplete((res, ex) -> {
                    if (ex == null) {
                        unsubscribe(res, threadId);
                    }
                });
            }
            acquireFailed(waitTime, unit, threadId);
            return false;
        } catch (ExecutionException e) {
            acquireFailed(waitTime, unit, threadId);
            return false;
        }
    
        try {
            // 剩余阻塞时间。上一次剩余的阻塞时间-(订阅消息耗费的时间(当前时间-上一次记录的当前时间))
            time -= System.currentTimeMillis() - current;
            if (time <= 0) {
                acquireFailed(waitTime, unit, threadId);
                return false;
            }
    
            while (true) {
                long currentTime = System.currentTimeMillis();
                // 再次尝试获取锁
                ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
                // lock acquired
                if (ttl == null) {
                    // 如果为null,说明获取锁成功,直接返回
                    return true;
                }
    
                // 剩余阻塞时间。总的阻塞时间-(获取锁消耗的时间(当前时间-上一次记录的当前时间))
                time -= System.currentTimeMillis() - currentTime;
                if (time <= 0) {
                    acquireFailed(waitTime, unit, threadId);
                    return false;
                }
    
                // waiting for message
                // 重置当前时间
                currentTime = System.currentTimeMillis();
                // 这一步操作很优美。尽量让当前线程阻塞最少的时间
                if (ttl >= 0 && ttl < time) {
                    // 如果别的线程持有锁的剩余过期时间小于获取锁的阻塞时间,就让当前线程阻塞ttl时间
                    commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } else {
                    // 如果别的线程持有锁的剩余过期时间大于获取锁的阻塞时间,就让当前线程阻塞time时间
                    commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
                }
    			// 计算锁的剩余阻塞时间
                time -= System.currentTimeMillis() - currentTime;
                if (time <= 0) {
                    acquireFailed(waitTime, unit, threadId);
                    return false;
                }
            }
        } finally {
            unsubscribe(commandExecutor.getNow(subscribeFuture), threadId);
        }
        //        return get(tryLockAsync(waitTime, leaseTime, unit));
    }
    
公平锁
  • 加锁逻辑:lock.tryLockInnerAsync(5000, leaseTime, TimeUnit.MILLISECONDS, threadInit, RedisCommands.EVAL_LONG)

    // waitTime:锁等待超时时间
    // leaseTime:锁过期时间
    // command:执行的命令,这里是EVAL_LONG
    @Override
    <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        // 获取线程等待时间:60000*5=300s=5min
        long wait = threadWaitTime;
        if (waitTime > 0) {
            wait = unit.toMillis(waitTime);
        }
        // 当前时间
        long currentTime = System.currentTimeMillis();
        if (command == RedisCommands.EVAL_NULL_BOOLEAN) {
            return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
                                  // remove stale threads
                                  "while true do " +
                                  "local firstThreadId2 = redis.call('lindex', KEYS[2], 0);" +
                                  "if firstThreadId2 == false then " +
                                  "break;" +
                                  "end;" +
                                  "local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2));" +
                                  "if timeout <= tonumber(ARGV[3]) then " +
                                  // remove the item from the queue and timeout set
                                  // NOTE we do not alter any other timeout
                                  "redis.call('zrem', KEYS[3], firstThreadId2);" +
                                  "redis.call('lpop', KEYS[2]);" +
                                  "else " +
                                  "break;" +
                                  "end;" +
                                  "end;" +
    
                                  "if (redis.call('exists', KEYS[1]) == 0) " +
                                  "and ((redis.call('exists', KEYS[2]) == 0) " +
                                  "or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then " +
                                  "redis.call('lpop', KEYS[2]);" +
                                  "redis.call('zrem', KEYS[3], ARGV[2]);" +
    
                                  // decrease timeouts for all waiting in the queue
                                  "local keys = redis.call('zrange', KEYS[3], 0, -1);" +
                                  "for i = 1, #keys, 1 do " +
                                  "redis.call('zincrby', KEYS[3], -tonumber(ARGV[4]), keys[i]);" +
                                  "end;" +
    
                                  "redis.call('hset', KEYS[1], ARGV[2], 1);" +
                                  "redis.call('pexpire', KEYS[1], ARGV[1]);" +
                                  "return nil;" +
                                  "end;" +
                                  "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                                  "redis.call('hincrby', KEYS[1], ARGV[2], 1);" +
                                  "redis.call('pexpire', KEYS[1], ARGV[1]);" +
                                  "return nil;" +
                                  "end;" +
                                  "return 1;",
                                  Arrays.asList(getRawName(), threadsQueueName, timeoutSetName),
                                  unit.toMillis(leaseTime), getLockName(threadId), currentTime, wait);
        }
    
        if (command == RedisCommands.EVAL_LONG) {
            return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
                                  // remove stale threads
                                  // 移除不新鲜的线程id,移除过期的线程
                                  // 
                                  "while true do " +
                                  "local firstThreadId2 = redis.call('lindex', KEYS[2], 0);" +
                                  "if firstThreadId2 == false then " +
                                  // list中没有等待线程,直接跳出循环
                                  "break;" +
                                  "end;" +
    							// 从zset中获取等待线程的超时时间
                                  "local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2));" +								
        						// 判断当前线程是否已经超时
                                  "if timeout <= tonumber(ARGV[4]) then " +
                                  // remove the item from the queue and timeout set
                                  // NOTE we do not alter any other timeout
                                  // 超时就从list和zset中移除等待线程
                                  "redis.call('zrem', KEYS[3], firstThreadId2);" +
                                  "redis.call('lpop', KEYS[2]);" +
                                  "else " +
                                  "break;" +
                                  "end;" +
                                  "end;" +
    
                                  // check if the lock can be acquired now,检查当前这把锁是否可以被获取到
                                  // 可以加锁的判断逻辑:
                                  // 当前不存在锁 and (当前不存在等待锁的线程 or 存在等待锁的线程,但是就是本次需要							加锁的线程)
                                  "if (redis.call('exists', KEYS[1]) == 0) " +
                                  "and ((redis.call('exists', KEYS[2]) == 0) " +
                                  "or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then " +
    
                                  // remove this thread from the queue and timeout set
                                  // 移除list和zset中等待的线程
                                  "redis.call('lpop', KEYS[2]);" +
                                  "redis.call('zrem', KEYS[3], ARGV[2]);" +
    
                                  // decrease timeouts for all waiting in the queue
                                  // 获取zset中等待的线程
                                  "local keys = redis.call('zrange', KEYS[3], 0, -1);" +
                                  "for i = 1, #keys, 1 do " +
                                  // 将所有的等待线程的等待时间减300s,// todo
                                  "redis.call('zincrby', KEYS[3], -tonumber(ARGV[3]), keys[i]);" +
                                  "end;" +
    
                                  // acquire the lock and set the TTL for the lease
                                  "redis.call('hset', KEYS[1], ARGV[2], 1);" +
                                  "redis.call('pexpire', KEYS[1], ARGV[1]);" +
                                  "return nil;" +
                                  "end;" +
    
                                  // check if the lock is already held, and this is a re-entry
                                  // 判断此次加锁的线程是否是当前持有锁的线程,如果是就把可重入次数+1,再次设置一个超								时时间
                                  "if redis.call('hexists', KEYS[1], ARGV[2]) == 1 then " +
                                  "redis.call('hincrby', KEYS[1], ARGV[2],1);" +
                                  "redis.call('pexpire', KEYS[1], ARGV[1]);" +
                                  "return nil;" +
                                  "end;" +
    
                                  // the lock cannot be acquired
                                  // check if the thread is already in the queue
                                  // 走到这里,说明前面加锁失败,锁已经被其它线程持有
                                  // 判断加锁线程是否已经在zset中存在了,存在说明之前已经尝试过一次加锁
                                  "local timeout = redis.call('zscore', KEYS[3], ARGV[2]);" +
                                  "if timeout ~= false then " +
                                  // the real timeout is the timeout of the prior thread
                                  // in the queue, but this is approximately correct, and
                                  // avoids having to traverse the queue
                                  // 返回 timeout-wait-current // todo
                                  "return timeout - tonumber(ARGV[3]) - tonumber(ARGV[4]);" +
                                  "end;" +
    
                                  // add the thread to the queue at the end, and set its timeout in the timeout set to the timeout of
                                  // the prior thread in the queue (or the timeout of the lock if the queue is empty) plus the
                                  // threadWaitTime
                                  // 从list中获取最后一个元素
                                  "local lastThreadId = redis.call('lindex', KEYS[2], -1);" +
                                  "local ttl;" +
                                  // 如果list里面有元素,说明等待队列中已经有其他线程了
                                  "if lastThreadId ~= false and lastThreadId ~= ARGV[2] then " +
                                  // ttl=等待队列中最后一个线程的等待时间-当前时间
                                  "ttl = tonumber(redis.call('zscore', KEYS[3], lastThreadId)) - tonumber(ARGV[4]);" +
                                  "else " +
                                  // ttl=当前锁的剩余过期时间
                                  "ttl = redis.call('pttl', KEYS[1]);" +
                                  "end;" +
                                  // 计算当前线程的等待时间=ttl+300s+当前时间
                                  "local timeout = ttl + tonumber(ARGV[3]) + tonumber(ARGV[4]);" +
                                  "if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then " +
                                  "redis.call('rpush', KEYS[2], ARGV[2]);" +
                                  "end;" +
                                  "return ttl;",
                                  // KEYS[1]:锁名称  KEYS[2]:redisson_lock_queue+[锁名称]  KEYS[3]:										redisson_lock_timeout+[锁名称]
                                  // redisson_lock_queue+[锁名称]:list数据结构,里面存储的是加锁失败的线程id,								rpush进去的
                                  // redisson_lock_timeout+[锁名称]:zset数据结构,里面存储的是加锁失败的线程id,								score存储的是超时时间
                                  // ARGV[1]:锁过期时间  ARGV[2]:id + ":" + threadId  ARGV[3]:											60000*5=300s=5min  ARGV[4]:当前时间
                                  Arrays.asList(getRawName(), threadsQueueName, timeoutSetName),
                                  unit.toMillis(leaseTime), getLockName(threadId), wait, currentTime);
        }
    
        throw new IllegalArgumentException();
    }
    
  • 释放锁:lock.unlockInnerAsync(threadInit)

    @Override
    protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                              // remove stale threads
    						// 移除等待队列中已经过期的等待线程
                              "while true do "
                              + "local firstThreadId2 = redis.call('lindex', KEYS[2], 0);"
                              + "if firstThreadId2 == false then "
                              + "break;"
                              + "end; "
                              + "local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2));"
                              + "if timeout <= tonumber(ARGV[4]) then "
                              + "redis.call('zrem', KEYS[3], firstThreadId2); "
                              + "redis.call('lpop', KEYS[2]); "
                              + "else "
                              + "break;"
                              + "end; "
                              + "end;"
    						// 如果当前没有线程持有锁
                              + "if (redis.call('exists', KEYS[1]) == 0) then " + 
                              "local nextThreadId = redis.call('lindex', KEYS[2], 0); " + 
                              "if nextThreadId ~= false then " +
                              // 获取等待队列中的第一元素,发布消息通知
                              "redis.call('publish', KEYS[4] .. ':' .. nextThreadId, ARGV[1]); " +
                              "end; " +
                              "return 1; " +
                              "end;" +
                              // 如果持有锁的线程不是当前需要释放锁的线程,直接返回
                              "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                              "return nil;" +
                              "end; " +
                              // 走到这里说明当前持有锁的线程是需要释放锁的线程,对加锁次数减1
                              "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
                              "if (counter > 0) then " +
                              // 判断加锁次数,大于0就对锁过期时间重置
                              "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                              "return 0; " +
                              "end; " +
    						// 如果加锁次数减到1,就删除锁
                              "redis.call('del', KEYS[1]); " +
                              "local nextThreadId = redis.call('lindex', KEYS[2], 0); " + 
                              "if nextThreadId ~= false then " +
                              // 删除锁以后,如果等待队列中还有线程,就发布消息通知
                              "redis.call('publish', KEYS[4] .. ':' .. nextThreadId, ARGV[1]); " +
                              "end; " +
                              "return 1; ",
                              Arrays.asList(getRawName(), threadsQueueName, timeoutSetName, getChannelName()),
                              LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId), System.currentTimeMillis());
    }
    
MultiLock
  • 简单案例演示

    @Test
    public void testMultiLock() {
        Config config = new Config();
        config.useSingleServer()
            .setAddress("redis://127.0.0.1:6379")
            .setDatabase(1)
            .setPassword("^Koron#3438$");
        RedissonClient redissonClient = Redisson.create(config);
        RLock lock1 = redissonClient.getLock("lock1");
        RLock lock2 = redissonClient.getLock("lock2");
        RLock lock3 = redissonClient.getLock("lock3");
        RedissonMultiLock multiLock = new RedissonMultiLock(lock1, lock2, lock3);
        multiLock.lock();
        multiLock.unlock();
    }
    
  • 使用场景

    下订单操作:之前我们的下订单示例是对每一个商品加一把锁。但是往往一个订单中会有多个商品,这里我们就可以锁定多个商品

  • 加锁逻辑

    // --------- 1 RedissonMultiLock
    @Override
    public void lock() {
        try {
            lockInterruptibly();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
    // --------- 2 RedissonMultiLock
    @Override
    public void lockInterruptibly() throws InterruptedException {
        lockInterruptibly(-1, null);
    }
    
    // --------- 3 RedissonMultiLock
    @Override
    public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
        // 获取基础的等待时间:3*1500
        long baseWaitTime = locks.size() * 1500;
        // 循环加锁
        while (true) {
            long waitTime;
            // 如果锁过期时间小于0
            if (leaseTime <= 0) {
                // 锁等待时间为:3*1500
                waitTime = baseWaitTime;
            } else {
                // 否则锁等待时间等于锁过期时间
                waitTime = unit.toMillis(leaseTime);
                if (waitTime <= baseWaitTime) {
                    waitTime = ThreadLocalRandom.current().nextLong(waitTime/2, waitTime);
                } else {
                    waitTime = ThreadLocalRandom.current().nextLong(baseWaitTime, waitTime);
                }
            }
    
            if (tryLock(waitTime, leaseTime, TimeUnit.MILLISECONDS)) {
                return;
            }
        }
    }
    
    // --------- 4 RedissonMultiLock
    @Override
    public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
        long newLeaseTime = -1;
        if (leaseTime > 0) {
            if (waitTime > 0) {
                newLeaseTime = unit.toMillis(waitTime)*2;
            } else {
                newLeaseTime = unit.toMillis(leaseTime);
            }
        }
    
        // 当前时间
        long time = System.currentTimeMillis();
        long remainTime = -1;
        if (waitTime > 0) {
            // 剩余时间:3*1500
            remainTime = unit.toMillis(waitTime);
        }
        // 计算锁等待时间
        long lockWaitTime = calcLockWaitTime(remainTime);
    
        // 允许加锁失败的次数,mutilLock默认为0
        int failedLocksLimit = failedLocksLimit();
        // 加锁成功的容器
        List<RLock> acquiredLocks = new ArrayList<>(locks.size());
        // 遍历所有的锁
        for (ListIterator<RLock> iterator = locks.listIterator(); iterator.hasNext();) {
            RLock lock = iterator.next();
            boolean lockAcquired;
            try {
                if (waitTime <= 0 && leaseTime <= 0) {
                    // 尝试获取锁
                    lockAcquired = lock.tryLock();
                } else {
                    // 尝试在指定等待时间内获取锁
                    long awaitTime = Math.min(lockWaitTime, remainTime);
                    lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
                }
            } catch (RedisResponseTimeoutException e) {
                // 出现异常,把当前加锁的这个释放掉
                unlockInner(Arrays.asList(lock));
                // 设置结果为false
                lockAcquired = false;
            } catch (Exception e) {
                lockAcquired = false;
            }
    		
            if (lockAcquired) {
                // 如果加锁成功,将当前的锁放到加锁成功的容器中
                acquiredLocks.add(lock);
            } else {
                // 如果加锁失败,判断加锁的数量是否等于加锁成功的数量,如果是的跳出循环
                if (locks.size() - acquiredLocks.size() == failedLocksLimit()) {
                    break;
                }
    		   // 只要有一个加锁失败,就把之前加锁成功的释放掉
                if (failedLocksLimit == 0) {
                    unlockInner(acquiredLocks);
                    // 如果锁等待时间小于0以后,就直接返回
                    if (waitTime <= 0) {
                        return false;
                    }
                    failedLocksLimit = failedLocksLimit();
                    // 清空加锁成功的容器
                    acquiredLocks.clear();
                    // reset iterator
                    // 重置迭代器到上一个锁
                    while (iterator.hasPrevious()) {
                        iterator.previous();
                    }
                } else {
                    failedLocksLimit--;
                }
            }
    		// 重新计算剩余时间
            if (remainTime > 0) {
                remainTime -= System.currentTimeMillis() - time;
                time = System.currentTimeMillis();
                if (remainTime <= 0) {
                    unlockInner(acquiredLocks);
                    return false;
                }
            }
        }
    
        if (leaseTime > 0) {
            acquiredLocks.stream()
                .map(l -> (RedissonBaseLock) l)
                .map(l -> l.expireAsync(unit.toMillis(leaseTime), TimeUnit.MILLISECONDS))
                .forEach(f -> f.toCompletableFuture().join());
        }
    
        return true;
    }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值