Redis是一个高效的NoSQL数据库,采用Key-Value保存数据,一般作为高速分布式缓存使用。

Redis Key的设计技巧

Redis作为高速缓存,要能通过Key快速的查到需要的数据,一般作为数据库的缓存,所以Redis的Key设计可参考数据库表。
以user表为例,数据库设计如下:

user_id user_name password email
1 zhangsan secret1 zhangsan@163.com
2 lisi secret2 lisi@163.com

Key是设计建议如下:

  1. Key字段通过冒号分割
  2. 表名作为Key前缀
  3. 表主键的字段名作为第二段
  4. 表主键的字段值作为第三段
  5. 要查询的字段作为第四段

现在要通过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,使用的时候在反序列化回来。如果使用这种方式,需要通过代码注释或文档对字段进行详细的说明,避免日后自己或他人维护时“相见不相识”。
场景使用场景包括:

  1. 普通字符串缓存
  2. int、long、float等其他基本类型
  3. 计数器(INCR,INCRBY,DECR,DECRBY等)
  4. 分布式锁(SETNX SET if Not eXists)
  5. Bitmap(SETBIT,GETBIT,BIT*),可用于海量数据统计或Bloom过滤器等。

List
List是有序列表,使用方式和Java中的List类似,支持列表头尾插入(LPUSH,RPUSH),头尾弹出(LPOP,RPOP),阻塞式弹出(BLPOP,BRPOP),按范围获取(LRANGE)等丰富的操作。
常见使用场景包括:

  1. 分布式消息队列
  2. 缓存门户首页热点文章、热点商品
  3. 分页获取数据

Hash
Hash类型类似Java中的HashMap,保存Key-Value键值对。可以直接把一个数据库表的行映射到缓存,也可以保存无嵌套类型的POJO。

Set
Set是无序不重复的集合,使用方式和Java中的Set类似,可以做高效的Set间的交、并、差集等操作(SINTER,SUNION,SDIFF)。

Sorted Set
有序集合,类似Java中的LinkedHashSet,但不是根据插入顺利来排序的,而是Set的每个元素可以关联一个double类型的分值(score)用于排序。基于这个特性,Sorted Set的典型场景包括:

  1. 排行榜(热搜):可以实现门户首页按点击量、按时间、按点赞数等排序场景。
  2. 带权重的消息队列:根据消息的重要程度,设置不同的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提供三个方法:

  1. pfadd:把数据增加到计数集合中
  2. pfcount:获取计数集合中数据的数量
  3. pfmerge:把两个计数集合的数据合并

具体的使用和原理请参考以下文章。

惭愧,原理部分我没完全看懂。

Bitmat实现的Bloom过滤器和HyperLogLog实现的计数统计的区别:

  1. Bloom过滤器用于查询一个数据在集合里是否存在,但无法统计数据的计数
  2. 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一次命令不能过多,否则会导致网络拥塞和客户端延迟。

Reids命令流程.PNG

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 事务可以一次执行多个命令, 并且带有以下三个重要的保证:

  1. 批量操作在发送 EXEC 命令前被放入队列缓存
  2. 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行
  3. 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中

一个事务从开始到执行会经历以下三个阶段:

  1. 开始事务
  2. 命令入队
  3. 执行事务

事务命令示例:

MULTI    #开始事务
SET book-name "Mastering C++ in 21 days"
GET book-name
SADD tag "C++" "Programming" "Mastering Series"
SMEMBERS tag
EXEC    #执行事务

原生批量命令(mget mset)Pipleline和事务的区别:

  1. 原生批量命令是原子性的,但是一次只能操作一个key。
  2. Pipeline把多条数据打包发送到服务端,一次性取回执行结果,命令是非原子性的。
  3. 事务可以保证多条命令的原子性,事务命令是一起执行的,中间不会插入其他命令;如果其中有命令执行失败,直接会被忽略,Redis没有回滚机制。

发布订阅模式(Pub/Sub)
Redis 发布订阅(Pub/Sub)是一种消息通信模式:发送者(Pub)发送消息,订阅者(Sub)接收消息。客户端可以订阅任意数量的频道。

订阅者监听消息:

SUBSCRIBE channel_name

Subscribe Channel.PNG

发送者发送消息:

PUBLISH channel_name "Message"

Public Message.PNG

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

乘着风
107 声望12 粉丝

五岁时,妈妈告诉我,人生的关键在于快乐。上学后,人们问我长大了要做什么,我写下“快乐”。他们告诉我,我理解错了题目,我告诉他们,他们理解错了人生。——约翰·列侬