redis实战(二)

眉间皱痕 提交于 2020-01-02 20:57:43

redis使用场景(String)

 上文中我们讲到redis一共有5中数据结构(String,Hash,List,Set,Zset),大家了解到了其底层数据结构以及存储方式,那这五种数据结构怎么使用呢?本期带大家了解下redis各种数据结构的使用场景。

命令介绍

String类型作为我们开发日常使用redis时可谓是最常用的场景,简单的key-value的的存储不仅简单而且方便,常用作缓存某些热点数据(key-Json)、计数器(限流)、分布式锁等。这里只列举常用命令  

//缓存热点数据 key-jsonString
#设置mykey的值为hello 默认的过时时间-1永久 通过EX 指定超时时间
set mykey hello
#获取mykey的值
get mykey  (return : hello)
//计数器
#设置mykey 10
set mykey 10
#incr 对key的值进行加1操作   incrby x 指定加x操作
incr mykey   (return : 11)
#程序中如果将redis中key值先拿出来 再加1  会出现线程安全问题 需要使用lua脚本解决此问题(将操作原子化 详情看使用场景)
//分布式锁
#setnx  如果key不存在则插入 如果存在不做任何操作
setnx mykey hello  (return : 1)
setnx mykey world  (return : 0)
get mykey          (return : hello)

场景介绍

缓存热点数据

使用string结构缓存 一般用来缓存热点数据,防止过多的请求对数据库造成额外的压力(毕竟数据库的查询牵涉连接的建立、以及磁盘IO的消耗)。当请求过来时,先进行查询redis,根据指定的key查看是否有数据,如果有直接返回,如果没有查询数据库进行并进行缓存 设立自动过期时间(毕竟redis基于内存,没有足够的空间)。那么这么做有什么缺点吗?我们来讨论下。

  1. 缓存穿透

    1. 是什么?

      key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库

    2. 怎么解决?

      有很多方法可以解决缓存穿透的问题。最常见的做法是采用布隆过滤器,将所有可能存在的数据hash到一个足够大的bitmap中。一个不可能存在的key会被这个bitmap过滤掉,从而避免了对底层存储系统的查询压力。还有一种比较常用的暴力的方法,如果查询数据为空,那么给一个默认值也进行缓存,但是缓存失效时间相应少一点。

      • 
        //1、使用布隆过滤器,该知识点较独立,如果需要后续会单独出一篇文章
        //2、缓存空数据
        
        //伪代码
        public class A{
        
            public Object getData(String key){
                String result = redis.get(key);
                if(StringUtils.isNotBlank(reuslt)){
                    return result;
                }
                result = map.getDB(key);
                if(StringUtils.isBlank(reuslt)){
                    result = "empty";
                }
                redis.set(key, result);
                return result;
            }
        }

         

  2. 缓存击穿

    1. 是什么?

      缓存击穿是指在同一时刻大量的请求访问同一个数据,但是该数据在此刻缓存失效,所以这些请求全部去访问数据库。导致DB压力

    2. 怎么解决?

      解决缓存击穿的最暴力无脑的方法当然是设置热点数据永不过期,当然这样对我们redis的内存占用也是大大的。着实不划算。所以一般的解决方式就是加锁,保证同一时间不会有大量的请求因为一条数据而多次访问数据库。废话不多说,上代码。

      public Object getData(String key){
          Object data = getRedisData(key);
          if(data == null){
              boolean flag = redisLock.getLock(key);
              if(flag){
                  //拿key作锁的获取条件,保证获取别的数据不会等待
                  Object data = queryDataFromDB(key);
                  setRedisData(key, data);
                  redisLock.unLock(key);
              }else{
                  //为获取到锁 等待100毫秒
                  Thread.sleep(100);
                  getData(key);
              }
          }else{
              return data;
          }
      }

       

  3. 缓存雪崩

    1. 是什么?

      缓存雪崩跟缓存击穿有点像,是指同一时间大量的key同时失效,多个请求访问不同的key击穿数据库。其实就是大批量的数据缓存失效,然后引起数据库的压力过大甚至down机。

    2. 怎么解决?

      不要脸的解决方式当然是数据永久有效。当然也是不怎么会用的。那么如果采用解决缓存击穿的同步锁呢?当然是可以解决问题,但是每个请求等待的100毫秒将会是对用户体验产生巨大的影响。网上给出的方案大都是缓存失效时间随机,采用redis集群并且每个分片失效时间不等。但是我觉得这样的方案依然不能保证完全避免雪崩。我这边的解决方案如下(不保证完全正确,但是我是这样处理的,^_^)

      // 采用一级缓存 二级缓存 做第一步缓冲
      //缓存一个正常时间的数据  以及 一个有效时间较长的数据
      public Object getData(String key){
          //获取一级缓存
          Object data = getRedisDataCache1(key);
          if(data != null){
              return data;
          }
          //获取二级缓存
          data = getRedisDataCache2(key);
          if(data != null){
              return data;
          }
          //获取锁
          bollean flag = redisLock.lock(key);
          if(flag){
              data = getDataFromDB(key);
              setRedisCache1(key, data);
              setRedisCache2(key, data);
              redisLock.unlock(key);
          }else{
              //等待100毫秒
              Thread.sleep(100);
              data = getData(key);
          }
          return data;
      }
      //上述代码 加了二级缓存后 可以保证一级缓存不会造成雪崩 那二级缓存如果同时失效呢?总不能二级缓存永久有效吧  
      //这里有两种选择 其一:二级缓存有效时间较长,通过其他技术保证二级缓存存的都是热点数据,
      //击穿二级缓存的数据都是不经常使用的数据,当然,这样做比较吃redis内存。
      //其二: 二级缓存时间相对也较短,对mysql层增加限流操作,或者熔断操作。
      //当达到mysql瓶颈时,进行熔断,如果是需要补偿的场景,记录熔断后的id,后续进行处理
      
      public Object getData(String key){
          //获取一级缓存
          Object data = getRedisDataCache1(key);
          if(data != null){
              return data;
          }
          //获取二级缓存
          data = getRedisDataCache2(key);
          if(data != null){
              return data;
          }
          data = getDataFromDb(key);
      }
      
      //此方法 增加限流 熔断 以及补偿机制
      public Object getDataFromDb(String key){
          .....
      }

       

计数器 

计数器的使用场景很多,但是几乎大同小异。如上述我们提到的限流,就是一个经常使用的场景(当然并不是限流只能通过这种方法实现,常见的限流有计数器、漏桶、令牌桶 这里不多做介绍)。使用计数器做限流,记录一个方法的访问次数,当达到一定次数的时候,就阻止其继续访问。原理很简单,直接上代码。

//跟着上边的逻辑
public Object getDataFromDb(String key){
    ....
}

@Around
//切面  切上边的方法
public void incr(){
    int count = redis.incr(key);
    if(count > 100){
        doOtherThing();
        log.log("达到限流上限");
    }
}

分布式锁

分布式锁直接上代码吧。没什么可说的,单机分布式锁就是基于SETNX。解锁使用lua脚本,操作原子性。

@Slf4j
public class RedisLock implements SimpleLock {
    private static final ThreadLocal<String> THREAD_LOCAL = ThreadLocal.withInitial(() -> null);
    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_SECONDS = "EX";
    private static final Long RELEASE_SUCCESS = 1L;

    private final JedisPool jedisPool;
    private Random random = new Random();
    private String lockKey;
    private int expiredSeconds;

    public RedisLock(JedisPool jedisPool, String lockKey, int expiredSeconds) {
        this.jedisPool = jedisPool;
        this.lockKey = lockKey;
        this.expiredSeconds = expiredSeconds;
    }

    
    public boolean lock() {
        String randomValue = generateRandomValue();
        THREAD_LOCAL.set(randomValue);
        boolean isLock = true;
        try (Jedis jedis = jedisPool.getResource()) {
            String result = jedis.set(lockKey, randomValue, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_SECONDS, expiredSeconds);
            if (!LOCK_SUCCESS.equalsIgnoreCase(result)) {
                log.warn("lock | Failed to set key: {}, value: {}, result: {}", lockKey, randomValue, result);
                isLock = false;
            }
            return isLock;
        } catch (Exception e) {
            log.warn("lock | Failed to setnx key: {}, value: {}, expiredSeconds: {}",
                    lockKey, randomValue, expiredSeconds, e);
        }
        return false;
    }

    
    public void unlock() {
        String randomValue = THREAD_LOCAL.get();
        if (StringUtils.isBlank(randomValue)) {
            return;
        }
        try (Jedis jedis = jedisPool.getResource()) {
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(randomValue));
            if (!RELEASE_SUCCESS.equals(result)) {
                log.warn("unlock | Failed to del key: {}, value: {}, result: {}", lockKey, randomValue, result);
            }
        } catch (Exception e) {
            log.warn("unlock | Failed to unlock key: {}, value: {}", lockKey, randomValue, e);
        }
    }

    public String getLockKey() {
        return lockKey;
    }

    private String generateRandomValue() {
        return String.valueOf(System.currentTimeMillis()) + String.format("%03d", random.nextInt(1000));
    }
}

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!