本文为个人学习笔记整理,仅供交流参考,非专业教学资料,内容请自行甄别
文章目录
一、Bigkey问题
Bigkey指的是,在Redis中,某个key对应的value
:
- 字符串类型的value:单个大小大于10kb
- 非字符串的value:集合中的元素过多,或者单个元素的key过大
Bigkey可能会导致以下的问题:
- 执行命令阻塞:因为Redis执行命令是单线程的,单个key对应的value过大,必然导致处理时间的增加,在处理该key时,其他的请求必须等待。
- 网络拥塞:key的value过大,占用带宽过大,处理的请求数量也就越少。
- 过期删除:如果大量的Bigkey同时到期,在删除时同样会面临执行命令阻塞的问题。
造成Bigkey的原因,主要是针对业务场景,Redis存储粒度设计不当,可能包含以下的情况:
- 将包含商品表中所有字段的对象,序列化作为String结构的value。
- 将所有用户的信息,作为Hash结构的value存储。
对于Bigkey的优化,可以从以下的方面入手:
- 将key在业务的层面进行拆分,例如存储用户的信息,可以分key存储。
- 进行存储时,仅仅存储业务上必要的字段数据。
如果无法避免使用Bigkey,在查询时不要使用getAll将所有的数据都取出,仅仅通过get查询需要的部分。并且给key设置不同的过期时间,防止集中过期。
二、命令使用
尽量不要使用查询所有的命令,例如hgetall
,smembers
,keys
等。如果有需要进行遍历,推荐使用渐进式的命令,例如scan
渐进式地遍历key,假设我先向Redis中存入10个key:
使用scan
命令,这里的count
不是代表要扫描多少个精确的值。下一次的cursor
应该是本次查询出的结果,cursor为0时代表扫描结束,语法如下:
SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]

后续的扫描:
但是使用该命令可能存在查询重复的问题,需要用户在客户端自己进行防重复处理。
同时在线上环境,一些危险操作应该禁用,例如flushall,flushdb等全量删除操作,可以使用rename
机制修改这些命令。
并且Redis的数据库功能,实际上对于并发提升没有帮助,因为默认的16个数据库都是存在同一个redis中的,用户访问0数据库的时候,对于1数据库的访问的请求实际是阻塞的。
对于批量操作,可以使用pipeline
提高效率,但是一次pipeline
发送的命令数量同样不应过多,pipeline
是Redis非原生的批量处理命令,相当于数据库的批量查询,省去了每次单条查询的IO操作。(十次单条查询,十次IO,将十次单条查询合并成一次批量查询,只需要一次IO),Redis处理数据的时间,远远小于每次数据发送,建立连接的时间。但是该操作不具有原子性,无法保证同一批的操作要么同时成功,要么同时失败。它允许一批次的命令,部分执行成功,部分执行失败。最后会把所有的结果都返回,包括成功和失败的。
三、连接池的设置
对于连接的管理,Redis同样采用了和线程池类似的池化技术
,其中有三个关键的参数,可以根据需要在客户端进行配置:
- maxTotal:也称为maxActive,为整个连接池允许的最大连接数,不能超过redis的最大连接数(maxClient)10000,大于最大连接数的所有请求全部拒绝掉。客户端的个数 * maxTotal 不能大于maxClient。
- maxIdle:连接池中允许最大空闲的连接数。
- minIdle:连接池中确保最小空闲的连接数。

假设我当前连接池中有50个最大的连接:
- minIdle设置为10,表示整个连接池中最少有10个空闲的连接常驻。
- maxidle设置为20,表示整个连接池中最多有20个空闲的连接常驻。
高并发的场景下来了30个连接,在高并发时期过去之后,连接会释放到20(maxidle),这20个连接尽管没有使用,也会常驻在redis中。
默认的空闲连接数,选择的是maxIdle ,如果修改配置,那连接数会释放到10(minIdle),剩下的10个连接尽管没有使用,也会常驻在redis中。
maxidle和maxtotal设置的相同,则高并发场景下,每次拿连接都不用重新生成,连接不会频繁生成和销毁。
Redis的连接池是懒加载的,所以在面对系统刚启动,就有大量请求访问Redis的场景下,需要进行连接池预热
:可以创建一些redis的连接,然后执行一些简单的命令。需要等到所有的命令执行完成后,再将连接全部归还到连接池。
java
List<Jedis> minIdleJedisList = new ArrayList<>(jedisPoolConfig.getMinIdle());
// 第一步:提前获取 minIdle 个 Jedis 连接,触发连接池初始化
for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
Jedis jedis = null;
try {
jedis = pool.getResource();
minIdleJedisList.add(jedis);
jedis.ping(); // 触发连接
} catch (Exception e) {
logger.error(e.getMessage(), e);
} finally {
// 注意:此处不能立刻 close,否则连接立即归还,等于只初始化了一个连接
}
}
// 第二步:统一归还连接到连接池
for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
Jedis jedis = null;
try {
jedis = minIdleJedisList.get(i);
if (jedis != null) {
jedis.close(); // 归还连接
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
四、Redis的缓存过期策略
Redis的缓存过期,主要可以分为:
- 被动删除:给key设置过期时间的情况,设置了过期时间,到达过期时间后并不会立刻删除,而是需要用户再去访问一次才会删除(惰性删除)
- 定期删除:定期寻找过期的key,直接删除,防止某个key一直没有被访问到,也就无法被删除。
- 当前运用的内存超过限定时,触发主动清理策略
- 设置了过期时间的key:LRU算法,淘汰最久没有使用的key。LFU算法,淘汰最近一段时间内访问最少的key,热点数据优先选用。
- 针对所有的key:上述两种,加上一种随机删除策略。
- 不处理,不会剔除任何数据,对于新的请求拒绝,并且只响应读取操作。
针对热点缓存的淘汰,不应该按照时间(LRU),应该按照次数(LFU)。因为假设某个热点key,在1分钟之前被访问了100次,某个冷门的key,在1个小时之前被访问了1次,然后在10s前又被访问了一次,如果按照LRU,那么被访问了100次的热点key反而会被淘汰。
在配置文件中可以设置内存上限
和触发淘汰策略的阈值 淘汰策略
,默认的淘汰策略是noeviction:不会剔除任何数据,拒绝所有写入操作并返回客户端错误信息
补充:布隆过滤器
解决缓存三大问题中的缓存穿透,最为普遍的操作是缓存空值。但是上一篇中提到,如果针对不同的不存在value的key进行恶意攻击,那么redis中会存在很多空值,对于内存的占用也是可观的。而布隆过滤器是解决缓存穿透的另一种方案,大量的不存在的数据,缓存空值在redis中,占用的空间远远大于布隆过滤器。
开启布隆过滤器时,用户在set某个key时,除了向缓存中存,还要存到布隆过滤器中:进行多次hash运算(key的hash运算结果,对于bitmap数组的长度取模),每次运算出结果对应的位置,数组中对应下标的bit位从0改成1:
进行三次不同的Hash运算
在get key时,执行和set key相同的hash运算。假设多次hash运算,有一次每次运算出结果对应的位置bit位为0,证明该key不存在。
可以通过Redisson去实现布隆过滤器:
java
public class RedissonBloomFilter {
public static void main(String[] args) {
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
//构造Redisson
RedissonClient redisson = Redisson.create(config);
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList");
//初始化布隆过滤器:预计元素为100000000L,误差率为3%,根据这两个参数会计算出底层的bit数组大小
bloomFilter.tryInit(100000000L, 0.03);
//将zhuge插入到布隆过滤器中
bloomFilter.add("test1");
//判断下面号码是否在布隆过滤器中
System.out.println(bloomFilter.contains("test1"));//false
System.out.println(bloomFilter.contains("test2"));//false
System.out.println(bloomFilter.contains("test3"));//true
}
}
总结
