redis常见使用场景
1 缓存
缓存现在几乎是所有中大型网站都在用的必杀技,合理的利用缓存不仅能够提升网站访问速度,还能大大降低数据库的压力。Redis提供了键过期功能,也提供了灵活的键淘汰策略,所以,现在Redis用在缓存的场合非常多
2 排行榜
很多网站都有排行榜应用的,如京东的月度销量榜单、商品按时间的上新排行榜等。Redis提供的有序集合数据类构能实现各种复杂的排行榜应用。
3 计数器
电商网站商品的浏览量、视频网站视频的播放数等都需要计数功能。为了保证数据实时性,每次浏览都得给+1,并发量高时如果每次都请求数据库操作无疑是种挑战和压力。Redis提供的incr命令来实现计数器功能,内存操作,性能非常好,非常适用于这些计数场景
4 分布式会话
集群模式下,在应用不多的情况下一般使用容器自带的session复制功能就能满足,当应用增多相对复杂的系统中,一般都会搭建以Redis等内存数据库为中心的session服务,session不再由容器管理,而是由session服务及内存数据库管理。
5 分布式锁
在很多互联网公司中都使用了分布式技术,分布式技术带来的技术挑战是对同一个资源的并发访问,如全局ID、减库存、秒杀等场景,并发量不大的场景可以使用数据库的悲观锁、乐观锁来实现,但在并发量高的场合中,利用数据库锁来控制资源的并发访问是不太理想的,大大影响了数据库的性能。可以利用Redis的setnx功能来编写分布式的锁,如果设置返回1说明获取锁成功,否则获取锁失败,实际应用中要考虑的细节要更多。
6 社交网络
点赞、踩、关注/被关注、共同好友等是社交网站的基本功能,社交网站的访问量通常来说比较大,而且传统的关系数据库类型不适合存储这种类型的数据,Redis提供的哈希、集合等数据结构能很方便的的实现这些功能。
7 消息队列
消息队列是大型网站必用中间件,如ActiveMQ、RabbitMQ、Kafka等流行的消息队列中间件,主要用于业务解耦、流量削峰及异步处理实时性低的业务。Redis提供了发布/订阅及阻塞队列功能,能实现一个简单的消息队列系统。另外,这个不能和专业的消息中间件相比。
redis数据类型
字符串:可以存储字符串、整数或者浮点数
常用命令:GET(获取key对应的值)、SET(设置key对应的值)、DEL(删除key和值)
列表:是一个链表,链表上的每个节点都包含一个字符串
常用命令:LPUSH和RPUSH(将元素推入链表的左端和右端)、LPOP和RPOP(从链表的左端和右端弹出元素)、LINDEX(获取链表在指定位置上的一个元素)、LRANG(获取链表在指定范围的元素)
集合:可以存储多个字符串,字符串都各不相同
常用命令:SADD(将元素添加到集合)、SREM(将元素从集合中移除)、SISMEMBER(快速检查一个元素是否存在于集合中)、SMEMBERS(获取集合包含的所有元素)、SINTER(交集计算)、SUNION(并集计算)、SDIFF(差集计算)
哈希表:包含键值对的无序散列表,和字符串一样,散列存储的值既可以是字符串,又可以是数字值,并且可以对数字值执行自增、自减操作
常用命令:HSET(在散列表中设置键值对)、HGET(在散列表中获取指定键的值)、HGETALL(获取散列表所有的键值对)、HDEL(从散列表中删除键值对)
有序集合:字符串成员(member)和浮点数分值(score)之间的有序映射
常用命令:ZADD(将指定分值的成员添加到有序集合)、ZRANGE(根据元素在有序集合中的位置,从有序集合中获取多个元素)、ZRANGEBYSCORE(获取有序集合在给定分值范围内的所有元素)、ZREM(从有序集合中移除成员)
redis数据结构
1 简单动态字符串
redis底层所有字符串都使用的是简单动态字符串,具有如下三个特性:可以高效执行长度计算、可以高效执行追加操作、二进制安全(程序不对字符串里保存的数据做任何假设,数据可以是以\0结尾的C字符串、也可以是单纯的字节数组,或者其他格式的数据)
struct sdshdr {
int len; // buf已使用长度
int free; //buf剩余可用长度
char buf[]; //实际保存字符串数据的地方
}
第一次创建时,len等于buf长度,free等于0
执行append操作时,如果新字符串的总长度小于SDS_MAX_PREALLOC,为字符串分配两倍所需长度的空间、否则分配所需长度加上SDS_MAX_PREALLOC数量的空间
如果执行APPEND操作的键很多,字符串体积又很多的话,这种预分配的策略会浪费内存,可以修改redis服务器,让它定期释放一些字符串键的预分配空间,从而更有效利用内存
2 双端链表
普通的双向链表,无特殊的地方
3 字典
字典结构体如下
typedef struct dict {
.........
dictht ht[2]; //哈希表 2个
int rehashidx; // rehash标志,值为-1时表示rehash未进行
}dict;
哈希表结构体如下
typedef struct dictht {
............
dictEntry **table; // 哈希桶
unsigned long size; // 指针数组的大小
unsigned long used; // 哈希表现有节点的数量
}dictht;
哈希表节点结构体如下
typedef struct dictEntry {
void *key; // 键
union {
void *val;
uint64_t u64;
int64_t s64;
} v; // 值
struct dictEntry *next;// 后继节点指针,采用链表法解决哈希碰撞
}dictEntry;
为了在字典的键值对不断增多的情况下仍然保持良好的性能,字典需要对使用的哈希表进行rehash扩容操作,尽量将used和size的比率维持在1:1, 比率ratio(装载因子)= used/size,rehash开始的时机:1 自然rehash:ratio >=1,且变量dict_can_resize为真 2 强制rehash:ratio大于变量dict_force_resize_ratio(默认为5)
dict_can_resize什么时候为假?redis 使用子进程对数据库执行后台持久化任务时,为了最大化的利用系统的copy on write机制,程序会暂时将dict_can_resize设置为假,避免执行自然rehash、减少程序对内存的碰撞
rehash的过程:
1 创建一个比ht[0]更大的哈希表ht[1]
2 将ht[0]中所有的键值对全部迁移到ht[1]
3 将原有的ht[0]清空,将ht[1]替换成新的ht[0]
渐进式rehash:在一个有很多键值对的字典里,某个用户在添加新的键值对时触发了rehash过程,如果rehas将所有的键值对都迁移完毕后才将结果返回给用户,这样用户体验是很不友好的。
渐进式rehash时机:
1 被动rehash:每次执行添加、查找、删除操作时,将哈希表上第一个不为空的索引上的所有节点全部迁移到ht[1]
2 主动rehash:redis服务器常规任务执行时
rehash过程中,所有的查找和删除操作,除了在ht[0]上进行,还需要在ht[1]上进行,所有的添加操作,都只在ht[1]上进行
4 跳表
跳表从高层索引开始,向下层层查找,可以在对数时间复杂度下完成查找、添加、删除操作。跳表最底层链表是按数据从小到大排序的,因此跳表可以非常方便的进行范围查找
跳表节点数据结构
typedef struct zskiplistNode {
robj *obj; // member对象
double score; // 分值
struct zskiplistNode *backward; // 后退指针
struct zskiplistLevel { // 层
struct zskiplistNode * forward; // 前进指针
unsigned int span; // 层跨越的节点数量
}level[];
}zskiplistNode;
跳表索引动态更新:当我们不停的向跳表中添加数据,如果不更新索引,就有可能出现两个节点之间数据非常多的情况,极端情况下会退化成单链表。跳表通过随机函数来维持平衡性,当我们向跳表中插入数据时,可以选择同时将这个数据插入到部分索引层,通过随机函数来确定将这个节点插入到哪几层索引中,如果随机函数生成k,就将此节点插入第一层到第k层这k层索引中
5 整数集合
typedef struct intset {
uint32_t encoding; // 元素使用的类型长度
uint32_t length; // 元素个数
int8_t contents[]; // 保存元素的数组
}intset;
contents数组有以下两个特性:1 没有重复元素 2 元素在数组中从小到大排列
6 压缩列表
pre_entry_length可能占用1字节也可能占用5字节
1字节:前一个entry的长度小于254字节
5字节:前一个entry的长度大于等于254字节
encoding和length两部分一起决定content部分的数据类型和长度
encoding长度两个bit,00、 01、 10 表示content部分保存这字符数组,11表示content部分保存着整数
redis数据类型的底层实现
1 字符串
字符串采用简单动态字符串来实现
2 哈希表
键值对比较少时采用压缩列表
键值对多时采用字典
3 列表
列表元素比较少时采用压缩列表
列表元素多时采用双端链表
4 集合
集合元素比较少时,并且集合的元素全部可以表示为long long类型值时,采用整数集合
集合元素比较多或者集合元素中有不能被表示为long long类型值时,采用字典,集合的元素保存到字典的键里边,字典的值统一设置为null
5 有序集合
集合元素比较少时,采用压缩列表
每个有序集合的元素以两个相邻的ziplist节点表示,第一个节点保存元素的member域,第二个节点保存元素的score域。多个元素之间按score值从小到大排序
集合元素多时,采用字典加跳表
使用字典,将member作为键,score作为值,可以在O(1)复杂度内检查member是否在有序集合中、取出member对应的分值
使用跳表,可以高效的处理范围性查找
为什么采用整数集合、压缩列表
整数集合、压缩列表采用了时间换空间的策略,时间复杂度变高了,但是内存节省了
整数集合、字典占用内存比较:字典需要存储两个hash table、每个hash table需要存储bulket和entry,每个entry中既有key、又有value、又有next指针,而整数集合中只需存储key
压缩列表、双向链表、字典、跳表占用内存比较:压缩列表pre_entry_length最多占用5字节、econding和length最多占用5字节。双向链表64位系统pre和next指针各占8字节。字典上边已分析,需要更多的额外内存。跳表需要双向链表和额外的层。这三个都比压缩列表占用更多的内存
为什么redis性能高
1 完全基于内存,绝大部分请求是内存操作
2 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗
3 使用多路I/O复用模型(epoll),非阻塞IO
4 底层高效的数据结构
redis事务
当客户端处于非事务状态下时,所有发送给服务器端的命令都会立即被服务器执行。但是,当客户端进入事务状态后(MULTI命令后进入事务状态),服务器在收到来自客户端的命令时,不会立即执行命令,而是将这些命令全部放进一个事务队列里,然后返回QUEUED,表示命令已入队。
当执行EXEC命令时,服务器根据客户端所保存的事务队列,以先进先出的方式执行事务队列中的命令。
DISCARD命令用于取消一个事务,清空客户端的整个事务队列,将客户端从事务状态调整回非事务状态。
WATCH命令只能在客户端进入事务状态前执行,在事务状态下执行WATCH会引发一个错误。WATCH命令用于在事务开始之前监听任意数量的键,当调用EXEC命令执行事务时,如果任意一个被监视的键被其他客户端修改了,整个事务不再执行,返回失败。
LUA脚本
redis事务可以确保事务执行过程中,键不被其他客户端并发修改(如果被其他客户端修改,事务失败),但是并不能高效执行CAS操作,CAS操作包括两个步骤:读取数据比较、设置数据。redis 2.6版本通过内嵌对lua环境的支持,可以高效执行CAS操作
REDIS数据持久化
redis运行时,数据维持在内存中,为了让这些数据在redis重启之后仍然可用,redis提供了rdb和aof两种持久化模式
rdb
rdb程序将当前内存中的数据库数据库快照保存到磁盘文件中,redis启动时,rdb程序可以通过载入rdb文件来恢复数据库。rdbSave用于生成rdb文件到磁盘,rdbLoad用于将rdb文件中的数据重新载入内存
SAVE命令会直接调用rdbSave函数,阻塞redis主进程,直到保存完成为止。主进程阻塞期间,服务器不能处理客户端的任何请求。
BGSAVE命令会fork一个子进程,子进程调用rdbSave生成rdb文件
优点:rdb对于文件备份和灾难恢复来说是个不错的选择,如果数据集比较大,采用rdb进行恢复的效率会更高
缺点:系统在定时持久化之前宕机,没来及写入磁盘的数据都将丢失
aof
aof以协议文本的方式,将所有对数据库进行过写入的命令及其参数记录到aof文件
aof有三种保存模式:不保存、每一秒钟保存一次、每执行一个命令保存一次
优点:数据安全性更高
缺点:对于相同数量的数据集,aof文件的大小比rdb文件大,数据恢复时间比rdb长。运行效率比rdb低
二者选择的标准,就是看系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能,待手动运行save的时候,再做备份(rdb)
redis缓存和数据库双写一致性问题
只能保证最终一致性,强一致性的数据不能放缓存
读操作:先读缓存,缓存没有的话,读数据库,然后将从数据库中读出的数据放入缓存
更新操作:
1 先更新数据库,然后删除缓存,待下次读取时更新缓存(更新数据频繁、读取数据不频繁场景),删除缓存可能会失败,可以增加一个消息队列进行补偿
2 先更新数据库,然后更新缓存,下次读取时读到最新的数据(更新跟读取一样频繁的场景),更新缓存可能会失败,可以增加一个消息队列进行补偿
redis过期策略和内存淘汰机制
过期策略:定期删除+惰性删除
定期删除:redis每隔100ms随机抽取部分key进行检查,如果有过期的key,将过期的key删除
惰性删除:在读取某个key的时候,redis会检查这个key是否设置了过期时间,是否超时,如果超时,将这个key删除
内存淘汰机制:如果定期删除没有删除key,也没有读取key进行惰性删除,那么内存使用会越来越高。需要进行内存淘汰。内存淘汰可选配置:
noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。应该没人用吧。
allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 Key。推荐使用。
allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 Key。应该也没人用吧,你不删最少使用 Key,去随机删。
volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 Key。这种情况一般是把 Redis 既当缓存,又做持久化存储的时候才用。不推荐。
volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 Key。依然不推荐。
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 Key 优先移除。不推荐。
如果key没有设置 expire ,那么 volatile-lru,volatile-random 和 volatile-ttl 策略的行为,和 noeviction(不删除) 基本上一致
缓存穿透
黑客查询一个一定不存在的数据,这个不存在的数据每次请求都到数据库查询,造成数据库压力
解决方法:
1 如果查询的数据不存在,仍然把这个空结果进行缓存,但设置一个比较短的过期时间,最长不超过5分钟
2 采用布隆过滤器,在控制层先进行校验,不存在直接丢弃
缓存雪崩
同一时间,缓存大面积失效,大量的查询都请求到数据库,造成缓存雪崩
解决方法:
1 不同的key,缓存失效时间加个随机值,设置不同的过期时间
2 缓存失效后,通过锁或者队列来控制读数据库写缓存线程的数量
3 二级缓存,两个缓存设置不同的过期时间。比如缓存A设置为短期,缓存B设置为长期
缓存预热
缓存预热就是系统上线后,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求时先查询数据库
线上如何获取redis所有的keys
keys命令可以获取redis所有的key
smembers命令可以获取集合所有的元素
在一个大的线上数据系统中,使用keys、smembers可能会造成阻塞
可以用下边四个scan命令来代替
SCAN命令用于迭代当前数据库中的数据库键。
SSCAN命令用于迭代集合键中的元素。
HSCAN命令用于迭代哈希键中的键值对。
ZSCAN命令用于迭代有序集合中的元素(包括元素成员和元素分值)
SCAN cursor [MATCH pattern] [COUNT count]