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秒扫描一次)