Redis
影响从数据库获取数据速度的主要因素
- 硬盘数据寻址,加载目标数据进内存;
- 硬盘的带宽,当并发量大的时候,硬盘带宽不够,依然需要排队查询;
IO多路复用模型epoll
用户是否为活跃用户
用户系统统计用户登录天数(判断用户是否为活跃用户,给不同类型的用户获取到优惠券不同,非活跃用户的优惠券额度稍微偏大)且时间窗口随机
- 使用redis的bitMap数据结构实现;
- 设计思路:
- bitmap内部存储是以8位每字节的字节流;每个星期为7天,舍弃每个字节的第8位不做存储,一到七位分别存储0和1表示改用户是否登录;
- 用户每次登录后端根据登录日期计算出属于一年的第几个星期的第几天,然后把数据存在对应的字节的对应位上;
- setbit <用户id> 6 1 设置用户在这一年的第一周的周六登录了
setbit <用户id> 9 1 设置用户在这一年的第二周的周一登录了
BITCOUNT <用户id> -2 -1 (获取倒数第一,第二个字节上1的个数)相当于获取最近两周用户的登录天数;
SET数据结构实现抽奖
将所有参与抽奖的用户加入set;
srandmember key 正数 取出一个去重的结果(不超过已有的集合)
srandmember key 负数 取出一个带重复结果,一定会满足给定的数量
公司1000人参加年会,一等奖抽10人:srandmember key 10;二等奖抽50人:srandmember key 50; 三等奖抽100人:randmember key 100;
redis常见的数据结构
string:在redis中主要以key-value存储数据的外在体现,redis底层采用了3种类型来存储String结构的数据:
int:存储8个字节的长整型数据;
embstr:存储小于44字节的SDS格式的压缩字符串,SDS数据内部包含一个char数组和len长度等属性,只需要分配一次内存,因为当前RedisObject对象与SDS用连续的内存空间进行存储的,根据偏移量就能获取到数据;
raw:存储大于44字节的字符串,需要分配两次内存空间,分别是当前redisObject和SDS数据;
list:根据list相关命令对该数据结构的外在使用体验类似于双向队列,redis底层采用的ziplist和quicklist两种数据结构来 进行数据存取:(在数据较少时采用ziplist,当数据较多时采用quicklist)
ziplist:压缩链表,分配的是一块连续的内存空间,所有元素紧挨着一起进行存储,相对于普通链表,消除普通链表附加的指针内存空间,减少了内存消耗;但是ziplist每次插入或删除一个元素时,都需要进行频繁的进行内存的扩大或减小,在内存分配上也会造成一定效率的损失;
quicklist:在redis里就是内部维护着若干个node,每一个node就是一个ziplist;
hash:该数据结构在使用上类似于hash表,redis底层采用的是ziplist和hashtable来实现的;
在redis配置文件中可以看到两个参数:hash-max-ziplist-entries = 512; hash-max-ziplist-value = 64;
这两个参数决定了redis在节点数小于512并且单个节点小于64字节的时候使用ziplist,反之则使用hashtable;
ziplis实现hash数据结构的方式为:将hash的同一个键值对紧挨着存储在ziplist里,key在前,value在后,这样就可以通过偏移量快速获取到key对应的value值;
hashtable实现hash数据结构的方式:底层使用字典,hash的每一个键值对使用字典每一个键值对来进行存储;
set:在存储上是一个去重版本的list,redis底层采用的是intset和hashtable来实现的;
当set中保存的数据全为整数且元素数量不超过512时,采用intset进行存储,反之则使用hashtable
在redis配置文件中可以配置参数:set-max-intset-entries = 512,可以配置该参数来决定set何时采用hashtable;
intset实现set数据结构的方式为:它的实现就是一个整数数组;
hashtable实现set数据结构的方式:底层使用字典,set的每一个键值对使用字典每一个键值对来进行存储;
sort set:为每个元素维护着一个分数的set集合,redis底层采用的是ziplist、skiplist来实现的;
当保存的元素长度都小于64字节,同时元素数量小于128时使用ziplist,同样可以通过两个参数zset-max-ziplist-entries = 128,zset-max-ziplist-value = 64来进行配置;
ziplis实现sort set数据结构的方式为:将sort set的每个元素的值和分数紧挨着存储在ziplist里,值在前,分数在后,这样就可以通过偏移量快速获取到元素对应的分数值;
skiplist + 字典来实现sort set数据结构的方式为: 是一个多层链表的数据结构,每个节点的层数是通过一定的概率来产生,这种多层结构的好处就是用冗余的存储空间来换取更快的查找效率;这里还使用了字典来维护每个元素值和它的分数,可以提高用元素查找分数的效率,字典查找元素的时间复杂度是O(1),而跳跃表的平均复杂度是O(logN);但是对于zrank,zrange这些范围查找的命令,字典需要额外的内存空间进行排序然后再获取结果,范围操作显然字典不具备优势,所以选择了两者的结合;
补充一句:对于我们编码者而言,完成业务功能选择出最佳的redis数据结构尤为重要,不同的数据结构完成相同的功能所需要执行的命令次数的成本是不一样的;例如:我们采用string数据结构采用多次命令来存储多组键值对来存储对象,也可以用hash采用一条命令来存储多组键值对,显然使用hash命令次数的成本更低;
redis单线程为什么也很快
- 基于内存的操作;
- 单线程可以避免不必要的上下文切换和竞争条件;
- 使用了 I/O 多路复用模型epoll;
缓存穿透
访问一个缓存和数据库中都没有的key,此时请求直接打在数据库上,并且由于数据库中也没有数据,没办法重建缓存;
解决方案: 1.接口校验一些不合法的key,可以过滤掉一些非法请求;
2.缓存一个带有过期时间的空值;
3.布隆过滤器:其主体思想就是利用小的内存空间过滤大样本的key;
4.布隆过滤器不能删除key,如果想要支持删除可以选择布谷鸟;
缓存击穿
热点key过期
- 加互斥锁,保证大量请求中只有一个请求获取到锁到数据库获取到数据,然后重建redis缓存后释放锁,其他请求再从redis获取数据;
互斥锁可以使用redis分布式锁,但是经过思考后发现这种场景是可以直接使用JVM进程级别的锁的,JVM锁可以保证单台服务器只有一个请求访问数据库,已经能够大大减轻数据库的压力了,而且实现简单带来的问题也会更少;
这里锁的粒度一定要注意,一定要以业务key为维度,不要对固定值进行上锁,这样会导致不同业务key相互阻塞,造成严重性能损耗;
缓存雪崩
大量热点key过期
如果业务对数据过期的时间点没有硬性要求,可以采用打散缓存过期时间的方案,也就是设置过期时间时间加上一个合理范围的随机时间;
加互斥锁;
redis分布式锁
- setnx;
- 过期时间;
- lua脚本保证原子性;
- 看门狗续期;
redisson:
zookeeper分布式锁,实现更简单,更好的数据一致性;
既然已经涉及到锁了,他对效率要求就没有那么强了,他要求的准确度和一致性是特别强的;
缓存淘汰策略
redis可以通过配置文件配置maxmermory限制最大内存大小,当内存redis内存满了,redis有以下缓存淘汰策略:
noeviction: 默认策略,不淘汰任何 key,直接返回错误;
allkeys-lru: 针对所有key,采用LRU算法对key进行淘汰,LRU算法的核心是:淘汰最长时间未使用的key;
allkeys-lfu: 针对所有key,采用LFU算法对key进行淘汰,LFU算法的核心是:淘汰一段时间内,最少使用次数的key;
allkeys-random: 针对所有key,随机对key进行淘汰;
volatile-lru: 针对所有设置了过期时间的key,采用LRU算法进行淘汰;
volatile-lfu: 针对所有设置了过期时间的key,采用LFU算法进行淘汰;
volatile-random: 针对所有设置了过期时间的key, 随机对key进行淘汰;
volatile-ttl: 针对所有设置了过期时间的key,淘汰ttl最小的,也就最先过期的key;
针对以上缓存淘汰策略的选择,需要根据时间情况进行选择:
首先需要考虑我们在技术选型时redis的定位是数据库还是缓存,如果是作为数据库的话,我们应该noevication,当达到内存上限应该直接返回错误;如果是作为缓存的话,我们的数据一般都是无需持久化且有过期时间的数据,所以这里我们经常会选择volatile-lru这种内存淘汰策略;
redis如何删除过期的key的
- 惰性删除:被动等待过期的key被访问然后对key进行是否过期的检查,过期了则进行删除;
- 定时删除:redis每10秒秒会对随机的20个key进行过期检测,删除所有过期的key,如果过期的key超过25%,则重复上述操作;
其实这里可以看出redis采用惰性删除加上定时删除目的:牺牲少量内存来保住redis的性能;
linux下的管道
- 衔接前一个命令的输出作为后一个命令的输入;
- 管道会触发创建子进程;
echo $$ | more
echo $BASHPID | more
$$ 的优先级高于管道cmd
Linux下父子进程
- 常规思想下,进程间数据是隔离的,但是Linux下可以使用export变量可以实现父子进程数据共享,但是进程间数据修改不会相互影响;
- fork子进程采用的使用copyonwrite(写时复制)的方式创建子进程的,而且不是拷贝父进程所有数据,而是拷贝父进程的所有指针指向父进程原来的数据;
redis持久化RDB和AOF
RDB redis会根据配置文件配置RDB触发时间及条件来fork一个子进程, 采用写时复制的方式将全量的数据写到rdb文件
优点:1.RDB文件是压缩的二进制文件,体积小,恢复速度快;
2.默认配置下持久化的频率不高,对redis性能影响较小;
缺点:在触发RDB时间窗口期间如果服务器挂了或者redis挂了,可能会丢失部分数据;
AOF redis会将默认(一秒内)执行的命令追加到aof文件,以redis命令的形式进行存储;
但是单纯的追加命令,会使得aof文件持续增大;
在redis4.0之前会进行AOF重写:会进行相同命令的合并(计数时会采用incryby),删除抵消的命令(一直新增key删除key操作)
在redis4.0之后也会进行aof重写,但是形式不同:它会首先保存一个RDB的全量数据,然后在下一个时间窗口到来之前,会以aof的形式产生增量数据;
IO对持久化的影响
redis是基于内存的数据库 —> 写操作会触发IO —> 可以去设置IO的级别:appendfsync NO, appendfsync always, appendfsync everysec 来控制AOF对redis性能的影响;
IO操作将数据写入到磁盘
Linux 操作系统中为了提升性能,使用了页缓存(page cache)。当AOF将命令追加到文件时,数据并没有正真落盘,而是在page cache中, 而想要数据落盘需要执行fsync / fdatasync 命令来强制刷盘; 当aof配置appendfsync NO时,就是不进行刷盘操作,buffer什么时候满了,什么时进行刷盘操作,可能会丢失buffer大小的数据;
redis分布式部署要解决的问题
- 单点故障; — 哨兵选举,过半机制
- 单台机器的压力; — 主从模式
- 容量; — 按照业务将数据分别存放到不同的节点,或者使用一致性Hash算法将数据存放在不同的节点;
过半机制
为什么推荐奇数台节点数,3个节点和4个节点都只能容忍有一个节点挂掉;
在启动sentinel时就需要在命令里加上选举的权重值,比如3个节点时就配置为2,4个节点时就配置为3
一致性Hash
hash函数离散性,在大样本的时候才会体现出均匀性,在节点较少的时候,很可能出现数据倾斜,所以经常这里会引入虚拟节点
虚拟节点的产生:可以使用节点的ip 后面拼接上一个数据(比如1-100),然后让这100个新数据进行hash落在环上,这样为这个节点产生了100个虚拟节点,可以防止数据倾斜的问题;
redis事务
WATCH:命令可以为Redis事务提供check-and-set类CAS行为
MULTI:开启一个redis事务
EXEC:负责触发并执行事务中的所有命令
DISCARD:取消事务;
redis事务不支持回滚:不需要处理事务相关的逻辑流程,使得redis内部保持简单且快速;
redis集群代理
twitter (不支持事务)
predixy
一组哨兵可以监控多组集群, 当监控多组集群时,predixy不支持事务;监控单组redis时支持事务;

redis-cluster进行集群代理
Redis-cluster并不支持处理多个keys的聚合命令,因为需要在不同的节点间处理数据,不能保持redis的高性能;
redis-cluster本身不支持事务,但是可以使用hash tag,来保证多个key落在同一个节点,来达到支持事务;
Hash算法有哪些
crc16, crc32, md5等
Redis-cluster的数据分片
Redis集群没有使用一致性hash, 而是引入了哈希槽的概念。
Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽.集群的每个节点负责一部分hash槽
举个例子,比如当前集群有3个节点,那么:
节点 A 包含 0 到 5500号哈希槽.
节点 B 包含5501 到 11000 号哈希槽.
节点 C 包含11001 到 16384号哈希槽.
添加或者移除节点只需要移动槽位即可,无需全量数据迁移;
redis-cluster提供迁移槽位的命令:
redis-cli -cluster reshard 127.0.0.1:80 然后输入将多少个槽位移动到那些节点;
客户端连接redis-cluster的任何一个节点都可以,如果访问数据不在当前节点,redis-cluster会通过槽位重定向到对应的节点然后返回数据;
redis客户端
Jedis, Redisson, lettuce;