Redis常见问题汇总
1、Redis为什么这么快
Redis有多快?单机可以支持约10w/s的QPS。
(1)纯内存操作
(2)5中数据类型选用的内部数据结构经过精心设计
(3)6.0以前采用单线程,省去了很多上下文切换的时间以及CPU消耗,不存在竞争条件,不用去考虑各种锁的问题
(4)基于非阻塞IO多路复用机制(select()、epoll())
通过一个线程监听多个socket的IO连接事件,从而实现在单个线程中处理多个连接的读写操作
这个线程是一个事件处理器(event loop);
该事件处理器会将每个连接对应的 文件描述符 注册到事件监听机制中,并在事件就绪时调用相应的读写函数来处理IO操作,这样就可以单线程处理多个连接IO操作
2、Redis的过期策略以及内存淘汰机制
redis采用的是定期删除+惰性删除策略。
2.1、为什么不用 定时删除 策略
定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下尤其不适用。
2.2、定期删除+惰性删除是如何工作的
(1)定期删除,redis默认每个100ms检查,检查有过期key则删除。
需要说明的是,redis不是每隔100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。
(2)于是,惰性删除派上用场。在获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了,如果过期了此时就会删除(懒汉式)。
2.3、采用定期删除+惰性删除就没其他问题了么?
不是的,如果定期删除没删除key,也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。在redis.conf中配置。
2.4、Redis内存淘汰机制
当Redis的内存使用达到配置的最大内存限制时,会触发内存淘汰机制来释放部分内存空间
(1)allkeys-lru(推荐)
当内存不足以容纳新写入数据时,移除最近最少使用的key
(2)allkeys-random(不推荐)
随机移除某个key
(3)volatile-lru(不推荐)
在设置了过期时间的键空间中,移除最近最少使用的key
(4)volatile-random(不推荐)
在设置了过期时间的键空间中,随机移除某个key
(5)volatile-ttl(不推荐)
3、Redis并发环境下使用
3.1、如何解决redis的并发竞争key问题
(1)事务
通过使用MULTI、EXEC和WATCH命令,可以将多个操作作为一个事务进行执行,但是生产环境redis一般是集群的,不适用。
(2)乐观锁
使用版本号(Version)来实现乐观锁。每次对键进行修改时,都会更新该键的版本号;在读取时,先获取当前版本号,然后进行操作,最后再次验证版本号是否一致,如果版本号不一致,说明该键已被其他客户端修改,需要重新尝试。
(3)分布式锁(推荐)
是一种在分布式系统中解决并发竞争的常见机制,可使用Redisson
(4)队列Queue
将需要对同一个键进行操作的请求放入队列中,然后通过一个单独的线程或来处理队列中的请求。这样可以保证对同一个键的操作按序执行,避免并发竞争。
3.2、MySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据
(1)缓存淘汰策略
redis 内存大小上升到一定大小时,就会施行数据淘汰策略,可选LRU最近最少使用淘汰策略;
(2)数据预热
在系统启动时,将热点数据预先加载到Redis中。可以使用jedis的管道(Pipeline )批量初始化加载到Redis。
Jedis jedis = JedisUtil.getJedis();
// 管道
Pipeline pipelined = jedis.pipelined();
for (int i = 0; i < 10000; i++) {
pipelined.set("cc_"+i, i+"");
}
//执行管道中的命令
pipelined.sync();
4、Redis与其他缓存对比及多级缓存
4.1、redis相比memcached有哪些优势?
(1)memcached只支持字符串,redis支持更为丰富的数据类型
(2)redis的速度比memcached快很多
(3)redis可以持久化数据到磁盘,memcached只能存在内存中
4.2、Redis的多级缓存
优先顺序:本地内存(咖啡因)-->redis(回填内存)--->数据库(回填内存、redis)
5、Redis 应用场景
5.1、普通缓存
5.2、会话缓存
将用户会话信息存储在Redis中,实现分布式会话管理
5.3、消息队列/发布订阅
list/set的lpush/rpop实现消息队列。
(1)将消息推入队列
使用lpush命令将消息推入队列的左侧(头部);
(2) 队列中取出消息
使用rpop命令从队列的右侧(尾部)弹出消息,List结构的话可实现先进先出的消息处理。
实现消息队列中list和set区别:
- list可实现消息的顺序性保证FIFO先进先出,set无序;
- list允许存储重复数据set不行;
5.4、排行榜/计数器
5.4.1、排行榜
(1)常见排行榜需求
- 添加一条记录:将一个用户或商品的得分添加到排行榜中;
- 更新一条记录:更新某个用户或商品的得分;
- 删除一条记录:将某个用户或商品从排行榜中删除;
- 查询排名:查询某个用户或商品的排名;
- 查询得分:查询某个用户或商品的得分;
- 查询排行榜:按照得分从高到低查询排行榜前N个用户或商品。
(2)实现原理(Sorted Set)
有序集合(Sorted Set)中的每个元素都有一个分值(score),可根据分值进行排序。
因此,我们可以将用户或商品的得分作为有序集合中的分值,将用户或商品的唯一标识作为有序集合中的成员。
(3)具体实现
- 添加/修改一条记录:redis.zadd("leaderboard", {"user1": 100});
- 删除一条记录:redis.zrem("leaderboard", "user1");
- 查询排名:redis.z revrank("leaderboard", "user1");
- 查询得分:redis.zscore("leaderboard", "user1");
- 查询排行榜:redis.zrevrange("leaderboard", 0, 9)
5.4.2、计数器
(1)String可变字符串实现
incry指令增加计数器的值
Jedis jedis = new Jedis("localhost");
// 存储计数值
jedis.set("counter", "0");
// 增加计数值
jedis.incrBy("counter", 5);
// 获取计数值
String counterValue = jedis.get("counter");
(2)Redisson的RAtomicLong原子类 实现
RedissonClient redisson = Redisson.create(config);
// 获取或创建计数器对象
RAtomicLong counter = redisson.getAtomicLong("counter");
counter.incrementAndGet(); // 增加计数值1
long counterValue = counter.get(); // 获取计数值
5.5、分布式锁(Redisson)
6、Redis性能优化及线上问题排查
6.1、redis优化方案
性能瓶颈:内存、网络贷款、CPU、持久化配置、锁竞争、阻塞操作、数据结构选择
6.1.1、合理选择合适的数据结构、回收策略、过期时间、持久化配置
6.1.2、内存优化
避免存储不必要的数据、大型数据。可通过压缩数、设置过期策略实现
6.1.3、批量操作选管道机制
批量操作或者数据预热时,使用java的客户端Jedis的Pipeline管道机制(redis没有该命令但是Jedis有)。
6.1.4、使用Kubernetes 的监测工具,监控各种性能参数
6.2、redis线上问题排查
6.2.1、开启 慢日志并分析(或Kubernetes分析)
config配置开启慢日志记录,例如设置阈值为5则保利最近500条记录。
可查看哪些命令执行耗时、哪些频繁、哪些消耗资源多。
6.2.2、bigkey排查
可通过bigkeys命令扫描,如:redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 1 ( -i 1代表1秒扫描一次)