目的
Redis是一种支持Key-Value等多种数据结构的存储系统。可用于缓存,事件发布或订阅,高速队列等场景。该数据库使用ANSI C语言编写,支持网络,提供字符串,哈希,列表,队列,集合结构直接存取,基于内存,可持久化。本文介绍Redis的数据类型和相关操作。
支持的语言
Redis的应用场景
1,会话缓存(最常用)
2,消息队列,比如支付
3,活动排行榜或计数
4,发布,订阅消息(消息通知)
5,商品列表,评论列表等
数据类型以及相关操作
Redis一共支持五种数据类:string(字符串),hash(哈希),list(列表),set(集合)和zset(sorted set有序集合),在3.2版本以后新添加geo经纬度支持,以下将对其类型的常用操作做说明。
命令使用前言
通大多数据库一样,redis所有的命令提供了帮助,可以使用help +命令名称查看其使用方法,帮助信息中不仅有命令用法,还有命令始于版本信息,分组等。
为了友好的使用,redis还将所有命令都进行了分组,同时使用help+@+组名进行查看每个组中所有命令,以下是所有分组信息。
上面以及介绍如何查看命令使用方法,所以在以下数据类型操作时候,只举例常用的命令,更多命令参考https://redis.io/commands
注意:redis在3.2版本新增geo数据类型。
1 | generic #一般命令组,对大多数类型适用 |
示例
服务操作
1 | slect#选择数据库(数据库编号0-15) |
事务操作
1 | DEL key #删除某个key |
string操作
它是redis的最基本的数据类型,一个键对应一个值,需要注意是一个键值最大存储512MB,redis中的整型也当作字符串处理。
1 | SET key value [EX seconds] [PX milliseconds] [NX|XX] #设置key为指定的字符串值。 |
list操作
列表中的元素索引从0开始,倒数的元素可以用“-”+倒数位置表示,如-2,代表倒数第二个元素,-1则代表最后一个元素。
Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边。
一个列表最多可以包含 2 32 - 1 个元素 (4294967295, 每个列表超过40亿个元素)。
1 | LPUSH key value [value ...] #从列表左边放入一个或者多个元素 |
hash操作
hash操作所有命令都以H开头。
Redis hash 是一个string类型的field和value的映射表,hash特别适合用于存储对象。
Redis 中每个 hash 可以存储 2 32 - 1 键值对(40多亿)。
1 | HDEL key field [field ...] #删除hash表中一个或多个字段 |
集合set操作
Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
集合中最大的成员数为 2 32 - 1 (4294967295, 每个集合可存储40多亿个成员)。
1 | SADD key member [member ...] #添加一个或多个元素到集合中 |
有序集合操作
Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数(score)却可以重复。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。 集合中最大的成员数为 2 32 - 1 (4294967295, 每个集合可存储40多亿个成员)。
1 | ZADD key [NX|XX] [CH] [INCR] score member [score member ...] #向一个有序集合添加成员(元素) |
GEO类型操作
Redis的GEO是 3.2 版本的新特性,对GEO(地理位置)的支持。这个功能可以将用户给定的地理位置信息储存起来, 并对这些信息进行操作。
geo类型命令不多,总共6个所以这里全部列举出来了。
1 | GEOADD key longitude latitude member [longitude latitude member ...] #将指定的地理空间位置(纬度、经度、名称)添加到指定的key中 |
Redis的发布与订阅
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。
Redis 客户端可以订阅任意数量的频道。
下图是三个客户端同时订阅同一个频道
下图是有新信息发送给频道1时,就会将消息发送给订阅它的三个客户端
运作原理
每个 Redis 服务器进程都维持着一个表示服务器状态的 redis.h/redisServer
结构, 结构的 pubsub_channels
属性是一个字典, 这个字典就用于保存订阅频道的信息:
1 | struct redisServer { |
其中,字典的键为正在被订阅的频道, 而字典的值则是一个链表, 链表中保存了所有订阅这个频道的客户端。
比如说,在下图展示的这个 pubsub_channels
示例中, client1
、 client2
和 client3
就订阅了 channel1
, 而client3也同时订阅了channel2。
当客户端调用 SUBSCRIBE 命令时, 程序就将客户端和要订阅的频道在 pubsub_channels
字典中关联起来。
SUBSCRIBE 命令的行为可以用伪代码表示如下:
1 | def SUBSCRIBE(client, channels): |
通过 pubsub_channels
字典, 程序只要检查某个频道是否为字典的键, 就可以知道该频道是否正在被客户端订阅; 只要取出某个键的值, 就可以得到所有订阅该频道的客户端的信息。
了解了 pubsub_channels
字典的结构之后, 解释 PUBLISH 命令的实现就非常简单了: 当调用 PUBLISH channel message
命令, 程序首先根据 channel
定位到字典的键, 然后将信息发送给字典值链表中的所有客户端。
订阅模式
redis的发布订阅不仅仅提供简单的订阅频道,还提供模式匹配订阅。模式订阅使用命令PSUBSCRIBE实现。
redisServer.pubsub_patterns
属性是一个链表,链表中保存着所有和模式相关的信息:
1 | struct redisServer { |
链表中的每个节点都包含一个 redis.h/pubsubPattern
结构:
1 | typedef struct pubsubPattern { |
client
属性保存着订阅模式的客户端,而 pattern
属性则保存着被订阅的模式。
每当调用 PSUBSCRIBE
命令订阅一个模式时, 程序就创建一个包含客户端信息和被订阅模式的 pubsubPattern
结构, 并将该结构添加到 redisServer.pubsub_patterns
链表中。
作为例子,下图展示了一个包含两个模式的 pubsub_patterns
链表, 其中 client123
和 client256
都正在订阅 tweet.shop.*
模式:
通过遍历整个 pubsub_patterns
链表,程序可以检查所有正在被订阅的模式,以及订阅这些模式的客户端。
当执行PUBLISH进行命令向channel命令发送消息时,PUBLISH 除了将 message
发送到所有订阅 channel
的客户端之外, 它还会将 channel
和 pubsub_patterns
中的模式进行对比, 如果 channel
和某个模式匹配的话, 那么也将 message
发送到订阅那个模式的客户端,例如一个客户端订阅了aa.bb.*频道,那么他会收到来自所有aa.bb开头的所有频道消息。
相关命令
1 | PSUBSCRIBE pattern [pattern ...] #使用模式订阅一个或多个符合给定模式的频道 |
实践
在以下示例中,将分别用SUBSCRIBE命令订阅aa.bb和使用PSUBSCRIBE模式订阅频道aa.bb*。
SUBSCRIBE订阅:
PSUBSCRIBE订阅:
此时我们使用PUBSH向aa.bb发送消息,返回接受到的频道数,两个订阅者都能收到消息。
订阅者1:
模式订阅者:
小结
- 订阅信息由服务器进程维持的
redisServer.pubsub_channels
字典保存,字典的键为被订阅的频道,字典的值为订阅频道的所有客户端。 - 当有新消息发送到频道时,程序遍历频道(键)所对应的(值)所有客户端,然后将消息发送到所有订阅频道的客户端上。
- 订阅模式的信息由服务器进程维持的
redisServer.pubsub_patterns
链表保存,链表的每个节点都保存着一个pubsubPattern
结构,结构中保存着被订阅的模式,以及订阅该模式的客户端。程序通过遍历链表来查找某个频道是否和某个模式匹配。 - 当有新消息发送到频道时,除了订阅频道的客户端会收到消息之外,所有订阅了匹配频道的模式的客户端,也同样会收到消息。
- 退订频道和退订模式分别是订阅频道和订阅模式的反操作。
事务
所谓事务应具有以下特效:原子性(Atomicity), 一致性(Consistency),隔离性(Isolation),持久性(Durability),简称ACID,但redis所提供的事务比较简单,它通过MULTI、EXEC、DISCARD和WATCH等命令实现事务。
而Redis只支持简单的事务,将执行命令放入队列缓存,当程序中有异常或命令出错,执行DISCARD清空缓存队列不执行队列中命令,其事务过程有以下特点:
- 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
- 事务是一个 泛原子 操作(这里我以泛原子称呼,在某些情况redis的事务不是原子性的,后续会说明):事务中的命令要么全部被执行,要么全部都不执行。
EXEC 命令负责触发并执行事务中的所有命令:
- 如果客户端在使用 MULTI 开启了一个事务之后,却因为断线而没有成功执行 EXEC ,那么事务中的所有命令都不会被执行。
- 另一方面,如果客户端成功在开启事务之后执行 EXEC ,那么事务中的所有命令都会被执行。
特别说明文中的 泛原子操作 :
- redis在开启事务以后,若执行命令具有显示的错误或者客户端中断则此次事务在执行EXEC命令时会调用DISCARD清空缓存队列不执行队列中的所有任务,此时是原子性的。
- 当执行命令过程中,命令没有显示的报错(例如LSET操作设置一个不存在的list),而是在EXEC调用时候某个命令出错,那么在这之前已经执行的命令将不会回滚,所以严格说来, redis并不支持原子性。
命令
1 | MULTI #用于标记事务块的开始。Redis会将后续的命令逐个放入队列中,然后才能使用EXEC命令执行缓存队列中的命令。 |
乐观锁机制
乐观锁:总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或检查再设置(CAS)操作实现。
redis通过WATCH命令实现乐观锁,作为WATCH命令的参数的键会受到Redis的监控,Redis能够检测到它们的变化。在执行EXEC命令之前,如果Redis检测到至少有一个键被修改了,那么整个事务便会中止运行,然后EXEC命令会返回一个nil值,提醒用户事务运行失败。
注意:WATCH命令需要在MULTI之前执行,不然redis会将其一个命令放入缓存队列中。
示例:在以下示例中通过一个客户端开启事务监听name键,另一个客户端在执行EXEC之前修改name键,此次事务将不会执行,并返回nil,如下。
原子性实践
为演示redis严格意义上将不支持原子性,做了一些简单实践。
从上面的结果可以看出,在开启事务前name 值为Rose,在开启事务先后执行了SET命令和LSET命令,但是LSET命令是错误的,当我们调用EXEC执行事务完事务以后,在回头看事务中的SET命令已经生效,并未回滚,因为在次过程中该命令没有显示的报错,所以可以说redis的事务不支持原子性。
Redis的持久化
redis持久有两种方式:快照(快照),仅附加文件(AOF).
快照
1,将存储在内存的数据以快照的方式写入二进制文件中,如默认dump.rdb中
2,保存900 1
#900秒内如果超过1个Key被修改,则启动快照保存
3,保存300 10
#300秒内如果超过10个Key被修改,则启动快照保存
4,保存60 10000
#60秒内如果超过10000个重点被修改,则启动快照保存
仅附加文件(AOF)
1,使用AOF持久时,服务会将每个收到的写命令通过写函数追加到文件中(appendonly.aof)
2,AOF持久化存储方式参数说明
1 | appendonly yes #开启AOF持久化存储方式 |
Redis的性能测试
自带相关测试工具
实际测试同时执行100万的请求