Redis7之实现分布式锁(九)

9.1 分布式锁需要的条件和刚需

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

9.2 编码

1 搭建环境

  • 创建工程 redis_distributed_lock2

  • POM

    <?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    	<modelVersion>4.0.0</modelVersion>
    	<parent>
    		<groupId>org.springframework.boot</groupId>
    		<artifactId>spring-boot-starter-parent</artifactId>
    		<version>2.7.10</version>
    		<relativePath/> <!-- lookup parent from repository -->
    	</parent>
    	<groupId>com.xfcy</groupId>
    	<artifactId>redis_distributed_lock2</artifactId>
    	<version>0.0.1-SNAPSHOT</version>
    	<name>redis_distributed_lock2</name>
    	<description>redis_distributed_lock2</description>
    	<properties>
    		<java.version>1.8</java.version>
    		<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>
    <!--		redisson-->
    		<dependency>
    			<groupId>org.redisson</groupId>
    			<artifactId>redisson</artifactId>
    			<version>3.13.4</version>
    		</dependency>
    
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-web</artifactId>
    		</dependency>
    
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-test</artifactId>
    			<scope>test</scope>
    		</dependency>
    
    		<!--SpringBootRedis整合依赖-->
    		<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>
    		<!--通用基础配置lombok/hutool-->
    		<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>
    
  • YML

    server.port=7777
    
    spring.application.name=redis_distributed_lock2
    # ========================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.238.111
    spring.redis.port=6379
    spring.redis.password=123456
    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
    
  • 主启动类

    @SpringBootApplication
    public class RedisDistributedLock2Application {
    
    	public static void main(String[] args) {
    		SpringApplication.run(RedisDistributedLock2Application.class, args);
    	}
    }
    
  • 业务类

    • Swagger2Config

      import org.springframework.beans.factory.annotation.Value;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      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.swagger2.annotations.EnableSwagger2;
      
      import java.time.LocalDateTime;
      import java.time.format.DateTimeFormatter;
      
      /**
       * @author 晓风残月Lx
       * @date 2023/4/1 10:25
       */
      @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("com.xfcy"))
                      .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

      import org.redisson.Redisson;
      import org.redisson.config.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;
      
      /**
       * @author 晓风残月Lx
       * @date 2023/4/1 10:31
       */
      @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

      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;
      
      /**
       * @author 晓风残月Lx
       * @date 2023/4/1 10:34
       */
      @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;
          }
      }
      
    • InvetoryController

      import com.xfcy.service.InventoryService;
      import io.swagger.annotations.Api;
      import io.swagger.annotations.ApiOperation;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.web.bind.annotation.GetMapping;
      import org.springframework.web.bind.annotation.RestController;
      
      /**
       * @author 晓风残月Lx
       * @date 2023/4/1 10:32
       */
      @RestController
      @Api(tags = "redis分布式锁测试")
      public class InvetoryController {
      
          @Autowired
          private InventoryService inventoryService;
      
          @ApiOperation("扣减库存sale,一次卖一个")
          @GetMapping(value = "/inventory/sale")
          public String sale()
          {
              return inventoryService.sale();
          }
      
      
          @ApiOperation("扣减库存saleByRedisson,一次卖一个")
          @GetMapping(value = "/inventory/saleByRedisson")
          public String saleByRedisson()
          {
              return inventoryService.saleByRedisson();
          }
      }
      

2 分布式锁

v2.0 - v6.0

InventoryService 实现的一个简单版本的 分布式锁

import cn.hutool.core.util.IdUtil;
import com.xfcy.mylock.DistributedLockFactory;
import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.api.RLock;
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;

/**
 * @author 晓风残月Lx
 * @date 2023/4/1 10:34
 */
@Service
@Slf4j
public class InventoryService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;

    /**
     *  v6.0       使用 Lua 脚本   将 final的判断 + del 弄成原子操作
     *                  问题: 兼顾锁的可重入性
     *             但是基本 v6.0 版本已经够用
     * @return
     */
    public String sale() {
        String retMessage = "";
        String key = "xfcyRedisLock";
        String value = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
        while (!stringRedisTemplate.opsForValue().setIfAbsent(key, value, 30L, TimeUnit.SECONDS)) {
            // 暂停20毫秒,进行递归重试
            try {
                Thread.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 {
            // 改进点,修改为Lua脚本的Redis分布式锁调用,必须保证原子性
            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), value);
        }
        return retMessage + "\t" + "服务端口号:" + port;
    }



    /**
     * v5.0        存在问题: final的判断 + del 不是一行原子操作,需要lua脚本进行修改
     * @return
     */
//    public String sale() {
//        String retMessage = "";
//        String key = "xfcyRedisLock";
//        String value = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
//        while (!stringRedisTemplate.opsForValue().setIfAbsent(key, value, 30L, TimeUnit.SECONDS)) {
//            // 暂停20毫秒,进行递归重试
//            try {
//                Thread.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 {
//            // 改进点,只能删除自己的key,而不是别的客户端
//            // 问题:不能保证原子操作
//            if (stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(value)) {
//                stringRedisTemplate.delete(key);
//            }
//        }
//        return retMessage + "\t" + "服务端口号:" + port;
//    }


    /**
     *  v4.0      加了过期时间 stringRedisTemplate.opsForValue().setIfAbsent(key, value, 30L, TimeUnit.SECONDS)
     *              问题:误删锁,如果线程A运行时间超出了过期时间
     *                      在线程A运行时,xfcyRedisLock这个key过期,另一个线程B进来加了key
     *                      线程A结束后,把线程B的锁删了
     *           stringRedisTemplate.delete(key);  只能自己删除自己的,需要添加判断是否是自己的锁
     * @return
     */
//    public String sale() {
//        String retMessage = "";
//        String key = "xfcyRedisLock";
//        String value = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
//
//        // 不用递归了,高并发下容易出错,我们用自旋替代递归方法调用;也不用if,用while来替代
//        while (!stringRedisTemplate.opsForValue().setIfAbsent(key, value, 30L, TimeUnit.SECONDS)) {
//            // 暂停20毫秒,进行递归重试
//            try {
//                Thread.sleep(20);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//        }
//        // 无法保证原子性
        stringRedisTemplate.expire(key, 30L, 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;
//    }


    /**
     * 3.2版  setnx  用while判断
     *          问题: setnx过后,正在进行业务逻辑操作时,没有走到finally之前,整个微服务down机了,导致锁一直存在
     *                      不是程序出了问题,如果程序问题,最后还是会执行finally
     *                没办法保证解锁(没有过期时间,该key一直存在),需要加入过期时间限定key
     */
//    public String sale() {
//        String retMessage = "";
//        String key = "xfcyRedisLock";
//        String value = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
//
//        // 不用递归了,高并发下容易出错,我们用自旋替代递归方法调用;也不用if,用while来替代
//        while (!stringRedisTemplate.opsForValue().setIfAbsent(key, value)) {
//            // 暂停20毫秒,进行递归重试
//            try {
//                Thread.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;
//    }


    /**
     * 3.1版   setnx 递归调用 容易导致 StackOverflowError
     *          高并发唤醒后推荐用while判断而不是if
     * @return
     */
//    public String sale() {
//        String retMessage = "";
//        String key = "xfcyRedisLock";
//        String value = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
//
//        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, value);
//        // flag = false 抢不到的线程要继续重试 。。。
//        if (!flag) {
//            // 暂停20毫秒,进行递归重试
//            try {
//                Thread.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;
//    }


/**
 * v2.0 单机版加锁配合nginx和jmeter压测后,不满足高并发分布式锁的性能要求,出现超卖
 */
//    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;
//    }
    

}

v7.0 - v8.0

v 8.0 其实面对不是特别高的并发场景足够用了,单机redis也够用了

  • 要兼顾锁的重入性
    • setnx不满足了,需要hash结构的hset
  • 上锁和解锁都用 Lua 脚本来实现原子性
  • 引入工厂模式 DistributedLockFactory, 实现 Lock 接口,实现redis的可重入锁
    • lock() 加锁的关键逻辑
      • 加锁 实际上就是在redis中,给Key键设置一个值,为避免死锁,并给定一个过期时间
      • 自旋
      • 续期
    • unlock() 解锁关键逻辑
      • 将 Key 键删除,但是也不能乱删,只能自己删自己的锁
  • 实现自动续期功能的完善,后台自定义扫描程序,如果规定时间内没有完成业务逻辑,会调用加钟自动续期的脚本

InventoryService

    @Autowired
    private DistributedLockFactory distributedLockFactory;


    /**
     * v8.0  实现自动续期功能的完善,后台自定义扫描程序,如果规定时间内没有完成业务逻辑,会调用加钟自动续期的脚本
     *
     * @return
     */
    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;

                // 演示自动续期的的功能
//                try {
//                    TimeUnit.SECONDS.sleep(120);
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
            } else {
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        } finally {
            redisLock.unlock();
        }
        return retMessage + "\t" + "服务端口号:" + port;
    }


    /**
     * v7.0     兼顾锁的可重入性   setnx不满足了,需要hash结构的hset
     * 上锁和解锁都用 Lua 脚本实现原子性
     * 引入工厂模式 DistributedLockFactory    实现Lock接口 ,实现 redis的可重入锁
     *
     * @return
     */
//    //private Lock redisDistributedLock = new RedisDistributedLock(stringRedisTemplate, "xfcyRedisLock");
//
//    public String sale() {
//        String retMessage = "";
//
//        Lock redisLock = distributedLockFactory.getDistributedLock("redis");
//        redisLock.lock();
//
//        //redisDistributedLock.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);
//
//                // 测试可重入性
//                //testReEntry();
//
//            } else {
//                retMessage = "商品卖完了,o(╥﹏╥)o";
//            }
//        } finally {
//            redisLock.unlock();
//            //redisDistributedLock.unlock();
//        }
//        return retMessage + "\t" + "服务端口号:" + port;
//    }
//
//    private void testReEntry() {
//        Lock redisLock = distributedLockFactory.getDistributedLock("redis");
//        redisLock.lock();
//
//        //redisDistributedLock.lock();
//        try {
//            System.out.println("测试可重入锁");
//        } finally {
//            redisLock.unlock();
//            //redisDistributedLock.unlock();
//        }
//    }

mylock/DistributedLockFactory

package com.xfcy.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;

/**
 * @author 晓风残月Lx
 * @date 2023/4/1 22:14
 */
@Component
public class DistributedLockFactory {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    private String lockName;
    private String uuidValue;

    public DistributedLockFactory() {
        this.uuidValue = IdUtil.simpleUUID();
    }

    public Lock getDistributedLock(String lockType) {
        if (lockType == null) {
            return null;
        }
        if (lockType.equalsIgnoreCase("REDIS")) {
            this.lockName = "xfcyRedisLock";
            return new RedisDistributedLock(stringRedisTemplate, lockName, uuidValue);
        }else if (lockType.equalsIgnoreCase("ZOOKEEPER")) {
            this.lockName = "xfcyZookeeperLock";
            // TODO zoookeeper 版本的分布式锁
            return null;
        }else if (lockType.equalsIgnoreCase("MYSQL")){
            this.lockName = "xfcyMysqlLock";
            // TODO MYSQL 版本的分布式锁
            return null;
        }
        return null;
    }

}

mylock/RedisDistributedLock

package com.xfcy.mylock;

import cn.hutool.core.util.IdUtil;
import com.sun.org.apache.xpath.internal.operations.Bool;
import org.springframework.beans.factory.annotation.Autowired;
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;

/**
 * @author 晓风残月Lx
 * @date 2023/4/1 21:38
 * 自研的redis分布式锁,实现 Lock 接口
 */
// @Component 引入DistributedLockFactory工厂模式,从工厂获得即可
public class RedisDistributedLock implements Lock {

    @Autowired
    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;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        if (time == -1L) {
            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))) {
                // 暂停 60ms
                Thread.sleep(60);
            }
            // 新建一个后台扫描程序,来监视key目前的ttl,是否到我们规定的 1/2 1/3 来实现续期
            resetExpire();
            return true;
        }
        return false;
    }

    @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
        Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
        if (null == flag) {
            throw new RuntimeException("this lock doesn't exists0");
        }
    }

    private void resetExpire() {
        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))) {
                    resetExpire();
                }
            }
        }, (this.expireTime * 1000) / 3);
    }


    // 下面两个用不上
    // 下面两个用不上
    // 下面两个用不上

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

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

9.3 缺点

如果我们的分布式锁的服务器挂了,也就是单点故障,我们添加一个从机,但是复制是异步的。

  • Client A 获取 master 中的锁
  • 在对密钥的写入传输到从机之前,主服务器崩溃了
  • 从机被提升为主机
  • Client B 获取对统一资源 A 已持有锁的锁,违反安全规定

你可能感兴趣的:(redis,分布式,redis,java)