字符串,用于数据缓存,常用于:计数器、限流、数据缓存、incr、dcr、分布式锁。
列表,用于数据缓存,使用场景:栈、队列、阻塞队列、消息队列、粉丝列表。
哈希,用于java对象的缓存,使用场景:缓存普通的java对象。
集合,Stirng类型的无序元素合集,但元素唯一,使用场景:用户标签、共同好友。
有序结合,元素唯一,同时根据score进行排序,默认升序。使用场景:top
字符串的存储结构分为:SDS和直接存储。编码方式分为:int、raw、embstr,主要体现在内存结构不同。
simple dynamic string 简单动态字符串,与普通的C语言数据结构做了优化,主要有一下几个有点:
直接存储可以整数化的字符串,长度小于long,超过后转化为embstr。
字符串长度小于44字节,使用embstr存储,创建时只需要分配一次内存,内存连续。但是属于只读,一旦修改后默认转化为raw。
44字节也是有考虑的,因此cpu的cache line=64字节,减去数据结构的20字节,因此只能存储44字节的数据。
字符串长度大于44字节,使用raw存储,创建时需要分配两次内存,内存不连续。
普通的双向列表,每个节点都有pre和next指针。缺点:当少量的小数据存储时,有可能指针的内存占用大于数据存储,造成了相当大的内存浪费。
ziplist:压缩列表,通过内存连续,减少指针,压缩内存占用。每个节点=前节点的长度+数据存储,正常。
优点:1 减少内存占用。2 少量数据,可以利用cpu的缓存行(64byte)。
缺点:1. 列表不能太长,即数据不能太多;2 单节点的数据不能太大。
原因:因为每个节点通过前一节点的长度+当前节点数据,如果节点过多,数据过大,频繁的修改容易触发批量的内存分配和数据移动。
在原有列表基础上,内置了ziplist,兼并两者的有点。quicklist整体是普通列表,有pre和next指针,但是列表的节点,存储的ziplist,当ziplist的内存过多,就单独开辟一个新的quicklist节点,保证每个ziplist的数据大小可控。
ziplist选择:当保存的所有键值对字符串长度小于 64 字节,并且键值对数量小于 512 时使用ziplist。
hash采用hashtable存储,也可以认为是字典结构。扩容时:采用渐进式的扩容,因为数据量过大一次性rehash,会造成线程阻塞,渐进式rehash,是持有两个数组,每次对hash的操作,都会操作原数组中的数据到新数组中。
当保存的所有键值对字符串长度小于 64 字节并且键值对数量小于 512 时使用ziplist ,否则使用字典的方式。
集合,元素唯一。采用hashtable存储。
有序集合,元素唯一,按照分数进行升序排序。采用ziplist、HashMap+skiplist两种存储方式。
说明:每个元素小于64字节,同时元素数量小于128时,采用ziplist存储。
之所以设计这么多数据机构,主要是提高访问速度。例如:ziplist减少了内存占用,利用缓存行。但是当数据量过大时,就需要切换成其他数据,例如:hashtable,quicklist,skiplist。
缓存,缓存相关的查询结果,方便第二次查询时,可以快速访问。
因为Redis是独立的部署,因此可以在分布式系统中,作为共享中介,例如:全局session,单点登录。
String配合setNx命令,实现分布式锁。
通过String存储int id,采用incr批量获取id。
通过String存储int,使用incr和decr,实现计数的加减。
通过String存储int,使用dcer,针对指定的ip或目标,进行访问限制。
主要是bitmap数据结构,可以通过bit数组的形式,记录每个id的存在与否。
list可以作为栈使用,利用先进后出。例如:用户发完微博后,lpush入队列,然后lrange,可以获取最新的微博。
利用List结构,生产者lpush,消费者rpop,即可完成相关的生产和消费。上述只是简单的应用,因为消费者rpop时,存在数据丢失的风险,可以使用rpoplpush:从尾部移除元素,并添加到指定列表的头部,并返回该元素。如果要考虑阻塞问题,可以使用:brpoplpush:从尾部移除元素,并添加到指定列表的头部,并返回该元素,如果尾部没有元素,一直阻塞到可以弹出,当然也支持等待超时。
set是无序,但是唯一的集合,可以通过spop移除随机元素,实现抽奖。
利用set,使用sadd表示点赞/签到/打卡,srem表示:取消点赞/签到/打卡。sismember 是否点赞/签到/打卡。
利用Set,通过sadd添加标签。
利用set,通过SDIFF:差集;SINTER:交集;SUNION :并集实现相关的互相关注,共同好友等。
利用Sort set,通过
利用String,对“你好,北京”,拆分成:“你”,“你好”,“北”,“北京”,上述拆分次作为key,value=“你好北京”,这就是存储了“你好北京”的倒排索引。
利用list,通过lpush,插入头部,显示最近的内容。
普通的有序列表,存在一个问题,如果查找某一个元素,需要全部遍历一遍,时间复杂度O(N),同样添删除元素,也需要从头遍历。降低了访问效率。
跳表:针对普通有序列表,抽出一层来,作为索引列表。
当查询数字30时,正常遍历,需要遍历7次,从3-30。而有了索引层之后,直接从3-18-30即可,只需要3次。这样就减少了遍历次数。 把索引层和数据层稍微旋转一下,就是个树,第二层索引层的3就是根节点,第二层索引层的3就是左子树,第一层的18就是右子树。
之所以用跳表取代树,主要原因:树的操作太复杂,尤其当添加节点/删除节点时,可能涉及数的旋转等操作。
跳表的缺陷:跳表实际是通过空间换时间,因此会有内存浪费。比如示例图中:第一层索引和第二层索引都是冗余数据。
主要指缓存数据与数据库数据的一致性,主要发生在数据变更后,如何确保数据库数据与redis中的数据一致。因为更新数据库与更新redis不是原子操作。
缓存策略中的一种。
写操作时:先写入数据库,后写入缓存。
缺点:
写操作时:先写入缓存,后写入数据库。
缺点:
写操作时:先写入数据库,后删除缓存。与“先写数据库,后写缓存”缺点一致,但是删除缓存比写入缓存的间隔时间更短,毕竟只有一个删除操作。
整体的写操作,都是按照先写入数据库,保证数据不丢失,然后再考虑不一致性问题。因此对唯一性要求更严格的系统,可以考虑其他的策略。例如:
缓存策略的一种:调用方只与缓存打交道,如果缓存未命中,由缓存组件负责数据库读写。
写操作时:当缓存中没有数据时,先写入缓存,后写入数据库,属于按写分配。
写操作时:当缓存中没有数据时,写入数据库,读取时再重新获取。
相对写穿透缓存策略而言,后置写是通过异步的形式更新到数据库。写缓存后发送异步通知,异步通知消费后,写数据库。
优点:1. 写缓存与写数据库分离,减少与磁盘的交互,提高效率;2. 可以对写数据库命令进行优化。例如:多个写操作,是否可以批量执行。
缺点:1. 存在数据丢失的风险。2. 有延迟。
适用场景:用于读少写多的场景,Linux系统的页缓存和MySQL InnoDB 引擎的Cache Pool其实就是使用的WriteBack策略。相较于Write through 而言拥有更高的写入性能.
已缓存了很多数据,但是每次请求都未命中。例如:数据id范围1-1000,但是通过非法伪造id(负数,1000以上),缓存肯定无法未命中,大量的读请求压力到数据库,这就是缓存穿透。
需要在读取缓存前,添加一个过滤器,这样通过过滤器的请求,才能读取缓存信息。
组织架构id、用户id等作为缓存的key,有一定规律,因此可以直接对id进行校验,例如:组织架构id,从1-8000,那么id不在该范围内,可以直接忽略。
如果缓存的key没有范围规律,可以考虑布隆过滤器。布隆过滤器有两种实现:1. 本地的布隆过滤器;2 redis的布隆过滤器。
当缓存失效后,瞬时有某个key的大量请求,最终请求落入数据库中。
缓存未命中后,请求数据库查询时,可以加锁,这样可以保证只有一个请求能落入数据库中,减轻数据库压力。
或者可以针对一些可能的热点key,走定时刷新机制。
大量的缓存,同一时刻失效,导致大量的请求落入数据库中。
可以封装redis操作,针对指定失效时间的key,添加随机数,是失效时间相对离散。当然上述方案主要是针对,对失效时间要求不严格的相关操作。
redis分布式锁的使用场景有:
利用redis提供的setNx命令,该命令只有当key不存在时,才能返回set成功。
上述锁释放时,需要给锁设置失效期,但是设置的失效期内,仍然没有完成业务操作,可能存在因为锁失效,导致其他线程获取锁的情景。
方案:看门狗,即通过定时任务自动给key续期,可以设置续期次数(一般三次左右)。
当前线程获取分布式锁后,如果再次获取时,需要支持锁重入。
方案:setNx时,value=线程id,加锁时,先判断线程id是否一致,如果一直说明可以重入,同时用另外的key记录重入次数,用于解锁。
缺陷:判断线程id与当前线程id是否一致,如果一致,设置重入次数,因此该操作并非原子操作,有可能判断一致后,锁被释放了,或者超期了,那么当前线程就不能重入。因此需要将上述命令整合,一次性在redis设置好,而不是通过java代码进行设置。
lua脚本:整合多个redis操作命令,生成一个lua命令(脚本),redis支持lua脚本的原子操作,通过lua脚本,完成多个redis命令的原子操作。
完成上述操作后,一个比较健全的redis分布式锁就算大功告成了,当然上述的能力,redisson框架已经全部实现,并且还提供了更加丰富的功能:可重入锁(Reentrant Lock)公平锁(Fair Lock)联锁(MultiLock)红锁(RedLock)读写锁(ReadWriteLock)信号量(Semaphore)可过期性信号量(PermitExpirableSemaphore)闭锁(CountDownLatch)。
主要有两种方式:RDB和AOP
快照模式:指定时间对整个redis拍照,生成照片,当redis重启后,直接使用之前的照片,恢复数据。
优点:只需要一个快照文件。缺点:存在数据丢失的风险。
操作日志模式:每次的key的先关修改操作,都追加到日志文件中。
优点:不存在数据丢失的风险。缺点:日志文件会很大(有日志压缩的优化),当日志文件很大时,重启恢复比较慢。
当然AOP不存在数据丢失的风险,是不严格的,因为AOP的redis命令,并不是直接写入日志文件的,而是先写入内存缓存中,定时刷新到日志文件中,所以还是存在一定的数据丢失风险。
两者结合,定时快照,然后在快照的基础上进行AOP,结合双方的优点。
当内存满了之后,继续有新的数据缓存进来,就需要进行缓存淘汰机制。
redis的缓存淘汰机制有两种:volatile(设置过期时间)和allkeys(所有的key)
volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
allkeys-random:从数据集中任意选择数据淘汰
no-enviction(驱逐):禁止驱逐数据
热点数据:高频率访问的数据,因为存在缓存淘汰,所以存在热点数据被淘汰的风险。那么如何保证热点数据不被淘汰呢?
热点数据分为:静态热点数据和动态热点数据。针对热点数据就不能走删除缓存实现更新,而是主动更新,组装数据更新缓存。
可以固定的缓存数据,例如:组织架构信息,秒杀商品等。针对静态热点数据,可以定时刷新。秒杀活动开始前,把相关商品和库存信息刷新到缓存中。
动态热点数据:因为突发事件,导致某些数据变成热点数据,例如微博的明星热门事件。因为这些热点是不可控,突发的,因此需要动态处理。
解决方案:需要一个热点动态监控系统,针对查询接口,利用AOP进行代理,把相关的查询条件异步通知动态监控系统,动态监控系统分析查询频率,主动刷新某些热点缓存数据,而不是通过删除来实现缓存刷新。
针对动态热点数据,其实仅仅依靠redis缓存,还是有一定的缺陷的,因为Redis的QPS有上限的(单机最高10W+),因此一般热点动态监控系统,是利用本地缓存+Redis缓存二级缓存机制,处理大流量高并发问题。当一个key被识别为热点数据后,数据访问会首先走本地缓存,同时主动刷新本地缓存,主动刷新缓存时,会直接调用redis缓存。
例如:sort、sunion、zunionstore、keys、scan,这些命令都是0(N),或者一次性取出list中的所有数据。
当服务请求量并不大,但Redis实例的CPU使用率很高,很有可能是使用了复杂度高的命令导致的。可以通过Redis提供了慢日志命令的统计功能排查。
尽量不要一次性获取大量数据,而是通过分批获取,减轻cpu的压力。
如果查询慢日志发现,并不是复杂度较高的命令导致的,例如都是SET、DELETE操作出现在慢日志记录中,那么你就要怀疑是否存在Redis写入了bigkey的情况。
Redis在写入数据时,需要为新的数据分配内存,当Redis中删除数据时,它会释放对应的内存空间。
如果一个key写入的数据非常大,Redis在分配内存时也会比较耗时。同样的,当删除这个key的数据时,释放内存也会耗时比较久。
redis也提供了lazy-free机制,但是还是尽量减少bigkey。
有时你会发现,平时在使用Redis时没有延时比较大的情况,但在某个时间点突然出现一波延时,而且报慢的时间点很有规律,例如某个整点,或者间隔多久就会发生一次。
如果出现这种情况,就需要考虑是否存在大量key集中过期的情况。
如果有大量的key在某个固定时间点集中过期,在这个时间点访问Redis时,就有可能导致延迟增加。
解决方案:针对失效期要求不严格的数据,将失效日期打散。正常设置的失效时间一般为整时,整分,但是可以将上述失效时间添加一个随机数(10以内),把失效时间打散。
redis达到缓存最大值后,再写入新的缓存,就要开启缓存淘汰,这样每次写入,都需要根据缓存淘汰策略,淘汰一批缓存,也会导致写入过慢。
此时需要检查redis内存设置是否过小,是否有大量key长期有效。
生成RDB和AOF都需要父进程fork出一个子进程进行数据的持久化,在fork执行过程中,父进程需要拷贝内存页表给子进程,如果整个实例内存占用很大,那么需要拷贝的内存页表会比较耗时,此过程会消耗大量的CPU资源,在完成fork之前,整个实例会被阻塞住,无法处理任何请求,如果此时CPU资源紧张,那么fork的时间会更长,甚至达到秒级。这会严重影响Redis的性能。
这种情况需要检查RDB和AOP的频率配置是否过高。
特点就是从某个时间点之后就开始变慢,并且一直持续。这时你需要检查一下机器的网卡流量,是否存在网卡流量被跑满的情况。
网卡负载过高,在网络层和TCP层就会出现数据发送延迟、数据丢包等情况。Redis的高性能除了内存之外,就在于网络IO,请求量突增会导致网卡负载变高。
如果出现这种情况,你需要排查这个机器上的哪个Redis实例的流量过大占满了网络带宽,然后确认流量突增是否属于业务正常情况,如果属于那就需要及时扩容或迁移实例,避免这个机器的其他实例受到影响。
redis实现延迟队列,利用SortSet的有序性,可以直接将到期时间作为score,定时从SortSet中,根据当前时间,获取数据。
Zrangebyscore :返回有序集合中指定分数区间的成员列表。有序集成员按分数值递增(从小到大)次序排列。
Zremrangebyscore:移除有序集中,指定分数(score)区间内的所有成员。
先获取,后移除,不是原子操作,数据重复的问题。所以还是需要通过lua脚本,先获取,存在数据,返回,同时删除。
当然针对大批量数据,使用同一个SortSet,还是存在风险,可以参考时间轮的概念,可以以5分钟作为一个SortSet,这样分散单个key的数量,如果5分钟数据量仍然很大,那么可以再细化,甚至10秒钟。
例如:10秒一个单位,20220909150010、20220909150020。
10秒一个单位,拆成100分:20220909150010_0,20220909150010_1,20220909150010_99。