0%

redis-faq

1. Redis 是单线程的, 为什么快?

  1. 单线程没有线程上下文切换问题, 没有资源竞争
  2. 采用IO多路复用的方式并发处理任务
  3. 纯内存操作, 没有磁盘IO限制
  4. CPU 不是 Redis 的使用瓶颈, 一般内存和网络带宽才可能是.

2. Redis 和其他 key-value 存储有什么不同

  • 所有命令都是原子性操作
  • 数据结构多样, 且对使用者透明
  • 运行在内存中, 可以持久化到磁盘

3. Redis 数据结构有哪些?

  • string
  • hash 表, H开头的命令
  • list
  • set
  • zset 有序集合

4. Redis 慢怎么办?

延迟
  1. 查看慢日志
    1
    2
    3
    4
    config set slowlog-log-slower-than 0 # 设置慢日志门槛, 到过这个值就会记录到慢日志中, 单位微秒
    config set slowlog-man-len 100 # 存储的慢日志数量

    slowlog show
  2. 检查最大内存策略导致
    对大对象进行删除, 过期和淘汰操作 (由于最大内存策略导致) 都属于阻塞操作
    如果经常对大对象进行删除, 过期和淘汰操作, 可尝试把大对象拆分成多个小对象.
    1
    config get maxmemory-policy

5. Redis 分布式锁

  1. 加锁

SET $resource_id $token EX $expired_time NX

resource_id: 锁名称, 用于标识资源的唯一ID
token: 随机值, 如果锁需要重入, 或者手动释放锁, 这个值必须保持一致.
expired_time: 锁自动过期时间

  1. 解锁

不使用 DEL 命令来释放锁, 而是发送一个 Lua 脚本, 这个脚本只在客户端传入的值和键的口令串相匹配时, 才对键进行删除.
比较 token 是否一致, 如果一致则可以解锁, 如果不一致, 说明锁已经被其他客户端获取了, 不能再操作.

1
2
3
4
5
6
if redis.call("get",KEYS[1]) == ARGV[1]
then
return redis.call("del",KEYS[1])
else
return 0
end

这个脚本可以通过 EVAL ...script... 1 resource_id token 命令来调用.

6. 常见问题

6.1. 缓存穿透

缓存穿透是指查询一定不存在的数据, 由于缓存没有命中时需要查询数据库, 查不到数据则不写入缓存, 这将导致这个不存在的数据每次请求都需要去数据库查询, 造成缓存穿透.

解决办法:

  1. 对所有可能查询的参数以hash形式存储, 在控制层先进行校验, 不符合则丢弃.
  2. 采用布隆过滤器, 将所有可能存在的数据哈希到一个足够大的bitmap中, 一个一定不存在的数据会被这个bitmap拦截掉, 从而避免了对底层存储系统的查询压力.
  3. 也可以采用一个更为简单粗暴的方法, 如果一个查询返回的数据为空 (不管是数 据不存在, 还是系统故障) , 我们仍然把这个空结果进行缓存, 但它的过期时间会很短, 最长不超过五分钟.

6.2. 缓存雪崩

如果缓存集中在一段时间内失效, 发生大量的缓存穿透, 所有的查询都落在数据库上, 造成了缓存雪崩.

这个没有完美解决办法, 但可以分析用户行为, 尽量让失效时间点均匀分布.大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线程 (进程) 写, 从而避免失效时大量的并发请求落到底层存储系统上.

解决办法:

  1. 在并发量不高的情况下, 可以使用加锁排队, 但是这样会影响系统吞吐量.
  2. 提前更新过期时间, 如果发现缓存即将失效, 则重新reload, 当然需要控制reload的数量
  3. 不同的key, 设置不同的过期时间, 让缓存失效的时间点尽量均匀

6.3. 缓存击穿

缓存击穿是指缓存中没有但数据库中有的数据 (一般是缓存时间到期) , 这时由于并发用户特别多, 同时读缓存没读到数据, 又同时去数据库去取数据, 引起数据库压力瞬间增大, 造成过大压力

  1. 设置热点数据永远不过期.
  2. 查询时加互斥锁, 保证同一时间只有一个线程区查询; 没有获取到锁的线程则轮询等待获取数据

6.4. 缓存一致性

场景: 在分布式系统中, 有一个频繁更新的数据要做缓存, 该怎么处理?

方案: 先更新数据库, 之后直接淘汰内存. 那么其他线程查询缓存时, 发现没有命中, 则通过查询数据库获取并缓存结果.

  1. 使用直接淘汰, 而不是更新缓存, 是为了防止高并发下更新缓存的顺序问题; 而如果要保证更新的顺序性, 需要有更新数据的版本, data + version 同时更新, 会有原子性的问题.
  2. 查询数据库需要考虑缓存击穿问题. 通过分布式锁保证只有一个线程去查询, 其他线程自旋等待.
  3. 如果要考虑数据更新完后, 还没来得及淘汰缓存, 就服务崩溃的问题. 也就是说不能等到下次更新数据时更新缓存. 那么这种依赖事务的数据不要放在缓存中. 如果要保证缓存一致性, 需要保证业务能幂等重试.

6.5. 什么样的数据能放在缓存中

  1. 业务完全依赖缓存中的数据, 如果缓存中没有数据则无法工作, 或者能够通过查询数据库恢复的数据.
  2. 缓存数据不一致不会影响业务.

7. Resource