还不知道怎样防止redis缓存击穿!?

71 阅读5分钟

都3202年了 你还不知道怎样防止redis缓存击穿!?

1148c412ba174cba94b33bd61bcdcc55~tplv-k3u1fbpfcp-zoom-in-crop-mark4536000.webp

这个问题是相当重要的,我们在使用过程中必须要考虑并解决,提供一种解决方案, 在服务是集群的情况下也可用。

先来一段伪代码:


* 查询商品分类信息

*/

@SuppressWarnings("unchecked")

public List<ProductCategory> findProductCategory() {

    Object obj = redisTemplate.opsForValue().get("hosjoy-b2b-product:product-category");

    if (obj == null) {

    //查数据库

    List<ProductCategory> categoryList = productCategoryMapper.selectProductCategory();

    //写入到 Redis,过期时间 2 小时 过期时间根据你们公司业务来定 对于不常变动的数据 缓存时间可以常些 这里只是伪代码

    redisTemplate.opsForValue().set("hosjoy-b2b-product:product-category", categoryList, Duration.ofHours(2L));

    return categoryList;

    } else {

    return (List<ProductCategory>) obj;

    }

}

同样,我先从 Redis 查商品,如果没有,再去数据库查询,并且更新到 Redis 然后返回。但是我们要知道商品分类这样的数据时高频访问的,否则也不用存 Redis 中了,只要用户浏览商品就一定会访问。上述代码看似没什么问题,实则问题非常大,本来一切正常的,但是由于我们刚刚设置了商品分类在 Redis 中的过期时间是两小时,两小时过后过期的这一瞬间,用户量大的情况下,几万甚至几十万请求(对于淘宝京东这样的量级其实已经很小了)在 Redis 中都没查到数据,然后都去访问数据库了,那毋庸置疑,数据库根本顶不住这么大的并发请求压力,瞬间就被击垮了。这种由于 Redis 中数据过期导致一瞬间大量请求直接访问 MySQL 导致 MySQL 被打挂的情况就叫做缓存击穿。

解决的方案其实也很简单,刚刚我们已经知道是由于 Redis 中数据过期那一瞬间大量请求直接打到 MySQL,那我在去 MySQL 查询这加个锁就行了。如果 Redis 中没有,先让一个请求去数据库查询,把值更新到 Redis,其他请求再从 Redis 中查询即可。


* 查询商品分类信息

*/

@SuppressWarnings("unchecked")

public List<ProductCategory> findProductCategory() {

  Object obj = redisTemplate.opsForValue().get("hosjoy-b2b-product:product-category");

  if (obj == null) {

    synchronized (this){

        //进入 synchronized 一定要先再查询一次 Redis,防止上一个抢到锁的线程已经更新过了

        obj = redisTemplate.opsForValue().get("hosjoy-b2b-product:product-category");

        if(obj != null){

            return (List<ProductCategory>) obj;

        }

        List<ProductCategory> categoryList = productCategoryMapper.selectProductCategory();

        redisTemplate.opsForValue().set("hosjoy-b2b-product:product-category", categoryList, Duration.ofHours(2L));

    }

    return categoryList;

  } else {

    return (List<ProductCategory>) obj;

  }

}

这样同一时间只有一个请求能访问 MySQL ,当它查询到数据更新到 Redis 之后释放锁,此时其他并发线程进入 synchronized 代码块首先查 Redis ,发现 Redis 已经有了,就不会再访问 MySQL

这里有几个细节点:

  • 由于 Spring 容器默认是单例的,这里我们在当前 Service 类,使用 synchronized(this) 对于当前应用是安全的
  • 进入 synchronized 代码块必须先查一次 Redis ,防止上一个抢到锁的线程已经更新过 Redis 了
  • 这里的场景没有必要一定使用分布式锁

你可能会奇怪,为什么这里不用分布式锁,毕竟我们生产环境的商品服务实例肯定是集群,使用 synchronized(this) 只能保证当前应用实例同时只有一个请求执行这段代码,不能保证集群中其他实例。值得注意的是我们这里并不是要对数据进行安全修改,我们仅仅是想要防止大量请求访问到 MySQL ,假设现在商品服务是 10 个实例组成的集群,那么这里的代码最坏的情况也就是 10 个请求同时访问 MySQL 查询,问题不大~~ 当然使用分布式锁肯定也没问题

关于redis使用还有其他很多要注意和解决使用场景 像:

  • 怎样保证 Redis 和 Mysql 数据一致性
  • 怎样防止缓存雪崩
  • 怎样防止缓存穿透
  • 等等~~

这些后续再更新吧 欢迎关注 你的关注就是我更新最大的动力!

另外 这里使用的还是redisTemplate直接使用 可以把它封装成一个RedisUtil,比如我封装的这样 展示一部分:

@Resource
private RedisTemplate<String, Object> redisTemplate;

// =============================common============================
/**
 * 指定缓存失效时间
 * @param key  键
 * @param time 时间(秒)
 */
public boolean expire(String key, long time) {
    try {
        if (time > 0) {
            redisTemplate.expire(key, time, TimeUnit.SECONDS);
        }
        return true;
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    }
}

/**
 * 根据key 获取过期时间
 * @param key 键 不能为null
 * @return 时间(秒) 返回0代表为永久有效
 */
public long getExpire(String key) {
    return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}


/**
 * 判断key是否存在
 * @param key 键
 * @return true 存在 false不存在
 */
public boolean hasKey(String key) {
    try {
        return redisTemplate.hasKey(key);
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    }
}


/**
 * 删除缓存
 * @param key 可以传一个值 或多个
 */
@SuppressWarnings("unchecked")
public void del(String... key) {
    if (key != null && key.length > 0) {
        if (key.length == 1) {
            redisTemplate.delete(key[0]);
        } else {
            redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
        }
    }
}


// ============================String=============================

/**
 * 普通缓存获取
 * @param key 键
 * @return 值
 */
public Object get(String key) {
    return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
 * 登录账号密码输错次数记录
 *
 */
public Integer getErrUsernameNum(String  key){
    if (ObjectUtil.isNotNull(redisTemplate.opsForValue().get(key))){
        return key==null?null:Integer.parseInt(redisTemplate.opsForValue().get(key).toString());
    }
    return null;
}




/**
 * 普通缓存放入
 * @param key   键
 * @param value 值
 * @return true成功 false失败
 */

public boolean set(String key, Object value) {
    try {
        redisTemplate.opsForValue().set(key, value);
        return true;
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    }
}


/**
 * 普通缓存放入并设置时间
 * @param key   键
 * @param value 值
 * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
 * @return true成功 false 失败
 */

public boolean set(String key, Object value, long time) {
    try {
        if (time > 0) {
            redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
        } else {
            set(key, value);
        }
        return true;
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    }
}

还有其他类型的封装 Map set list
完整的代码 可去关注我的Gitee 昵称:弥勒大大 也可点击下方链接去取

gitee.com/ytlll/SomeU…

SomeUtils: 开发过程中使用的一些Util, 之后会加入其他的Util文件 欢迎关注 、转发、 fork (gitee.com)