文章

Redis分布式锁与淘汰机制

Redis分布式锁与淘汰机制

什么是分布式锁

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

案例

  • 新建两个model:boot_redis01,boot_redis02

  • pom

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    
    org.springframework.boot
    spring-boot-starter-web
              
         
    org.springframework.boot
    spring-boot-starter-actuator
              
    org.springframework.boot
    spring-boot-starter-data-redis
              
    org.apache.commons
    commons-pool2
              
    redis.clients
    jedis
    3.1.0
              
    org.springframework.boot
    spring-boot-starter-aop
              
    org.redisson
    redisson
    3.13.4
              
    org.springframework.boot
    spring-boot-devtools
    runtime
    true
              
    org.projectlombok
    lombok
    true
              
    junit
    junit
    4.12
    
  • yml

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    server.port=1111
      
    spring.redis.database=0
    spring.redis.host=
    spring.redis.port=6379
    #连接池最大连接数(使用负值表示没有限制)默认8
    spring.redis.lettuce.pool.max-active=8
    #连接池最大阻塞等待时间(使用负值表示没有限制)默认-1
    spring.redis.lettuce.pool.max-wait=-1
    #连接池中的最大空闲连接默认8
    spring.redis.lettuce.pool.max-idle=8
    #连接池中的最小空闲连接默认0
    spring.redis.lettuce.pool.min-idle=0
    
  • 启动类

  • 1
    2
    3
    4
    5
    6
    
    @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
    public class BootRedis01Application {
    	public static void main(String[] args) {
            SpringApplication.run(BootRedis01Application.class);
    	}
    }
    
  • 配置类

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    @Configuration
    public class RedisConfig {
      
    	/**
     	* 保证不是序列化后的乱码配置
    	*/
    	@Bean
    	public RedisTemplate, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory)	{
            RedisTemplate, Serializable> redisTemplate = new RedisTemplate();
    		redisTemplate.setKeySerializer(new StringRedisSerializer());
    		redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
    		redisTemplate.setConnectionFactory(connectionFactory);
            return redisTemplate;
    	}
    }
    
  • 业务类

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    
    @RestController
    public class GoodController {
      
    	@Autowired
    	private StringRedisTemplate stringRedisTemplate;
      
    	@Value("${server.port}")
    	private String serverPort;
      
    	@GetMapping("/buy_goods")
    	public String buy_Goods(){
      
            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);
      
            if (goodsNumber > 0){
    		int realNumber = goodsNumber - 1;
    		stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
    		System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: 		"+serverPort);
                return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort;
    		}else {
                System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: 				"+serverPort);
    		}
    		return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort;
    	}
      
    }
    
  • 测试

  • img

单机版没加锁

  • 问题:没有加锁,并发下数字不对,出现超卖现象

  • 思考

    • 加synchronized

    • 加ReentrantLock

    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      
      @RestController
      public class GoodController {
          
          @Autowired
          private StringRedisTemplate stringRedisTemplate;
          
          @Value("${server.port}")
          private String serverPort;
          
          private final Lock lock = new ReentrantLock();
          
          @GetMapping("/buy_goods")
          public String buy_Goods() {
          
              if (lock.tryLock()) {
                  try {
                      String result = stringRedisTemplate.opsForValue().get("goods:001");
                      int goodsNumber = result == null ? 0 : Integer.parseInt(result);
                      if (goodsNumber > 0) {
                          int realNumber = goodsNumber - 1;
                          stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
                          System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
                          return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
                      }
                  } finally {
                      lock.unlock();
                  }
              } else {
                  System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
              }
              return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
          }
          
      }
      
  • 修改后版本

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    
    @RestController
    public class GoodController {
      
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
      
        @Value("${server.port}")
        private String serverPort;
      
        @GetMapping("/buy_goods")
        public String buy_Goods() {
            synchronized(this) {
                String result = stringRedisTemplate.opsForValue().get("goods:001");
                int goodsNumber = result == null ? 0 : Integer.parseInt(result);
      
                if (goodsNumber > 0) {
                    int realNumber = goodsNumber - 1;
                    stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
                    System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
                    return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
                } else {
                    System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
                }
                return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
            }
        }
    }
    
  • 在单机环境下,可以使用synchronized或Lock来实现。但是在分布式系统中,因为竞争的线程可能不在同一个节点上(同一个jvm中),所以需要一个让所有进程都能访问到的锁来实现,比如redis或者zookeeper来构建;不同进程jvm层面的锁就不管用了,那么可以利用第三方的一个组件,来获取锁,未获取到锁,则阻塞当前想要运行的线程

nginx分布式微服务架构

  • 分布式部署后,单机锁还是出现超卖现象,需要分布式锁

  • Redis具有极高的性能,且其命令对分布式锁支持友好,借助SET命令即可实现加锁处理.

  • 加入nginx,实现负载均衡

  • image-20210830223957359

  • 修改后版本

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    
    @RestController
    public class GoodController {
        public static final String REDIS_LOCK_KEY = "lockhhf";
      
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
      
        @Value("${server.port}")
        private String serverPort;
      
        @GetMapping("/buy_goods")
        public String buy_Goods() {
      
            String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
            //setIfAbsent() 就是如果不存在就新建
            Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value); //setnx
      
            if (!lockFlag) {
                return "抢锁失败,┭┮﹏┭┮";
            } else {
                String result = stringRedisTemplate.opsForValue().get("goods:001");
                int goodsNumber = result == null ? 0 : Integer.parseInt(result);
      
                if (goodsNumber > 0) {
                    int realNumber = goodsNumber - 1;
                    stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
                    System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
                    stringRedisTemplate.delete(REDIS_LOCK_KEY); //释放锁
                    return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
                } else {
                    System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
                }
                return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
            }
        }
    }
    

异常无法释放锁

  • 出异常的话,可能无法释放锁, 必须要在代码层面finally释放锁

  • 加锁解锁,lock/unlock必须同时出现并保证调用

  • 修改后版本

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    
    @RestController
    public class GoodController {
      
        public static final String REDIS_LOCK_KEY = "lockhhf";
      
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
      
        @Value("${server.port}")
        private String serverPort;
      
        @GetMapping("/buy_goods")
        public String buy_Goods() {
      
            String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
            try {
                //setIfAbsent() 就是如果不存在就新建
                Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value); //setnx
      
                if (!lockFlag) {
                    return "抢锁失败,┭┮﹏┭┮";
                } else {
                    String result = stringRedisTemplate.opsForValue().get("goods:001");
                    int goodsNumber = result == null ? 0 : Integer.parseInt(result);
      
                    if (goodsNumber > 0) {
                        int realNumber = goodsNumber - 1;
                        stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
                        System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
      
                        return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
                    } else {
                        System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
                    }
                    return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
                }
            } finally {
                stringRedisTemplate.delete(REDIS_LOCK_KEY); //释放锁
            }
      
        }
    }
    

微服务机器宕机了

  • 部署了微服务jar包的机器挂了,代码层面根本没有走到finally这块, 没办法保证解锁,这个key没有被删除,需要加入一个过期时间限定key

  • 需要对lockKey有过期时间的设定

  • 修改后版本

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    
    @RestController
    public class GoodController {
      
        public static final String REDIS_LOCK_KEY = "lockhhf";
      
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
      
        @Value("${server.port}")
        private String serverPort;
      
        @GetMapping("/buy_goods")
        public String buy_Goods() {
      
            String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
            try {
                //setIfAbsent() 就是如果不存在就新建
                Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value); //setnx
                stringRedisTemplate.expire(REDIS_LOCK_KEY, 10 L, TimeUnit.SECONDS);
                if (!lockFlag) {
                    return "抢锁失败,┭┮﹏┭┮";
                } else {
                    String result = stringRedisTemplate.opsForValue().get("goods:001");
                    int goodsNumber = result == null ? 0 : Integer.parseInt(result);
      
                    if (goodsNumber > 0) {
                        int realNumber = goodsNumber - 1;
                        stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
                        System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
      
                        return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
                    } else {
                        System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
                    }
                    return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
                }
      
            } finally {
                stringRedisTemplate.delete(REDIS_LOCK_KEY); //释放锁
            }
      
        }
    }
    

原子性操作

  • 设置key+过期时间分开了,必须要合并成一行具备原子性

  • 修改后版本

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    
    @RestController
    public class GoodController {
        public static final String REDIS_LOCK_KEY = "lockhhf";
      
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
      
        @Value("${server.port}")
        private String serverPort;
      
        @GetMapping("/buy_goods")
        public String buy_Goods() {
      
            String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
            try {
                //setIfAbsent() == setnx 就是如果不存在就新建,同时加上过期时间保证原子性
                Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value, 10 L, TimeUnit.SECONDS);
      
                if (!lockFlag) {
                    return "抢锁失败,┭┮﹏┭┮";
                } else {
                    String result = stringRedisTemplate.opsForValue().get("goods:001");
                    int goodsNumber = result == null ? 0 : Integer.parseInt(result);
      
                    if (goodsNumber > 0) {
                        int realNumber = goodsNumber - 1;
                        stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
                        System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
      
                        return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
                    } else {
                        System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
                    }
                    return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
                }
            } finally {
                stringRedisTemplate.delete(REDIS_LOCK_KEY); //释放锁
            }
        }
    }
    

删除了别人的锁

  • 张冠李戴,删除了别人的锁

  • img

  • 只能自己删除自己的,不许动别人的

  • 修改后版本

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    
    @RestController
    public class GoodController {
        public static final String REDIS_LOCK_KEY = "lockhhf";
      
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
      
        @Value("${server.port}")
        private String serverPort;
      
        @GetMapping("/buy_goods")
        public String buy_Goods() {
      
            String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
            try {
                //setIfAbsent() == setnx 就是如果不存在就新建,同时加上过期时间保证原子性
                Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value, 10 L, TimeUnit.SECONDS);
                  
                if (!lockFlag) {
                    return "抢锁失败,┭┮﹏┭┮";
                } else {
                    String result = stringRedisTemplate.opsForValue().get("goods:001");
                    int goodsNumber = result == null ? 0 : Integer.parseInt(result);
      
                    if (goodsNumber > 0) {
                        int realNumber = goodsNumber - 1;
                        stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
                        System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
      
                        return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
                    } else {
                        System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
                    }
                    return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
                }
      
            } finally {
                if (value.equalsIgnoreCase(stringRedisTemplate.opsForValue().get(REDIS_LOCK_KEY))) {
                    stringRedisTemplate.delete(REDIS_LOCK_KEY); //释放锁
                }
            }
      
        }
    }
    

判断条件与删除动作的原子性

  • finally块的判断+del删除操作不是原子性的
redis事务
  • 用redis自身的事务

  • img

  • 修改后版本

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    
    @RestController
    public class GoodController {
        public static final String REDIS_LOCK_KEY = "lockhhf";
      
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
      
        @Value("${server.port}")
        private String serverPort;
      
        @GetMapping("/buy_goods")
        public String buy_Goods() {
      
            String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
      
            try {
                //setIfAbsent() == setnx 就是如果不存在就新建,同时加上过期时间保证原子性
                Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value, 10 L, TimeUnit.SECONDS);
                  
                if (!lockFlag) {
                    return "抢锁失败,┭┮﹏┭┮";
                } else {
                    String result = stringRedisTemplate.opsForValue().get("goods:001");
                    int goodsNumber = result == null ? 0 : Integer.parseInt(result);
      
                    if (goodsNumber > 0) {
                        int realNumber = goodsNumber - 1;
                        stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
                        System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
                        return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
                    } else {
                        System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
                    }
                    return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
                }
            } finally {
                while (true) {
                    stringRedisTemplate.watch(REDIS_LOCK_KEY); //加事务,乐观锁
                    if (value.equalsIgnoreCase(stringRedisTemplate.opsForValue().get(REDIS_LOCK_KEY))) {
                        stringRedisTemplate.setEnableTransactionSupport(true);
                        stringRedisTemplate.multi(); //开始事务
                        stringRedisTemplate.delete(REDIS_LOCK_KEY);
                        List list = stringRedisTemplate.exec();
                        if (list == null) { //如果等于null,就是没有删掉,删除失败,再回去while循环那再重新执行删除
                            continue;
                        }
                    }
                    //如果删除成功,释放监控器,并且breank跳出当前循环
                    stringRedisTemplate.unwatch();
                    break;
                }
            }
      
        }
    }
    
Lua脚本
  • Redis可以通过eval命令保证代码执行的原子性

  • 工具类

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    public class RedisUtils {
      
        private static JedisPool jedisPool;
      
        static {
            JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
            jedisPoolConfig.setMaxTotal(20);
            jedisPoolConfig.setMaxIdle(10);
      
            jedisPool = new JedisPool(jedisPoolConfig, "ip", 6379, 100000);
        }
      
        public static Jedis getJedis() throws Exception {
            if (null != jedisPool) {
                return jedisPool.getResource();
            }
            throw new Exception("Jedispool is not ok");
        }
    }
    
  • 修改后版本

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    
    @RestController
    public class GoodController {
        public static final String REDIS_LOCK_KEY = "lockhhf";
      
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
      
        @Value("${server.port}")
        private String serverPort;
      
        @GetMapping("/buy_goods")
        public String buy_Goods() throws Exception {
      
            String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
      
            try {
                //setIfAbsent() == setnx 就是如果不存在就新建,同时加上过期时间保证原子性
                Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value, 10 L, TimeUnit.SECONDS);
                  
                if (!lockFlag) {
                    return "抢锁失败,┭┮﹏┭┮";
                } else {
                    String result = stringRedisTemplate.opsForValue().get("goods:001");
                    int goodsNumber = result == null ? 0 : Integer.parseInt(result);
      
                    if (goodsNumber > 0) {
                        int realNumber = goodsNumber - 1;
                        stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
                        System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
                        return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
                    } else {
                        System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
                    }
                    return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
                }
            } finally {
                Jedis jedis = RedisUtils.getJedis();
      
                String script = "if redis.call('get', KEYS[1]) == ARGV[1]" + "then " +
                    "return redis.call('del', KEYS[1])" + "else " + "  return 0 " + "end";
                try {
                    Object result = jedis.eval(script, Collections.singletonList(REDIS_LOCK_KEY), Collections.singletonList(
                        value));
                    if ("1".equals(result.toString())) {
                        System.out.println("------del REDIS_LOCK_KEY success");
                    } else {
                        System.out.println("------del REDIS_LOCK_KEY error");
                    }
                } finally {
                    if (null != jedis) {
                        jedis.close();
                    }
                }
            }
      
        }
    }
    

分布式锁如何续期

  • 确保redisLock过期时间大于业务执行时间的问题

  • 集群+CAP对比zookeeper

  • redis:AP,redis异步复制造成的锁丢失, 比如:主节点没来的及把刚刚set进来这条数据给从节点,就挂了。此时如果集群模式下,就得上Redisson来解决

  • zookeeper:CP

  • redis集群环境下,我们自己写的也不OK, 直接上RedLock之Redisson落地实现

  • 配置类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    
    /**
     * 保证不是序列化后的乱码配置
     */
    @Configuration
    public class RedisConfig {
      
        @Value("${spring.redis.host}")
        private String redisHost;
      
        @Bean
        public RedisTemplate, Serializable > redisTemplate(LettuceConnectionFactory connectionFactory) {
            RedisTemplate,
            Serializable > redisTemplate = new RedisTemplate();
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
            redisTemplate.setConnectionFactory(connectionFactory);
            return redisTemplate;
        }
      
        @Bean
        public Redisson redisson() {
            Config config = new Config();
            config.useSingleServer().setAddress("redis://" + redisHost + ":6379").setDatabase(0);
            return (Redisson) Redisson.create(config);
        }
    }
    
  • 修改后版本

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    
    @RestController
    public class GoodController {
        public static final String REDIS_LOCK_KEY = "lockhhf";
      
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
      
        @Value("${server.port}")
        private String serverPort;
      
        @Autowired
        private Redisson redisson;
      
        @GetMapping("/buy_goods")
        public String buy_Goods() {
      
            String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
      
            RLock redissonLock = redisson.getLock(REDIS_LOCK_KEY);
            redissonLock.lock();
            try {
                String result = stringRedisTemplate.opsForValue().get("goods:001");
                int goodsNumber = result == null ? 0 : Integer.parseInt(result);
      
                if (goodsNumber > 0) {
                    int realNumber = goodsNumber - 1;
                    stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
                    System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
                    return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
                } else {
                    System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
                }
                return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
      
            } finally {
                redissonLock.unlock();
            }
        }
    }
    
  • 可能会报错

  • image-20210830225542514

  • 出现这个错误的原因:是在并发多的时候就可能会遇到这种错误,可能会被重新抢占

  • 不见得当前这个锁的状态还是在锁定,并且本线程持有

  • 修改后版本

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    
    @RestController
    public class GoodController {
        public static final String REDIS_LOCK_KEY = "lockhhf";
      
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
      
        @Value("${server.port}")
        private String serverPort;
      
        @Autowired
        private Redisson redisson;
      
        @GetMapping("/buy_goods")
        public String buy_Goods() {
      
            String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
      
            RLock redissonLock = redisson.getLock(REDIS_LOCK_KEY);
            redissonLock.lock();
            try {
                String result = stringRedisTemplate.opsForValue().get("goods:001");
                int goodsNumber = result == null ? 0 : Integer.parseInt(result);
      
                if (goodsNumber > 0) {
                    int realNumber = goodsNumber - 1;
                    stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
                    System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
                    return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
                } else {
                    System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
                }
                return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
      
            } finally {
                //还在持有锁的状态,并且是当前线程持有的锁再解锁
                if (redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()) {
                    redissonLock.unlock();
                }
      
            }
        }
    }
    

总结

  • synchronized 单机版oK,上分布式
  • nginx分布式微服务 单机锁不行
  • 取消单机锁 上redis分布式锁setnx
  • 只加了锁,没有释放锁, 出异常的话,可能无法释放锁, 必须要在代码层面finally释放锁
  • 宕机了,部署了微服务代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要有lockKey的过期时间设定
  • 为redis的分布式锁key,增加过期时间此外,还必须要setnx+过期时间必须同一行的原子性操作
  • 必须规定只能自己删除自己的锁,你不能把别人的锁删除了,防止张冠李戴,1删2,2删3
  • lua或者事务
  • redis集群环境下,我们自己写的也不OK直接上RedLock之Redisson落地实现

缓存过期淘汰策略

  • 查看redis最大占用内存
  • 配置文件
  • image-20210831224521761
  • redis默认内存多少可以用?如果不设置最大内存大小或者设置最大内存大小为0,在64位操作系统中不限制内存大小;在32位操作系统下最大使用3GB内存
  • 一般在生产上如何配置?一般推荐redis设置内存为最大物理内存的四分之三,也就是0.75
  • 如何修改redis内存设置
  • 配置文件
  • img
  • 命令
  • img
  • 什么命令查看redis内存使用情况?info memory
  • 如果内存打满了会怎样?如果redis内存使用超出了设置的最大值会怎样?
  • img

删除策略

  • redis过期键的删除策略
    • 如果一个键是过期的,那它到了过期时间之后是不是马上就从内存中被被删除呢??
    • 如果不是,那过期后到底什么时候被删除呢??是个什么操作?
  • 三种不同的删除策略
    • 定时删除:Redis不可能时时刻刻遍历所有被设置了生存时间的key,来检测数据是否已经到达过期时间,然后对它进行删除。立即删除能保证内存中数据的最大新鲜度,因为它保证过期键值会在过期后马上被删除,其所占用的内存也会随之释放。但是立即删除对cpu是最不友好的。因为删除操作会占用cpu的时间,如果刚好碰上了cpu很忙的时候,比如正在做交集或排序等计算的时候,就会给cpu造成额外的压力,让CPU心累,时时需要删除,忙死;这会产生大量的性能消耗,同时也会影响数据的读取操作。总结:对CPU不友好,用处理器性能换取存储空间(拿时间换空间)
    • 惰性删除:数据到达过期时间,不做处理。等下次访问该数据时,如果未过期,返回数据;发现已过期,删除,返回不存在。惰性删除策略的缺点是,它对内存是最不友好的。如果一个键已经过期,而这个键又仍然保留在数据库中,那么只要这个过期键不被删除,它所占用的内存就不会释放。在使用惰性删除策略时,如果数据库中有非常多的过期键,而这些过期键又恰好没有被访问到的话,那么它们也许永远也不会被删除(除非用户手动执行FLUSHDB),我们甚至可以将这种情况看作是一种内存泄漏–无用的垃圾数据占用了大量的内存,而服务器却不会自己去释放它们,这对于运行状态非常依赖于内存的Redis服务器来说,肯定不是一个好消息;总结:对memory不友好,用存储空间换取处理器性能(拿空间换时间)
    • 定期删除:定期删除策略是前两种策略的折中:定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。周期性轮询redis库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度;特点1:CPU性能占用设置有峰值,检测频度可自定义设置;特点2:内存压力不是很大,长期占用内存的冷数据会被持续清理;总结:周期性抽查存储空间(随机抽查,重点抽查)
    • image-20210831230241319

淘汰策略

  • 八种淘汰策略
    • noeviction: 不会驱逐任何key
    • allkeys-lru: 对所有key使用LRU算法进行删除(常用)
    • volatile-lru: 对所有设置了过期时间的key使用LRU算法进行删除
    • allkeys-random: 对所有key随机删除
    • volatile-random: 对所有设置了过期时间的key随机删除
    • volatile-ttl: 删除马上要过期的key
    • allkeys-lfu: 对所有key使用LFU算法进行删除
    • volatile-lfu: 对所有设置了过期时间的key使用LFU算法进行删除
  • 如何配置,修改
  • img
  • img

LRU算法

  • LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的数据予以淘汰。
  • 算法来源
  • img
  • 设计思想
    • 所谓缓存,必须要有读+写两个操作,按照命中率的思路考虑,写操作+读操作时间复杂度都需要为O(1)
    • 必须有顺序之分,以区分最近使用的和很久没用到的数据排序。
    • 写和读操作 一次搞定。
    • 如果容量(坑位)满了要删除最不长用的数据,每次新访问还要把新的数据插入到队头(按照业务你自己设定左右那一边是队头)
    • 查找快,插入快,删除快,且还需要先后排序
  • LRU的算法核心是哈希链表,本质就是HashMap+DoubleLinkedList 时间复杂度是O(1),哈希表+双向链表的结合体

借助LinkedHashMap实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class LRUCacheDemo<K,V> extends LinkedHashMap<K, V> {

private int capacity;//缓存坑位

public LRUCacheDemo(int capacity) {
super(capacity,0.75F,false);
        this.capacity = capacity;
}

@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return super.size() > capacity;
}

public static void main(String[] args) {
        LRUCacheDemo lruCacheDemo = new LRUCacheDemo(3);

lruCacheDemo.put(1,"a");
lruCacheDemo.put(2,"b");
lruCacheDemo.put(3,"c");
System.out.println(lruCacheDemo.keySet());

lruCacheDemo.put(4,"d");
System.out.println(lruCacheDemo.keySet());

lruCacheDemo.put(3,"c");
System.out.println(lruCacheDemo.keySet());
lruCacheDemo.put(3,"c");
System.out.println(lruCacheDemo.keySet());
lruCacheDemo.put(3,"c");
System.out.println(lruCacheDemo.keySet());
lruCacheDemo.put(5,"x");
System.out.println(lruCacheDemo.keySet());
}
}

/**
 * true
 * [1, 2, 3]
 * [2, 3, 4]
 * [2, 4, 3]
 * [2, 4, 3]
 * [2, 4, 3]
 * [4, 3, 5]
 * */

/**
 [1, 2, 3]
 [2, 3, 4]
 [2, 3, 4]
 [2, 3, 4]
 [2, 3, 4]
 [3, 4, 5]
 */

自实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
public class LRUCacheDemo {


    //map负责查找,构建一个虚拟的双向链表,它里面安装的就是一个个Node节点,作为数据载体。

    //1.构造一个node节点作为数据载体
    class Node<K, V> {
        K key;
        V value;
        Node<K,V> prev;
        Node<K,V> next;

        public Node() {
            this.prev = this.next = null;
        }

        public Node(K key, V value) {
            this.key = key;
            this.value = value;
            this.prev = this.next = null;
        }

    }

    //2 构建一个虚拟的双向链表,,里面安放的就是我们的Node
    class DoubleLinkedList<K,V> {
        Node<K,V> head;
        Node<K,V> tail;

        public DoubleLinkedList() {
            head = new Node();
            tail = new Node();
            head.next = tail;
            tail.prev = head;
        }

        //3. 添加到头
        public void addHead(Node<K,V> node) {
            node.next = head.next;
            node.prev = head;
            head.next.prev = node;
            head.next = node;
        }

        //4.删除节点
        public void removeNode(Node<K, V > node) {
            node.next.prev = node.prev;
            node.prev.next = node.next;
            node.prev = null;
            node.next = null;
        }

        //5.获得最后一个节点
        public Node getLast() {
            return tail.prev;
        }
    }

    private int cacheSize;
    Map<Integer,Node<Integer, Integer >> map;
    DoubleLinkedList<Integer, Integer > doubleLinkedList;

    public LRUCacheDemo(int cacheSize) {
        this.cacheSize = cacheSize; //坑位
        map = new HashMap(); //查找
        doubleLinkedList = new DoubleLinkedList();
    }

    public int get(int key) {
        if (!map.containsKey(key)) {
            return -1;
        }

        Node<Integer,Integer > node = map.get(key);
        doubleLinkedList.removeNode(node);
        doubleLinkedList.addHead(node);

        return node.value;
    }
    //saveOrUpdate method
    public void put(int key, int value) {
        if (map.containsKey(key)) { //update
            Node<Integer,Integer > node = map.get(key);
            node.value = value;
            map.put(key, node);

            doubleLinkedList.removeNode(node);
            doubleLinkedList.addHead(node);
        }
        else {
            if (map.size() == cacheSize) //坑位满了
            {
                Node<Integer,Integer > lastNode = doubleLinkedList.getLast();
                map.remove(lastNode.key);
                doubleLinkedList.removeNode(lastNode);
            }

            //新增一个
            Node<Integer,Integer > newNode = new Node(key, value);
            map.put(key, newNode);
            doubleLinkedList.addHead(newNode);

        }
    }

    public static void main(String[] args) {

        LRUCacheDemo lruCacheDemo = new LRUCacheDemo(3);

        lruCacheDemo.put(1, 1);
        lruCacheDemo.put(2, 2);
        lruCacheDemo.put(3, 3);
        System.out.println(lruCacheDemo.map.keySet());

        lruCacheDemo.put(4, 1);
        System.out.println(lruCacheDemo.map.keySet());

        lruCacheDemo.put(3, 1);
        System.out.println(lruCacheDemo.map.keySet());
        lruCacheDemo.put(3, 1);
        System.out.println(lruCacheDemo.map.keySet());
        lruCacheDemo.put(3, 1);
        System.out.println(lruCacheDemo.map.keySet());
        lruCacheDemo.put(5, 1);
        System.out.println(lruCacheDemo.map.keySet());

    }
}

/**
 * true
 * [1, 2, 3]
 * [2, 3, 4]
 * [2, 4, 3]
 * [2, 4, 3]
 * [2, 4, 3]
 * [4, 3, 5]
 * */

/**
 [1, 2, 3]
 [2, 3, 4]
 [2, 3, 4]
 [2, 3, 4]
 [2, 3, 4]
 [3, 4, 5]
 */
本文由作者按照 CC BY 4.0 进行授权