Redis7之实现分布式锁(九)

2023-10-27

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 已持有锁的锁,违反安全规定
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Redis7之实现分布式锁(九) 的相关文章

  • SWIG 类型映射 uint8_t* 从 C/C++ 到 java.nio.ByteBuffer

    我正在尝试将输入和输出缓冲区从 C 传递给 java 类 出于效率原因 我需要使用 ByteBuffer 这两个缓冲区都是在 C 部分中分配的 我需要将它们传递给一个 java 函数 该函数将使用输入缓冲区进行一些计算并将结果写入输出缓冲区
  • 使用 Java-Large 文件查询 JSON 文件

    我正在尝试使用 java 解析下面的 JSON 文件 我需要能够 按 ID 或名称或对象中的任何字段搜索文件 也在字段中搜索空值 搜索应返回整个对象 该文件将会很大 并且搜索应该仍然很省时 id 1 name Mark Robb last
  • Java简单加密

    我想加密存储在磁盘上的文本 配置 文件 尝试使用DES http en wikipedia org wiki Data Encryption Standard加密 我在客户端计算机上遇到了致命错误 后来我发现该算法无法处理重音字符 我怀疑这
  • 如何更改 JComboBox 下拉列表的宽度?

    我有一个可编辑的JComboBox其中包含单个字母值的列表 因此 组合框非常小 每个字母都有特殊的含义 对于很少使用的字母 有时用户并不清楚 因此我创建了一个自定义ListCellRenderer显示下拉列表中每个字母的含义 不幸的是 这个
  • TestNG 启动期间发生内部错误

    我创建了一个 TestNG 类 FirstTest java 当我将测试用例作为 TestNG Test 运行时 出现以下错误 期间发生内部错误 启动 FirstTest java lang NullPointerException Ecl
  • Criteria eager fetch-joined 集合以避免 n+1 选择

    假设 Item 和 Bid 是实体 一个 Item 有多个 Bid 它们被映射到休眠在典型的父子关系中
  • 如何在 Android 中恢复我的音频?

    我必须实现用于创建具有暂停和恢复状态的音频的应用程序 当我的应用程序作为启动时音频启动 当我按下模拟器上的后退按钮时 音频音乐处于暂停状态 但是当我的活动回来时从停止状态到前台我的音频音乐未恢复 这是我的代码 public class Au
  • Spring boot 404错误自定义错误响应ReST

    我正在使用 Spring boot 来托管 REST API 即使浏览器正在访问 URL 以及自定义数据结构 我也希望始终发送 JSON 响应 而不是使用标准错误响应 我可以使用 ControllerAdvice 和 ExceptionHa
  • SimpleDateFormat 无法正确处理 DD

    我正在尝试获得这样的格式 2013 06 15 17 45 我在代码中执行以下操作 Date d new Date SimpleDateFormat ft new SimpleDateFormat YYYY MM DD HH mm Stri
  • Eclipse 内容协助无法在枚举常量参数列表中工作

    使用 eclipse 当我输入以下内容时 public enum Foo A Integer private final Integer integer private Foo Integer integer this integer in
  • Hibernate、MySQL 视图和 hibernate.hbm2ddl.auto = 验证

    我可以在 Hibernate 中使用 MySQL 视图 将它们视为表 即 该实体与为表创建的实体没有什么不同 但是 当 Hibernate 设置为验证模型时 我的应用程序将不会部署 因为它找不到视图 因为它假设它是一个表 是否可以在启用部署
  • Android 防火墙与 VpnService

    我正在尝试使用 BS 项目的 VpnService 为 Android 实现一个简单的防火墙 我选择 VpnService 因为它将在非 root 设备上运行 它将记录连接并让您过滤连接 基于IP 有一个应用程序可以做到这一点 因此这是可能
  • 当用户使用相同的凭据登录两次时如何使用户会话无效

    我正在使用带有 Richfaces 和 Facelets 的 JSF 1 2 我有一个应用程序 其中包含许多会话范围的 Bean 和一些应用程序 Bean 假设用户使用 Firefox 登录 创建一个会话 ID A 然后他打开 Chrome
  • Knuth-Morris-Pratt 算法

    解决方案是Knuth Morris Pratt 算法 https en wikipedia org wiki Knuth E2 80 93Morris E2 80 93Pratt algorithm 干草堆 AAAAAAAAA 针 AAA
  • Java中的运算符重载和覆盖

    运算符重载和运算符重写有什么区别 它们在继承和控制台程序中是否相同 Java 不支持运算符重载和重写 检查以下引用自的描述 http java sun com docs white langenv Simple doc2 html http
  • 酷还是傻? Catch(异常[NamingException, CreateException] e)

    我正在编写一些代码 我注意到异常处理中的一种模式让我思考 try do stuff throws JMS Create and NamingException catch NamingException e log1 e rollback
  • 在 JSF 自定义验证器中区分 ajax 请求和完整请求

    我的验证器需要知道它是完整请求还是 ajax 请求 在我当前的解决方案中 我检查 http 请求标头X Requested With元素 public void validate FacesContext context UICompone
  • Java Timer 类:如果其中一个任务抛出异常,则计时器任务停止执行

    new Timer scheduleAtFixedRate new TimerTask Override public void run System out println run throw new SomeRandomExceptio
  • 为什么 Cassandra 客户端在生产中没有 epoll 时会失败? [复制]

    这个问题在这里已经有答案了 当我在本地运行服务时 我收到一条警告 指出 epoll 不可用 因此它使用 NIO 很公平 当我将其部署到 Kubernetes 中时 我得到了以下信息 这导致服务无法运行 2017 03 29T19 09 22
  • 为什么我的 Java 路径中添加了“L”?

    我在我的类路径中加载了一个 jar 在 iReport 中 如果重要的话 我确信它具有所需的方法 但是当我尝试测试连接 从而调用该 jar 时 我得到一个 java lang NoSuchMethodError 说它正在引用班上 Lorg

随机推荐

  • MySql取消密码强度验证

    一 修改MySql配置文件 my cnf 一般情况下 MySql的配置文件 my cnf 会在 etc 目录下 如果没有 可以使用以下命令查找位置 sudo find name my cnf 编辑配置文件 sudo vi etc my cn
  • 不要迷恋我,虽然我利用Python来耍植物大战僵尸

    目录 前言 游戏的安装 思路 一句话总结 大概的思路 实战 确定修改哪一款游戏的数据 代码 地址的寻找 视频教程 修改数据 代码 效果 完整的源码 前言 大家好 我叫善念 上篇文章我许了一个愿 就是想让大家多多关注我 然后我的粉丝就蹭蹭的涨
  • python PIL open 无法打开 webp,jpeg等图像 ,报错 PIL.UnidentifiedImageError: cannot identify image file

    问题 使用 PIL Image open 能正常打开大部分图像文件 但是webp格式的图像无法打开 有一些jpg png图像也无法打开 报错 PIL UnidentifiedImageError cannot identify image
  • 简析:阿里巴巴最近20个月的拆分史

    author skate time 2013 02 20 在不到20个月的时间里 阿里巴巴集团接连发生几轮分拆 还伴以将云OS剥离出阿里云这样的突发动作 在大公司界 无论是传统公司还是互联网公司 这种情况是罕见的 一般公司 经不起这样频繁的
  • c++基础(华清远见学习之星)

    最近刚接触c 与c还是有很多不同的 如果有其写c的操作 头文件用
  • Linux查看CPU支持的指令集

    gcc march native Q help target grep march 或者 cat proc cpuinfo Intel的CPU 可以去官网查看能支持什么指令集 https ark intel com content www
  • STM32F103基于W5500实现Modbus简单TCP通信

    文章目录 一 Modbus TCP协议 1 查询报文 2 响应报文 二 从机代码 1 初始化从机网络 2 简单响应函数 3 main函数循环等待连接 三 效果 四 总结 五 源码 一 Modbus TCP协议 功能码 作用 01 读取线圈状
  • 正则表达式和通配符的区别

    http www eetop cn blog html 65 554165 26125 html http blog csdn net whxlovehy article details 6052366 Explain 1 1 正则表达式是
  • Python Selenium/WebDriver 操作手册新版

    写在前面 本文为个人整理手册 有错误的地方欢迎指正 参考链接较多 重点参考 侵权删 什么是Selenium 通俗的解释 引用 Selenium是一个Web的自动化测试工具 最初是为网站自动化测试而开发的 类型像我们玩游戏用的按键精灵 可以按
  • Hadoop集群搭建记录

    本文目录 写在前面 写在前面 本系列文章索引以及一些默认好的条件在 传送门 因为课程需要 我们要进行Eclipse的安装操作 eclipse需要是CentOS下的 网址在 传送门 在出现的页面中 根据自身的机型选择合适的类型 博主为x86
  • 基础三 * 下 【vim 编辑器】 【管道】【文件内容浏览命令】

    目录 vim 编辑器 管道 文件内容浏览命令 练习 vim 编辑器 前言须知 1 vim 是个啥 其实 vim 类似于 windows 上的记事本 能够编辑 保存 复制 粘贴 搜索 替换等等
  • AI开放平台能力集合

    背景 随着AI技术的兴起及其逐步在各业务领域落地 越来越多的公司将其业务中使用到的底层AI能力开放出来 通过付费的模式提供给不具备建立AI能力的公司使用 AI技术包含非常多不同的方向 如文档识别 人脸人体识别 NLP语义分析以及大数据挖掘等
  • Linaro 作为白金会员加盟 Zephyr 项目

    转载自 https www zephyrproject org linaro joins zephyrtm project platinum member 作者 Zephyr 本文地址 https linux cn article 7817
  • Mybatis-Plus:实现自定义SQL

    目录 1 简介 2 自定义SQL具体实现 2 1 注解SQL 2 2 Wrapper传参 注解SQL 2 3 Wrapper传参 xml文件SQL 2 4 正常传参 XML文件SQL 3 总结 1 简介 Mybatis Plus 以下简称M
  • mybaties-plus 代码成器使用笔记

    1 简介 MyBatis Plus Generator 可以生成 Controller Service Mapper Entity 也支持自写 SQL 的 mapper 步骤 1 数据库中创建相应表 2 引入maven依赖 freemark
  • 用gdb调试core dump文件

    尊重原创 http blog chinaunix net u2 83905 showart 2134570 html 在Unix系统下 应用程序崩溃 一般会产生core文件 如何根据core文件查找问题的所在 并做相应的分析和调试 是非常重
  • php获取当前文件夹下所有图片大小,PHP获取文件夹大小函数用法实例

    本文实例讲述了PHP获取文件夹大小函数用法 分享给大家供大家参考 具体如下 获取文件夹大小 function getDirSize dir handle opendir dir while false FolderOrFile readdi
  • layui layer弹出层通过offset属性定位弹出层在光标处弹出(event.clientY和event.clientX)失败。

    将弹出层弹出位置定位到光标处 大小超过父弹出层的部分无法显示 js 页面层 自定义 more click function event layer open id moreMenu type 1 title false closeBtn 0
  • BACnet协议栈apdu_set_confirmed_handler函数中的确认型回调函数是如何传参的

    BACnet协议栈中的确认型回调函数通常会被传入三个参数 BACNET ADDRESS src uint8 t apdu和uint16 t apdu len BACNET ADDRESS src参数表示请求的源地址 它是一个指向BACNET
  • Redis7之实现分布式锁(九)

    9 1 分布式锁需要的条件和刚需 独占性 任何时刻有且只有一个线程持有这个锁 高可用 若redis集群环境下 不能因为某一个节点挂了而出现获取锁和释放锁失败的情况 高并发请求下 依旧性能很好 防死锁 不能出现死锁问题 必须有超时重试机制或者