百味皆苦 java后端开发攻城狮

Redis分布式锁与淘汰机制

2020-03-05
百味皆苦

什么是分布式锁

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

案例

  • 新建两个model:boot_redis01,boot_redis02

  • pom

  • 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

  • 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
    
  • 启动类

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

  • @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;
    	}
    }
    
  • 业务类

  • @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

    • @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;
          }
          
      }
      
  • 修改后版本

  • @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

  • 修改后版本

  • @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必须同时出现并保证调用

  • 修改后版本

  • @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有过期时间的设定

  • 修改后版本

  • @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+过期时间分开了,必须要合并成一行具备原子性

  • 修改后版本

  • @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

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

  • 修改后版本

  • @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

  • 修改后版本

  • @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命令保证代码执行的原子性

  • 工具类

  • 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");
        }
    }
    
  • 修改后版本

  • @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落地实现

  • 配置类

    /**
     * 保证不是序列化后的乱码配置
     */
    @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);
        }
    }
    
  • 修改后版本

  • @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

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

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

  • 修改后版本

  • @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实现

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]
 */

自实现

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]
 */

Comments

Content