可以设置一个本地缓存,在 Redis 缓存失效的时候先去查询本地缓存而非查询数据库。
缓存击穿
缓存击穿:大量的请求同时查询一个 key 时,此时这个 key 正好失效了,就会导致大量的请求都落到数据库。缓存击穿是查询缓存中失效的 key,而缓存穿透是查询不存在的 key。
解决方法:
1、加互斥锁。在并发的多个请求中,只有第一个请求线程能拿到锁并执行数据库查询操作,其他的线程拿不到锁就阻塞等着,等到第一个线程将数据写入缓存后,直接走缓存。可以使用Redis分布式锁实现,代码如下:
public String get(String key) {
String value = redis.get(key);
if (value == null) { //缓存值过期
String unique_key = systemId + ":" + key;
//设置30s的超时
if (redis.set(unique_key, 1, 'NX', 'PX', 30000) == 1) { //设置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(unique_key);
} else { //其他线程已经到数据库取值并回写到缓存了,可以重试获取缓存值
sleep(50);
get(key); //重试
}
} else {
return value;
}
}
2、热点数据不过期。直接将缓存设置为不过期,然后由定时任务去异步加载数据,更新缓存。这种方式适用于比较极端的场景,例如流量特别特别大的场景,使用时需要考虑业务能接受数据不一致的时间,还有就是异常情况的处理,保证缓存可以定时刷新。
缓存预热
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
解决方案:
- 直接写个缓存刷新页面,上线时手工操作一下;
- 数据量不大,可以在项目启动的时候自动进行加载;
- 定时刷新缓存;
缓存降级
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
缓存降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:
- 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
- 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
- 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
- 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。
Redis 怎么实现消息队列?
使用list类型保存数据信息,rpush生产消息,lpop消费消息,当lpop没有消息时,可以sleep一段时间,然后再检查有没有信息,如果不想sleep的话,可以使用blpop, 在没有信息的时候,会一直阻塞,直到信息的到来。
BLPOP queue 0 //0表示不限制等待时间
BLPOP和LPOP命令相似,唯一的区别就是当列表没有元素时BLPOP命令会一直阻塞连接,直到有新元素加入。
redis可以通过pub/sub主题订阅模式实现一个生产者,多个消费者,当然也存在一定的缺点,当消费者下线时,生产的消息会丢失。
PUBLISH channel1 hi
SUBSCRIBE channel1
UNSUBSCRIBE channel1 //退订通过SUBSCRIBE命令订阅的频道。
PSUBSCRIBE channel?*
按照规则订阅。
PUNSUBSCRIBE channel?*
退订通过PSUBSCRIBE命令按照某种规则订阅的频道。其中订阅规则要进行严格的字符串匹配,PUNSUBSCRIBE *
无法退订channel?*
规则。
Redis 怎么实现延时队列
使用sortedset,拿时间戳作为score,消息内容作为key,调用zadd来生产消息,消费者用zrangebyscore
指令获取N秒之前的数据轮询进行处理。
pipeline的作用?
redis客户端执行一条命令分4个过程: 发送命令、命令排队、命令执行、返回结果。使用pipeline
可以批量请求,批量返回结果,执行速度比逐条执行要快。
使用pipeline
组装的命令个数不能太多,不然数据量过大,增加客户端的等待时间,还可能造成网络阻塞,可以将大量命令的拆分多个小的pipeline
命令完成。
原生批命令(mset和mget)与pipeline
对比:
-
原生批命令是原子性,pipeline
是非原子性。pipeline命令中途异常退出,之前执行成功的命令不会回滚。
-
原生批命令只有一个命令,但pipeline
支持多命令。
LUA脚本
Redis 通过 LUA 脚本创建具有原子性的命令: 当lua脚本命令正在运行的时候,不会有其他脚本或 Redis 命令被执行,实现组合命令的原子操作。
在Redis中执行Lua脚本有两种方法:eva l
和eva lsha
。eva l
命令使用内置的 Lua 解释器,对 Lua 脚本进行求值。
//第一个参数是lua脚本,第二个参数是键名参数个数,剩下的是键名参数和附加参数
> eva l "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"
lua脚本作用
1、Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令。
2、Lua脚本可以将多条命令一次性打包,有效地减少网络开销。
应用场景
举例:限制接口访问频率。
在Redis维护一个接口访问次数的键值对,key
是接口名称,value
是访问次数。每次访问接口时,会执行以下操作:
- 通过
aop
拦截接口的请求,对接口请求进行计数,每次进来一个请求,相应的接口访问次数count
加1,存入redis。
- 如果是第一次请求,则会设置
count=1
,并设置过期时间。因为这里set()
和expire()
组合操作不是原子操作,所以引入lua
脚本,实现原子操作,避免并发访问问题。
- 如果给定时间范围内超过最大访问次数,则会抛出异常。
private String buildLuaScript() {
return "local c" +
"\nc = redis.call('get',KEYS[1])&quo