Redis 简介 Redis 是完全开源的,遵守 BSD 协议,是一个高性能的 key-value 数据库。
Redis 与其他 key - value 缓存产品有以下三个特点:
Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
Redis不仅仅支持简单的key-value
类型的数据,同时还提供list
,set
,zset
,hash
等数据结构的存储。
Redis支持数据的备份,即master-slave
模式的数据备份。
Redis 优势
性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
丰富的数据类型
原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。
Redis运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,因为数据量不能大于硬件内存。
在内存数据库方面的另一个优点是,相比在磁盘上相同的复杂的数据结构,在内存中操作起来非常简单,这样Redis可以做很多内部复杂性很强的事情。
同时,在磁盘格式方面他们是紧凑的以追加的方式产生的,因为他们并不需要进行随机访问。
Redis 数据类型 String(字符串)
string 是 redis 最基本的类型,你可以理解成与 Memcached 一模一样的类型,一个 key 对应一个 value。
string 类型是二进制安全的。意思是 redis 的 string 可以包含任何数据。比如jpg图片或者序列化的对象。
string 类型是 Redis 最基本的数据类型,string 类型的值最大能存储 512MB。
1 2 3 4 redis 127.0.0.1:6379> SET runoob "holle word" OK redis 127.0.0.1:6379> GET runoob "holle word"
应用场景
缓存功能
:String字符串是最常用的数据类型,不仅仅是redis,各个语言都是最基本类型,因此,利用redis作为缓存,配合其它数据库作为存储层,利用redis支持高并发的特点,可以大大加快系统的读写速度、以及降低后端数据库的压力。
计数器
:许多系统都会使用redis作为系统的实时计数器,可以快速实现计数和查询的功能。而且最终的数据结果可以按照特定的时间落地到数据库或者其它存储介质当中进行永久保存。
统计多单位的数量
:eg,uid:gongming count:0 根据不同的uid更新count数量。
共享用户session
:用户重新刷新一次界面,可能需要访问一下数据进行重新登录,或者访问页面缓存cookie,这两种方式做有一定弊端,1)每次都重新登录效率低下 2)cookie保存在客户端,有安全隐患。这时可以利用redis将用户的session集中管理,在这种模式只需要保证redis的高可用,每次用户session的更新和获取都可以快速完成。大大提高效率。
Hash(哈希) Redis hash数据结构 是一个键值对(key-value)集合,它是一个 string 类型的 field 和 value 的映射表, redis本身就是一个key-value型数据库,因此hash数据结构相当于在value中又套了一层key-value型数据。 所以redis中hash数据结构
特别适合存储关系型对象
1 2 3 4 5 6 redis 127.0.0.1:6379> HMSET runoob field1 "Hello" field2 "World" "OK" redis 127.0.0.1:6379> HGET runoob field1 "Hello" redis 127.0.0.1:6379> HGET runoob field2 "World"
应用场景
由于hash数据类型的key-value的特性,用来存储关系型数据库中表记录,是redis中哈希类型最常用的场景。一条记录作为一个key-value,把每列属性值对应成field-value存储在哈希表当中,然后通过key值来区分表当中的主键。
经常被用来存储用户相关信息。优化用户信息的获取,不需要重复从数据库当中读取,提高系统性能。
每个 hash 可以存储 232 -1 键值对(40多亿)。
List(列表) Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。
1 2 3 4 5 6 7 redis 127.0.0.1:6379> lpush runoob redis (integer ) 1 redis 127.0.0.1:6379> lpush runoob mongodb (integer ) 2 redis 127.0.0.1:6379> lrange runoob 0 10 1) "rabbitmq" 2) "mongodb"
list类型是用来存储多个有序的字符串的,列表当中的每一个字符看做一个元素
一个列表当中可以存储有一个或者多个元素,redis的list支持存储2^32次方-1个元素。
redis可以从列表的两端进行插入(pubsh)和弹出(pop)元素,支持读取指定范围的元素集, 或者读取指定下标的元素等操作。redis列表是一种比较灵活的链表数据结构,它可以充当队列或者栈的角色。
redis列表是链表型的数据结构,所以它的元素是有序的,而且列表内的元素是可以重复的。意味着它可以根据链表的下标获取指定的元素和某个范围内的元素集。
应用场景
消息队列:reids的链表结构,可以轻松实现阻塞队列,可以使用左进右出的命令组成来完成队列的设计。比如:数据的生产者可以通过Lpush命令从左边插入数据,多个数据消费者,可以使用BRpop命令阻塞的“抢”列表尾部的数据。
文章列表或者数据分页展示的应用。比如,我们常用的博客网站的文章列表,当用户量越来越多时,而且每一个用户都有自己的文章列表,而且当文章多时,都需要分页展示,这时可以考虑使用redis的列表,列表不但有序同时还支持按照范围内获取元素,可以完美解决分页查询功能。大大提高查询效率。
Set(集合) Redis 的 Set 是 string 类型的无序集合。 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。sadd 命令
添加一个 string 元素到 key 对应的 set 集合中,成功返回 1,如果元素已经在集合中返回 0。
1 2 3 4 5 6 7 8 9 10 11 12 redis 127.0.0.1:6379> sadd runoob redis (integer ) 1 redis 127.0.0.1:6379> sadd runoob mongodb (integer ) 1 redis 127.0.0.1:6379> sadd runoob rabbitmq (integer ) 1 redis 127.0.0.1:6379> sadd runoob rabbitmq (integer ) 0 redis 127.0.0.1:6379> smembers runoob 1) "redis" 2) "rabbitmq" 3) "mongodb"
注意:以上实例中 rabbitmq 添加了两次,但根据集合内元素的唯一性,第二次插入的元素将被忽略。
应用场景
标签
:比如我们博客网站常常使用到的兴趣标签,把一个个有着相同爱好,关注类似内容的用户利用一个标签把他们进行归并。
共同好友功能,共同喜好
,或者可以引申到二度好友之类的扩展应用。
统计网站的独立IP
。利用set集合当中元素不唯一性,可以快速实时统计访问网站的独立IP。
zset(sorted set:有序集合) Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。 不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。 zset的成员是唯一的,但分数(score)却可以重复。 zadd 命令 添加元素到集合,元素在集合中存在则更新对应scorezadd key score member
1 2 3 4 5 6 7 8 9 10 11 12 redis 127.0.0.1:6379> zadd runoob 0 redis (integer ) 1 redis 127.0.0.1:6379> zadd runoob 0 mongodb (integer ) 1 redis 127.0.0.1:6379> zadd runoob 0 rabbitmq (integer ) 1 redis 127.0.0.1:6379> zadd runoob 0 rabbitmq (integer ) 0 redis 127.0.0.1:6379> ZRANGEBYSCORE runoob 0 1000 1) "mongodb" 2) "rabbitmq" 3) "redis"
应用场景
排行榜
:有序集合经典使用场景。例如视频网站需要对用户上传的视频做排行榜,榜单维护可能是多方面:按照时间、按照播放量、按照获得的赞数等。
用Sorted Sets来做带权重的队列
,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。
redis 服务命令 连接 Redis 连接命令主要是用于连接 redis 服务。
实例
以下实例演示了客户端如何通过密码验证连接到 redis 服务,并检测服务是否在运行:
1 2 3 4 redis 127.0.0.1:6379> AUTH "password" OK redis 127.0.0.1:6379> PING PONG
Quit 命令 Redis Quit 命令
用于关闭与当前客户端与redis服务的连接。 一旦所有等待中的回复(如果有的话)顺利写入到客户端,连接就会被关闭。
redis Quit 命令基本语法如下:
1 redis 127.0.0.1:6379> QUIT
事务 Redis 事务可以一次执行多个命令, 并且带有以下三个重要的保证:
批量操作在发送 EXEC 命令前被放入队列缓存。
收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。
在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。
一个事务从开始到执行会经历以下三个阶段:
开始事务
命令入队
执行事务
实例
以下是一个事务的例子, 它先以 MULTI 开始一个事务, 然后将多个命令入队到事务中, 最后由 EXEC 命令触发事务, 一并执行事务中的所有命令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 redis 127.0.0.1:6379> MULTI OK redis 127.0.0.1:6379> SET book-name "Mastering C++ in 21 days" QUEUED redis 127.0.0.1:6379> GET book-name QUEUED redis 127.0.0.1:6379> SADD tag "C++" "Programming" "Mastering Series" QUEUED redis 127.0.0.1:6379> SMEMBERS tag QUEUED redis 127.0.0.1:6379> EXEC 1) OK 2) "Mastering C++ in 21 days" 3) (integer ) 3 4) 1) "Mastering Series" 2) "C++" 3) "Programming"
发布订阅–原理/使用场景 发布订阅简述 Redis提供了基于“发布/订阅”模式的消息机制。此种模式下,消息发布者和订阅者不进行直接通信,发布者客户端向指定的频道(channel) 发布消息,订阅该频道的每个客户端都可以收到该消息, 如图1所示。Redis提供了若干命令支持该功能,在实际应用开发时,能够为此类问题提供实现方法。
使用场景 业务解耦
聊天室、公告牌、服务之间利用消息解耦都可以使用发布订阅模式。 下面以简单的服务解耦进行说明。如图2所示,图中有两套业务,上面为视频管理系统,负责管理视频信息; 下面为视频服务面向客户,用户可以通过各种客户端(手机、 浏览器、 接口) 获取到视频信息。
假如视频管理员在视频管理系统中对视频信息进行了变更,希望及时通知给视频服务端,就可以采用发布订阅的模式,发布视频信息变化的消息到指定频道,视频服务订阅这个频道及时更新视频信息,通过这种方式可以有效解决两个业务的耦合性。
框架应用
Redisson的分布式锁的实现就采用了发布订阅模式:获取锁时,若获取不成功则订阅释放锁的消息,在收到释放锁的消息前阻塞,收到释放锁的消息后再去循环获取锁。
异步处理
可以采用Redis的发布订阅模式来实现异步处理,从而提高并发量。 比如,秒杀功能就可以这样做:
秒杀之前,将产品的库存从数据库同步到Redis
秒杀时,通过lua脚本保证原子性
扣减库存
将订单数据通过Redis的发布订阅功能发布出去
返回1(表示成功)
订单数据的Redis订阅者处理订单数据
发布命令 Redis主要提供了发布消息
、订阅频道
、取消订阅
以及按照模式订阅和取消订阅
等命令。
发布消息 命令 publish channel message
面操作会向channel: sports频道发布一条消息“Tim won the championship”,返回结果为订阅者个数,因为此时没有订阅,所以返回结果为0
1 2 127.0.0.1:6379> publish channel:sports "Tim won the championship" (integer ) 0
订阅消息 命令 subscribe channel [channel …]
订阅者可以订阅一个或多个频道,下面操作为当前客户端订阅了channel:sports
频道:
1 2 3 4 5 127.0.0.1:6379> subscribe channel:sports Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "channel:sports" 3) (integer ) 1
此时另一个客户端发布一条消息:
1 2 232127.0.0.1:6379> publish channel:sports "James lost the championship" (integer ) 1
当前订阅者客户端会收到如下消息:
1 2 3 4 5 6 127.0.0.1:6379> subscribe channel:sports Reading messages... (press Ctrl-C to quit) ... 1) "message" 2) "channel:sports" 3) "James lost the championship"
如果有多个客户端同时订阅了channel:sports,整个过程如图3所示。
两点需要注意
客户端在执行订阅命令之后进入了订阅状态,只能接收subscribe
、psubscribe
、 unsubscribe
、 punsubscribe
的四个命令。
新开启的订阅客户端,无法收到该频道之前的消息,因为Redis不会对发布的消息进行持久化。
缓存过期策略 定时过期 每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。
优点
:该策略可以立即清除过期的数据,对内存很友好.
缺点
:若过期key很多,删除这些key会会占用大量的CPU资源去处理过期的数据,从而影响性能。
惰性过期 只有当访问一个key时,才会判断该key是否已过期,过期则清除。
优点
:该策略可以最大化地节省CPU资源:删除操作只发生在取出key的时候发生,而且只删除当前key,所以对CPU时间的占用是比较少的,而且此时的删除是已经到了非做不可的地步(如果此时还不删除的话,我们就会获取到了已经过期的key了)
缺点
:对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存,甚至导致内存泄漏。
定期过期
每隔一定的时间,会扫描一定数量的的key,并清除其中已过期的key。
该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
Redis中同时使用了惰性过期和定期过期两种过期策略。
缓存淘汰策略 实际上redis定义了【八种内存淘汰策略】来处理redis内存满的情况
noeviction
:直接返回错误,不淘汰任何已经存在的redis键allkeys-lru
:所有的键使用lru算法进行淘汰volatile-lru
:有过期时间的使用lru算法进行淘汰allkeys-random
:随机删除redis键volatile-random
:随机删除有过期时间的redis键volatile-ttl
:删除快过期的redis键volatile-lfu
:根据lfu算法从有过期时间的键删除allkeys-lfu
:根据lfu算法从所有键删除
设置合适缓存容量
具体结合应用数据实际访问特点和成本开销来综合考虑。一般建议把缓存容量设置为总数据量的 15% 到 30%,兼顾访问性能和内存空间开销。 对于 Redis 来说,一旦确定了缓存最大容量,比如 4GB,就可以执行下面命令:CONFIG SET maxmemory 4gb
不过,缓存被写满是不可避免的。
缓存替换需要解决两个问题:
淘汰策略有哪些 Redis 4.0 之前一共实现了 6 种内存淘汰策略,在 4.0 之后,又增加了 2 种策略。我们可以按照是否会进行数据淘汰把它们分成两类
不进行数据淘汰的策略,只有 noeviction 这一种。
会进行淘汰的 7 种其他策略。 会进行淘汰的 7 种策略,我们可以再进一步根据淘汰候选数据集的范围把它们分成两类:
在设置了过期时间的数据中进行淘汰,包括 volatile-random
、volatile-ttl
、volatilelru
、volatile-lfu(Redis 4.0 后新增)
四种。
在所有数据范围内进行淘汰,包括 allkeys-lru
、allkeys-random
、allkeys-lfu(Redis 4.0 后新增)
三种。
默认情况下,redis在使用的内存空间超过maxmemory值时,并不会淘汰数据,也就是设置的noeviction 策略。 对应到redis缓存,也就是值,一旦缓存被写满了,再有写请求到来时,redis不会提供服务,而是直接返回错误。(因此不推荐使用)
对于 volatile-random
、volatile-ttl
、volatile-lru
和 volatile-lfu
这四种淘汰策略,它们筛选的候选数据范围,被限制在已经设置了过期时间的键值对上。也正是因为如此,即使缓存没有写满,这些数据如果过期了,也会被删除。volatile-ttl
在筛选时,会针对设置了过期时间的键值对,根据过期时间的先后进行删除,越早过期的越先被删除。volatile-random
就像它的名称一样,在设置了过期时间的键值对中,进行随机删除。volatile-lru
会使用 LRU 算法筛选设置了过期时间的键值对。volatile-lfu
会使用 LFU 算法选择设置了过期时间的键值对。(LFU算法会在LRU算法的基础上,同时考虑数据的访问时效性和数据的访问次数)
对于allkeys-lru
、allkeys-random
、allkeys-lfu
这三种淘汰策略的备选淘汰数据范围,扩大到了所有键值对,无论这些键值对是否设置了过期时间。allkeys-random
策略,从所有键值对中随机选择并删除数据;allkeys-lru
策略,使用 LRU 算法在所有数据中进行筛选。allkeys-lfu
策略,使用 LFU 算法在所有数据中进行筛选 也就是说,如果一个键值对被删除策略选中了,即使它的过期时间还没到,也需要被删除。当然,如果它的过期时间到了但未被策略选中,同样也会被删除。
如何使用呢 优先使用allkeys-lru策略。这样,可以充分利用LRU算法的优势,把最近最常访问的数据留在缓存中,提升应用的访问性能。如果你的业务数据中有明显的冷热数据的区分,建议使用allkeys-lru策略 如果业务应用中的数据访问频率不大,没有明显的冷热数据区分,建议使用allkeys-random策略,随机淘汰数据即可 如果业务有置顶的需求,比如置顶新闻、置顶视频,那么,可以使用 volatile-lru策略,同时不给这些置顶数据设置过期时间。这样一来,这些需要置顶的数据一直不会被删除,而其他数据会在过期时根据 LRU 规则进行筛选
LRU算法 LRU 算法的全称是 Least Recently Used,从名字上就可以看出,这是按照最近最少使用的原则来筛选数据,最不常用的数据会被筛选出来,而最近频繁使用的数据会留在缓存中。
那具体是怎么筛选的呢? LRU会把所有的数据组织成一个链表,链表的头和尾分别表示MRU端和LRU段,分别代表最近最常使用的数据和最近最不常用的数据。
我们现在有数据 6、3、9、20、5。如果数据 20 和 3 被先后访问,它们都会从现有的链表位置移到 MRU 端,而链表中在它们之前的数据则相应地往后移一位。
因为,LRU 算法选择删除数据时,都是从 LRU 端开始,所以把刚刚被访问的数据移到 MRU 端,就可以让它们尽可能地留在缓存中。
如果有一个新数据 15 要被写入缓存,但此时已经没有缓存空间了,也就是链表没有空余位置了,那么,LRU 算法做两件事:
数据 15 是刚被访问的,所以它会被放到 MRU 端;
算法把 LRU 端的数据 5 从缓存中删除,相应的链表中就没有数据 5 的记录了。
其实,LRU 算法背后的想法非常朴素:
它认为刚刚被访问的数据,肯定还会被再次访问,所以就把它放在 MRU 端; 长久不访问的数据,肯定就不会再被访问了,所以就让它逐渐后移到 LRU 端,在缓存满时,就优先删除它。
不过,LRU算法在实际实现时,需要用链表管理所有的缓存数据,这会带来额外的空间开销。 而且,当有数据被访问时,需要在链表上把该数据移动到MRU端,如果有大量数据被访问,就会带来很多链表移动操作,会很耗时,进而降低redis缓存性能。
所以,在redis中,LRU算法被做了简化,以减轻淘汰数据对缓存性能的影响。
具体来说,redis会默认记录每一个数据的最近一次访问的时间戳(由键值对数据结构RedisObject 中的 lru 字段记录)。 然后,redis在决定淘汰的数据时,第一次会随机选出N个数据,把它们作为一个候选集合。 接下来,redis会比较这N个数据的lru字段,把lru字段值最小的数据从缓存中淘汰出去。
Redis 提供了一个配置参数 maxmemory-samples,这个参数就是 Redis 选出的数据个数N。 例如,我们执行如下命令,可以让 Redis 选出 100 个数据作为候选数据集:
1 CONFIG SET maxmemory-samples 100
当再次淘汰数据时,redis需要挑选数据进入第一次淘汰时创建的候选集合。 这里的挑选标准是:能进入候选集合的数据的lru字段值小于候选集合中最小的 lru 值。
当有新数据进入候选集合后,如果候选数据集合中的数据个数达到了maxmemory-samples,redis就把候选集合集中lru子弹值最下的数据淘汰出去。
这样一来,Redis 缓存不用为所有的数据维护一个大链表,也不用在每次数据访问时都移动链表项,提升了缓存的性能。
处理被淘汰的数据 一般来说,一旦被淘汰的数据选定后:
如果这个数据时干净数据,那我们就直接删除;
如果这个数据是脏数据,我们需要把它写回数据库;
判断数据脏还是净 干净数据和脏数据的区别就在于,和最初从后端数据库里读取时的值相比,有没有被修改过。干净数据一直没有被修改,所以后端数据库里的数据也是最新值。在替换时,它可以被直接删除。 而脏数据就是曾经被修改过的,已经和后端数据库中保存的数据不一致了。此时,如果不把脏数据写回到数据库中,这个数据的最新值就丢失了,就会影响应用的正常使用。
不过,对于redis来说,它决定了被淘汰的数据后,会把它们删除。 即使淘汰的数据时脏数据,redis也不会把它们写回数据库。 所以,我们在使用redis缓存时,如果数据被修改了,需要在数据修改时就将它写回数据库。 否则,这个脏数据被淘汰时,会被redis删除,而数据库里也没有最新的数据了。
淘汰策略配置 最大内存配置 方式一:修改配置文件中的maxmemory
,放开注释, 根据业务需求设置大小
1 2 #设置最大内存大小为100MB maxmemory <100mb
方式二:命令行设置
1 2 3 4 127.0.0.1:6379> config get maxmemory 127.0.0.1:6379> config set maxmemory 100mb
淘汰策略配置 方式一:修改配置文件中的 maxmemory-policy
,放开注释,根据业务需求修改即可
1 2 #noeviction 为默认的策略,根据业务需求修改即可 maxmemory-policy noeviction
方式二:命令行设置
#命令行查看当前淘汰策略
127.0.0.1:6379> config get maxmemory-policy
#noeviction 为默认的策略
127.0.0.1:6379> config set maxmemory-policy noeviction
小结 Redis 4.0 版本以后一共提供了 8 种数据淘汰策略,从淘汰数据的候选集范围来看,我们有两种候选范围:一种是所有数据都是候选集,一种是设置了过期时间的数据是候选集。另外,无论是面向哪种候选数据集进行淘汰数据选择,我们都有三种策略,分别是随机选择,根据 LRU 算法选择,以及根据 LFU 算法选择。当然,当面向设置了过期时间的数据集选择淘汰数据时,我们还可以根据数据离过期时间的远近来决定。
一般来说,缓存系统对于选定的被淘汰数据,会根据其是干净数据还是脏数据,选择直接删除还是写回数据库。但是,在 Redis 中,被淘汰数据无论干净与否都会被删除,所以,这是我们在使用 Redis 缓存时要特别注意的:当数据修改成为脏数据时,需要在数据库中也把数据修改过来
选择哪种缓存策略是值得我们多加琢磨的,它在筛选数据方面是否能筛选出可能被再次访问的数据,直接决定了缓存效率的高与低。 很简单的一个对比,如果我们使用随机策略,刚筛选出来的要被删除的数据可能正好又被访问了,此时应用就只能花费几毫秒从数据库中读取数据了。而如果使用 LRU 策略,被筛选出来的数据往往是经过时间验证了,如果在一段时间内一直没有访问,本身被再次访问的概率也很低了。
所以,建议,先根据是否有始终会被频繁访问的数据(例如置顶消息),来选择淘汰数据的候选集,也就是决定是针对所有数据进行淘汰,还是针对设置了过期时间的数据进行淘汰。候选数据集范围选定后,建议优先使用 LRU 算法,也就是,allkeys-lru 或volatile-lru 策略。
当然,设置缓存容量的大小也很重要,我的建议是:结合实际应用的数据总量、热数据的体量,以及成本预算,把缓存空间大小设置在总数据量的 15% 到 30% 这个区间就可以
返回 redis 系列