手写 Redis 分布式锁

手写 Redis 分布式锁

面试题

  • Redis 除了拿来做缓存,你还见过基于 Redis 的什么用法?
    • 数据共享,分布式 Session
    • 分布式锁
    • 全局 ID
    • 计算器,点赞
    • 位统计
    • 购物车
    • 轻量级消息队列
      • list
      • stream
    • 抽奖
    • 点赞、签到、打卡
    • 差集交集并集,用户关注、可能认识的人,推荐模型
    • 热点新闻、搜索排行榜
  • Redis 做分布式锁的时候需要注意的问题?
  • 你们公司自己实现的分布式锁是否用的 setnx 命令实现?这个是最合适的吗?你如何考虑分布式锁的可重入问题?
  • 如果 Redis 是单点部署的,会带来什么问题?那你准备怎么解决单点问题?
  • Redis 集群模式下,比如主从模式,CAP 方面有没有问题呢?
  • 那你简单的介绍一下 Redlock 吧?你简历上写 Redisson,你谈谈?
  • Redis 分布式锁如何续期,看门狗知道吗?

锁的种类

  • 单机版同一个 JVM 虚拟机内synchronized 或者 Lock 接口

  • 分布式多个不通 JVM 虚拟机,单机的线程锁机制不再起作用,资源类在不同的服务器之间共享了。

分布式锁的条件和刚需

  • 独占性:任何时刻只能有且仅有一个线程持有
  • 高可用
    • 若 Redis 集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败的情况
    • 高并发请求下,依旧性能好使
  • 防死锁:杜绝死锁,必须有超时控制机制或者撤销操作,有个兜底终止跳出方案
  • 不乱抢:防止张冠李戴,不能私下解别人的锁,自己加锁只能自己释放
  • 重入性:同一个节点的同一个线程如果获得锁之后,他也可以再次获取这个锁

分布式锁

set key value [EX seconds] [PX milliseconds] [NX|XX]
  • EX:Key 在多少秒之后过期
  • PX:Key 在多少毫秒之后过期
  • NX:当 Key 不存在的时候,才创建 Key,效果等同于 setnx
  • XX:当 Key 存在的时候,覆盖 Key
127.0.0.1:6379> set lock pay ex 10 NX
OK
127.0.0.1:6379> get lock
"pay"
127.0.0.1:6379> set lock order ex 10 NX
OK
127.0.0.1:6379> set lock pay ex 10 NX
(nil)
127.0.0.1:6379> get lock
(nil)
127.0.0.1:6379>

如果 setnxexpire 两个命令分开执行,由于非原子性,可能存在潜在风险

重点

JUC 中 AQS 锁的规范落地参考 + 可重入锁考虑 + Lua 脚本 + Redis 命令一步步实现分布式锁

基础案例(Spring Boot + Redis)

使用场景:多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击)。

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>dev.matrixlab.redislock</groupId>
    <artifactId>redis_distributed_lock2</artifactId>
    <version>1.0-SNAPSHOT</version>
 
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.12</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
 
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <lombok.version>1.16.18</lombok.version>
    </properties>
 
 
 
    <dependencies>
        <!--SpringBoot通用依赖模块-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--SpringBoot与Redis整合依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!--swagger2-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
        <!--通用基础配置boottest/lombok/hutool-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.8</version>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
 
</project>
application.properties
server.port=7777
 
spring.application.name=redis_distributed_lock
# ========================swagger2=====================
# http://localhost:7777/swagger-ui.html
swagger2.enabled=true
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
 
# ========================redis单机=====================
spring.redis.database=0
spring.redis.host=192.168.111.185
spring.redis.port=6379
spring.redis.password=111111
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
RedisDistributedLockApp7777.java
package dev.matrixlab.redislock;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
@SpringBootApplication
public class RedisDistributedLockApp7777 {
    public static void main(String[] args) {
        SpringApplication.run(RedisDistributedLockApp7777.class,args);
    }
}
package dev.matrixlab.redislock.config;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;

@Configuration
@EnableSwagger2
public class Swagger2Config {
    @Value("${swagger2.enabled}")
    private Boolean enabled;

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .enable(enabled)
                .select()
                .apis(RequestHandlerSelectors.basePackage("dev.matrixlab.redislock")) //你自己的package
                .paths(PathSelectors.any())
                .build();
    }
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("springboot利用swagger2构建api接口文档 "+"\t"+ DateTimeFormatter.ofPattern("yyyy-MM-dd").format(LocalDateTime.now()))
                .description("springboot+redis整合")
                .version("1.0")
                .termsOfServiceUrl("https://www.baidu.com/")
                .build();
    }
}
RedisConfig.java
package dev.matrixlab.redislock.config;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
 
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
        RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        //设置key序列化方式string
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //设置value的序列化方式json
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
 
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
 
        redisTemplate.afterPropertiesSet();
 
        return redisTemplate;
    }
}
InventoryService.java
package dev.matrixlab.redislock.service;
 
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
 
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
@Service
@Slf4j
public class InventoryService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;
 
    private Lock lock = new ReentrantLock();
 
    public String sale() {
        String retMessage = "";
        lock.lock();
        try {
            //1 查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //3 扣减库存
            if(inventoryNumber > 0) {
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
                System.out.println(retMessage);
            } else {
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        } finally {
            lock.unlock();
        }
        return retMessage+"\t"+"服务端口号:"+port;
    }
}
InventoryController.java
package dev.matrixlab.redislock.controller;
 
import cn.hutool.core.util.IdUtil;
import dev.matrixlab.redislock.service.InventoryService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
 
import java.util.concurrent.atomic.AtomicInteger;
 
@RestController
@Api(tags = "redis分布式锁测试")
public class InventoryController {
    @Autowired
    private InventoryService inventoryService;
 
    @ApiOperation("扣减库存,一次卖一个")
    @GetMapping(value = "/inventory/sale")
    public String sale() {
        return inventoryService.sale();
    }
}

手写分布式锁

0x01. 初始化版本简单添加

代码示例

InventoryService.java
package dev.matrixlab.redislock.service;
 
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
 
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
@Service
@Slf4j
public class InventoryService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;
 
    private Lock lock = new ReentrantLock();
 
    public String sale() {
        String retMessage = "";
        lock.lock();
        try {
            //1 查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //3 扣减库存
            if(inventoryNumber > 0) {
                stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
                System.out.println(retMessage);
            } else {
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        } finally {
            lock.unlock();
        }
        return retMessage+"\t"+"服务端口号:"+port;
    }
}

注意点

增加了synchronized 或者 Lock

0x02. nginx 分布式微服务架构

问题

分布式需求

当 V2.0 版本代码分布式部署后,单机锁还是出现超卖现象,需要分布式锁

nginx 配置负载均衡
启动两个微服务
模拟高并发

出现超卖现象

Bug-Why

为什么加了 synchronized 或者 Lock 还是没有控制住?

单机环境下,可以使用 synchronized 或 Lock 来实现。

但在分布式系统中,因为竞争的线程可能不在同一个节点上(同一个 JVM 中),所以需要一个让所有进程都能访问到的锁来实现(比如 Redis 或者 Zookeeper 来构建)

不同进程 JVM 层面的锁就不管用了,那么可以利用第三方的一个组件,来获取锁。未获取到锁,则阻塞当前想要运行的线程。

分布式锁作用
  • 跨进程 + 跨服务
  • 解决超卖
  • 防止缓存击穿

解决方法

0x03. Redis 分布式锁

3.1 版

InventoryService.java
package dev.matrixlab.redislock.service;
 
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
 
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
@Service
@Slf4j
public class InventoryService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;
 
    private Lock lock = new ReentrantLock();
 
    public String sale() {
        String retMessage = "";
        String key = "RedisLock";
        String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
 
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue);
        if(!flag) {
            //暂停20毫秒后递归调用
            try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
            sale();
        } else {
            try {
                //1 查询库存信息
                String result = stringRedisTemplate.opsForValue().get("inventory001");
                //2 判断库存是否足够
                Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
                //3 扣减库存
                if(inventoryNumber > 0) {
                    stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
                    retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
                    System.out.println(retMessage);
                }else{
                    retMessage = "商品卖完了,o(╥﹏╥)o";
                }
            } finally {
                stringRedisTemplate.delete(key);
            }
        }
        return retMessage+"\t"+"服务端口号:"+port;
    }
}
存在的 Bug
  • 上述使用递归重试的方法,容易导致 StackOverflowError

3.2 版

InventoryService.java
package dev.matrixlab.redislock.service;
 
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
 
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
@Service
@Slf4j
public class InventoryService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;
 
    private Lock lock = new ReentrantLock();
 
    public String sale() {
        String retMessage = "";
        String key = "RedisLock";
        String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
        while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue)) {
            //暂停20毫秒,类似CAS自旋
            try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
        } try {
            //1 查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //3 扣减库存
            if(inventoryNumber > 0) {
                stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
                System.out.println(retMessage);
            } else {
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        } finally {
            stringRedisTemplate.delete(key);
        }
        return retMessage+"\t"+"服务端口号:"+port;
    }
}
改进
  • 多线程判断思想 JUC 里面说过的虚假唤醒,用 while 替代 if
  • 使用自旋替代递归重试
存在的 Bug
  • 部署了微服务的 Java 程序机器宕机,代码层面根本没有走到 finally 这块,没有办法保证解锁(无过期时间,该 Key 一直存在),需要加入一个过期时间限定 Key。

0x04. 宕机与过期 + 防止死锁

4.1 版

InventoryService.java
package dev.matrixlab.redislock.service;
 
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
 
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
@Service
@Slf4j
public class InventoryService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;
 
    private Lock lock = new ReentrantLock();
 
    public String sale() {
        String retMessage = "";
        String key = "RedisLock";
        String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
 
        while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue)) {
            //暂停毫秒
            try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
        }
		stringRedisTemplate.expire(key, 10L, TimeUnit.SECONDS);
        try {
            //1 查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //3 扣减库存
            if(inventoryNumber > 0) {
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
                System.out.println(retMessage);
            } else {
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        } finally {
            stringRedisTemplate.delete(key);
        }
        return retMessage+"\t"+"服务端口号:"+port;
    }
}
存在的 Bug
  • 设置 Key 和过期时间分开,存在潜在风险,必须要合并成一行,具备原子性。

4.2 版

InventoryService.java
package dev.matrixlab.redislock.service;
 
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
 
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
@Service
@Slf4j
public class InventoryService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;
 
    private Lock lock = new ReentrantLock();
 
    public String sale() {
        String retMessage = "";
        String key = "RedisLock";
        String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
 
        while (!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue, 30L, TimeUnit.SECONDS)) {
            //暂停毫秒
            try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
        }
 
        try {
            //1 查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //3 扣减库存
            if (inventoryNumber > 0) {
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
                System.out.println(retMessage);
            } else {
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        } finally {
            stringRedisTemplate.delete(key);
        }
        return retMessage+"\t"+"服务端口号:"+port;
    }
}
改进
  • 加锁和过期时间设置必须同一行,保证原子性。
存在的 Bug
  • 实际业务处理时间超过了默认设置 Key 的过期时间
  • 删除别人的锁

0x05. 防止误删 key 的问题

InventoryService.java
package dev.matrixlab.redislock.service;
 
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
 
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
@Service
@Slf4j
public class InventoryService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;
 
    private Lock lock = new ReentrantLock();
 
    public String sale() {
        String retMessage = "";
        String key = "RedisLock";
        String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
 
        while (!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue, 30L, TimeUnit.SECONDS)) {
            //暂停毫秒
            try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
        } 
        try {
            //1 查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //3 扣减库存
            if (inventoryNumber > 0) {
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t"+uuidValue;
                System.out.println(retMessage);
            } else {
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        } finally {
            // v5.0判断加锁与解锁是不是同一个客户端,同一个才行,自己只能删除自己的锁,不误删他人的
            if (stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(uuidValue)){
                stringRedisTemplate.delete(key);
            }
        }
        return retMessage+"\t"+"服务端口号:"+port;
    }
}

改进

  • 不可随意删除别人创建的锁

存在的Bug

  • finally 代码块的判断和 delete 删除操作不是原子性的

0x06. Lua 保证原子性

Lua 脚本编写 Redis 分布式锁

https://redis.io/docs/reference/patterns/distributed-locks/ (opens in a new tab)

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

Lua 脚本浅谈

Redis 调用 Lua 脚本通过 eval 命令保证代码执行的原子性,直接用 return 返回脚本执行后的结果值。

Lua 脚本初识
  • eval luascript numkeys [key [key ...]] [arg [arg ...]]

  • helloword 入门

    • hello lua

      127.0.0.1:6379> EVAL "return 'hello lua'" 0
      "hello lua"
      127.0.0.1:6379> 
    • set k1 v1 get k1

      127.0.0.1:6379> set k1 v1
      OK
      127.0.0.1:6379> get k1
      "v1"
      127.0.0.1:6379> EVAL "redis.call('set', 'k1', 'v1') return redis.call('get', 'k1')" 0
      "v1"
      127.0.0.1:6379> get k1
      "v1"
      127.0.0.1:6379>
    • mset

      127.0.0.1:6379> EVAL "return redis.call('mset', KEYS[1], ARGV[1], KEYS[2], ARGV[2])" 2 k1 k2 11 12
      OK
      127.0.0.1:6379> get k1
      "11"
      127.0.0.1:6379> get k2
      "12"
      127.0.0.1:6379> 
Lua 脚本进一步
  • Redis 分布式 Lua 脚本官网练习

    127.0.0.1:6379> set RedisLock 1111-2222-3333
    OK
    127.0.0.1:6379> get RedisLock
    "1111-2222-3333"
    127.0.0.1:6379> eval "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 RedisLock 1111-2222-3333
    (integer) 1
    127.0.0.1:6379> del RedisLock
    (integer) 0
    127.0.0.1:6379> eval "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 RedisLock 1111-2222-3333
    (integer) 0
    127.0.0.1:6379> 
  • 条件判断语法

    if (布尔条件) then
      业务代码
    elseif (布尔条件) then
      业务代码
    elseif (布尔条件) then
      业务代码
    else
      业务代码
    end
  • 提交判断案例

    127.0.0.1:6379> eval "if KEYS[1] > KEYS[2] then return ARGV[1] else return ARGV[2] end" 2 7 5 1 0
    "1"
    127.0.0.1:6379> eval "if KEYS[1] > KEYS[2] then return ARGV[1] else return ARGV[2] end" 2 7 8 1 0
    "0"
    127.0.0.1:6379> 

程序实现

InventoryService.java
package dev.matrixlab.redislock.service;
 
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
 
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
@Service
@Slf4j
public class InventoryService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;
 
    private Lock lock = new ReentrantLock();
 
    public String sale() {
        String retMessage = "";
        String key = "RedisLock";
        String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
 
        while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30L,TimeUnit.SECONDS)) {
            //暂停毫秒
            try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
        }
 
        try {
            //1 查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //3 扣减库存
            if (inventoryNumber > 0) {
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t"+uuidValue;
                System.out.println(retMessage);
            } else {
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        } finally {
            //V6.0 将判断+删除自己的合并为lua脚本保证原子性
            String luaScript =
                    "if (redis.call('get',KEYS[1]) == ARGV[1]) then " +
                        "return redis.call('del',KEYS[1]) " +
                    "else " +
                        "return 0 " +
                    "end";
            stringRedisTemplate.execute(new DefaultRedisScript<>(luaScript, Boolean.class), Arrays.asList(key), uuidValue);
        }
        return retMessage+"\t"+"服务端口号:"+port;
    }
}

0x07. 可重入锁 + 设计模式

问题

如何兼顾锁的可重入性问题?

可重入锁(有名递归锁)

概述

可重入锁是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。Java 中 ReentrantLocksynchronized 都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

“可重入锁”解释
  • 可:可以
  • 重:再次
  • 入:进入
  • 锁:同步锁
  • 进入什么:进入同步域(即同步代码块/方法或显式锁锁定的代码)

一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。即自己可以获取自己的内部锁。

JUC 知识复习,可重入锁出现 Bug 会如何影响程序
  • 死锁:如果可重入锁的实现有 Bug,导致锁的释放出现问题,可能会发生死锁。当一个线程在持有锁的情况下再次尝试获取该锁时,如果由于 Bug 导致无法正确释放锁资源,其他线程将无法获取到锁,从而导致死锁的发生。
  • 死循环:如果可重入锁的实现有 Bug,导致某个线程无法正确释放锁,那么当其他线程尝试获取同一个锁时,它们可能会陷入等待状态,从而导致死循环。这会消耗大量的 CPU 资源,并且可能导致整个程序的性能下降。
  • 程序逻辑错误:可重入锁的 Bug 可能导致锁的获取和释放顺序出现问题,从而破坏了程序的预期逻辑。例如,如果在某个线程持有锁期间又尝试获取同一个锁,但由于 Bug 导致锁的状态无法正确更新,可能会导致程序逻辑上的错误或数据一致性问题。
  • 并发安全性问题:可重入锁的 Bug 可能导致多个线程在并发访问共享资源时出现问题。例如,如果某个线程持有锁并修改了共享资源,但由于 Bug 导致其他线程无法正确获取锁,那么可能会导致数据竞争和并发安全性问题。
可重入锁种类
  • 隐式锁(即 synchronized 关键字使用的锁)默认是可重入锁

    指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。

    简单的来说就是:在一个 synchronized 修饰的方法或代码块的内部调用本类的其他 synchronized 修饰的方法或代码块时,是永远可以得到锁的。与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。

    • 同步块

      ReEntryLockDemo.java
      package dev.matrixlab.juc.senior.prepare;
       
      public class ReEntryLockDemo {
          public static void main(String[] args) {
              final Object objectLockA = new Object();
       
              new Thread(() -> {
                  synchronized (objectLockA) {
                      System.out.println("-----外层调用");
                      synchronized (objectLockA) {
                          System.out.println("-----中层调用");
                          synchronized (objectLockA) {
                              System.out.println("-----内层调用");
                          }
                      }
                  }
              }, "a").start();
          }
      }
    • 同步方法

      ReEntryLockDemo.java
      package dev.matrixlab.juc.senior.prepare;
       
      /**
       * 在一个Synchronized修饰的方法或代码块的内部调用本类的其他Synchronized修饰的方法或代码块时,是永远可以得到锁的
       */
      public class ReEntryLockDemo {
          public synchronized void m1() {
              System.out.println("-----m1");
              m2();
          }
          public synchronized void m2() {
              System.out.println("-----m2");
              m3();
          }
          public synchronized void m3() {
              System.out.println("-----m3");
          }
       
          public static void main(String[] args) {
              ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();
       
              reEntryLockDemo.m1();
          }
      }
  • synchronized 的重入的实现机理

    每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。

    当执行 monitorenter 时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java 虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加 1。

    在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加 1,否则需要等待,直至持有线程释放该锁。

    当执行 monitorexit 时,Java 虚拟机则需将锁对象的计数器减 1。计数器为零代表锁已被释放。

  • 显式锁(即 Lock)也有 ReentrantLock 这样的可重入锁

    ReEntryLockDemo.java
    package dev.matrixlab.juc.senior.prepare;
     
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
     
    /**
     * 在一个Synchronized修饰的方法或代码块的内部调用本类的其他Synchronized修饰的方法或代码块时,是永远可以得到锁的
     */
    public class ReEntryLockDemo {
        static Lock lock = new ReentrantLock();
     
        public static void main(String[] args) {
            new Thread(() -> {
                lock.lock();
                try {
                    System.out.println("----外层调用lock");
                    lock.lock();
                    try {
                        System.out.println("----内层调用lock");
                    } finally {
                        // 这里故意注释,实现加锁次数和释放次数不一样
                        // 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。
                        lock.unlock(); // 正常情况,加锁几次就要解锁几次
                    }
                } finally {
                    lock.unlock();
                }
            }, "a").start();
     
            new Thread(() -> {
                lock.lock();
                try {
                    System.out.println("b thread----外层调用lock");
                } finally {
                    lock.unlock();
                }
            }, "b").start();
        }
    }

lock/unlock 配合可重入锁进行 AQS 源码分析

切记,一般而言,你 lock 了几次就要 unlock 几次。

Redis 数据类型替代可重入锁计数问题

  • HSET

  • Map<String, Map<Object, Object>>

  • 案例命令

    127.0.0.1:6379> HEXISTS RedisLock 1111-2222
    (integer) 0
    127.0.0.1:6379> HSET RedisLock 1111-2222 1
    (integer) 1
    127.0.0.1:6379> HINCRBY RedisLock 1111-2222 1
    (integer) 2
    127.0.0.1:6379> HINCRBY RedisLock 1111-2222 1
    (integer) 3
    127.0.0.1:6379> HGET RedisLock 1111-2222
    "3"
    127.0.0.1:6379> HINCRBY RedisLock 1111-2222 -1
    (integer) 2
    127.0.0.1:6379> HINCRBY RedisLock 1111-2222 -1
    (integer) 1
    127.0.0.1:6379> HINCRBY RedisLock 1111-2222 -1
    (integer) 0
    127.0.0.1:6379> HGET RedisLock 1111-2222
    "0"
    127.0.0.1:6379>
    hset key field value
    #    锁名字 请求线程的uuid + threadID 加锁次数
  • 总结

    setnx 只能解决又无的问题,够用但是不完美

    hset 不但解决有无,还解决可重入问题

设计思路

目前有 2 条支线,目的是保证同一个时候只能有一个线程持有锁进去 Redis 做扣减库存动作

  • 保证加锁/解锁 lock/unlock

    一个靠谱分布式锁需要具备的条件和刚需

    • 独占性
    • 高可用
    • 防死锁
    • 不乱抢
    • 重入性
  • 扣减库存 redis 命令的原子性

Lua 脚本

redis 命令过程分析
127.0.0.1:6379> HEXISTS RedisLock 1111-2222
(integer) 0
127.0.0.1:6379> HSET RedisLock 1111-2222 1
(integer) 1
127.0.0.1:6379> HINCRBY RedisLock 1111-2222 1
(integer) 2
127.0.0.1:6379> HINCRBY RedisLock 1111-2222 1
(integer) 3
127.0.0.1:6379> HGET RedisLock 1111-2222
"3"
127.0.0.1:6379> HINCRBY RedisLock 1111-2222 -1
(integer) 2
127.0.0.1:6379> HINCRBY RedisLock 1111-2222 -1
(integer) 1
127.0.0.1:6379> HINCRBY RedisLock 1111-2222 -1
(integer) 0
127.0.0.1:6379> HGET RedisLock 1111-2222
"0"
127.0.0.1:6379> DEL RedisLock
(integer) 1
127.0.0.1:6379>
加锁 Lua 脚本 lock
  • 先判断 Redis 分布式锁这个 Key 是否存在

    EXISTS key

    • 返回 0 说明不存在,HSET 新建当前线程属于自己的锁 UUID:ThreadID

      127.0.0.1:6379> HSET RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 1
      (integer) 1
    • 返回 1 说明已经有锁,需进一步判断是不是当前线程自己的锁

      # HEXISTS key UUID:ThreadID
      127.0.0.1:6379> HEXISTS RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1
      (integer) 1
      • 返回 0 说明不是自己的

      • 返回 1 说明是自己的锁,自增 1 次表示重入

        127.0.0.1:6379> HEXISTS 0c90d37cb6ec42268861b3d739f8b3a8:1
        (integer) 1
        127.0.0.1:6379> HINCRBY 0c90d37cb6ec42268861b3d739f8b3a8:1 1
        (integer) 2
        127.0.0.1:6379> HINCRBY 0c90d37cb6ec42268861b3d739f8b3a8:1 1
        (integer) 3
        127.0.0.1:6379> 
  • 上述设计修改为 Lua 脚本

    • V1

      相同部分是否可以替换处理?hincrby 命令可否替代 hset 命令。

      if redis.call('exists','key') == 0 then
        redis.call('hset','key','uuid:threadid',1)
        redis.call('expire','key',30)
        return 1
      elseif redis.call('hexists','key','uuid:threadid') == 1 then
        redis.call('hincrby','key','uuid:threadid',1)
        redis.call('expire','key',30)
        return 1
      else
        return 0
      end
    • V2

      if redis.call('exists','key') == 0 or redis.call('hexists','key','uuid:threadid') == 1 then
        redis.call('hincrby','key','uuid:threadid',1)
        redis.call('expire','key',30)
        return 1
      else
        return 0
      end
    • V3

      if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then 
        redis.call('hincrby',KEYS[1],ARGV[1],1) 
        redis.call('expire',KEYS[1],ARGV[2]) 
        return 1 
      else
        return 0
      end
      KKV
      keyKEYS[1]RedisLock
      valueARGV[1]0c90d37cb6ec42268861b3d739f8b3a8:1
      过期时间值ARGV[2]30 秒
  • 测试

    127.0.0.1:6379> EVAL "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end" 1 RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 30
    (integer) 1
    127.0.0.1:6379> HGET RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1
    "1"
    127.0.0.1:6379> EVAL "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end" 1 RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 30
    (integer) 1
    127.0.0.1:6379> EVAL "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end" 1 RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 30
    (integer) 1
    127.0.0.1:6379> HGET RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1
    "3"
    127.0.0.1:6379> 
加锁 Lua 脚本 unlock
  • 设计思路:有锁且还是自己的锁

    # HEXISTS key uuid:ThreadID
    127.0.0.1:6379> HEXISTS RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1
    (integer) 0
    • 返回 0,说明根本没有锁,程序块返回 nil

    • 不是 0,说明有锁且是自己的锁,直接调用 HINCRBY -1 表示每次减个 1,解锁 1 次,知道它变为 0 表示可以删除该锁 Key,DEL KEY

    127.0.0.1:6379> EVAL "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end" 1 RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 30
    (integer) 1
    127.0.0.1:6379> EVAL "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end" 1 RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 30
    (integer) 1
    127.0.0.1:6379> EVAL "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end" 1 RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 30
    (integer) 1
    127.0.0.1:6379> HEXISTS RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1
    (integer) 1
    127.0.0.1:6379> HGET RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1
    "3"
    127.0.0.1:6379> HINCRBY RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 -1
    (integer) 2
    127.0.0.1:6379> HINCRBY RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 -1
    (integer) 1
    127.0.0.1:6379> HINCRBY RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 -1
    (integer) 0
    127.0.0.1:6379> DEL RedisLock
    (integer) 1
    127.0.0.1:6379> 
  • 上述设计修改为 Lua 脚本

    • V1

      if redis.call('HEXISTS',lock,uuid:threadID) == 0 then
       return nil
      elseif redis.call('HINCRBY',lock,uuid:threadID,-1) == 0 then
       return redis.call('del',lock)
      else 
       return 0
      end
    • V2

      if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then
       return nil
      elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then
       return redis.call('DEL',KEYS[1])
      else
       return 0
      end
  • 全套流程测试

    127.0.0.1:6379> EVAL "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end" 1 RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 30
    (integer) 1
    127.0.0.1:6379> EVAL "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end" 1 RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 30
    (integer) 1
    127.0.0.1:6379> EVAL "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end" 1 RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1 30
    (integer) 1
    127.0.0.1:6379> EVAL "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then return nil elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then return redis.call('del',KEYS[1]) else return 0 end" 1 RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1
    (integer) 0
    127.0.0.1:6379> EVAL "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then return nil elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then return redis.call('del',KEYS[1]) else return 0 end" 1 RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1
    (integer) 0
    127.0.0.1:6379> EVAL "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then return nil elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then return redis.call('del',KEYS[1]) else return 0 end" 1 RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1
    (integer) 1
    127.0.0.1:6379> EVAL "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then return nil elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then return redis.call('del',KEYS[1]) else return 0 end" 1 RedisLock 0c90d37cb6ec42268861b3d739f8b3a8:1
    (nil)
    127.0.0.1:6379> 

程序实现

  • 复原程序为初始无锁版

    InventoryService.java
    package dev.matrixlab.redislock.service;
     
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.stereotype.Service;
     
    @Service
    @Slf4j
    public class InventoryService {
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
        @Value("${server.port}")
        private String port;
     
        public String sale() {
            String retMessage = "";
            //1 查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //3 扣减库存
            if (inventoryNumber > 0) {
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t";
                System.out.println(retMessage);
            } else {
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
            return retMessage+"\t"+"服务端口号:"+port;
        }
    }
     
  • 新建 RedisDistributedLock 类并实现 JUC 里面的 Lock 接口

  • 满足 JUC 里面的 AQS 对 Lock 锁的接口规范定义来进行实现落地代码

  • 结合设计模式开发属于自己的 Redis 分布式锁工具类

    • Lua 脚本

      • 加锁 lock

        if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then 
          redis.call('hincrby',KEYS[1],ARGV[1],1) 
          redis.call('expire',KEYS[1],ARGV[2]) 
          return 1 
        else
          return 0
        end
      • 解锁 unlock

        if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then 
          return nil 
        elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then 
          return redis.call('del',KEYS[1]) 
        else 
          return 0
        end
    • 工厂设计模式引入

      • 通过实现 JUC 里面的 Lock 接口,实现 Redis 分布式锁 RedisDistributedLock

        RedisDistributedLock.java
        package dev.matrixlab.redislock.mylock;
         
        import cn.hutool.core.util.IdUtil;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.data.redis.core.StringRedisTemplate;
        import org.springframework.data.redis.core.script.DefaultRedisScript;
        import org.springframework.data.redis.support.collections.DefaultRedisList;
        import org.springframework.stereotype.Component;
         
        import java.util.Arrays;
        import java.util.concurrent.TimeUnit;
        import java.util.concurrent.locks.Condition;
        import java.util.concurrent.locks.Lock;
         
        //@Component 引入DistributedLockFactory工厂模式,从工厂获得而不再从spring拿到
        public class RedisDistributedLock implements Lock {
            private StringRedisTemplate stringRedisTemplate;
         
            private String lockName; //KEYS[1]
            private String uuidValue; //ARGV[1]
            private long expireTime; //ARGV[2]
            public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName) {
                this.stringRedisTemplate = stringRedisTemplate;
                this.lockName = lockName;
                this.uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId(); //UUID:ThreadID
                this.expireTime = 30L;
            }
            
            @Override
            public void lock() {
                tryLock();
            }
            
            @Override
            public boolean tryLock() {
                try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}
                return false;
            }
         
            /**
             * 实现加锁功能,实现这一个干活的就OK,全盘通用
             * @param time
             * @param unit
             * @return
             * @throws InterruptedException
             */
            @Override
            public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
                if (time != -1L) {
                    this.expireTime = unit.toSeconds(time);
                }
                String script =
                        "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
                                "redis.call('hincrby',KEYS[1],ARGV[1],1) " +
                                "redis.call('expire',KEYS[1],ARGV[2]) " +
                                "return 1 " +
                        "else " +
                                "return 0 " +
                        "end";
         
                System.out.println("script: "+script);
                System.out.println("lockName: "+lockName);
                System.out.println("uuidValue: "+uuidValue);
                System.out.println("expireTime: "+expireTime);
         
                while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
                    TimeUnit.MILLISECONDS.sleep(50);
                }
                return true;
            }
         
            /**
             *实现解锁功能
             */
            @Override
            public void unlock() {
                String script =
                        "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
                        "   return nil " +
                        "elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " +
                        "   return redis.call('del',KEYS[1]) " +
                        "else " +
                        "   return 0 " +
                        "end";
                // nil = false 1 = true 0 = false
                System.out.println("lockName: "+lockName);
                System.out.println("uuidValue: "+uuidValue);
                System.out.println("expireTime: "+expireTime);
                Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime));
                if(flag == null)
                {
                    throw new RuntimeException("This lock doesn't EXIST");
                }
         
            }
         
            //===下面的redis分布式锁暂时用不到=======================================
            @Override
            public void lockInterruptibly() throws InterruptedException {
         
            }
         
            @Override
            public Condition newCondition() {
                return null;
            }
        }
      • inventoryService 直接使用上面的代码设计,可能出现哪些问题?

        Lock redisDistributedLock = new RedisDistributedLock(stringRedisTemplate, "RedisLock");
         
        ...
         
        redisDistributedLock.lock(); 

        这样 redis 获得锁被写死了,假如以后有 Zookeeper,MySQL 做分布式锁,需要引入工厂模式

      • 引入工厂模式改造 7.1 版

        DistributedLockFactory.java
        package dev.matrixlab.redislock.mylock;
         
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.data.redis.core.StringRedisTemplate;
        import org.springframework.stereotype.Component;
         
        import java.util.concurrent.locks.Lock;
         
        @Component
        public class DistributedLockFactory {
            @Autowired
            private StringRedisTemplate stringRedisTemplate;
            private String lockName;
         
            public Lock getDistributedLock(String lockType) {
                if (lockType == null) return null;
         
                if (lockType.equalsIgnoreCase("REDIS")) {
                    lockName = "RedisLock";
                    return new RedisDistributedLock(stringRedisTemplate,lockName);
                } else if (lockType.equalsIgnoreCase("ZOOKEEPER")) {
                    //TODO zookeeper版本的分布式锁实现
                    return new ZookeeperDistributedLock();
                } else if (lockType.equalsIgnoreCase("MYSQL")) {
                    //TODO mysql版本的分布式锁实现
                    return null;
                }
                return null;
            }
        }
        RedisDistributedLock.java
        package dev.matrixlab.redislock.mylock;
         
        import cn.hutool.core.util.IdUtil;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.data.redis.core.StringRedisTemplate;
        import org.springframework.data.redis.core.script.DefaultRedisScript;
        import org.springframework.data.redis.support.collections.DefaultRedisList;
        import org.springframework.stereotype.Component;
         
        import java.util.Arrays;
        import java.util.concurrent.TimeUnit;
        import java.util.concurrent.locks.Condition;
        import java.util.concurrent.locks.Lock;
         
        //@Component 引入DistributedLockFactory工厂模式,从工厂获得而不再从spring拿到
        public class RedisDistributedLock implements Lock {
            private StringRedisTemplate stringRedisTemplate;
         
            private String lockName;//KEYS[1]
            private String uuidValue;//ARGV[1]
            private long   expireTime;//ARGV[2]
         
            public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName) {
                this.stringRedisTemplate = stringRedisTemplate;
                this.lockName = lockName;
                this.uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();//UUID:ThreadID
                this.expireTime = 30L;
            }
            
            @Override
            public void lock() {
                tryLock();
            }
         
            @Override
            public boolean tryLock() {
                try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}
                return false;
            }
         
            /**
             * 实现加锁功能,实现这一个干活的就OK,全盘通用
             * @param time
             * @param unit
             * @return
             * @throws InterruptedException
             */
            @Override
            public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
                if (time != -1L) {
                    this.expireTime = unit.toSeconds(time);
                }
                String script =
                        "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
                                "redis.call('hincrby',KEYS[1],ARGV[1],1) " +
                                "redis.call('expire',KEYS[1],ARGV[2]) " +
                                "return 1 " +
                        "else " +
                                "return 0 " +
                        "end";
                System.out.println("script: "+script);
                System.out.println("lockName: "+lockName);
                System.out.println("uuidValue: "+uuidValue);
                System.out.println("expireTime: "+expireTime);
                while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
                    TimeUnit.MILLISECONDS.sleep(50);
                }
                return true;
            }
         
            /**
             * 实现解锁功能
             */
            @Override
            public void unlock() {
                String script =
                        "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
                        "   return nil " +
                        "elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " +
                        "   return redis.call('del',KEYS[1]) " +
                        "else " +
                        "   return 0 " +
                        "end";
                // nil = false 1 = true 0 = false
                System.out.println("lockName: "+lockName);
                System.out.println("uuidValue: "+uuidValue);
                System.out.println("expireTime: "+expireTime);
                Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime));
                if(flag == null) {
                    throw new RuntimeException("This lock doesn't EXIST");
                }
            }
         
            //===下面的redis分布式锁暂时用不到=======================================
            @Override
            public void lockInterruptibly() throws InterruptedException {
         
            }
         
            @Override
            public Condition newCondition() {
                return null;
            }
        }
        InventoryService.java
        package dev.matrixlab.redislock.service;
         
        import ch.qos.logback.core.joran.conditional.ThenAction;
        import cn.hutool.core.util.IdUtil;
        import cn.hutool.core.util.StrUtil;
        import dev.matrixlab.redislock.mylock.DistributedLockFactory;
        import dev.matrixlab.redislock.mylock.RedisDistributedLock;
        import lombok.extern.slf4j.Slf4j;
        import org.omg.IOP.TAG_RMI_CUSTOM_MAX_STREAM_FORMAT;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.beans.factory.annotation.Value;
        import org.springframework.data.redis.core.RedisTemplate;
        import org.springframework.data.redis.core.StringRedisTemplate;
        import org.springframework.data.redis.core.script.DefaultRedisScript;
        import org.springframework.stereotype.Service;
         
        import java.util.Arrays;
        import java.util.concurrent.TimeUnit;
        import java.util.concurrent.atomic.AtomicInteger;
        import java.util.concurrent.locks.Lock;
        import java.util.concurrent.locks.ReentrantLock;
         
        @Service
        @Slf4j
        public class InventoryService {
            @Autowired
            private StringRedisTemplate stringRedisTemplate;
            @Value("${server.port}")
            private String port;
            @Autowired
            private DistributedLockFactory distributedLockFactory;
            
            public String sale() {
         
                String retMessage = "";
         
                Lock redisLock = distributedLockFactory.getDistributedLock("redis");
                redisLock.lock();
                try {
                    //1 查询库存信息
                    String result = stringRedisTemplate.opsForValue().get("inventory001");
                    //2 判断库存是否足够
                    Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
                    //3 扣减库存
                    if (inventoryNumber > 0) {
                        inventoryNumber = inventoryNumber - 1;
                        stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(inventoryNumber));
                        retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t服务端口:" +port;
                        System.out.println(retMessage);
                        return retMessage;
                    }
                    retMessage = "商品卖完了,o(╥﹏╥)o"+"\t服务端口:" +port;
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    redisLock.unlock();
                }
                return retMessage;
            }
        }
      • 单机 + 并发测试

可重入性测试

可重入测试
InventoryService 类新增可重入测试方法
InventoryService.java
package dev.matrixlab.redislock.service;
 
import dev.matrixlab.redislock.mylock.DistributedLockFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
 
import javax.annotation.Resource;
import java.util.concurrent.locks.Lock;
 
@Service
@Slf4j
public class InventoryService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;
    @Autowired
    private DistributedLockFactory distributedLockFactory;
 
    public String sale() {
        String retMessage = "";
        Lock redisLock = distributedLockFactory.getDistributedLock("redis");
        redisLock.lock();
        try {
            //1 查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //3 扣减库存
            if (inventoryNumber > 0) {
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t";
                System.out.println(retMessage);
                testReEnter();
            } else {
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            redisLock.unlock();
        }
        return retMessage+"\t"+"服务端口号:"+port;
    }
 
    private void testReEnter() {
        Lock redisLock = distributedLockFactory.getDistributedLock("redis");
        redisLock.lock();
        try {
            System.out.println("################测试可重入锁#######");
        } finally {
            redisLock.unlock();
        }
    }
}
 
 
/**
 //1 查询库存信息
 String result = stringRedisTemplate.opsForValue().get("inventory001");
 //2 判断库存是否足够
 Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
 //3 扣减库存
 if(inventoryNumber > 0) {
 stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
 retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t";
 System.out.println(retMessage);
 }else{
 retMessage = "商品卖完了,o(╥﹏╥)o";
 }
 */

出现 ThreadID 一致了,但是 UUID 不一致。

引入工厂模式改造 7.2 版
DistributedLockFactory.java
package com.atguigu.redislock.mylock;
 
import cn.hutool.core.util.IdUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
 
import java.util.concurrent.locks.Lock;
 
@Component
public class DistributedLockFactory {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    private String lockName;
    private String uuidValue;
 
    public DistributedLockFactory() {
        this.uuidValue = IdUtil.simpleUUID();//UUID
    }
 
    public Lock getDistributedLock(String lockType) {
        if (lockType == null) return null;
 
        if (lockType.equalsIgnoreCase("REDIS")) {
            lockName = "RedisLock";
            return new RedisDistributedLock(stringRedisTemplate,lockName,uuidValue);
        } else if(lockType.equalsIgnoreCase("ZOOKEEPER")) {
            //TODO zookeeper版本的分布式锁实现
            return new ZookeeperDistributedLock();
        } else if(lockType.equalsIgnoreCase("MYSQL")) {
            //TODO mysql版本的分布式锁实现
            return null;
        }
        return null;
    }
}
RedisDistributedLock.java
package dev.matrixlab.redislock.mylock;
 
import cn.hutool.core.util.IdUtil;
import lombok.SneakyThrows;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
 
import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
 
public class RedisDistributedLock implements Lock {
    private StringRedisTemplate stringRedisTemplate;
    private String lockName;
    private String uuidValue;
    private long   expireTime;
 
    public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName,String uuidValue) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.lockName = lockName;
        this.uuidValue = uuidValue+":"+Thread.currentThread().getId();
        this.expireTime = 30L;
    }
 
    @Override
    public void lock() {
        this.tryLock();
    }
 
    @Override
    public boolean tryLock() {
        try {
            return this.tryLock(-1L,TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return false;
    }
 
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        if(time != -1L) {
            expireTime = unit.toSeconds(time);
        }
 
        String script =
                "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
                    "redis.call('hincrby',KEYS[1],ARGV[1],1) " +
                    "redis.call('expire',KEYS[1],ARGV[2]) " +
                    "return 1 " +
                "else " +
                    "return 0 " +
                "end";
        System.out.println("lockName: "+lockName+"\t"+"uuidValue: "+uuidValue);
 
        while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime))) {
            try { TimeUnit.MILLISECONDS.sleep(60); } catch (InterruptedException e) { e.printStackTrace(); }
        }
 
        return true;
    }
 
    @Override
    public void unlock() {
        String script =
                "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
                    "return nil " +
                "elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " +
                    "return redis.call('del',KEYS[1]) " +
                "else " +
                        "return 0 " +
                "end";
        System.out.println("lockName: "+lockName+"\t"+"uuidValue: "+uuidValue);
        Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
        if(flag == null) {
            throw new RuntimeException("没有这个锁,HEXISTS查询无");
        }
    }
 
    //=========================================================
    @Override
    public void lockInterruptibly() throws InterruptedException {
 
    }
 
    @Override
    public Condition newCondition() {
        return null;
    }
}
InventoryService.java
package dev.matrixlab.redislock.service;
 
import cn.hutool.core.util.IdUtil;
import dev.matrixlab.redislock.mylock.DistributedLockFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
 
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
/* 
 * InventoryService 类新增可重入测试方法
 */
@Service
@Slf4j
public class InventoryService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;
    @Autowired
    private DistributedLockFactory distributedLockFactory;
 
    public String sale() {
        String retMessage = "";
        Lock redisLock = distributedLockFactory.getDistributedLock("redis");
        redisLock.lock();
 
        try {
            //1 查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //3 扣减库存
            if (inventoryNumber > 0) {
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
                System.out.println(retMessage);
                this.testReEnter();
            } else {
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            redisLock.unlock();
        }
        return retMessage+"\t"+"服务端口号:"+port;
    }
 
    private void testReEnter() {
        Lock redisLock = distributedLockFactory.getDistributedLock("redis");
        redisLock.lock();
 
        try {
            System.out.println("################测试可重入锁####################################");
        } finally {
            redisLock.unlock();
        }
    }
}
单机 + 并发 + 可重入性测试

0x08. 自动续期

问题

确保 redisLock 过期时间大于业务执行时间的问题,Redis 分布式锁如何续期?

CAP

  • Redis 集群是 AP

    redis 异步复制造成的锁丢失。比如,主节点没来得及把刚刚 set 进来这条数据给从节点,master 就挂了,从机上位但从机上无该数据。

  • Zookeeper 集群式 CP

    • CP

      Redis-ZooKeeper-CP-原理

      zk 是 CP 原理。假如 1 号机注册给 server1,server1 同步给 server2,server2 同步给各个 follower,为保证一致性,只有整个过程都成功了,1 号机才收到注册成功。

    • 故障

      Redis-ZooKeeper-CP-故障

      当 leader 重启或者网络故障下,整个 zk 集群会重新选举 master,选举期间 client 不可以注册,即 zk 不可用,所以牺牲了可用性 A。只有选举出新老大后,系统才恢复注册。故 zk 为零保证数据一致性牺牲了可靠性。由于在大型分布式系统中故障难以避免,leader 出故障可能性很高,所以很多大型系统都不会选择 zk。

  • Euraka 集群式 AP

    Redis-Eureka-AP-原理

    Eureka 是 AP 原理,保证高可用性而牺牲了一致性。client1 注册给 server1,server1 直接告诉 client 成功,client1 高兴的走了,剩下的就是 server1 再同步给 server2,为保证高可用,都是异步同步的。

  • Nacos 集群式 AP

    服务注册与发现框架CAP 模型控制台管理社区活跃度
    EurekaAP支持低(2.x 版本闭源)
    ZookeeperCP不支持
    ConsulCP支持
    NacosAP支持

Lua 脚本实现自动续期

if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then
  return redis.call('expire',KEYS[1],ARGV[2])
else
  return 0
end

新增自动续期功能

RedisDistributedLock.java
package dev.matrixlab.redislock.mylock;
 
import cn.hutool.core.util.IdUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.support.collections.DefaultRedisList;
import org.springframework.stereotype.Component;
 
import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
 
public class RedisDistributedLock implements Lock {
    private StringRedisTemplate stringRedisTemplate;
 
    private String lockName;//KEYS[1]
    private String uuidValue;//ARGV[1]
    private long   expireTime;//ARGV[2]
 
    public RedisDistributedLock(StringRedisTemplate stringRedisTemplate,String lockName,String uuidValue) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.lockName = lockName;
        this.uuidValue = uuidValue+":"+Thread.currentThread().getId();
        this.expireTime = 30L;
    }
 
    @Override
    public void lock() {
        tryLock();
    }
 
    @Override
    public boolean tryLock() {
        try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}
        return false;
    }
 
    /**
     * 实现加锁功能,实现这一个干活的就OK,全盘通用
     * @param time
     * @param unit
     * @return
     * @throws InterruptedException
     */
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        if(time != -1L) {
            this.expireTime = unit.toSeconds(time);
        }
 
        String script =
                "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
                        "redis.call('hincrby',KEYS[1],ARGV[1],1) " +
                        "redis.call('expire',KEYS[1],ARGV[2]) " +
                        "return 1 " +
                        "else " +
                        "return 0 " +
                        "end";
 
        System.out.println("script: "+script);
        System.out.println("lockName: "+lockName);
        System.out.println("uuidValue: "+uuidValue);
        System.out.println("expireTime: "+expireTime);
 
        while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
            TimeUnit.MILLISECONDS.sleep(50);
        }
        this.renewExpire();
        return true;
    }
 
    /**
     * 实现解锁功能
     */
    @Override
    public void unlock() {
        String script =
                "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
                        "   return nil " +
                        "elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " +
                        "   return redis.call('del',KEYS[1]) " +
                        "else " +
                        "   return 0 " +
                        "end";
        // nil = false 1 = true 0 = false
        System.out.println("lockName: "+lockName);
        System.out.println("uuidValue: "+uuidValue);
        System.out.println("expireTime: "+expireTime);
        Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime));
        if(flag == null) {
            throw new RuntimeException("This lock doesn't EXIST");
        }
    }
 
    private void renewExpire() {
        String script =
                "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then " +
                        "return redis.call('expire',KEYS[1],ARGV[2]) " +
                        "else " +
                        "return 0 " +
                        "end";
 
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                if (stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
                    renewExpire();
                }
            }
        },(this.expireTime * 1000)/3);
    }
 
    //===下面的redis分布式锁暂时用不到=======================================
    @Override
    public void lockInterruptibly() throws InterruptedException {
 
    }
 
    @Override
    public Condition newCondition() {
        return null;
    }
}
InventoryService.java
package dev.matrixlab.redislock.service;
 
import cn.hutool.core.util.IdUtil;
import dev.matrixlab.redislock.mylock.DistributedLockFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
 
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
@Service
@Slf4j
public class InventoryService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;
    @Autowired
    private DistributedLockFactory distributedLockFactory;
 
    public String sale() {
        String retMessage = "";
        Lock redisLock = distributedLockFactory.getDistributedLock("redis");
        redisLock.lock();
        try {
            //1 查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //3 扣减库存
            if (inventoryNumber > 0) {
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
                System.out.println(retMessage);
                //暂停几秒钟线程,为了测试自动续期
                try { TimeUnit.SECONDS.sleep(120); } catch (InterruptedException e) { e.printStackTrace(); }
            } else{
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            redisLock.unlock();
        }
        return retMessage+"\t"+"服务端口号:"+port;
    }
 
 
    private void testReEnter() {
        Lock redisLock = distributedLockFactory.getDistributedLock("redis");
        redisLock.lock();
        try {
            System.out.println("################测试可重入锁####################################");
        } finally {
            redisLock.unlock();
        }
    }
}

InventoryService

  • 记得去掉可重入测试 testReEnter()
  • InventoryService 业务逻辑里面故意 sleep 一段时间测试自动续期

总结

  • synchronized 单机版可以,但是在分布式系统中无法使用
  • nginx 分布式微服务单机锁不行
  • 取消单机锁,使用 redis 分布式锁 setnx
    • 只加锁,没有释放锁;如果出现异常,可能无法释放锁;必须在代码层面 finally 释放锁
    • 如果发生宕机,部署了微服务代码层面根本没有走到 finally 代码块,没办法保证解锁,这个 key 没有被删除,需要有 lockKey 的过期时间设定
    • 为 redis 的分布式锁 key 增加过期时间,并且必须保证 setnxexpire 的两条命令的原子性操作
      • 必须规定自己的锁只能自己删除,无法删除别人的锁
      • unlock 变为 Lua 脚本保证
      • 锁重入,hset 代替 setnxlock 变为 Lua 脚本保证
      • 锁的自动续期