1. 在项目中缓存是如何使用的?
结合自己的公司的项目, 回答以下问题:
- 项目哪里用了缓存?
- 为什么要用?
- 用了可能会带来什么问题?
- 怎么解决这些问题?
- 项目的缓存架构是怎么样的?
如果面试官没有问这些问题, 我们也要主动和面试官聊聊.
2. 为什么要在项目中用缓存?
(1) 高性能
如果不使用缓存, 每次请求都有较大的延迟, 比如600ms, 而如果每次请求都走缓存, 可能2ms就搞定了.
(2) 高并发
在高并发场景下, 比如秒杀之类的促销活动, 如果所有请求都直接查询数据库, 会导致数据库宕机, 这个时候就需要缓存来分担数据库的压力.
3. 用了缓存之后可能会带来什么问题? 如何解决?
(1) 缓存与数据库双写不一致
我们先了解下最经典的缓存和数据库的读写模式:
- 读的时候先读缓存, 再读数据库. 如果缓存中没有, 则从数据库中读取数据写入缓存.
- 修改数据的时候, 先删除对应的缓存, 再更新数据库. (或者先更新数据库, 再删除缓存)
从上面这个读写模式中我们可以发现, 在修改数据的时候, 只会更新数据库, 而不会同步更新缓存, 缓存是下次读的时候再更新. 这样做的原因是更新缓存的代价比较大, 比如对于一些比较复杂的业务场景, 缓存数据可能涉及到多张表的查询计算, 同时这个缓存数据还不一定会被频繁的访问, 所以综合考虑, 修改数据的时候直接删除缓存, 下次读的时候再更新缓存是最合适的.
缓存与数据库双写不一致是什么情况呢? 修改数据的时候, 先删除对应的缓存, 再更新数据库, 如果在高并发的场景下, 删除缓存后还没来得及更新数据库, 这时又来了一个读请求, 将数据库里的旧数据更新到了缓存中, 导致缓存与数据库的数据不一致. 解决办法是, 根据数据的唯一标识, 将同一数据的读写请求写入同一个队列中, 每个队列使用一个线程来处理, 使读写请求串行化地执行. 这里可以优化下, 如果队列中已经存在读请求, 那下一次读请求就不用再入队了, 可以等待一段时间, 待前一个读请求更新缓存后再去查询缓存, 如果缓存中不存在就直接查数据库.
这里要注意一点, 读写串行化执行是非常影响吞吐量的, 在这种情况下要保持之前的吞吐量, 需要多部署好几倍的机器, 所以除非那些严格要求缓存和数据库数据一致的场景, 我们都不要使用这个方案.
(2) 缓存雪崩
redis系统崩溃后, 导致原本走redis的请求全部到了MySQL, 导致MySQL也直接崩溃. 解决办法就是redis系统要做主从和哨兵来保证高可用, 同时加个本地缓存(ehcache)来缓存一部分热点数据, 再使用限流组件(hystrix)来限制流量. 这样调整之后, 请求先查询本地缓存, 然后再查询redis缓存, 最后再查询MySQL. 当redis系统整个崩溃后, 我们可以通过限流组件来限制流量, 使MySQL正常运行.
还有一种缓存雪崩的情况是, 我们给缓存设置了相同的失效时间, 导致缓存在某一时刻同时失效, 请求全部到达MySQL, 导致MySQL崩溃. 解决办法是为缓存设置失效时间时加上一个随机值, 如set(key, value, time + Math.random()*10000).
(3) 缓存穿透
这种情况一般出现在恶意攻击的场景, 黑客不断请求数据库里不存在的数据(比如id=-999), 数据库里不存在那redis里肯定也不存在, 这就会导致请求都绕过redis, 直接到达MySQL, 导致MySQL崩溃. 解决办法就是如果在MySQL里查不到, 就直接在redis中写入一个NULL(过期时间不要太长), 这样下次请求就直接走redis. 或者使用布隆过滤器(Bloom Filter介绍), 先使用布隆过滤器判断key是否存在, 存在再走redis和MySQL, 不存在就直接return.
(4) 缓存击穿
我们给一个热点数据设置了过期时间, 当这个数据过期时, 大量的请求就会到达MySQL, 导致MySQL崩溃. 解决办法就是设置热点数据永不过期, 或使用reids互斥锁来更新缓存.
public static String getData(String key) throws InterruptedException {
//从Redis查询数据
String result = getDataByKV(key);
//参数校验
if (StringUtils.isBlank(result)) {
try {
//获得锁
if (reenLock.tryLock()) {
//去数据库查询
result = getDataByDB(key);
//校验
if (StringUtils.isNotBlank(result)) {
//插进缓存
setDataToKV(key, result);
}
} else {
//休眠一会再拿
Thread.sleep(100L);
result = getData(key);
}
} finally {
//释放锁
reenLock.unlock();
}
}
return result;
}
(5) 缓存并发竞争
一般出现缓存并发竞争的场景:
- 多个客户端并发地写同一个key, 且写命令的执行顺序对结果会有影响
- 多个客户端并发地读同一个key, 如果满足条件就修改值后写回去.
解决办法就是使用乐观锁或分布式锁. 乐观锁和分布式锁介绍
4. 聊聊redis的线程模型
前置知识: 网络编程之BIO/NIO基础
Redis的线程模型叫做文件事件处理器(reactor模式), 由4个部分组成: 多个socket连接, IO多路复用器, 文件事件分派器, 事件处理器.
文件事件处理器是以单线程模式运行的, 通过IO多路复用器监听多个socket, 当事件发生时, 由文件事件分派器调用具体的事件处理器进行处理. 详细流程如下:
- 客户端向Redis发送建立socket连接的请求.
- IO多路复用器监听到AE_READABLE事件, 交给文件事件分派器进行处理.
- 文件事件分派器调用连接应答处理器, 建立socket连接通道, 然后将该socket连接的AE_READABLE事件与命令请求处理器绑定.
- 客户端向Redis发送命令请求.
- IO多路复用器监听到AE_READABLE事件, 交给文件事件分派器进行处理.
- 文件事件分派器调用命令请求处理器, 执行客户端的命令请求.
- 客户端读取命令结果时会产生AE_WRITABLE事件, 命令回复处理器将命令执行结果返回给客户端.
(1) 事件
- AE_READABLE
当客户端请求建立socket连接或客户端对socket进行写操作时, 会产生AE_READABLE事件. - AE_WRITABLE
当客户端对socket进行读操作时, 会产生AE_WRITABLE事件.
当一个socket同时发生了AE_READABLE事件和AE_WRITABLE事件, 那么文件事件分派器优先处理AE_READABLE事件, 然后再处理AE_WRITABLE事件.
客户端向redis发送命令请求, redis端就会发生AE_READABLE事件. redis执行完命令后的返回结果是AE_WRITABLE事件.
(2) 事件处理器
- 连接应答处理器
处理客户端建立socket的请求, 为其创建socket连接. - 命令请求处理器
执行客户端的命令请求. - 命令回复处理器
返回命令执行结果.
5. 为什么redis是单线程的却还可以支撑高并发?(为什么单线程的redis比多线程的memcached效率要高得多?)
- 纯内存操作.
- 基于非阻塞的IO多路复用机制.
- 单线程反而避免了多线程频繁的上下文切换问题.
6. redis和memcached有什么区别?
- memcached只支持简单的key-value, 而redis拥有更丰富的数据类型.
- memcached的数据只能保存在内存中, 而redis支持数据持久化.
- memcached没有原生的集群模式, 而redis拥有.
- memcached是多线程的, 而redis是单线程的, 单线程的redis性能更高.
7. redis有哪些数据类型? 分别适合什么业务场景?
(1) string
字符串. 可以用来做计数器(比如点赞), 验证码, 限流(访问计数, 设置过期时间)等
(2) hash
哈希键值对. 一般用来存对象, 如用户中心的数据(未读消息数, 未读评论数, 最近收听等).
(3) list
列表. 可以用来做粉丝列表, 书籍评论列表, 滚动分页或叫下拉刷新(lrange), 消息队列等.
(4) set
无序集合, 自动去重. 一般用于去重, 交集, 并集, 差集操作, 比如共同好友功能.
(5) sorted set
有序集合, 自动去重且排序. 可以用来做排行榜, 热门评论等.
8. redis的过期策略是怎样的? 内存淘汰机制都有哪些? 手写一个LRU算法?
(1) redis的过期策略
redis中设置了过期时间的数据, 并不是到了过期时间就立即删除, 而是采用定期删除+惰性删除的策略.
- 定期删除
redis默认每隔100ms就随机抽取一些设置了过期时间的数据, 检查其是否过期, 如果过期就删除. - 惰性删除
客户端获取数据时, redis会先检查数据是否过期, 如果过期就删除, 不返回结果.
这两种策略结合起来也不能保证过期的数据会被删除, 如果大量过期的数据堆积内存中, 最后导致内存耗尽, 这时就需要使用内存淘汰机制.
(2) 内存淘汰机制
- volatile-lru
在设置了过期时间的数据中, 淘汰最近最少使用的. - volatile-random
在设置了过期时间的数据中, 随机淘汰一个. - volatile-ttl
在设置了过期时间的数据中, 淘汰最早过期的. - allkeys-lru
在所有数据中, 淘汰最近最少使用的. - allkeys-random
在所有数据中, 随机淘汰一个. - noeviction
直接报错.
(3) 手写一个LRU算法
没必要手写最原始的LRU算法, 借助jdk来实现即可.
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
//容量
private final int CACHE_SIZE;
public LRUCache(int cacheSize) {
/*
1. 因为当LinkedHashMap中的数据达到 initialCapacity * loadFactor 时会进行扩容, 所以初始容量这么设置
2. accessOrder设置为true时, 表示让LinkedHashMap按照数据的访问顺序来排序, 否则是插入顺序.
*/
super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true);
this.CACHE_SIZE = cacheSize;
}
/**
* 当LinkedHashMap里的数据满了之后, 下次插入数据时就淘汰最近最少使用的数据
* @param eldest
* @return
*/
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > CACHE_SIZE;
}
}
9. 如何保证redis的高并发和高可用? redis主从复制的原理是什么? redis哨兵的原理是什么?
(1) 如何保证redis的高并发和高可用?
主从架构, 一主多从. 单主用来写入数据, 可以提供几万的QPS, 多从用来查询数据, 可以提供10W+的QPS. 然后使用哨兵来进行主备切换, 保证redis的高可用.
如果缓存的数据量很大, 单机无法容纳, 那就不能使用主从架构, 需要使用redis集群.
(2) redis主从复制的原理
见Redis基础.
(3) redis哨兵的原理
见Redis基础.
10. redis的持久化有哪几种方式?不同的持久化机制都有什么优缺点?持久化机制具体底层是如何实现的?
见Redis基础.
11. redis集群模式的工作原理能说一下么?在集群模式下, redis的key是如何寻址的?分布式寻址都有哪些算法?了解一致性hash算法吗?
见Redis基础.
12. 你们公司redis生产集群的部署架构是怎样的?
(1) 回答方向
- 你们公司的redis是主从架构, 还是集群架构?
- 如果是主从架构, 有没有做高可用?
- 如果是集群架构, 用的是哪种集群方案?
- 有没有开启持久化机制, 确保数据可以进行恢复?
- 线上redis给几个G的内存? 设置了哪些参数?
- 压测后你们redis集群承载多少QPS?
(2) 实例
我们公司使用的是集群架构, 总共有10台机器, 5主5从, 5个主节点对外提供读写服务, 5个从节点对外提供读服务. 每个主节点的读写高峰QPS会达到5万, 5台主节点最多是25万QPS. 每个机器的配置是32G内存+8核CPU+1T磁盘, 分配给redis进程的内存是10G, 因为超过10G可能会有问题, 总共可以存储50G的数据. 因为每个主节点都挂载了一个从节点, 当主节点挂掉之后会自动进行主备切换, 所以集群是高可用的. 我们往redis中写入的是商品数据, 每条数据10KB, 100条数据是1M, 10万条数据是1G, 常驻内存的是200万条商品数据, 占用内存是20G, 仅仅不到总内存的50%. 目前高峰期每秒3500左右的请求量.
来源:CSDN
作者:椰子Tyshawn
链接:https://blog.csdn.net/litianxiang_kaola/article/details/104656089