大数据时代NoSQL开始大行其道,其中常用于缓存的Redis可谓风头正盛,是大小公司技术架构中必不可少的一种中间件,也是职场技术同仁们必知必会的一种技术。本场Chat将从各个方面对Redis进行全面的讲解并分析常见问题。本场Chat将涉及如下内容
- Redis的基本概念及背景知识
- 五种常用数据对象及应用场景
- 数据对象的底层实现方式
- Jedis的使用
- Redis持久化策略
- Redis的事务机制
- 分布式锁的实现及改进策略
- Redis的删除策略
- Cluster集群模式
- 缓存穿透、缓存雪崩、缓存击穿
适合人群:不了解Redis的新手,对Redis的实现机制感兴趣的技术人员
本文的全部内容来自我个人在Redis学习过程中整理的博客,是该博客专栏的精华部分。在书写过程中过滤了流程性的上下文,例如部署环境、配置文件等,而致力于像读者讲述其中的核心部分,如果读者有意对过程性内容深入探究,可以移步MaoLinTian的Blog,在这篇索引目录里找到答案分布式技术相关专栏索引,需要注意的是,本文的内容学习来源来自于书籍《Redis的设计与实现》及《黑马程序员-Redis视频教程》,特作相关说明。
Redis的基本概念及背景
首先我们要知道什么是NoSQL,什么又是Redis,为什么需要用Redis,Redis有哪些使用场景?
什么是非关系型数据库
NoSQL = Not Only SQL,也就是非关系型数据库,既然有了SQL,为什么还需要NoSQL?这和时代背景有很大的关系,我们所处的时代可以划分为Web1.0和Web2.0时代:
- Web1.0,是基于浏览器,用户通过浏览器获取内容信息。
- Web2.0,是基于1.0,增加了用户与系统的交互,使用者既是网络内容的获取者,也是网络数据的制造者,例如:论坛、博客、微博等相关社交类型的平台。
我们当前身处Web2.0时代,面对很多问题:
- High performance - 高并发读写,在Web2.0时代,需要依据用户个性化需要高并发读写,关系型数据库读还可以,写就很难做到了。例如论坛这样的站点, 网站的用户并发性非常高,往往达到每秒上万次读写请求,对于传统关系型数据库来说,硬盘I/O是一个很大的瓶颈
- Huge Storage - 海量数据的高效率存储和访问,海量数据高效率存储和访问, 网站每天产生的数据量是巨大的,对于关系型数据库来说,在一张包含海量数据的表中查询,效率是非常低的,类似FaceBook这样的社交网站、社区。
- High Scalability &High Availability - 高可拓展性和高可用性, 在基于Web的结构当中,数据库是最难进行横向扩展的,当一个应用系统的用户量和访问量与日俱增的时候,数据库却没有办法像Web server和App Server那样简单的通过添加更多的硬件和服务节点来扩展性能和负载能力。对于很多需要提供24小时不间断服务的网站来说,对数据库系统进行升级和扩展是非常痛苦的事情,往往需要停机维护和数据迁移。
这些问题主要是由于关系型数据库要求的:事务一致性、读写实时性、和复杂SQL的查询,这些都是导致关系型数据库性能差的原因,而这些场景和严格的要求在很多场景下不必要了,例如社交网络。NoSQL因为它的易扩展、大数据量高性能、灵活的数据模型和高可用在社区时代可以发挥很大的作用。
NoSQL的分类
NoSQL 依据存储的内容也分很多种,让我们从这些类别里来定位Redis吧!依据使用场景来划分类别,按照上面我们提到的三种优点,看看哪种能发挥极致:
- 面向高性能并发读写的key-value数据库:key-value数据库的主要特点是具有极高的并发读写性能,Redis,Tokyo Cabinet,Flare就是这类的代表
- 面向海量数据访问的面向文档数据库:这类数据库的特点是,可以在海量的数据中快速的查询数据,典型代表为MongoDB以及CouchDB
- 面向可扩展性的分布式数据库:这类数据库想解决的问题就是传统数据库存在可扩展性上的缺陷,这类数据库可以适应数据量的增加以及数据结构的变化
也正是因为Redis极高的并发读写能力,所以被常用作缓存。
Redis的应用场景
Redis有着广泛的应用场景。典型应用是:内容缓存,主要用于处理大量数据的高访问负载,优点就是快速查询。
- 缓存,缓存现在几乎是所有中大型网站都在用的必杀技,合理的利用缓存不仅能够提升网站访问速度,还能大大降低数据库的压力。Redis提供了键过期功能,也提供了灵活的键淘汰策略,所以,现在Redis用在缓存的场合非常多。
- 排行榜,很多网站都有排行榜应用的,如京东的月度销量榜单、商品按时间的上新排行榜等。Redis提供的有序集合数据类构能实现各种复杂的排行榜应用。
- 计数器,什么是计数器,如电商网站商品的浏览量、视频网站视频的播放数等。为了保证数据实时效,每次浏览都需+1,并发量高时如果每次都请求数据库操作无疑是种挑战和压力。Redis提供的incr命令来实现计数器功能,内存操作,性能非常好,非常适用于这些计数场景。
- 分布式会话,集群模式下,在应用不多的情况下一般使用容器自带的session复制功能就能满足,当应用增多相对复杂的系统中,一般都会搭建以Redis等内存数据库为中心的session服务,session不再由容器管理,而是由session服务及内存数据库管理。
- 分布式锁,很多互联网公司中都使用了分布式技术,分布式技术带来的技术挑战是对同一个资源的并发访问,如全局ID、减库存、秒杀等场景,并发量不大的场景可以使用数据库的悲观锁、乐观锁来实现,但在并发量高的场合中,利用数据库锁来控制资源的并发访问是不太理想的,大大影响了数据库的性能。
- 社交网络,点赞、踩、关注/被关注、共同好友等是社交网站的基本功能,社交网站的访问量通常来说比较大,而且传统的关系数据库类型不适合存储这种类型的数据,Redis提供的哈希、集合等数据结构能很方便的的实现这些功能。
- 最新列表,Redis列表结构,LPUSH可以在列表头部插入一个内容ID作为关键字,LTRIM可用来限制列表的数量,这样列表永远为N个ID,无需查询最新的列表,直接根据ID去到对应的内容页即可。
- 消息系统,消息队列是大型网站必用中间件,如ActiveMQ、RabbitMQ、Kafka等流行的消息队列中间件,主要用于业务解耦、流量削峰及异步处理实时性低的业务。Redis提供了发布/订阅及阻塞队列功能,能实现一个简单的消息队列系统。
这些场景都是当下大型电商网站和社交网站需要的。所以Reids火也不奇怪,其它的NoSQL数据库的应用场景都比较窄,类似文档存储和图片存储等,都在特定应用场景下使用。
五种常用数据对象及应用场景
Redis是高性能键值对数据库,支持的键值数据类型:字符串类型 、哈希类型、列表类型 、集合类型、有序集合类型 , 这些类型的操作方式和结构需要详细了解。
字符串类型
字符串的操作命令有很多,常用的操作命令有如下几种,涉及到:设置及获取值,获取并修改值,自增值,自减值,追加字符串等操作:
set key value
:设置指定 key 的值。get key
:获取指定 key 的值。getset key value
:将给定 key 的值设为 value ,并返回 key 的旧值(old value)。incr key
:将 key 中储存的数字值增一。incr key increment
:将 key 所储存的值加上给定的增量值(increment)decr key
:将 key 中储存的数字值减一。decr key decrement
:key 所储存的值减去给定的减量值(decrement) 。append key value
:如果 key 已经存在并且是一个字符串, APPEND 命令将指定的 value 追加到该 key 原来值(value)的末尾。del key
:删除该key。
需要注意的是,操作状态返回的0和1要和返回结果做区分,并且set操作其实是有则更新,无则新增。常用场景如下:
- 场景一:利用数值操作特性Incr指令为分布式数据库主键自增,例如数据库做分库分表后仍然希望所有数据能保持主键单调递增。
- 场景二:利用key的生命周期做投票系统,在投票的场景中,我们经常会有一天可投几次票这样的限制,那么这个就需要一个过期时间,例如每天最多可以投5张票,可以把用户id作为key,当value大于5时不允许继续投,24小时后key销毁,重新设置。
- 场景三:利用数值操作特性Incr指令刷新热点数据,例如分别设置微博大V的粉丝数、点赞数为key,然后刷新数量。
以上就是字符串类型的介绍。String的场景利用了String的Incr指令、过期Key的特性
哈希类型
Hash是一个String类型的Field和Value的映射表,Hash特别适合用于存储对象。Redis 中每个 hash 可以存储 2^32 - 1
个键值对(40多亿),常用操作如下
hset key field value
:将哈希表 key 中的字段 field 的值设为 value 。hget key field
:获取存储在哈希表中指定字段的值。hmset key field1 value1 field2 value2 ...
:同时将多个 field-value (域-值)对设置到哈希表 key 中。时间复杂度为O(n)hmget key field1 field2...
:获取所有给定字段的值。时间复杂度为O(n)hgetall key
:获取在哈希表中指定 key 的所有字段和值 。时间复杂度为O(n)hdel key field1 field2...
:删除一个或多个哈希表字段。返回值为0则表示删除的属性不存在del key
:删除该key,也就是删除该哈希。
还有一些自增及判断的命令:
hincrby key field increment
:为哈希表 key 中的指定字段的整数值加上增量 increment 。hlen key
:获取哈希表中字段的数量hvals key
:同时将多个 field-value (域-值)对设置到哈希表 key 中。时间复杂度为O(n)hexists key field
:查看哈希表 key 中,指定的字段是否存在。时间复杂度为O(n)
由于Hash的这些特性,常有如下的应用场景:
- 场景一:利用hash的对象存储特性设置用户的购物车,一个人的购物车可以看做一个对象,而商品可以当做field,数量可以当做value,然后对购物车进行各种操作。
- 场景二 :利用hash作为商品秒杀计数对象完成商品秒杀系统,一个商品秒杀系统可以看作一个对象,而秒杀的商品可以当作field,数量可以当作value,设置value为该商品的余量,然后使用hincrby来进行秒杀业务,降低数量。
和String类型相比,Hash更适合数据的呈现,而不适合数据的更新,具体为什么,可以在下一小节其底层结构上一窥究竟。Hash的场景利用了Hash的结构特性存储对象信息
列表类型
List的核心特点是顺序性,其底层主要实现为一个双向链表,为什么说主要,因为本节提到的各种数据对象底层都有不止一种实现方式。简单常用的一些相关相关命令:
lpush key value1 [value2]
:将一个或多个值插入到列表头部,从左侧添加,最后添加的在最左边lrange key start stop
:获取列表指定范围内的元素, 假如共有6个元素,[0,-2]表示从列表头到倒数第二个,[0,-1]和[0,5]效果一样。rpush key value1 [value2]
:将一个或多个值插入到列表尾部,从右侧添加,最后添加的在最右边lpop key
:移出并获取列表的第一个元素,弹出列表头rpop key
:移除列表的最后一个元素,返回值为移除的元素。llen key
:获取列表长度。lpushx key value
:将一个值插入到已存在的列表头部,仅队列存在时有效。当队列不存在时,不进行任何操作。rpushx key value
:将一个值插入到已存在的列表尾部,仅队列存在时有效。当队列不存在时,不进行任何操作。
当然还有些相对复杂的命令:
lrem key count value
:移除count个为value的元素,如果count大于0,则从左向右数,如果count小于0,从右向左数,如果count等于0,删除全部为value的元素。lset key index value
:通过索引设置列表元素的值,列表头的索引是0.linsert key before|after pivot value
:在列表的元素前或者后插入元素
列表有如下的一些应用场景:
- 场景一:利用blpop特性实现任务队列,轮询从任务队列里取数据【可以同时从多个队列获取】,如果取到数据就返回,如果没有数据就等待设置时间持续获取,直到数据过期
- 场景二:利用list顺序特性实现朋友圈点赞,因为点赞等信息都是有顺序性的,而且修改的效率高,适合使用list来操作,点赞用rpush,取消点赞用lrem
- 场景三:利用list顺序特性进行分布式日志顺序性展示,使用list顺序性实现多路数据汇总展示,利用其栈的特性实现最新的消息最先展示,即组合使用rpush和rpop
在List的场景下,主要利用了List的顺序性、队列和栈的双重特性
集合类型
Set 是String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。集合中最大的成员数为 2^32 - 1
(40多亿个成员),常用操作如下:
sadd key member1 [member2]
:向集合添加一个或多个成员srem key member1 [member2]
:移除集合中一个或多个成员smembers key
:返回集合中的所有成员sismember key member
:判断 member 元素是否是集合 key 的成员scard key
:获取集合的成员数srandmember key [count]
:返回集合中一个或多个随机数spop key
:移除并返回集合中的一个随机元素
set集合之间的操作通过如下命令实现:
sdiff key1 [key2]
:返回给定所有集合的差集,两个集合的第一个不同数字。sdiffstore destination key1 [key2]
:返回给定所有集合的差集并存储在 destination 中sinter key1 [key2]
:返回给定所有集合的交集sinterstore destination key1 [key2]
:返回给定所有集合的交集并存储在 destination 中sunion key1 [key2]
:返回所有给定集合的并集sunionstore destination key1 [key2]
:返回所有给定集合的并集存储在 destination 集合中smove source destination member
:将 member 元素从 source 集合移动到 destination 集合
set的主要特性是不重复性,我们看基于这样的特性,常用的有哪些场景呢?
- 场景一:利用set特性随机获取不重复数据实现简单推荐系统,系统汇集好读者的所有爱好标签后,使用
srandmember
指令实现随机推送三个用户喜欢的标签数据 - 场景二:利用set交并差实现推荐系统池,例如获取中老年群体用户中共同的爱好,可以用
sinter
来实现,获取我在中国地质大学中的一度人脉、二度人脉等可以使用sunion
来实现,获取我喜欢但是我妈不喜欢的电视节目,可以用sdiff
来实现。 - 场景三:利用set不重复特征获取所有业务系统权限,我们想要获取所有用户的所有权限,取出其中补充五的业务系统权限该怎么做?我们可以设置用户为一个set集合,他的权限为value,把所有权限放到set
- 场景四:利用set不重复特征获取UV和IP数据,UV即网站被不同用户访问的次数,相同用户切换IP地址,UV不变,使用set存储用户cookie信息,统计UV量。IP即网站被不同IP的访问次数,相同IP访问,不同用户访问,IP不变。使用set存储IP信息,统计IP量。
- 场景五:利用set不重复特征实现黑白名单,可以在黑名单中添加IP或设备或用户,通过不重复特性,设置唯一的黑名单
Set的场景主要利用了set的不重复特性、随机取值特性和并交差集的特性
有序集合类型
Zset和集合一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。Redis正是通过分数来为集合中的成员进行从小到大的排序。有序集合的成员是唯一的,但分数(score)却可以重复,常用操作如下:
zadd key score1 member1 [score2 member2]
:向有序集合添加一个或多个成员,或者更新已存在成员的分数zscore key member
:返回有序集中,成员的分数值zcard key
:获取有序集合的成员数zrem key member [member ...]
:移除有序集合中的一个或多个成员zrange key start stop [withscores]
:通过索引区间返回有序集合成指定区间内的成员,如果需要同时返回scores ,带上后边那段,默认分数从低到高zrevrange key start stop [withscores]
:返回索引区间集中指定区间内的成员,分数从高到底zrangebyscore key min max [withscores] [limit]
:通过分数区间返回有序集合指定区间内的成员,默认分数从低到高zremrangebyrank key start stop
:移除有序集合中给定的排名区间的所有成员zremrangebyscore key min max
:移除有序集合中给定的分数区间的所有成员zincrby key increment member
:有序集合中对指定成员的分数加上增量 incrementzcount key min max
:计算在有序集合中指定区间分数的成员数
Zset最经典的应用场景就是进行排行榜设置了。当然除此之外还有些带权重的操作都类似:
- 场景一:利用set不重复排序特征实现计数器组合排序排行榜功能,为所有参与排名的资源进行排序
- 场景二:利用set不重复排序特征实现基于时效性任务提醒,队列中全部为vip,按照会员时间长短排序,短时间到期后提醒下一个快到期的任务。
- 场景三:利用set不重复排序特征实现带权重任务队列,仅是任务队列可以通过队列,但是如果队列中的任务有优先级,则需要使用带权重的。
ZSet的场景主要利用了set的不重复特性和分数排序特性
数据对象的底层实现方式
上一小节我们提到的五种数据类型其实就是Redis的数据对象,我们先来看看数据对象的类型:Redis的key都是string类型的,以上各类型说的其实都是value的类型,以下是对象的几个优点:
- 通过这五种不同类型的对象,Redis可以在执行命令之前,根据对象的类型来判断一个对象是否可以执行给定的命令。适配场景
- 使用对象的另一个好处是,我们可以针对不同的使用场景,为对象设置多种不同的数据结构实现,从而优化对象在不同场景下的使用效率,提升效率
- Redis的对象系统还实现了基于引用计数技术的内存回收机制,当程序不再使用某个对象的时候,这个对象所占用的内存就会被自动释放,内存回收
- Redis还通过引用计数技术实现了对象共享机制,这一机制可以在适当的条件下,通过让多个数据库键共享同一个对象来节约内存,节约内存
- Redis的对象带有访问时间记录信息,该信息可以用于计算数据库键的空转时长,在服务器启用了maxmemory功能的情况下,空转时长较大的那些键可能会优先被服务器删除,内存回收
每次当我们在Redis的数据库中新创建一个键值对时,我们至少会创建两个对象,一个对象用作键值对的键(键对象),另一个对象用作键值对的值(值对象)。Redis中的每个对象都由一个redisObject结构表示,该结构中和保存数据有关的三个属性分别是type属性、encoding属性和ptr属性:
redisObject结构: typedef struct redisObject{ //类型 unsigned type:4; //编码 unsigned encoding:4; //指向底层实现数据结构的指针 void *ptr; ….. }
- 对象的type属性记录了对象的类型,REDIS_STRING、REDIS_HASH、REDIS_LIST、REDIS_SET、REDIS_ZSET,对于Redis数据库保存的键值对来说,键总是一个字符串对象,而值则可以是字符串对象、列表对象、哈希对象、集合对象或者有序集合对象的其中一种
- 对象的encoding属性记录了对象所使用的编码,也即是说这个对象使用了什么数据结构作为对象的底层实现,接下来会详细介绍下使用的数据编码
- 对象的ptr指针指向对象的底层实现数据结构,而这些数据结构由对象的encoding属性决定
也就是一个对象包含自身的数据结构属性,实际使用的编码类型以及数据对象对实际数据编码的指针。
数据对象和数据结构
在Redis中会涉及很多数据结构,比如SDS,双向链表、字典、压缩列表、整数集合、跳跃表等。数据结构有如下几种:
结构常量 | 结构对应的底层数据结构 |
REDIS_ENCODING_INT | long类型的整数 |
REDIS_ENCODING_EMBSTR | embstr编码的简单动态字符串SDS |
REDIS_ENCODING_RAW | 简单动态字符串SDS |
REDIS_ENCODING_HT | 字典 |
REDIS_ENCODING_LINKEDLIST | 双向链表 |
REDIS_ENCODING_ZIPLIST | 压缩列表 |
REDIS_ENCODING_INTSET | 整数集合 |
REDIS_ENCODING_SKIPLIST | 跳跃表 |
每种类型的对象都至少使用了两种不同的编码,在内容长短发生变化的时候数据对象会自动切换适合的数据编码,且切换后不可逆
数据对象 | 数据编码 | 备注 |
String | int | long类型的整数 |
embstr | sds实现 <=32 字节 | |
raw | sds实现 > 32字节 | |
List | ziplist | 压缩列表实现 |
linkedlist | 双端链表实现 | |
Set | intset | 整数集合实现 |
hashtable | 字典实现 | |
Hash | ziplist | 压缩列表实现 |
hashtable | 字典实现 | |
Zset | ziplist | 压缩列表实现 |
skiplist | 跳跃表+字典实现 |