都3202年了 你还不知道怎样防止redis缓存击穿!?
这个问题是相当重要的,我们在使用过程中必须要考虑并解决,提供一种解决方案, 在服务是集群的情况下也可用。
先来一段伪代码:
* 查询商品分类信息
*/
@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 昵称:弥勒大大 也可点击下方链接去取
SomeUtils: 开发过程中使用的一些Util, 之后会加入其他的Util文件 欢迎关注 、转发、 fork (gitee.com)