标签 : Java与NoSQL
Redis(REmote DIctionary Server) is an open source (BSD licensed), in-memory data structure store, used as database, cache and message broker. It supports data structures such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs and geospatial indexes with radius queries. Redis has built-in replication, Lua scripting, LRU eviction, transactions and different levels of on-disk persistence, and provides high availability via Redis Sentinel and automatic partitioning with Redis Cluster.
<a href="http://www.infoq.com/cn/articles/tq-why-choose-redis">为什么使用 Redis及其产品定位</a>
Redis没有其他外部依赖, 编译安装过程非常简单.
编译安装
<code>wget http://download.redis.io/releases/redis-3.0.5.tar.gz</code>
<code>make</code>(32位机器:<code>make 32bit</code>)
<code>make test</code>
<code>make PREFIX=${redis-path} install</code>
安装完成后,在<code>${redis-path}/bin/</code>下生成如下二进制文件:
工具
描述
redis-server
服务端
redis-cli
客户端
redis-benchmark
Redis性能测试工具
redis-check-aof
AOF文件修复工具
redis-check-dump
RDB文件检测工具
redis-sentinel
Sentinel服务器(仅在2.8之后)
配置
<code>cp ${redis-3.0.5}/redis.conf ${redis-path}</code>
注: 使Redis以后台进程的形式运行: 编辑redis.conf配置文件,设置<code>daemonize yes</code>.
启动
<code>${redis-path}/bin/redis-server ./redis.conf</code>
连接
<code>${redis-path}/bin/redis-cli</code>连接服务器
<code>- h</code>: 指定server地址
<code>- p</code>: 指定server端口
<code>KEYS pattern</code> 查询key
Redis支持通配符格式: <code>*, ? ,[]</code>:
<code>*</code>
通配任意多个字符
<code>?</code>
通配单个字符
<code>[]</code>
通配括号内的某1个字符
<code>\x</code>
转意符
<code>RANDOMKEY</code> 返回一个随机存在的key
<code>EXISTS key</code> 判断key是否存在
<code>TYPE key</code> 返回key存储类型
<code>SET key value</code> 设置一对key-value
<code>DEL key [key...]</code> 删除key
注: 返回真正删除的key数量, 且<code>DEL</code>并不支持通配符.
<code>RENAME[NX] key new_key</code> 重命名
NX: not exists <code>new_key</code>不存在才对key重命名.
<code>move key DB</code> 移动<code>key</code>到另外一个DB
一个Redis进程默认打开16个DB,编号0~15(可在redis.conf中配置,默认为0),使用<code>SELECT n</code>可在多个DB间跳转.
<code>TTL/PTTL key</code> 查询key有效期(以秒/毫秒为单位,默认-1永久有效)
对于不存在的key,返回-2; 对于已过期/永久有效的key,都返回-1
<code>EXPIRE/PEXPIRE key n</code> 设置key有效期
<code>PERSIST key</code> 指定永久有效
字符串<code>Strings</code>是Redis最基本的数据类型,它能存储任何形式的字符串,如用户邮箱/JSON化的对象甚至是一张图片(二进制数据).一个字符串允许存储的最大容量为512MB. 字符串类型也是其他4种数据类型的基础,其他数据类型和字符串的区别从某种角度来说只是组织字符串的形式不同.
我们使用**Jedis**客户端连接Redis并存储文章数据(关于本篇博客实践部分的详细场景讲解,可以参考[Redis入门指南][5]一书,在此就不再赘述,下同).
使用Jedis需要在pom.xml中添加如下依赖:
applicationContext.xml
使用Spring来管理Reids的连接.
DO: Articles文章
DAO
上面代码使用了Spring与MessagePack的部分功能,因此需要在pom.xml中添加如下依赖:
功能
关键词
增/减指定整数
<code>INCREBY/DECY key number</code>
增加指定浮点数
<code>INCREBYFLOAT key number</code>
尾部追加
<code>APPEND key value</code>
获取字符串长度
<code>STRLEN key</code>
同时设置多个键值
<code>MSET key value [key value ...]</code>
同时获得多个键值
<code>MGET key [key ...]</code>
返回旧值并设置新值
<code>GETSET key value</code>
位操作
<code>GETBIT</code>/<code>SETBIT</code>/<code>BITCOUNT</code>/<code>BITOP</code>
散列<code>Hash</code>类型的键值是一种字典结构, 其存储了字段(filed)和字段值(value)的映射. 但value只能是字符串,不支持其他数据类型, 且一个<code>Hash</code>类型Key键可以包含至多232-1个字段.
<code>HSET</code>不区分插入还是更新,当key不存在时,<code>HSET</code>会自动建立并插入.插入返回1, 更新返回0.
前面使用String存储整篇文章实际上有一个弊端, 如只需要更新文章标题,需要将篇文章都做更新然后存入Redis,费时费力.因此我们更推荐使用<code>Hash</code>来存储文章数据:
这样即使需要为文章新添加字段, 也只需为该<code>Hash</code>再添加一新key即可, 比如<code><slug, 文章缩略名></code>.
值获取字段名
<code>HKEYS key</code>
只获取字段值
<code>HVALS key</code>
获取字段数量
<code>HLEN key</code>
注: 除了<code>Hash</code>, Redis的其他数据类型同样不支持类型嵌套, 如集合类型的每个元素只能是字符串, 不能是另一个集合或<code>Hash</code>等.
列表<code>List</code>可以存储一个有序的字符串列表, 其内部使用双向链表实现, 所以向列表两端插入/删除元素的时间复杂度为<code>O(1)</code>,而且越接近两端的元素速度就越快.
LLEN命令的时间复杂度为O(1): Reids会保存链表长度, 不必每次遍历统计.
考虑到评论时需要存储评论的全部数据(姓名/联系方式/内容/时间等),所以适合将一条评论的各个元素序列化为String之后作为列表的元素存储:
DO: Comment
获得指定索引元素值
<code>LINDEX key index</code>
设置指定索引元素值
<code>LSET key index value</code>
插入元素
<code>LINSERT key BEFORE|AFTER pivoit value</code>
将元素从一个列表转入另一个列表
<code>RPOPLPUSH source destination</code>
等待[弹出/转移][头/尾]元素
<code>BLPOP</code>/<code>BRPOP</code>/<code>BRPOPLPUSH</code>
<code>RPOPLPUSH</code>是一个很有意思的命令: 先执行<code>RPOP</code>, 再执行<code>LPUSH</code>, 先从source列表右边中弹出一个元素, 然后将其加入destination左边, 并返回这个元素值, 整个过程是原子的.
根据这一特性可将List作为循环队列使用:source与destination相同,<code>RPOPLPUSH</code>不断地将队尾的元素移到队首.好处在于在执行过程中仍可不断向队列中加入新元素,且允许多个客户端同时处理队列.
集合<code>Set</code>内的元素是无序且唯一的,一个集合最多可以存储232-1个字符串.集合类型的常用操作是插入/删除/判断是否存在, 由于集合在Redis内部是使用值为空的HashTable实现, 所以这些操作的时间复杂度为<code>O(1)</code>, 另外, Set最方便的还是多个集合之间还可以进行并/交/差的运算.
考虑到一个文章的所有标签都是互不相同的, 且对标签的保存顺序并没有特殊的要求, 因此<code>Set</code>比较适用:
在提出这样的需求之后, 前面的<code>posts:[ID]:tags</code> 文章维度的存储结构就不适用了, 因此借鉴索引倒排的思想, 我们使用<code>tags:[tag]:posts</code>这种标签维度的数据结构:
在这种结构下, 根据标签搜索文章就变得不费吹灰之力, 而<code>Set</code>自带交/并/补的支持, 使得多标签文章搜索有也变得十分简单:
获得集合中元素数
<code>SCARD key</code>
集合运算并将结果存储
<code>SDIFFSTORE/SINTERSTORE/SUNIONSTORE destination key [key ...]</code>
随机获得集合中的元素
<code>SRANDMEMBER key [count]</code>
随机弹出集合中的一个元素
<code>SPOP key</code>
有序集合<code>Sorted-Sets</code>在<code>Set</code>基础上为每个元素都关联了一个分数[<code>score</code>],这使得我们不仅可以完成插入/删除和判断元素是否存在等操作,还能够获得与<code>score</code>有关的操作(如<code>score</code>最高/最低的前N个元素、指定<code>score</code>范围内的元素).<code>Sorted-Sets</code>具有以下特点: 1) 虽然集合中元素唯一, 但<code>score</code>可以相同. 2) 内部基于<code>HashTable</code>与<code>SkipList</code>实现,因此即使读取中间部分的数据速度也很快(<code>O(log(N))</code>). 3) 可以通过更改元素<code>score</code>值来元素顺序(与List不同).
要按照文章的点击量排序, 就必须再额外使用一个<code>Sorted-Set</code>类型来实现, 文章ID为元素,以该文章点击量为元素分数.
获得集合中的元素数目
<code>ZCARD key</code>
获得指定分数范围内的元素个数
<code>ZCOUNT key min max</code>
获得元素排名
<code>ZRANK/ZREVRANK key member</code>
<code>ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]</code>
<code>ZINTERSTORE</code>用来计算多个<code>Sorted-Set</code>的交集并将结果存储在<code>destination</code>, 返回值为<code>destination</code>中的元素个数.
<code>AGGREGATE</code>:
<code>destination</code>中元素的分数由<code>AGGREGATE</code>参数决定:<code>SUM</code>(和/默认), <code>MIN</code>(最小值), <code>MAX</code>(最大值)
<code>WEIGHTS</code>
通过<code>WEIGHTS</code>参数设置每个集合的权重,在参与运算时元素的分数会乘上该集合的权重.
<code>ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]</code>
用法类似
参考以往RDBMS的设计经验: 1. 将表名转换为key前缀, 如<code>user:</code>. 2. 第2段放置用于区分key的字段, 对应于RDBMS中的主键, 如<code>user:[uid]:</code>. 3. 第3段放置要存储的列名, 如<code>user:[uid]:email</code>.
用户模块数据分3个Key存储: 用户ID由<code>user:count</code>自增生成(<code>String</code>), 用户email与id映射关系由<code>user:email.to.id</code>存储(<code>Hash</code>), 用户真实数据由<code>user:[id]:data</code>存储(<code>Hash</code>):
User(domain)
UserDAO
UserService
关系模块数据由2个Key存储: 关注由<code>relation:following:[id]</code>存储(<code>Set</code>), 被关注由<code>relation:follower:[id]</code>存储(<code>Set</code>): 这样存的优势是既可以快速的查询关注列表, 也可以快速的查询粉丝列表, 而且还可以基于Redis对<code>Set</code>的支持, 做共同关注功能.
Relation(domain)
RelationDAO
RelationService
发微博功能我们采用推模式实现: 为每个用户建立一个信箱<code>List</code>, 存储关注的人发的微博, 因此每个用户在发微博时都需要获取自己的粉丝列表, 然后为每个粉丝推送一条微博数据(考虑到一个用户关注的人过多, 因此实际开发中只存最新1000条即可). 由此微博模块数据由4个Key存储: 微博ID由<code>miblog:count</code>自增生成(<code>String</code>), 微博真实数据由<code>miblog:[id]:data</code>存储(<code>Hash</code>), 自己发的微博由<code>miblog:[uid]:my</code>存储(<code>List</code>), 推送给粉丝的微博由<code>miblog:[uid]:flow</code>存储(<code>List</code>):
MiBlog(domain)
MiBlogDAO
MiBlogService
<dl></dl>
<dt>参考&扩展</dt>
<dd></dd>
<a href="http://www.infoq.com/cn/articles/weibo-relation-service-with-redis/">微博关系服务与Redis的故事</a>