1、基础数据结构
1.1.String
key和value的形式,不同数据接口差异就在value可以是String类型,整数类型等;
内部结构实现类似于java的ArrayList,采用与分配冗余空间的方式老减少内存的频繁分配;
当字符串小于1MB时,扩容都是加倍现有的空间;当字符串大于1MB时,扩容时一次只会多扩1MB;字符串最大长度为512MB
字符串结构叫做SDS,Simple Dynamic String它的结构如下:
struct SDS<T>{
T capacity; //数组容量
T len; //数组长度
byte flags; //特殊标志位
byte[] content; //数组内容
}
两种存储方式:
长度特别短使用embstr形式存储
长度超过44字节时采用raw
这里为什么是44可以参考:【Redis6快速深入学习04】Redis字符串(String)的使用和原理-CSDN博客
redis的append了解下
1.2.Hash
相当于java中的HashMap,无序字典,数组+链表结构
redis的hash的值只能是String,refresh方式和java也不一样,为了性能是渐进的方式,在refresh时会保留新旧两个hash结构,查询时会查询两个hash,在后续的定时任务以及hash操作指令中,逐渐将旧hash的内容迁移到新hash中。
扩容:正常情况下,当hash表中的元素个数等于数组的长度时,就会开始扩容,扩容的新数组是原数组的2倍。当redis在做bgsave,为了减少内存页的过多分离(copy on write),redis尽量不去扩容,如果数组非常满了,元素个数达到数组长度5倍,还是强制扩容。
缩容:元素个数低于数组长度的10%就会缩容,缩容不考虑bgsave
1.3.List
相当于java中的linkList,redis的列表结构常用来做异步队列使用
右边进左边出:队列
右边进右边出:栈
底层使用快速链表(quicklist)的结构;
元素较少时使用一块连续内存存储ziplist(压缩列表),当数据量较多时才会改成quicklist
1.4.Set
相当于java中的HashSet,内部键值对都是无序的、唯一的。
1.5.ZSet
类似于java中的sortedSet和hashMap的结合体,一方面它是一个set,保证value唯一,另一方面它可以为每个value赋予一个score,代表权重。
内部采用跳跃列表数据结构实现;
2、分布式锁超时问题
redission的看门狗(watch doy)机制,自动续期
3、位图、hyperLogLog、布隆过滤器
3.1、位图
场景:一些bool类型的存储,如用户一年的签到记录,如果使用正常的key/value会有365条记录,当数据量很大时,存储空间很吓人;
redis提供了位图数据结构,这样每天签到记录只占用一个位,大大节省了空间;
用法:
为某个键值某位置设置值
setbit key 位置 0/1
setbit 20230308 11 1; //意思可理解为:2023年03月08日用户id为11的用户在线
获取某个键值某位置的值
getbit day11 11; //获取day11的11位置的值
统计某个键值的数量
bitcount day11; //计算day11的数量
查找:bittop day11 0;
3.2、hyperLogLog
场景:统计网站每天UV,数据量较小时设置Set即可,当用户量非常大时,set集合统计就浪费空间
redis提供hyperLogLog提供布精确的去重方案,误差在0.81%
用法:
pfadd key user1; //往key中增加用户
pfcount key; //统计key的计数
pfmerge key3 key1 key2;//将key1、key2的统计合并到key3
需要占用12KB的空间
原理:
3.3、布隆过滤器
场景:3.2中没有pfcontains方法,怎么去重?正常使用数据记录维护当然可以,但占用的数据和存储空间也是很大的。
redis提供了布隆过滤器(bloom filter)他就是解决去重问题,并在空间上节省90%,只是有稍微不准确,存在一定误判,针对存在场景存在误判,但对于不存在是没有误判的;
用法:
bf.add key user1;//向key中添加user1
bf.exists key user1;//判断user1是否存在
原理:
当一个元素加入布隆过滤器中的时候,会进行如下操作:
使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)
根据得到的哈希值,在位数组中把对应下标的值置为 1
对给定元素再次进行相同的哈希计算
得到哈希值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值存在布隆过滤器当中,如果存在一个值不为 1,说明该元素不在布隆过滤器中
4、redis的实现原理
redis是单线程的
非阻塞IO
多路复用,时间轮训
5、持久化
5.1.RDB(快照)
使用操作系统的多进程Copy On Write
save:同步执行快照
bgsave:异步执行快照
5.2.AOF(append only file)
将每个命令追加到磁盘文件中;
会对AOF进行瘦身
6、淘汰策略
当Redis内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交互,会让redis性能急剧下降,当内存超出maxmemory时,redis提供了集中可选策略来让用户自己决定如何腾出心空间以继续提供读写服务;
6.1、noeviction:默认策略
不会继续服务写请求,不会丢数据,业务不能继续进行;
6.2、volatile-lru
尝试淘汰设置了过期时间的key,最少使用的key优先被淘汰。没有设置过期时间的不会被淘汰。
6.3、volatile-ttl
尝试淘汰设置了过期时间的key,比较key剩余的ttl,ttl越小优先被淘汰。没有设置过期时间的不会被淘汰。
6.4、volatile-random
尝试淘汰设置了过期时间的key,随机选择。没有设置过期时间的不会被淘汰。
6.5、allkeys-lru
全体key中,最少使用的key优先被淘汰;
6.6、allkeys-random
全体key中,随机
7、缓存击穿、缓存雪崩、缓存穿透
7.1缓存击穿
单个key过期,并发场景下导致数据库压力过大而崩溃
解决方案:
1、互斥锁,查询数据库时候采用互斥锁(分布式锁)
2、热点数据预加载、
3、自动刷新
7.2缓存雪崩
多个key同时过期,并发场景下导致数据库压力过大而崩溃
解决方案:
1、构建多级缓存架构: nginx缓存 + redis缓存 +其他缓存(ehcache等)
2、使用锁或队列: 用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况
3、设置过期标志更新缓存: 记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存。
4、将缓存失效时间分散开: 比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
5、不设置过期时间: 热点数据可以考虑不失效,后台异步更新缓存,适用于不严格要求缓存一致性的场景。
6、双key策略:主key设置过期时间,备key不设置过期时间,当主key失效时,直接返回备key值。
7.3缓存穿透
缓存中没查到,查询数据库也没有,高并发时,导致数据库压力过大出现崩溃
解决方案:
1、缓存空对象: 当 MySQL 返回空对象时, Redis 将该对象缓存起来,同时为其设置一个过期时间。当用户再次发起相同请求时,就会从缓存中拿到一个空对象,用户的请求被阻断在了缓存层,从而保护了后端数据库,但是这种做法也存在一些问题,虽然请求进不了 MySQL ,但是这种策略会占用 Redis 的缓存空间。
2、业务逻辑前置校验: 在业务请求的入口处进行数据合法性校验,检查请求参数是否合理、是否包含非法值、是否恶意请求等,提前有效阻断非法请求。比如,根据年龄查询时,请求的年龄为-10岁,这显然是不合法的请求参数,直接在参数校验时进行判断返回。
3、用户黑名单限制: 当发生异常情况时,实时监控访问的对象和数据,分析用户行为,针对故意请求、爬虫或攻击者,进行特定用户的限制;
4、进行实时监控: 当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务
5、布隆过滤器: 我们知道,布隆过滤器判定不存在的数据,那么该数据一定不存在,利用它的这一特点可以防止缓存穿透。