1.缓存适用场景
1.度密集型
2.存在热点数据
3.对响应时间要求高
4.对一致性要求不严格
5.需要分布式锁的时候
2.热点key
热点key,是指某个或者某些少量的key热度特别高,相比其他key而言访问特别频繁
解决思路:
a.热点key备份,key后面跟一个随机数,将key备份到好几个分配上。访问的时候也拿key+随机数进行访问。
b.热点key拆分,根据业务逻辑进行key拆分
c.本地缓存
堆内缓存:使用简单,jdk HashTable、Guava Cache等
堆外缓存:可以支持更大缓存空间,Ehcache、MapDB等
3.大key问题
string类型value特别大,大于512k。集合类型元素特别多,大于1w。
问题:由于redis单线程,获取key的时候,会导致其他命令阻塞
解决手段:根据业务场景拆分key,hash或者其他模式进行key的路由。
4. redis高效原因
a. 内存操作,非阻塞IO(IO多路复用),利用select poll epoll处理事件流。
b. Redis采用单线程处理命令,避免了不必要的上下文切换和竞争条件,也不存在多进程切换导致的CPU消耗
5. 分布式锁
5.1 redis用于高性能场景,保证AP。如果redis master宕机不保证强一致性,可能同时2个线程都获取到锁。
zookeeper用于强一致性和高可用场景,保证CP。
5.2 redis加锁使用SETNX加锁,同时设置一个超时时间(SET命令加NX选项,加上超时时间),超过该时间则自动释放锁,锁的value值可以设置为客户端的标识,释放锁的时候进行判断。
5.3 解锁的时候使用lua脚本解锁,保证判断和解锁动作原子性
5.4 redis锁缺点:1.过期时间设置太长或者太短都不合理,太长阻塞其他线程获取锁,太短任务还未执行完锁已经释放。2.获取不到锁无法阻塞。3.锁无法重入。基于以上问题,有人开发了一个Redisson jar包。Redisson是Redis官网推荐的java语言实现分布式锁的项目,是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)具体参考:https://ifeve.com/%E6%85%A2%E8%B0%88-redis-%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81-%E4%BB%A5%E5%8F%8A-redisson-%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/
5.5 服务器宕机,锁如何释放。redis靠超时时间被动释放,Redisson释放得更快一些,通过设置比较短的超时时间,然后守护线程不停的续租。zookeeper靠心跳检测,连接断开自动释放锁
5.6 锁等待实现。1.Redisson依靠pubsub订阅一个channel,锁释放后信号量释放。其他线程都在等待信号量,一旦释放就好唤醒获取锁。
2.在Zookeeper中方案就是Watch机制,监听一个节点是否产生变化,若变化会收到一个通知,当获取不到锁之后监听锁的那个临时节点即可,不过需要注意一个惊群效应,什么是惊群效应呢?假设A节点获取到锁,同时B、C、D、E也在获取锁,此时BCDE节点阻塞等待释放锁,当A节点释放锁之后,BCDE会同时起来争夺锁,但其实只有一个节点会获得到锁,就会浪费N-1个节点的系统资源去获取锁,惊动群而做无用功,解决方案是按顺序来,首先获取锁失败之后注册一个顺序节点,按照自己的顺序,向前一个节点注册Watch,这样一个个来即可解决惊群效应。具体参考:https://zhuanlan.zhihu.com/p/152240168
6. redis实现消息队列
redis作为消息队列:不保证不丢,生产者没有ack保证消息一定到达,消费端没有offset告知消费成功,是一个轻量级队列。Redis消息队列仅适用于轻量级、对时延有高要求、但是对消息可靠性不做高要求的场景
lpush 左边生产
rpush 右边生产
lpop 左边消费
rpop 右边消费
brpop 右边消费,阻塞等待一定时间
RPOPLPUSH 右边消费,消费的同时元素放入备份队列,消费成功后从备份队列删除。
7. redis实现排行榜
sorted set通过SkipList(跳跃表)和HashTable(哈希表)的双端口数据结构实现,删除、添加和更新一个元素的时间复杂度为O(log(N)),都是非常快速的操作;又由于成员的位置都是有序的,所以访问一个元素并获取其分数值时间复杂度为O(1);当要求排序的时候,Redis无需要做任何工作了,因为已经全部排好序了
7. redis实现最新项目列表
web项目通常有,列出最新的关注者列表,最新回复列表等。这个用redis队列实现,队列有一个容量,永远保留最新的数据。
8. guava cache
8.1相比java的map优点:
1.自动加载数据进入缓存
2.缓存控制总量,使用LRU算法淘汰内存
3.根据key访问时间计算过期时间
8.2数据结构:

8.3回收机制:
容量回收:到达容量上限,使用LRU算法淘汰缓存
定时回收:expireAfterAccess:多久没有访问后回收,expireAfterWrite:写访问多久后回收
引用回收:弱引用存储key/value,没有其他引用时,缓存可以被回收
8.4回收机制
调用get方法的时候进行回收,发现数据过期后会锁住segement去加载数据,其他线程会阻塞,容易导致线程阻塞。建议使用refreshAfterWrite替换expireAfterWrite,当前线程阻塞去获取数据,其他线程返回旧值。如果同时很多key都过期了,很多线程去加载数据也会阻塞,这个时候可以把加载数据的任务提交给线程池,所有的线程返回旧值
参考:https://albenw.github.io/posts/df42dc84/
9. 缓存的一些坑
9.1缓存一致性
3种方案对比:
a.先更新数据库后更新缓存不一致场景:两个线程A和B都做更新动作,A更新数据库,B更新数据库,B更新缓存,A更新缓存。
b.先删缓存后更新数据库不一致场景:A线程更新,B线程查询。A线程删除缓存,B线程查询不到缓存从数据库查询旧数据更新缓存,A线程更新数据库。
c.先更新数据库后删除缓存不一致场景:A线程更新,B线程查询。缓存刚好失效,B线程查询DB获得旧值,A线程更新数据库,删除缓存key,B线程将旧值设置缓存。这个概率比较小了,一般采取这种方案,如果一定要解决这种不一致,可以延迟删除缓存key
c方案里面删除缓存key如果失败了怎么办,如何补偿。一般的方案是消费mysql binlog通过mq异步删除。
参考文档:https://blog.csdn.net/koli6678/article/details/88202245
9.2缓存雪崩
同时很多key缓存失效,或者一个key缓存失效,同时大流量涌入数据库,导致数据库宕机。
方案对比:
1.保证只有一个查询查不到数据的时候去load DB。一般用redis的setnx命令加锁
2.缓存的value中设置一个时间,比key的过期数据小。如果发现过期了,加锁进行缓存更新,其他请求依然返回旧数据。
3.方案2的改进,如果发现过期了,提交异步任务进行缓存更新,主线程返回旧数据。
9.3缓存穿透
查询一个一定不存在的数据,如果每次缓存差不多,就去查询DB,导致DB压力激增。
解决方案:
1.DB不存在的时候,缓存一个空,设置过期时间
2.布隆过滤器
10. redis数据结构
1.ziplist
压缩队列,与linklist相比,无指针记录上下节点,通过偏移量进行计算和取值,是一片连续的内存空间。
2.intset
set元素少的时候使用intset
3.sorted set
有序的键值对,value是一个浮点数称为score。内部使用ziplist或者skiplist+hashtable实现
问题:
扩容如何不影响服务
迁移过程中,槽会被标记为迁移中,如果访问的key属于此槽,访问看在不在,如果不在重定位到新槽。
网友评论