redis# 查看所有键 keys * # 键总数 dbsize # 键是否存在 exists ke # 删除key del key # 键过期 expire key seconds ttl hello # 有3种返回值:大于等于0的数:键剩余的过期时间;-1:键没设置过期时间;-2:键不存在 # 键的数据结构类型 type key
Redis的5种数据结构
可以看到每种数据结构都有两种以上的内部编码实现,
例如list数据结构包含了linkedlist和ziplist两种内部编码。
同时有些内部编码,例如ziplist,可以作为多种外部数据结构的内部实现,
可以通过object encoding命令查询内部编码:
redis>object encoding hello "embstr" > object encoding mylist "ziplist"
Redis这样设计有两个好处:
Redis使用了单线程架构和I/O多路复用模型来实现高性能的内存数据库服务,
本节首先通过多个客户端命令调用的例子说明Redis单线程命令处理机制,
接着分析Redis单线程模型为什么性能如此之高,最终给出为什么理解单线程模型是使用和运维Redis的关键。
- 引出单线程模型
现在开启了三个redis-cli客户端同时执行命令。 客户端1设置一个字符串键值对:
redis127.0.0.1:6379> set hello world
客户端2对counter做自增操作:
redis127.0.0.1:6379> incr counter
客户端3对counter做自增操作:
redis127.0.0.1:6379> incr counter
Redis客户端与服务端的模型可以简化成图2-3,每次客户端调用都经历了发送命令、执行命令、返回结果三个过程
其中第2步是重点要讨论的,因为Redis是单线程来处理命令的,所以一条命令从客户端达到服务端不会立刻被执行,
所有命令都会进入一个队列中,然后逐个被执行。
所以上面3个客户端命令的执行顺序是不确定的(如图2-4所示),但是可以确定不会有两条命令被同时执行(如图2-5所示),所以两条incr命令无论怎么执行最终结果都是2,
不会产生并发问题,这就是Redis单线程的基本模型。
但是像发送命令、返回结果、命令排队肯定不像描述的这么简单,Redis使用了I/O多路复用技术来解决I/O的问题,
- 为什么单线程还能这么快?
通常来讲,单线程处理能力要比多线程差,例如有10000斤货物,每辆车的运载能力是每次200斤,那么要50次才能完成,但是如果有50辆车,只要安排合理,只需要一次就可以完成任务。
那么为什么Redis使用单线程模型会达到每秒万级别的处理能力呢?可以将其归结为三点:
3. 单线程避免了线程切换和竞态产生的消耗
单线程能带来几个好处:
单线程会有一个问题:
如图2-7所示,字符串类型的值实际可以是字符串(简单的字符串、复杂的字符串(例如JSON、XML))、数字 (整数、浮点数),甚至是二进制(图片、音频、视频),但是值最大不能超过512MB
redisset key value [ex seconds] [px milliseconds] [nx|xx]
下面操作设置键为hello,值为world的键值对,返回结果为OK代表设置 成功:
redis127.0.0.1:6379> set hello world OK
set命令有几个选项:
除了set选项,Redis还提供了setex和setnx两个命令:
redissetex key seconds value setnx key value
它们的作用和ex和nx选项是一样的。下面的例子说明了set、setnx、set xx的区别。 当前键hello不存在:
127.0.0.1:6379> exists hello (integer) 0
设置键为hello,值为world的键值对:
127.0.0.1:6379> set hello world OK
因为键hello已存在,所以setnx失败,返回结果为0:
127.0.0.1:6379> setnx hello redis (integer) 0
因为键hello已存在,所以set xx成功,返回结果为OK:
127.0.0.1:6379> set hello jedis xx OK
**setnx和setxx在实际使用中有什么应用场景吗?
以setnx命令为例子,由于Redis的单线程命令处理机制,如果有多个客户端同时执行setnx key value,
根据setnx的特性只有一个客户端能设置成功,setnx可以作为分布式锁的一种实现方案,
Redis官方给出了使用setnx实现分布式锁的方法:http://redis.io/topics/distlock。
get key
下面操作获取键hello的值:
127.0.0.1:6379> get hello "world"
如果要获取的键不存在,则返回nil(空):
127.0.0.1:6379> get not_exist_key (nil)
mset key value [key value ...]
下面操作通过mset命令一次性设置4个键值对:
127.0.0.1:6379> mset a 1 b 2 c 3 d 4 OK
mget key [key ...] 下面操作批量获取了键a、b、c、d的值:
127.0.0.1:6379> mget a b c d 1) "1" 2) "2" 3) "3" 4) "4"
如果有些键不存在,那么它的值为nil(空),结果是按照传入键的顺序返回:
127.0.0.1:6379> mget a b c f 1) "1" 2) "2" 3) "3" 4) (nil)
使用mget命令后,要执行n次get命令操作只需要按照图2-9的方式来完 成,具体耗时如下:
n次get时间 = 1次网络时间 + n次命令时间
Redis可以支撑每秒数万的读写操作,但是这指的是Redis服务端的处理能力,
对于客户端来说,一次命令除了命令时间还是有网络时间,
假设网络时间为1毫秒,命令时间为0.1毫秒(按照每秒处理1万条命令算),
那么执行1000次get命令和1次mget命令的区别如表2-1,
因为Redis的处理能力已经足够高,对于开发人员来说,网络可能会成为性能的瓶颈。
incr key
incr命令用于对值做自增操作,返回结果分为三种情况:
除了incr命令,Redis提供了decr(自减)、incrby(自增指定数字)、 decrby(自减指定数字)、incrbyfloat(自增浮点数):
decr key incrby key increment decrby key decrement incrbyfloat key increment
很多存储系统和编程语言内部使用CAS机制实现计数功能,会有一定的CPU开销,
但在Redis中完全不存在这个问题,因为Redis是单线程架构,任何命令到了Redis服务端都要顺序执行。
追加值
append key value
append可以向字符串尾部追加值,例如:
127.0.0.1:6379> get key "redis" 127.0.0.1:6379> append key world (integer) 10 127.0.0.1:6379> get key "redisworld"
字符串长度
strlen key
例如,当前值为redisworld,所以返回值为10:
127.0.0.1:6379> get key "redisworld" 127.0.0.1:6379> strlen key (integer) 10
下面操作返回结果为6,因为每个中文占用3个字节
127.0.0.1:6379> set hello "世界" OK 127.0.0.1:6379> strlen hello (integer) 6
设置并返回原值
getset key value
getset和set一样会设置值,但是不同的是,它同时会返回键原来的值, 例如:
127.0.0.1:6379> getset hello world (nil) 127.0.0.1:6379> getset hello redis "world"
设置指定位置的字符
setrange key offeset value
下面操作将值由pest变为了best:
127.0.0.1:6379> set redis pest OK 127.0.0.1:6379> setrange redis 0 b (integer) 4 127.0.0.1:6379> get redis "best"
获取部分字符串
getrange key start end
start和end分别是开始和结束的偏移量,偏移量从0开始计算,例如下面 操作获取了值best的前两个字符。
127.0.0.1:6379> getrange redis 0 1 "be"
表2-2是字符串类型命令的时间复杂度,开发人员可以参考此表,结合 自身业务需求和数据大小选择适合的命令。
字符串类型的内部编码有3种:
Redis会根据当前值的类型和长度决定使用哪种内部编码实现。
图2-10是比较典型的缓存使用场景,其中Redis作为缓存层,MySQL作为存储层,
绝大部分请求的数据都是从Redis中获取。
由于Redis具有支撑高并发的特性,所以缓存通常能起到加速读写和降低后端压力的作用。
下面伪代码模拟了图2-10的访问过程:
UserInfo getUserInfo(long id){ ... }
// 定义键 userRedisKey = "user:info:" + id; // 从Redis获取值 value = redis.get(userRedisKey); if (value != null) { // 将值进行反序列化为UserInfo并返回结果 userInfo = deserialize(value); return userInfo; }
开发提示
与MySQL等关系型数据库不同的是,Redis没有命令空间,而且也没有 对键名有强制要求(除了不能使用一些特殊字符)。
但设计合理的键名,有利于防止键冲突和项目的可维护性,
比较推荐的方式是使用“业务名:对象名:id:[属性]”作为键名(也可以不是分号)。
例如MySQL的数据库名为vs,用户表名为user,
那么对应的键可以用"vs:user:1","vs:user:1:name"来表示,
如果当前Redis只被一个业务使用,甚至可以去掉“vs:”。
如果键名比较长,例如“user:{uid}:friends:messages:{mid}”,
可以在能描述键含义的前提下适当减少键的长度,
例如变为“u:{uid}:fr:m:{mid}”,从而减少由于键过长的内存浪费
// 从MySQL获取用户信息 userInfo = mysql.get(id); // 将userInfo序列化,并存入Redis redis.setex(userRedisKey, 3600, serialize(userInfo)); // 返回结果 return userInfo
许多应用都会使用Redis作为计数的基础工具,它可以实现快速计数、查询缓存的功能,
同时数据可以异步落地到其他数据源。
例如笔者所在团队的视频播放数系统就是使用Redis作为视频播放数计数的基础组件,
用户每播放一次视频,相应的视频播放数就会自增1:
long incrVideoCounter(long id) { key = "video:playCount:" + id; return redis.incr(key); }
开发提示
实际上一个真实的计数系统要考虑的问题会很多:
防作弊、按照不同维度计数,数据持久化到底层数据源等。
如图2-11所示,一个分布式Web服务将用户的Session信息(例如用户登 录信息)保存在各自服务器中,这样会造成一个问题,出于负载均衡的考 虑,
分布式服务会将用户的访问均衡到不同服务器上,用户刷新一次访问可 能会发现需要重新登录,这个问题是用户无法容忍的。
为了解决这个问题,可以使用Redis将用户的Session进行集中管理,
如图2-12所示,在这种模式下只要保证Redis是高可用和扩展性的,
每次用户更新或者查询登录信息都直接从Redis中集中获取。
很多应用出于安全的考虑,会在每次进行登录时,让用户输入手机验证码,从而确定是否是用户本人。
但是为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过5次
phoneNum = "138xxxxxxxx"; key = "shortMsg:limit:" + phoneNum; // SET key value EX 60 NX isExists = redis.set(key,1,"EX 60","NX"); if(isExists != null || redis.incr(key) <=5){ // 通过 }else{ // 限速 }
几乎所有的编程语言都提供了哈希(hash)类型,
它们的叫法可能是哈希、字典、关联数组。在Redis中,哈希类型是指键值本身又是一个键值对 结构,
形如value={{field1,value1},...{fieldN,valueN}},
Redis键值对和哈希类型二者的关系可以用图2-14来表示。
hset key field value
下面为user:1添加一对field-value:
127.0.0.1:6379> hset user:1 name tom (integer) 1
如果设置成功会返回1,反之会返回0。此外Redis提供了hsetnx命令,它 们的关系就像set和setnx命令一样,只不过作用域由键变为field。
hget key field
例如,下面操作获取user:1的name域(属性)对应的值:
127.0.0.1:6379> hget user:1 name "tom"
如果键或field不存在,会返回nil:
127.0.0.1:6379> hget user:2 name (nil) 127.0.0.1:6379> hget user:1 age (nil)
hdel key field [field ...]
hdel会删除一个或多个field,返回结果为成功删除field的个数,例如:
127.0.0.1:6379> hdel user:1 name (integer) 1 127.0.0.1:6379> hdel user:1 age (integer) 0
hlen key
例如user:1有3个field:
127.0.0.1:6379> hset user:1 name tom (integer) 1 127.0.0.1:6379> hset user:1 age 23 (integer) 1 127.0.0.1:6379> hset user:1 city tianjin (integer) 1 127.0.0.1:6379> hlen user:1 (integer) 3
hmget key field [field ...] hmset key field value [field value ...]
hmset和hmget分别是批量设置和获取field-value,hmset需要的参数是key 和多对field-value,hmget需要的参数是key和多个field。例如:
127.0.0.1:6379> hmset user:1 name mike age 12 city tianjin OK 127.0.0.1:6379> hmget user:1 name city 1) "mike" 2) "tianjin"
hexists key field
例如,user:1包含name域,所以返回结果为1,不包含时返回0:
127.0.0.1:6379> hexists user:1 name (integer) 1
hkeys key
hkeys命令应该叫hfields更为恰当,它返回指定哈希键所有的field,例 如:
127.0.0.1:6379> hkeys user:1 1) "name" 2) "age" 3) "city"
hvals key
下面操作获取user:1全部value:
127.0.0.1:6379> hvals user:1 1) "mike" 2) "12" 3) "tianjin"
hgetall key
下面操作获取user:1所有的field-value:
127.0.0.1:6379> hgetall user:1 1) "name" 2) "mike" 3) "age" 4) "12" 5) "city" 6) "tianjin"
开发提示
在使用hgetall时,如果哈希元素个数比较多,会存在阻塞Redis的可能。
如果开发人员只需要获取部分field,可以使用hmget,
如果一定要获取全部field-value,可以使用hscan命令,该命令会渐进式遍历哈希类型,
hincrby hincrbyfloat hincrby key field hincrbyfloat key field
hincrby和hincrbyfloat,就像incrby和incrbyfloat命令一样,但是它们的作 用域是filed。
hstrlen key field
例如hget user:1name的value是tom,那么hstrlen的返回结果是3: 98
127.0.0.1:6379> hstrlen user:1 name (integer) 3
表2-3是哈希类型命令的时间复杂度,开发人员可以参考此表选择适合 的命令
哈希类型的内部编码有两种:
下面的示例演示了哈希类型的内部编码,以及相应的变化。
127.0.0.1:6379> hmset hashkey f1 v1 f2 v2 OK 127.0.0.1:6379> object encoding hashkey "ziplist"
127.0.0.1:6379> hset hashkey f3 "one string is bigger than 64 byte...忽略..." OK 127.0.0.1:6379> object encoding hashkey "hashtable"
127.0.0.1:6379> hmset hashkey f1 v1 f2 v2 f3 v3 ...忽略... f513 v513 OK 127.0.0.1:6379> object encoding hashkey "hashtable"
图2-15为关系型数据表记录的两条用户信息,用户的属性作为表的列,每条用户信息作为行。
到目前为止,我们已经能够用三种方法缓存用户信息,下面给出三种方案的实现方法和优缺点分析:
set user:1:name tom set user:1:age 23 set user:1:city beijing
所以此种方案一般不会在生产环境使用。
set user:1 serialize(userInfo)
部数据取出进行反序列化,更新后再序列化到Redis中
hmset user:1 name tomage 23 city beijing
列表中的每个字符串称为元素(element),一个列表最多可以存储2^32-1个元素。在Redis中,
可以对列表两端插入(push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等(如图2-18和图2-19所示)
列表类型有两个特点:
从右边插入元素
rpush key value [value ...]
下面代码从右向左插入元素c、b、a:
127.0.0. 1:6379> rpush listkey c b a (integer) 3
lrange0-1命令可以从左到右获取列表的所有元素:
127.0.0.1:6379> lrange listkey 0 -1 1) "c" 2) "b" 3) "a"
从左边插入元素
lpush key value [value ...]
使用方法和rpush相同,只不过从左侧插入,这里不再赘述。
向某个元素前或者后插入元素
linsert key before|after pivot value
linsert命令会从列表中找到等于pivot的元素,在其前(before)或者后 (after)插入一个新的元素value,例如下面操作会在列表的元素b前插入java:
127.0.0.1:6379> linsert listkey before b java (integer) 4
返回结果为4,代表当前命令的长度,当前列表变为:
127.0.0.1:6379> lrange listkey 0 -1 1) "c" 2) "java" 3) "b" 4) "a"
获取指定范围内的元素列表
lrange key start end
lrange操作会获取列表指定索引范围所有的元素。索引下标有两个特点:
127.0.0.1:6379> lrange listkey 1 3 1) "java" 2) "b" 3) "a"
获取列表指定索引下标的元素
lindex key index
例如当前列表最后一个元素为a:
127.0.0.1:6379> lindex listkey -1 "a"
获取列表长度
llen key
例如,下面示例当前列表长度为4:
127.0.0.1:6379> llen listkey (integer) 4
从列表左侧弹出元素
lpop key 110
如下操作将列表最左侧的元素c会被弹出,弹出后列表变为java、b、a:
127.0.0.1:6379>t lpop listkey "c" 127.0.0.1:6379> lrange listkey 0 -1 1) "java" 2) "b" 3) "a"
从列表右侧弹出
rpop key
它的使用方法和lpop是一样的,只不过从列表右侧弹出,这里不再赘述。
删除指定元素
lrem key count value
lrem命令会从列表中找到等于value的元素进行删除,根据count的不同分为三种情况:
例如向列表从左向右插入5个a,那么当前列表变为“a a a a a java b a”,
下面操作将从列表左边开始删除4个为a的元素:
127.0.0.1:6379> lrem listkey 4 a (integer) 4 127.0.0.1:6379> lrange listkey 0 -1 1) "a" 2) "java" 3) "b" 4) "a"
按照索引范围修剪列表
ltrim key start end
例如,下面操作会只保留列表listkey第2个到第4个元素:
127.0.0.1:6379> ltrim listkey 1 3 OK 127.0.0.1:6379> lrange listkey 0 -1 1) "java" 2) "b" 3) "a"
修改指定索引下标的元素:
lset key index newValue
下面操作会将列表listkey中的第3个元素设置为python:
127.0.0.1:6379> lset listkey 2 python OK 127.0.0.1:6379> lrange listkey 0 -1 1) "java" 2) "b" 3) "python"
阻塞式弹出如下:
blpop key [key ...] timeout brpop key [key ...] timeout
blpop和brpop是lpop和rpop的阻塞版本,它们除了弹出方向不同,使用 方法基本相同,所以下面以brpop命令进行说明,brpop命令包含两个参数:
列表为空:如果timeout=3,那么客户端要等到3秒后返回,如果timeout=0,那么客户端一直阻塞等下去:
127.0.0.1:6379> brpop list:test 3 (nil) (3.10s) 127.0.0.1:6379> brpop list:test 0 ...阻塞...
如果此期间添加了数据element1,客户端立即返回:
127.0.0.1:6379> brpop list:test 3 1) "list:test" 2) "element1" (2.06s)
列表不为空:客户端会立即返回。
127.0.0.1:6379> brpop list:test 0 1) "list:test" 2) "element1"
在使用brpop时,有两点需要注意。
此时另一个客户端分别向list:2和list:3插入元素:127.0.0.1:6379> brpop list:1 list:2 list:3 0 ..阻塞..
客户端会立即返回list:2中的element2,因为list:2最先有可以弹出的元素:client-lpush> lpush list:2 element2 (integer) 1 client-lpush> lpush list:3 element3 (integer) 1
127.0.0.1:6379> brpop list:1 list:2 list:3 0 1) "list:2" 2) "element2_1"
此时另一个客户端lpush一个元素到list:test列表中:客户端1: client-1> brpop list:test 0 ...阻塞... 客户端2: client-2> brpop list:test 0 ...阻塞... 客户端3: client-3> brpop list:test 0 ...阻塞...
那么客户端1最会获取到元素,因为客户端1最先执行brpop,而客户端2 和客户端3继续阻塞:client-lpush> lpush list:test element (integer) 1
client> brpop list:test 0 1) "list:test" 2) "element"
有关列表的基础命令已经介绍完了,表2-5是这些命令的时间复杂度,开发人员可以参考此表选择适合的命令
列表类型的内部编码有两种。
下面的示例演示了列表类型的内部编码,以及相应的变化。
当元素个数较少且没有大元素时,内部编码为ziplist:
127.0.0.1:6379> rpush listkey e1 e2 e3 (integer) 3 127.0.0.1:6379> object encoding listkey "ziplist"
当元素个数超过512个,内部编码变为linkedlist:
127.0.0.1:6379> rpush listkey e4 e5 ...忽略... e512 e513 (integer) 513 127.0.0.1:6379> object encoding listkey "linkedlist"
或者当某个元素超过64字节,内部编码也会变为linkedlist:
127.0.0.1:6379> rpush listkey "one string is bigger than 64 byte............... ................." (integer) 4 127.0.0.1:6379> object encoding listkey 116 "linkedlist"
开发提示
Redis3.2版本提供了quicklist内部编码,简单地说它是以一个ziplist为节点的linkedlist,
它结合了ziplist和linkedlist两者的优势,为列表类型提供了一种更为优秀的内部编码实现,
它的设计原理可以参考Redis的另一个作者Matt Stancliff的博客https://matt.sh/redis-quicklist。
1.消息队列
如图2-21所示,Redis的lpush+brpop命令组合即可实现阻塞队列,生产者客户端使用lrpush从列表左侧插入元素,多个消费者客户端使用brpop命令阻塞式的“抢”列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。
2.文章列表
每个用户有属于自己的文章列表,现需要分页展示文章列表。此时可以考虑使用列表,因为列表不但是有序的,同时支持按照索引范围获取元素
hmset acticle:1 title xx timestamp 1476536196 content xxxx ... 118 hmset acticle:k title yy timestamp 1476512536 content yyyy ...
lpush user:1:acticles article:1 article3 ... lpush user:k:acticles article:5 ...
articles = lrange user:1:articles 0 9 for article in {articles} hgetall {article}
使用列表类型保存和获取文章列表会存在两个问题。
开发提示
实际上列表的使用场景很多,在选择时可以参考以下口诀:
集合(set)类型也是用来保存多个的字符串元素,但和列表类型不一样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素
一个集合最多可以存储2^32-1个元素。
Redis除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集,合理地使用好集合类型,能在实际开发中解决很多实际问题
添加元素
sadd key element [element ...]
返回结果为添加成功的元素个数,例如:
127.0.0.1:6379> exists myset (integer) 0 127.0.0.1:6379> sadd myset a b c (integer) 3 127.0.0.1:6379> sadd myset a b (integer) 0
删除元素
srem key element [element ...]
返回结果为成功删除元素个数,例如:
127.0.0.1:6379> srem myset a b (integer) 2 127.0.0.1:6379> srem myset hello (integer) 0
计算元素个数
scard key
scard的时间复杂度为O(1),它不会遍历集合所有元素,而是直接用Redis内部的变量,例如:
127.0.0.1:6379> scard myset (integer) 1
判断元素是否在集合中
sismember key element
如果给定元素element在集合内返回1,反之返回0,例如:
127.0.0.1:6379> sismember myset c (integer) 1
随机从集合返回指定个数元素
srandmember key [count]
[count]是可选参数,如果不写默认为1,例如:
127.0.0.1:6379> srandmember myset 2 1) "a" 2) "c" 127.0.0.1:6379> srandmember myset "d"
从集合随机弹出元素
spop key
spop操作可以从集合中随机弹出一个元素,例如下面代码是一次spop后,集合元素变为"d b a":
127.0.0.1:6379> spop myset "c" 127.0.0.1:6379> smembers myset 1) "d" 2) "b" 3) "a"
需要注意的是Redis从3.2版本开始,spop也支持[count]参数。
srandmember和spop都是随机从集合选出元素,两者不同的是spop命令执行后,元素会从集合中删除,而srandmember不会。
获取所有元素
smembers key
下面代码获取集合myset所有元素,并且返回结果是无序的:
127.0.0.1:6379> smembers myset 1) "d" 2) "b" 3) "a"
smembers和lrange、hgetall都属于比较重的命令,如果元素过多存在阻塞Redis的可能性,这时候可以使用sscan来完成,有关sscan命令2.7节会介绍。
现在有两个集合,它们分别是user:1:follow和user:2:follow:
127.0.0.1:6379> sadd user:1:follow it music his sports (integer) 4 127.0.0.1:6379> sadd user:2:follow it news ent sports (integer) 4
求多个集合的交集
sinter key [key ...]
例如下面代码是求user:1:follow和user:2:follow两个集合的交集, 返回结果是sports、it:
127.0.0.1:6379> sinter user:1:follow user:2:follow 1) "sports" 2) "it"
求多个集合的并集
suinon key [key ...]
例如下面代码是求user:1:follow和user:2:follow两个集合的并集,返回结果是sports、it、his、news、music、ent:
127.0.0.1:6379> sunion user:1:follow user:2:follow 1) "sports" 2) "it" 3) "his" 4) "news" 5) "music" 6) "ent"
求多个集合的差集
sdiff key [key ...]
例如下面代码是求user:1:follow和user:2:follow两个集合的差集,返回结果是music和his:
127.0.0.1:6379> sdiff user:1:follow user:2:follow 1) "music" 2) "his"
前面三个命令如图2-23所示。
将交集、并集、差集的结果保存
sinterstore destination key [key ...] suionstore destination key [key ...] sdiffstore destination key [key ...]
集合间的运算在元素较多的情况下会比较耗时,
所以Redis提供了上面三个命令(原命令+store)将集合间交集、并集、差集的结果保存在 destination key中,
例如下面操作将user:1:follow和user:2:follow两个集合的交集结果保存在user:1_2:inter中,user:1_2:inter本身也是集合类型:
127.0.0.1:6379> sinterstore user:1_2:inter user:1:follow user:2:follow (integer) 2 127.0.0.1:6379> type user:1_2:inter set 127.0.0.1:6379> smembers user:1_2:inter 1) "it" 2) "sports"
至此有关集合的命令基本已经介绍完了,表2-6给出集合常用命令的时 间复杂度,开发人员可以根据自身需求进行选择。
集合类型的内部编码有两种:
下面用示例来说明:
127.0.0.1:6379> sadd setkey 1 2 3 4 (integer) 4 127.0.0.1:6379> object encoding setkey "intset"
127.0.0.1:6379> sadd setkey 1 2 3 4 5 6 ... 512 513 (integer) 509 127.0.0.1:6379> scard setkey (integer) 513 127.0.0.1:6379> object encoding listkey "hashtable"
127.0.0.1:6379> sadd setkey a (integer) 1 127.0.0.1:6379> object encoding setkey "hashtable"
集合类型比较典型的使用场景是标签(tag)。
例如一个用户可能对娱乐、体育比较感兴趣,另一个用户可能对历史、新闻比较感兴趣,这些兴趣点就是标签。
有了这些数据就可以得到喜欢同一个标签的人,以及用户的共同喜好的标签,这些数据对于用户体验以及增强用户黏度比较重要。
例如一个电子商务的网站会对不同标签的用户做不同类型的推荐,比如对数码产品比较感兴趣的人,在各个页面或者通过邮件的形式给他们推荐最新的数码产品,通常会为网站带来更多的利益。下面使用集合类型实现标签功能的若干功能。
sadd user:1:tags tag1 tag2 tag5 sadd user:2:tags tag2 tag3 tag5 ... sadd user:k:tags tag1 tag2 tag4 ...
sadd tag1:users user:1 user:3 sadd tag2:users user:1 user:2 user:3 ... sadd tagk:users user:1 user:2 ...
开发提示
用户和标签的关系维护应该在一个事务内执行,防止部分命令失败造成的数据不一致,
有关如何将两个命令放在一个事务,第3章会介绍事务以及Lua的使用方法。
srem user:1:tags tag1 tag5 ...
srem tag1:users user:1 srem tag5:users user:1 ...
(3)和(4)也是尽量放在一个事务执行。
可以使用sinter命令,来计算用户共同感兴趣的标签,如下代码所示:
sinter user:1:tags user:2:tags
开发提示
前面只是给出了使用Redis集合类型实现标签的基本思路,
实际上一个标签系统远比这个要复杂得多,不过集合类型的应用场景通常为以下几种:
本文作者:Eric
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!