Redis性能优化
Redis性能测试
阿里Redis性能优化
使用批量操作减少网络传输
Redis命令执行步骤:1、发送命令;2、命令排队;3、命令执行;4、返回结果。其中 1 与 4 消耗时间 --> Round Trip Time(RTT,往返时间),也就是数据在网络上传输的时间,使用批量操作大幅减少RTT。
实现:Lua脚本
Lua 脚本同样支持批量操作多条命令。一段 Lua 脚本可以视作一条命令执行,可以看作是 原子操作 。也就是说,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰,Lua 脚本中支持一些简单的逻辑处理比如使用命令读取值并在 Lua 脚本中进行处理。
存在问题:
-
如果 Lua 脚本运行时出错并中途结束,之后的操作不会进行,但是之前已经发生的写操作不会撤销,所以即使使用了 Lua 脚本,也不能实现类似数据库回滚的原子性。
-
Redis Cluster 下 Lua 脚本的原子操作也无法保证了,原因同样是无法保证所有的 key 都在同一个 hash slot(哈希槽)上。
大量Key集中过期问题
Redsi采用定期删除 + 惰性 / 懒汉式删除策略
问题:如果遇到大量过期key的话,客户端请求必须等待定期清理过期key任务线程执行完成,因为定期删除任务是在Redis主线程中执行的,就导致客户端请求无法被及时处理。
解决方案:
- 给key设置随机过期时间。(建议)
- 惰性删除 / 延迟释放。采用异步方式延迟释放key使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。
Redis bigKey(大Key)
概念:一个key的value所占的内存比较大,这个key就可以看作是bigkey。
大概标准:
- String类型的value不超过1MB
- 符合类型(List、Hash、Set、Sorted Set)不超过5000个
处理工具:
- redis-rdb-tools
- rdb_bigkeys
- 阿里云Redis分析
Redis hotKey(热Key)
概念:一个Key访问的次数明显多于其他Key,例如Redis实例每秒处理请求达5000次,其中一个key的每秒访问量就达2000次 --> hotKey
出现原因:某热点数据访问量暴涨(重大搜索事件、参与秒杀的商品)
导致问题:
- 占用大量CPU 和 带宽
- 如果访问突然访问的hotKey突然超出Redis的处理能力,Redis就会直接宕机,在此情况下,大量请求将落到后面的数据库上,导致数据库崩溃。
处理工具:
-
京东开源工具
-
阿里云Redis分析
缓存穿透
概念:大量请求的Kye是不合理的,根本不存在与缓存中,也不存在于数据库中。就会导致这些请求直接到数据库上,根本没有经过缓存着一层,对数据库造成压力。
举个例子:某个黑客故意制造一些非法的 key 发起大量请求,导致大量请求落到数据库,结果数据库上也没有查到对应的数据。也就是说这些请求最终都落到了数据库上,对数据库造成了巨大的压力。
解决办法:参数校验
1. 布隆过滤器
目的:解决海量数据(缓存穿透、海量数据去重)
概念:二进制向量 / 位数组 和 一系列随机映射函数(哈希函数)两部分组成的数据结构。
特点:占用空间少效率高,但是返回的结果是概率性的,而不是非常准确,添加到集合中的元素越多,报错的可能性越大,并且放在布隆过滤器中的元素不容易删除。
Bloom Filter 会使用一个较大的 bit 数组来保存所有的数据,数组中的每个元素都只占用 1 bit ,并且每个元素只能是 0 或者 1(代表 false 或者 true),这也是 Bloom Filter 节省内存的核心所在。这样来算的话,申请一个 100w 个元素的位数组只占用 1000000Bit / 8 = 125000 Byte = 125000/1024 KB ≈ 122KB 的空间。
原理:
- 当一个元素加入布隆过滤器中:
- 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)
- 根据得到的哈希值,在位数组中把对应下标的值设置为1。
- 判断一个元素是否存在于布隆过滤器中:
- 对给定元素再次进行相同的哈希函数计算。
- 的到值之后判断位数组中的每个元素是否都为1,如果值都为1,那么说明这个值在布隆过滤器中,如果存在的值不为1,说明该元素不在布隆过滤器中。
使用场景:
- 判断给定数据是否存在:黑名单功能(判断一个IP或手机号码是否在黑名单中)、判断一个数字是否在搜索数据集中(数据集很大,上亿),防止缓存穿透(判断缓存中是否包含某个元素)。
- 去重:爬给定网站的时候对已经爬取过的URL去重、对巨量的QQ号/订单号去重。
实现:
- 一个合适大小的位数组保存数据
- 几个不同的哈希函数
- 添加元素到位数组(布隆过滤器)的方法实现
- 判断元素是否存在于位数组的方法实现
import java.util.BitSet;
public class MyBloomFilter {
/**
* 位数组的大小
*/
private static final int DEFAULT_SIZE = 2 << 24;
/**
* 通过这个数组可以创建 6 个不同的哈希函数
*/
private static final int[] SEEDS = new int[]{3, 13, 46, 71, 91, 134};
/**
* 位数组。数组中的元素只能是 0 或者 1
*/
private BitSet bits = new BitSet(DEFAULT_SIZE);
/**
* 存放包含 hash 函数的类的数组
*/
private SimpleHash[] func = new SimpleHash[SEEDS.length];
/**
* 初始化多个包含 hash 函数的类的数组,每个类中的 hash 函数都不一样
*/
public MyBloomFilter() {
// 初始化多个不同的 Hash 函数
for (int i = 0; i < SEEDS.length; i++) {
func[i] = new SimpleHash(DEFAULT_SIZE, SEEDS[i]);
}
}
/**
* 添加元素到位数组
*/
public void add(Object value) {
for (SimpleHash f : func) {
bits.set(f.hash(value), true);
}
}
/**
* 判断指定元素是否存在于位数组
*/
public boolean contains(Object value) {
boolean ret = true;
for (SimpleHash f : func) {
ret = ret && bits.get(f.hash(value));
}
return ret;
}
/**
* 静态内部类。用于 hash 操作!
*/
public static class SimpleHash {
private int cap;
private int seed;
public SimpleHash(int cap, int seed) {
this.cap = cap;
this.seed = seed;
}
/**
* 计算 hash 值
*/
public int hash(Object value) {
int h;
return (value == null) ? 0 : Math.abs((cap - 1) & seed * ((h = value.hashCode()) ^ (h >>> 16)));
}
}
}
Redis中的布隆过滤器
使用Docker安装Redis中的布隆过滤器redis-stack
2. 接口限流
根据用户或者IP对接口限流,对于一场频繁访问的行为,采取黑名单机制,例如将异常IP列入黑名单。
缓存击穿和雪崩都可以配合接口限流来解决
分布式服务限流实战
缓存击穿
概念:请求的key对应的是热点数据,该数据存在于数据库中,但不存在于缓存中(缓存中的那份数据已经过期),瞬间大量的请求直接发送到数据库,对于数据库造成了巨大的压力,有可能会直接多请求宕机。
例子:秒杀进行过程中,缓存中的某个秒杀商品的数据突然过期,这就导致瞬时大量对该商品的请求直接落到数据库上,对数据库造成了巨大的压力。
解决:
- 提前预热(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。
- 加锁(看情况):在缓存失效后,通过设置互斥锁确保只有一个请求去查询数据库并更新缓存。
缓存穿透和缓存击穿有什么区别?
缓存穿透中,请求的 key 既不存在于缓存中,也不存在于数据库中。
缓存击穿中,请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期) 。
缓存雪崩
概念:缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力
例子:数据库中的大量数据在同一时间过期,这个时候突然有大量的请求需要访问这些过期的数据。这就导致大量的请求直接落到数据库上,对数据库造成了巨大的压力。
解决:
针对 Redis 服务不可用的情况:
- Redis 集群:采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。Redis Cluster 和 Redis Sentinel 是两种最常用的 Redis 集群实现方案。
- 多级缓存:设置多级缓存,例如本地缓存 + Redis 缓存的二级缓存组合,当 Redis 缓存出现问题时,还可以从本地缓存中获取到部分数据。
针对大量缓存同时失效的情况:
- 设置随机失效时间(可选):为缓存设置随机的失效时间,例如在固定过期时间的基础上加上一个随机值,这样可以避免大量缓存同时到期,从而减少缓存雪崩的风险。
- 提前预热(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。
- 持久缓存策略(看情况):虽然一般不推荐设置缓存永不过期,但对于某些关键性和变化不频繁的数据,可以考虑这种策略。
缓存预热如何实现?
常见的缓存预热方式有两种:
- 使用定时任务,比如 xxl-job,来定时触发缓存预热的逻辑,将数据库中的热点数据查询出来并存入缓存中。
- 使用消息队列,比如 Kafka,来异步地进行缓存预热,将数据库中的热点数据的主键或者 ID 发送到消息队列中,然后由缓存服务消费消息队列中的数据,根据主键或者 ID 查询数据库并更新缓存。
缓存雪崩和缓存击穿有什么区别?
缓存雪崩和缓存击穿比较像,但缓存雪崩导致的原因是缓存中的大量或者所有数据失效,缓存击穿导致的原因主要是某个热点数据不存在与缓存中(通常是因为缓存中的那份数据已经过期)。
Redis集群面试题
Redis Sentinel:
- 什么是 Sentinel? 有什么用?
- Sentinel 如何检测节点是否下线?主观下线与客观下线的区别?
- Sentinel 是如何实现故障转移的?
- 为什么建议部署多个 sentinel 节点(哨兵集群)?
- Sentinel 如何选择出新的 master(选举机制)?
- 如何从 Sentinel 集群中选择出 Leader ?
- Sentinel 可以防止脑裂吗?
Redis Cluster:
- 为什么需要 Redis Cluster?解决了什么问题?有什么优势?
- Redis Cluster 是如何分片的?
- 为什么 Redis Cluster 的哈希槽是 16384 个?
- 如何确定给定 key 的应该分布到哪个哈希槽中?
- Redis Cluster 支持重新分配哈希槽吗?
- Redis Cluster 扩容缩容期间可以提供服务吗?
- Redis Cluster 中的节点是怎么进行通信的?