Redis是一个高效的NoSQL数据库,采用Key-Value保存数据,一般作为高速分布式缓存使用。
Redis Key的设计技巧
Redis作为高速缓存,要能通过Key快速的查到需要的数据,一般作为数据库的缓存,所以Redis的Key设计可参考数据库表。
以user表为例,数据库设计如下:
user_id | user_name | password | |
---|---|---|---|
1 | zhangsan | secret1 | zhangsan@163.com |
2 | lisi | secret2 | lisi@163.com |
Key是设计建议如下:
- Key字段通过冒号分割
- 表名作为Key前缀
- 表主键的字段名作为第二段
- 表主键的字段值作为第三段
- 要查询的字段作为第四段
现在要通过user_id
(1)快速查询用户user_name
(zhangsan)
则缓存数据设计如下:
set user:user_id:1:username zhangsan
如果user_name
字段是索引字段,且需要频繁通过user_name查询用户信息,则可以把查询关键字段。
则缓存数据设计如下:
set user:user_name:zhangsan:user_id 1
然后在通过user_id查询其他用户信息。
当然,Key的设计还是需要根据业务需要灵活处理,这里只是给出一个一般思路。比如以关键字+日期为Key进行日期相关的统计。
Redis数据类型
基本类型
Redis基础的数据类型包括String、List、Hash(Map)、Set、SortedSet 5种。
String
String是最基本的数据类型,它不但可以保存字符串,也用来保存int,long,float等单值数据,也可以作为计数器。很多项目也会把对象通过JSON序列化后保存为String,使用的时候在反序列化回来。如果使用这种方式,需要通过代码注释或文档对字段进行详细的说明,避免日后自己或他人维护时“相见不相识”。
场景使用场景包括:
- 普通字符串缓存
- int、long、float等其他基本类型
- 计数器(INCR,INCRBY,DECR,DECRBY等)
- 分布式锁(SETNX
SET if Not eXists
) - Bitmap(SETBIT,GETBIT,BIT*),可用于海量数据统计或Bloom过滤器等。
List
List是有序列表,使用方式和Java中的List类似,支持列表头尾插入(LPUSH,RPUSH),头尾弹出(LPOP,RPOP),阻塞式弹出(BLPOP,BRPOP),按范围获取(LRANGE)等丰富的操作。
常见使用场景包括:
- 分布式消息队列
- 缓存门户首页热点文章、热点商品
- 分页获取数据
Hash
Hash类型类似Java中的HashMap,保存Key-Value键值对。可以直接把一个数据库表的行映射到缓存,也可以保存无嵌套类型的POJO。
Set
Set是无序不重复的集合,使用方式和Java中的Set类似,可以做高效的Set间的交、并、差集等操作(SINTER,SUNION,SDIFF)。
Sorted Set
有序集合,类似Java中的LinkedHashSet,但不是根据插入顺利来排序的,而是Set的每个元素可以关联一个double类型的分值(score)用于排序。基于这个特性,Sorted Set的典型场景包括:
- 排行榜(热搜):可以实现门户首页按点击量、按时间、按点赞数等排序场景。
- 带权重的消息队列:根据消息的重要程度,设置不同的score。
高级类型
Bitmap
通过基本类型中String的BIT操作方法,实现位操作,包括SETBIT,GETBIT,BITCOUNT,BITOP等,可用于海量数据统计或Bloom过滤器等。比如统计日活用户量,每Bit表示一个用户,index为用户ID(假设ID为int型),初始化全部为0,用户登录后对应Bit设置为1,通过统计1的个数即可统计用户日活量。
Bloom过滤器原理是把数据经过多个Hash函数进行散列计算,得到多个值;在Bitmap中把这些值对应的bit设置为1;查询时,把这些bit对应的数据查询出来;如果不全为1,说明数据在Bitmap中不存在。Java中可以使用com.google.guava库来做Bloom过滤器的计算。
Bitmap用于Bloom过滤器的实现,请看这里
HyperLogLog
Redis HyperLogLog是用来做计数统计的算法,HyperLogLog的优点是在输入元素的数量或者体积非常非常大时,计算计数所需的空间总是固定且很小的。每个HyperLogLog键只需要花费12KB内存,就可以计算接近2^64个不同元素的计数。这和计算计数时,元素越多耗费内存就越多的集合形成鲜明对比。
比如,我们统计网站每个页面每天的UV(用户访问数),按一般的思路,我需要用集合记录每个页面访问用户的ID来统计访问数,如果用户量大是,存储用户ID会占用很大存储空间。而使用HyperLogLog,我可以通过最大12K空间存储多大2^64组数据的计数,并把计数误差保持在0.81%以内。HyperLogLog提供三个方法:
- pfadd:把数据增加到计数集合中
- pfcount:获取计数集合中数据的数量
- pfmerge:把两个计数集合的数据合并
具体的使用和原理请参考以下文章。
惭愧,原理部分我没完全看懂。
Bitmat实现的Bloom过滤器和HyperLogLog实现的计数统计的区别:
- Bloom过滤器用于查询一个数据在集合里是否存在,但无法统计数据的计数
- HyperLogLog计数统计只能统计集合里数据数量,但无法知道某个数据是否存在
Pipleline
Redis客户端执行一条命令分为以下四个步骤:
1.发送命令 → 2.命令排队 → 3.命令执行 → 4.返回结果
合起来称为Round Trip Time(RTT,往返时间)。RTT的主要时间花费在上述1和4两步的网络传输上,尤其是在网络延迟大的情况。Redis虽然提供了mget、mset等批量命令优化RTT。但大部分命令(如hgetall,并没有mhgetall)不支持批量操作,如果一次性需要执行大量命令时,会花费大量RTT,降低服务的吞吐量。
使用Pipeline流水线技术,允许客户端将多个命令打包发给服务器,再一并读取结果即可。通过此技术可提升Redis服务的吞吐量。Pipeline需要客户端和服务端共同合作完成,所有并没有提供对应的命令行。另外需要注意,Pipeline一次命令不能过多,否则会导致网络拥塞和客户端延迟。
Jedis redis = new Jedis(ip, port);
Pipeline pipe = redis.pipelined(); //生成流水线
for (int i = 0; i < 10000; i++) {
pipe.hmset("key_" + i, "data" + i); //把命令封装到Pipe,此时命令并未发生,还停留在客户端
}
List<Object> list = pipe.syncAndReturnAll(); //把封装后的Pipe一次性发给Redis,并返回结果
//pipe.sync(); //提交命令,不返回结果
Redis事务
Redis 事务可以一次执行多个命令, 并且带有以下三个重要的保证:
- 批量操作在发送 EXEC 命令前被放入队列缓存
- 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行
- 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中
一个事务从开始到执行会经历以下三个阶段:
- 开始事务
- 命令入队
- 执行事务
事务命令示例:
MULTI #开始事务
SET book-name "Mastering C++ in 21 days"
GET book-name
SADD tag "C++" "Programming" "Mastering Series"
SMEMBERS tag
EXEC #执行事务
原生批量命令(mget mset)Pipleline和事务的区别:
- 原生批量命令是原子性的,但是一次只能操作一个key。
- Pipeline把多条数据打包发送到服务端,一次性取回执行结果,命令是非原子性的。
- 事务可以保证多条命令的原子性,事务命令是一起执行的,中间不会插入其他命令;如果其中有命令执行失败,直接会被忽略,Redis没有回滚机制。
发布订阅模式(Pub/Sub)
Redis 发布订阅(Pub/Sub)是一种消息通信模式:发送者(Pub)发送消息,订阅者(Sub)接收消息。客户端可以订阅任意数量的频道。
订阅者监听消息:
SUBSCRIBE channel_name
发送者发送消息:
PUBLISH channel_name "Message"
Lua脚本
Redis 脚本使用 Lua 解释器来执行脚本。 Redis 2.6 版本通过内嵌支持 Lua 环境。执行脚本的常用命令为EVAL。
Eval 命令的基本语法如下:
EVAL script numkeys key [key ...] arg [arg ...]
脚本示例:
EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。