一般用redis常规的写法:
1:客户端发起请求。
2:判断redis中是否有数据,如果有返回给客户端,没有则请求数据库,查出数据返回给客户端。
带来的问题一:缓存穿透
1:如果此时有人恶意的攻击呢?发起几十亿万条redis和mysql中都不存在的数据,请求访问你的网站,数据库不就挂了。
解决办法1:使用布隆过滤器
redis中没有数据,请求布隆过滤器拦截请求mysql和redis中不存在的key的请求。如果布隆过滤器中对应的key,在请求数据库,没有则返回一个非法访问
2:缓存空数据
在客户端请求redis时发现没有key,接着请求mysql发现也没有key,此时就把key缓存起来,value设置为null。(缺点只适合单一key多次访问数据库的情况)
带来的问题二:缓存击穿
客户端访问数据的时候,redis中没有数据,mysql中有数据,相当于直接跳过了redis。
为何会发生
用户访问这条数据的时候,热点数据过期时间刚好到了。
问题
1:如果此时这条数据很热门,秒级有几十亿万次的访问量,数据库不就挂了。
解决
1:设置热点数据永远不过期。
2:发现redis中没有数据,加入分布式锁(拦截请求),接着查询redis,查询数据库后,把这条数据重新加入到缓存,释放锁,之后剩余的请求,请求redis时就可以查到数据了
Redisson 版本解决缓存击穿
public Item_kill getItemKill(int id) {
/**
* 布隆过滤器解决缓存穿透
*/
if (!bloomFilter.mightContain(id)) {
log.warn("非法秒杀商品id" + id);
return null;
}
/**
* 从缓存中获取秒杀商品的信息,如果此时热点数据恰好过期了呢?
*/
Item_kill itemKill = (Item_kill) redisTemplate.opsForValue().get(CACHE_PRE + "[" + id + "]");
if (itemKill != null) {
return itemKill;
}
/**
* 定义唯一标识key
*/
String key = new StringBuffer().append(id).toString();
RLock lock = redissonClient.getLock(key);
boolean ok = false;
Item_kill item_kill = null;
try {
ok = lock.tryLock(30, 10, TimeUnit.SECONDS);
if(ok){
/**
* 从缓存中获取秒杀商品的信息
*/
itemKill = (Item_kill) redisTemplate.opsForValue().get(CACHE_PRE + "[" + id + "]");
if (itemKill != null) {
return itemKill;
}
item_kill = itemKillMapper.selectByPrimaryKey(id);
/**
* 将热点数据重新加入到缓存,解决热点数据失效的问题
*/
redisTemplate.opsForValue().set(CACHE_PRE + "[" + id + "]",item_kill);
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
return item_kill;
}
Jredis版本解决缓存击穿
/**
* 解决缓存穿透(redis没有数据,db有数据,穿透一层)
*/
@GetMapping("/test")
public String test() {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
System.err.println(Thread.currentThread().getName() + " : " + getKey("test"));
}, "线程: " + i).start();
}
return "ok";
}
String getKey(String key) {
Jedis jedis = jedisPool.getResource();
String value = jedis.get(key);
if (null == value) {
Long setnx = jedis.setnx("setnx", "1");
//加分布式锁,确保只有一个线程进行查数据库,其他加锁失败的线程重试 getKey()
if (1 == setnx) {
//查db
System.err.println(Thread.currentThread().getName() + " 查db");
value = "dbValue";
//将 db 值更新回redis
jedis.set(key, value);
jedis.expire(key, 10);
//释放分布式锁
jedis.del("setnx");
} else {
System.err.println(Thread.currentThread().getName() + "竞争锁失败,重试查缓存");
value = getKey(key);
}
}
jedis.close();
return value;
}
模拟请求:可以看到10个请求打过来,只查一次 db,剩余的线程全部重试查询去了
通过对接口限流缓解缓存击穿
3:做接口的限流降级(举个例子) aop,单机版
@Aspect
@Component
public class limitAspect {
private ConcurrentHashMap<String, Semaphore> semaphores = new ConcurrentHashMap<>();
@Pointcut("@annotation(com.zzh.service.aspects.limit.Limit)")
public void limit() {
}
/**
* @param
* @method 对接口进行限流
*/
@Around("limit()")
public R before(ProceedingJoinPoint joinPoint) throws NoSuchMethodException, ClassNotFoundException {
//获取被该注解作用的对象
Object target = joinPoint.getTarget();
//获取被该注解作用的对象名字
String targetName = target.getClass().getName();
//获取被该注解作用的对象的class
Class<?> aClass = target.getClass();
//获取请求的参数
Object[] methodParam = joinPoint.getArgs();
//获取被该注解作用的方法的名字
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
// //根据参数,获取对应的参数类型
// Class<?>[] argTypes = ReflectUtils.getClasses(methodParam);
// //得到注解作用的方法了
// Method method = aClass.getDeclaredMethod(methodName, argTypes);
// //获取该方法上的Limit注解
// Limit annotation = method.getAnnotation(Limit.class);
// //获取该方法上设置的最大限流量
// int i = annotation.maxLimit();
// System.out.println("最大流量为:" + i);
StringBuffer bufferKey = new StringBuffer().append(methodName).append(targetName);
String key = String.valueOf(bufferKey);
Method[] methods = aClass.getMethods();
Limit annotation = null;
//遍历所有的方法
for (Method method : methods) {
//根据获取到的方法名字,匹配获取该方法
if (methodName.equals(method.getName())) {
Class<?>[] parameterTypes = method.getParameterTypes();
//方法中的参数匹配,精确匹配方法
if (parameterTypes.length == args.length) {
annotation = method.getAnnotation(Limit.class);
}
}
}
if (null != annotation) {
Semaphore semaphore = semaphores.get(key);
if (null == semaphore) {
//semaphores.put()
//初始化各个接口的访问流量
//System.out.println("maxLimit:" + annotation.maxLimit());
semaphores.putIfAbsent(String.valueOf(key), new Semaphore(annotation.maxLimit()));
semaphore = semaphores.get(key);
}
try {
//当达到最大的访问的流量后,只有等有空闲的流量时,别的人才能加入
semaphore.acquire();
//执行方法
joinPoint.proceed();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (Throwable throwable) {
throwable.printStackTrace();
} finally {
semaphore.release();
}
}
return R.ok();
}
}
自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(value = ElementType.METHOD)
public @interface Limit {
//默认限制流量
int maxLimit() default 10;
}
接口限流(这样的话,这个登录的接口就最大支持20个人同时访问了)
@RestController
public class logController {
@PostMapping("/login")
@Limit(maxLimit = 20)
public R login() {
}
}
三:缓存雪崩
介绍:
redis挂了,所有的请求都达到了数据库
解决
1:使用缓存集群,保证缓存高可用
使用 Redis Sentinel 和 Redis Cluster 实现高可用缓存
2:使用Hystrix
做一些限流或者熔断的兜底策略