Redis的9种数据类型及使用场景

在具体描述这几种数据类型之前,我们先通过一张图了解下 Redis 内部内存管理中是如何描述这些不同数据类型的:

Redis的9种数据类型及使用场景_第1张图片

首先Redis内部使用一个redisObject对象来表示所有的key和value,redisObject最主要的信息如上图所示:type代表一个value对象具体是何种数据类型,encoding是不同数据类型在redis内部的存储方式,比如:type=string代表value存储的是一个普通字符串,那么对应的encoding可以是raw或者是int,如果是int则代表实际redis内部是按数值型类存储和表示这个字符串的,当然前提是这个字符串本身可以用数值表示,比如:"123" "456"这样的字符串。

这需要特殊说明一下vm字段,只有打开了Redis的虚拟内存功能,此字段才会真正的分配内存,该功能默认是关闭状态的,该功能会在后面具体描述。通过上图我们可以发现Redis使用redisObject来表示所有的key/value数据是比较浪费内存的,当然这些内存管理成本的付出主要也是为了给Redis不同数据类型提供一个统一的管理接口,实际作者也提供了多种方法帮助我们尽量节省内存使用,我们随后会具体讨论。

redis支持丰富的数据类型,不同的场景使用合适的数据类型可以有效的优化内存数据的存放空间:

  1. string:最基本的数据类型,二进制安全的字符串,最大512M。

  1. list:按照添加顺序保持顺序的字符串列表。

  1. set:无序的字符串集合,不存在重复的元素。

  1. sorted set:已排序的字符串集合。

  1. hash:key-value对的一种集合。

  1. bitmap:更细化的一种操作,以bit为单位。

  1. hyperloglog:基于概率的数据结构。 # 2.8.9新增

  1. Geo:地理位置信息储存起来, 并对这些信息进行操作 # 3.2新增

  1. 流(Stream)# 5.0新增

常用的前五种数据类型和使用场景总结如下:

1,String:

常用命令:

get, set, setnx, incr, decr, mget

使用场景:

缓存,热点数据

分布式session

分布式锁

INCR计数器

文章的阅读量点赞量

全局ID

2,List

常用命令

lpush:从列表List的最左边插入一个元素

lpop:从列表List的左边移出一个元素

rpush:从列表List的右边插入一个元素

rpop:从列表List的右边移出一个元素

llen:打印当前列表List的元素个数

lrange:按指定范围列出来

使用场景:

公众账号的关注列表,粉丝列表

Redis的分页功能,消息队列,发红包场景

3,Set

常用命令

sadd:往set中添加数据

srem:从set中删除数据

spop: 命令移除并返回集合中的一个随机元素

scard:查看set中存在的元素个数

sismember:查看set中是否存在某个数据

使用场景:

问答的点赞,喜欢,收藏数量,商品的筛选,相互关注

有共同爱好喜好的人,可能认识的人,标签

4,Hash

常用命令

hget:通过key值,从hash里取对应的value

hset:往hash里,添加key-value

hmget:一次性获取多个key的value

hgetall:获取所有

hkeys:获取所有的key

hvals: 获取所有的values

使用场景:

储存用户信息,用户主页访问量,组合查询

购物车里面的全选(hgetal)

5,ZSet

常用命令

zadd:添加,如果值存在添加,将会重新排序

zrem:删除指定的一个成员或多个成员。

zcard:查看zset集合的成员个数

zrange:查看Zset指定范围的成员

zrank:获取zset成员的下标位置

zcount:获取zset集合指定分数之间存在的成员个数

使用场景:

排行榜

商品的评价标签,可以记录商品的标签,统计标签次数,增加标签次数,按标签的分值进行排序

百度搜索热点

反spam系统

6,BitMap 位图

有一些数据只有两个属性

Bitmaps是一个可以对位进行操作的字符串,我们可以把Bitmaps想象成是一串二进制数字,每个位置只存储0和1

使用场景:

用户签到,统计活跃用户,用户在线状态

7,HyperLogLog 基数统计

统计大量去重元素数量

使用场景:

比如注册 IP 数、每日访问 IP 数、页面实时UV)、在线用户数等

8,Geo 地理位置

Geo本身不是一种数据结构,它本质上还是借助于Sorted Set(ZSET),并且使用GeoHash技术进行填充

应用场景

比如现在比较火的直播业务,我们需要查询附近的主播,那么GEO就可以很好的实现这个功能。

1,主播开播的时候写入主播Id的经纬度,

2,主播关播的时候删除主播Id元素,这样就维护了一个具有位置信息的在线主播集合提供给线上检索。

9,Streams 流

Streams就是Redis实现的内存版kafka。支持多播的可持久化的消息队列,用于实现发布订阅功能

String 字符串

常用命令:

setnx,set,get,decr,incr,mget 等。

应用场景:

字符串是最常用的数据类型,他能够存储任何类型的字符串,当然也包括二进制、JSON化的对象、甚至是Base64编码之后的图片。在Redis中一个字符串最大的容量为512MB,可以说是无所不能了。redis的key和string类型value限制均为512MB。虽然Key的大小上限为512M,但是一般建议key的大小不要超过1KB,这样既可以节约存储空间,又有利于Redis进行检索

  • 缓存,热点数据

  • 分布式session

  • 分布式锁

  • INCR计数器

  • 文章的阅读量,微博点赞数,允许一定的延迟,先写入 Redis 再定时同步到数据库

  • 全局ID

  • INT 类型,INCRBY,利用原子性

  • INCR 限流

  • 以访问者的 IP 和其他信息作为 key,访问一次增加一次计数,超过次数则返回 false。

  • setbit 位操作

内部编码:

  • int:8 个字节的长整型(long,2^63-1)

  • embstr:小于等于44个字节的字符串,embstr格式的SDS(Simple Dynamic String)

  • raw:SDS大于 44 个字节的字符串

通过下图可以直观感受一下字符串类型和哈希类型的区别:

Redis的9种数据类型及使用场景_第2张图片

redis 为什么要自己写一个SDS的数据类型,主要是为了解决C语言 char[] 的四个问题

  • 字符数组必须先给目标变量分配足够的空间,否则可能会溢出

  • 查询字符数组长度 时间复杂度O(n)

  • 长度变化,需要重新分配内存

  • 通过从字符串开始到结尾碰到的第一个\0来标记字符串的结束,因此不能保存图片、音频、视频、压缩文件等二进制(bytes)保存的内容,二进制不安全

redis SDS

  • 不用担心内存溢出问题,如果需要会对 SDS 进行扩容

  • 因为定义了 len 属性,查询数组长度时间复杂度O(1) 固定长度

  • 空间预分配,惰性空间释放

  • 根据长度 len来判断是结束,而不是 \0

Redis的9种数据类型及使用场景_第3张图片

为什么要有embstr编码呢?他比raw的优势在哪里?embstr编码将创建字符串对象所需的空间分配的次数从raw编码的两次降低为一次。因为emstr编码字符串的素有对象保持在一块连续的内存里面,所以那个编码的字符串对象比起raw编码的字符串对象能更好的利用缓存。并且释放embstr编码的字符串对象只需要调用一次内存释放函数,而释放raw编码对象的字符串对象需要调用两次内存释放函数,如图所示,左边emstr编码,右边是raw编码:

Redis的9种数据类型及使用场景_第4张图片

Hash 哈希表

常用命令:

hget,hsetnx,hset,hvals,hgetall,hmset,hmget 等。

redis> HSET phone myphone nokia-1110
(integer) 1

redis> HEXISTS phone myphone
(integer) 1

redis> HSET people jack "Jack Sparrow"
(integer) 1

redis> HSET people gump "Forrest Gump"
(integer) 1

redis> HGETALL people
1) "jack"# 域2) "Jack Sparrow"# 值3) "gump"4) "Forrest Gump"

应用场景:

Redis的9种数据类型及使用场景_第5张图片
Redis的9种数据类型及使用场景_第6张图片
Redis的9种数据类型及使用场景_第7张图片
Redis的9种数据类型及使用场景_第8张图片
Redis的9种数据类型及使用场景_第9张图片

List 列表

常用命令:

lpush,rpush,lpop,rpop,lrange等。

127.0.0.1:6379> lpush list one   # 将一个值或者多个值,插入到列表的头部(左)(integer) 1
127.0.0.1:6379> lpush list two
(integer) 2
127.0.0.1:6379> lpush list three 
(integer) 3
127.0.0.1:6379> lrange list 0 -1   # 查看全部元素
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> lrange list 0 1    # 通过区间获取值
1) "three"
2) "two"
127.0.0.1:6379> rpush list right   # 将一个值或者多个值,插入到列表的尾部(右)(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "right"
127.0.0.1:6379>

列表(list)用来存储多个有序的字符串(可重复),每个字符串称为元素;一个列表可以存储2^32-1个元素。Redis中的列表支持两端插入和弹出,并可以获得指定位置(或范围)的元素,可以充当数组、队列、栈等

应用场景

比如 twitter 的关注列表粉丝列表等都可以用 Redis 的 list 结构来实现,可以利用lrange命令,做基于Redis的分页功能,性能极佳,用户体验好。

消息队列

列表类型可以使用 rpush 实现先进先出的功能,同时又可以使用 lpop 轻松的弹出(查询并删除)第一个元素,所以列表类型可以用来实现消息队列

Redis的9种数据类型及使用场景_第10张图片
Redis的9种数据类型及使用场景_第11张图片
Redis的9种数据类型及使用场景_第12张图片

Redis 列表类型的特点如下:

  • 列表中所有的元素都是有序的,所以它们是可以通过索引获取的lindex 命令。并且在 Redis 中列表类型的索引是从 0 开始的。

  • 列表中的元素是可以重复的,也就是说在 Redis 列表类型中,可以保存同名元素

内部编码:

  • ziplist(压缩列表):当列表中元素个数小于 512(默认)个,并且列表中每个元素的值都小于 64(默认)个字节时,Redis 会选择用 ziplist 来作为列表的内部实现以减少内存的使用。当然上述默认值也可以通过相关参数修改:list-max-ziplist-entried(元素个数)、list-max-ziplist-value(元素值)。

  • linkedlist(链表):当列表类型无法满足 ziplist 条件时,Redis 会选择用 linkedlist 作为列表的内部实现。

Set 集合

常用命令:

sadd,spop,smembers,sunion,scard,sscan,sismember等。

# 初始化
sadd poker T1 T2 T3 T4 T5 T6 T7 T8 T9 T10 TJ TQ TK X1 X2 X3 X4 X5 X6 X7 X8 X9 X10 XJ XQ XK M1 M2 M3 M4 M5 M6 M7 M8 M9 M10 MJ MQ MK F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 FJ FQ FK XW DW

# 数量
scard poker

# 复用扑克牌
sunionstore pokernew poker

# 成员
smembers poker

# 是否包含
sismember poker T1

# 随机读取
spop poker

# 设置
sadd user1tag tagID1 tagID2 tagID3
sadd user2tag tagID2 tagID3
sadd user3tag tagID2 tagID4 tagID5

# 获取共同拥有的tag(交集)
sinter user1tag user2tag user3tag

# 获取拥有的所有tag(并集)
sunion user1tag user2tag user3tag

# 获取两个之间的区别(差集)
sdiff user2tag user3tag

应用场景:

Redis set 对外提供的功能与 list 类似是一个列表的功能,特殊之处在于 set 是可以自动去重的,当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。

点赞,喜欢,收藏数量

Redis的9种数据类型及使用场景_第13张图片

商品筛选

Redis的9种数据类型及使用场景_第14张图片

筛选商品,苹果,IOS,屏幕6.0-6.24,内存大小256G

sinter brand:apple brand:ios screensize:6.0-6.24 memorysize:256GB

存储社交关系

用户(编号user001)关注

sadd focus:user001 user003

sadd focus:user002 user003 user004

相互关注

sadd focus:user001 user002

sadd focus:user002 user001

Redis的9种数据类型及使用场景_第15张图片

可能认识的人

Redis的9种数据类型及使用场景_第16张图片

实现方式:

set 的内部实现是一个 value 永远为 null 的 HashMap,实际就是通过计算 hash 的方式来快速排重的,这也是 set 能提供判断一个成员是否在集合内的原因。

Redis 中的集合类型,也就是 set。在 Redis 中 set 也是可以保存多个字符串的,经常有人会分不清 list 与 set,下面我们重点介绍一下它们之间的不同:

  • set 中的元素是不可以重复的,而 list 是可以保存重复元素的。

  • set 中的元素是无序的,而 list 中的元素是有序的。

  • set 中的元素不能通过索引下标获取元素,而 list 中的元素则可以通过索引下标获取元素。

  • 除此之外 set 还支持更高级的功能,例如多个 set 取交集、并集、差集等

为什么 Redis 要提供 sinterstore、sunionstore、sdiffstore 命令来将集合的交集、并集、差集的结果保存起来呢?这是因为 Redis 在进行上述比较时,会比较耗费时间,所以为了提高性能可以将交集、并集、差集的结果提前保存起来,这样在需要使用时,可以直接通过 smembers 命令获取。

内部编码

  • intset(整数集合):当集合中的元素都是整数,并且集合中的元素个数小于set-max-intset-entries 参数时,默认512,Redis 会选用 intset 作为底层内部实现。

  • hashtable(哈希表):当上述条件不满足时,Redis 会采用 hashtable 作为底层实现。

Sorted set 有序集合

常用命令:

zadd,zrange,zrem,zcard,zscore,zcount,zlexcount等

redis> ZRANGE salary 0 -1 WITHSCORES    # 测试数据
1) "tom"
2) "2000"
3) "peter"
4) "3500"
5) "jack"
6) "5000"

redis> ZSCORE salary peter              # 注意返回值是字符串
"3500"

redis> ZCOUNT salary 2000 5000          # 计算薪水在 2000-5000 之间的人数
(integer) 3

# rank:key 100: 分数 u1: ID
# 初始化 时间复杂度: O(log(N))
zadd rank 100 u1
zadd rank 200 u2
zadd rank 300 u3
zadd rank 400 u4
zadd rank 500 u5

# 数量
zcard rank

# 内容(正序、倒序)时间复杂度: O(log(N)+M)
zrange rank 0 -1
zrange rank 0 -1 withscores

zrevrange rank 0 -1 withscores
zrevrange rank 0 -2 withscores
zrevrange rank 0 -1

# 获取用户分数(不存在返回空) 时间复杂度: O(1)
zscore rank u1

# 修改分数
zadd rank 800 u3

# 修改分数(累加:新增或减少,返回修改后的分数)
zincrby rank 100 u3
zincrby rank -100 u3

# 查询分数范围内容(正序、倒序)
zrangebyscore rank (200 800 withscores
zrevrangebyscore rank (200 800 withscores

# 移除元素
zrem rank u3
Redis的9种数据类型及使用场景_第17张图片
Redis的9种数据类型及使用场景_第18张图片

应用场景

Redis sorted set 的使用场景与 set 类似,区别是 set 不是自动有序的,而 sorted set 可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。当你需要一个有序的并且不重复的集合列表,那么可以选择 sorted set 数据结构,比如 twitter 的 public timeline 可以以发表时间作为 score 来存储,这样获取时就是自动按时间排好序的。点击数做出排行榜。

1.商品的评价标签,可以记录商品的标签,统计标签次数,增加标签次数,按标签的分值进行排序

Redis的9种数据类型及使用场景_第19张图片

2.百度搜索热点

Redis的9种数据类型及使用场景_第20张图片
Redis的9种数据类型及使用场景_第21张图片

3.反spam系统

作为一个电商网站被各种spam攻击是少不免(垃圾评论、发布垃圾商品、广告、刷自家商品排名等)针对这些spam制定一系列anti-spam规则,其中有些规则可以利用redis做实时分析

譬如:1分钟评论不得超过2次、5分钟评论少于5次等

#获取5秒内操作记录
$res = $redis->zRangeByScore('user:1000:comment', time() - 5, time());
#判断5秒内不能评论
if (!$res) {
    $redis->zAdd('user:1000:comment', time(), '评论内容');
} else {
    echo '5秒之内不能评论';
}
 
#5秒内评论不得超过2次
if($redis->zRangeByScore('user:1000:comment',time()-5 ,time())==1)
echo '5秒之内不能评论2次';
 
#5秒内评论不得少于2次
if(count($redis->zRangeByScore('user:1000:comment',time()-5 ,time()))<2)
echo '5秒之内不能评论2次';

BitMap 位图

在应用场景中,有一些数据只有两个属性,比如是否是学生,是否是党员等等,对于这些数据,最节约内存的方式就是用bit去记录,以是否是学生为例,1代表是学生,0代表不是学生。那么1000110就代表7个人中3个是学生,这就是BitMaps的存储需求。Bitmaps是一个可以对位进行操作的字符串,我们可以把Bitmaps想象成是一串二进制数字,每个位置只存储0和1。下标是Bitmaps的偏移量。BitMap 就是通过一个 bit 位来表示某个元素对应的值或者状态, 其中的 key 就是对应元素本身,实际上底层也是通过对字符串的操作来实现。Redis从2.2.0版本开始新增了setbit,getbit,bitcount等几个bitmap相关命令。虽然是新命令,但是并没有新增新的数据类型,因为setbit等命令只不过是在set上的扩展。

使用场景一:用户签到

很多网站都提供了签到功能(这里不考虑数据落地事宜),并且需要展示最近一个月的签到情况

Redis的9种数据类型及使用场景_第22张图片
connect('127.0.0.1');

//用户uid
$uid = 1;

//记录有uid的key
$cacheKey = sprintf("sign_%d", $uid);

//开始有签到功能的日期
$startDate = '2017-01-01';

//今天的日期
$todayDate = '2017-01-21';

//计算offset
$startTime = strtotime($startDate);
$todayTime = strtotime($todayDate);
$offset = floor(($todayTime - $startTime) / 86400);

echo "今天是第{$offset}天" . PHP_EOL;

//签到
//一年一个用户会占用多少空间呢?大约365/8=45.625个字节,好小,有木有被惊呆?
$redis->setBit($cacheKey, $offset, 1);

//查询签到情况
$bitStatus = $redis->getBit($cacheKey, $offset);
echo 1 == $bitStatus ? '今天已经签到啦' : '还没有签到呢';
echo PHP_EOL;

//计算总签到次数
echo $redis->bitCount($cacheKey) . PHP_EOL;

/**
 * 计算某段时间内的签到次数
 * 很不幸啊,bitCount虽然提供了start和end参数,但是这个说的是字符串的位置,而不是对应"位"的位置
 * 幸运的是我们可以通过get命令将value取出来,自己解析。并且这个value不会太大,上面计算过一年一个用户只需要45个字节
 * 给我们的网站定一个小目标,运行30年,那么一共需要1.31KB(就问你屌不屌?)
 */
//这是个错误的计算方式
echo $redis->bitCount($cacheKey, 0, 20) . PHP_EOL;

使用场景二:统计活跃用户

使用时间作为cacheKey,然后用户ID为offset,如果当日活跃过就设置为1

那么我该如果计算某几天/月/年的活跃用户呢(暂且约定,统计时间内只有有一天在线就称为活跃),有请下一个redis的命令

命令 BITOP operation destkey key [key ...]

说明:对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。

说明:BITOP 命令支持 AND 、 OR 、 NOT 、 XOR 这四种操作中的任意一种参数

bitop and destKey key1 key2....  //交
bitop or destKey key1 key2....   //并
bitop not destKey key1 key2....  //非
bitop xor destKey key1 key2....  //异或
 array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10),

    '2017-01-11' => array(1, 2, 3, 4, 5, 6, 7, 8),

    '2017-01-12' => array(1, 2, 3, 4, 5, 6),

    '2017-01-13' => array(1, 2, 3, 4),
    '2017-01-14' => array(1, 2)

);
//批量设置活跃状态

foreach ($data as $date => $uids) {
    $cacheKey = sprintf("stat_%s", $date);
    foreach ($uids as $uid) {
        $redis->setBit($cacheKey, $uid, 1);
    }
}
$redis->bitOp('AND', 'stat', 'stat_2017-01-10', 'stat_2017-01-11', 'stat_2017-01-12') . PHP_EOL;
//总活跃用户:6

echo "总活跃用户:" . $redis->bitCount('stat') . PHP_EOL;

$redis->bitOp('AND', 'stat1', 'stat_2017-01-10', 'stat_2017-01-11', 'stat_2017-01-14') . PHP_EOL;


//总活跃用户:2

echo "总活跃用户:" . $redis->bitCount('stat1') . PHP_EOL;

$redis->bitOp('AND', 'stat2', 'stat_2017-01-10', 'stat_2017-01-11') . PHP_EOL; //总活跃用户:8

echo "总活跃用户:" . $redis->bitCount('stat2') . PHP_EOL;

使用场景三:用户在线状态

前段时间开发一个项目,对方给我提供了一个查询当前用户是否在线的接口。不了解对方是怎么做的,自己考虑了一下,使用bitmap是一个节约空间效率又高的一种方法,只需要一个key,然后用户ID为offset,如果在线就设置为1,不在线就设置为0,和上面的场景一样,5000W用户只需要6MB的空间。

Redis的9种数据类型及使用场景_第23张图片
Redis的9种数据类型及使用场景_第24张图片

HyperLogLog 基数统计

HyperLogLog算法时一种非常巧妙的近似统计大量去重元素数量的算法,它内部维护了16384个桶来记录各自桶的元素数量,当一个元素过来,它会散列到其中一个桶。当元素到来时,通过 hash 算法将这个元素分派到其中的一个小集合存储,同样的元素总是会散列到同样的小集合。这样总的计数就是所有小集合大小的总和。使用这种方式精确计数除了可以增加元素外,还可以减少元素

Redis的9种数据类型及使用场景_第25张图片

一个HyperLogLog实际占用的空间大约是 13684 * 6bit / 8 = 12k 字节。但是在计数比较小的时候,大多数桶的计数值都是零。如果 12k 字节里面太多的字节都是零,那么这个空间是可以适当节约一下的。Redis 在计数值比较小的情况下采用了稀疏存储,稀疏存储的空间占用远远小于 12k 字节。相对于稀疏存储的就是密集存储,密集存储会恒定占用 12k 字节。

内部编码

HyperLogLog 整体的内部结构就是 HLL 对象头 加上 16384 个桶的计数值位图。它在 Redis 的内部结构表现就是一个字符串位图。你可以把 HyperLogLog 对象当成普通的字符串来进行处理。

应用场景

Redis 的基数统计,这个结构可以非常省内存的去统计各种计数,比如注册 IP 数、每日访问 IP 数、页面实时UV)、在线用户数等。但是它也有局限性,就是只能统计数量,而没办法去知道具体的内容是什么。当然用集合也可以解决这个问题。但是一个大型的网站,每天 IP 比如有 100 万,粗算一个 IP 消耗 15 字节,那么 100 万个 IP 就是 15M。而 HyperLogLog 在 Redis 中每个键占用的内容都是 12K,理论存储近似接近 2^64 个值,不管存储的内容是什么,它一个基于基数估算的算法,只能比较准确的估算出基数,可以使用少量固定的内存去存储并识别集合中的唯一元素。而且这个估算的基数并不一定准确,是一个带有 0.81% 标准错误的近似值。

HyperLogLog 主要的应用场景就是进行基数统计。这个问题的应用场景其实是十分广泛的。例如:对于 Google 主页面而言,同一个账户可能会访问 Google 主页面多次。于是,在诸多的访问流水中,如何计算出 Google 主页面每天被多少个不同的账户访问过就是一个重要的问题。那么对于 Google 这种访问量巨大的网页而言,其实统计出有十亿 的访问量或者十亿零十万的访问量其实是没有太多的区别的,因此,在这种业务场景下,为了节省成本,其实可以只计算出一个大概的值,而没有必要计算出精准的值

这个数据结构的命令有三个:PFADD、PFCOUNT、PFMERGE

redis> PFADD databases "Redis" "MongoDB" "MySQL"
(integer) 1
 
redis> PFADD databases "Redis"  # Redis 已经存在,不必对估计数量进行更新
(integer) 0

redis> PFCOUNT databases
(integer) 3

Geo 地理位置

Redis 的 GEO 特性在 Redis 3.2 版本中推出, 这个功能可以将用户给定的地理位置信息储存起来, 并对这些信息进行操作。GEO的数据结构总共有六个命令:geoadd、geopos、geodist、georadius、georadiusbymember、gethash,GEO使用的是国际通用坐标系WGS-84。

1.GEOADD:添加地理位置

2.GEOPOS:查询地理位置(经纬度),返回数组

3.GEODIST:计算两位位置间的距离

4.GEORADIUS:以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。

5.GEORADIUSBYMEMBER:以给定的地理位置为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。

127.0.0.1:6379> geoadd kcityGeo 116.405285 39.904989 "beijing"
(integer) 1
127.0.0.1:6379> geoadd kcityGeo 121.472644 31.231706 "shanghai"
(integer) 1 
127.0.0.1:6379> geodist kcityGeo beijing shanghai km
"1067.5980"
127.0.0.1:6379> geopos kcityGeo beijing
1) 1) "116.40528291463851929"
   2) "39.9049884229125027"
127.0.0.1:6379> geohash kcityGeo beijing
1) "wx4g0b7xrt0"
127.0.0.1:6379> georadiusbymember kcityGeo beijing 1200 km withdist withcoord asc count 5
1) 1) "beijing"
   2) "0.0000"
   3) 1) "116.40528291463851929"
      2) "39.9049884229125027"
2) 1) "shanghai"
   2) "1067.5980"
   3) 1) "121.47264629602432251"
      2) "31.23170490709807012"

内部编码

但是,需要说明的是,Geo本身不是一种数据结构,它本质上还是借助于Sorted Set(ZSET),并且使用GeoHash技术进行填充。Redis中将经纬度使用52位的整数进行编码,放进zset中,score就是GeoHash的52位整数值。在使用Redis进行Geo查询时,其内部对应的操作其实就是zset(skiplist)的操作。通过zset的score进行排序就可以得到坐标附近的其它元素,通过将score还原成坐标值就可以得到元素的原始坐标。

总之,Redis中处理这些地理位置坐标点的思想是:二维平面坐标点 --> 一维整数编码值 --> zset(score为编码值) --> zrangebyrank(获取score相近的元素)、zrangebyscore --> 通过score(整数编码值)反解坐标点 --> 附近点的地理位置坐标。

应用场景

比如现在比较火的直播业务,我们需要检索附近的主播,那么GEO就可以很好的实现这个功能。

  • 一是主播开播的时候写入主播Id的经纬度,

  • 二是主播关播的时候删除主播Id元素,这样就维护了一个具有位置信息的在线主播集合提供给线上检索。

Streams 流

这是Redis5.0引入的全新数据结构,用一句话概括Streams就是Redis实现的内存版kafka。支持多播的可持久化的消息队列,用于实现发布订阅功能,借鉴了 kafka 的设计。Redis Stream的结构有一个消息链表,将所有加入的消息都串起来,每个消息都有一个唯一的ID和对应的内容。消息是持久化的,Redis重启后,内容还在。

Redis的9种数据类型及使用场景_第26张图片

每个Stream都有唯一的名称,它就是Redis的key,在我们首次使用xadd指令追加消息时自动创建。

每个Stream都可以挂多个消费组,每个消费组会有个游标last_delivered_id在Stream数组之上往前移动,表示当前消费组已经消费到哪条消息了。每个消费组都有一个Stream内唯一的名称,消费组不会自动创建,它需要单独的指令xgroup create进行创建,需要指定从Stream的某个消息ID开始消费,这个ID用来初始化last_delivered_id变量。

每个消费组(Consumer Group)的状态都是独立的,相互不受影响。也就是说同一份Stream内部的消息会被每个消费组都消费到。

同一个消费组(Consumer Group)可以挂接多个消费者(Consumer),这些消费者之间是竞争关系,任意一个消费者读取了消息都会使游标last_delivered_id往前移动。每个消费者者有一个组内唯一名称。

消费者(Consumer)内部会有个状态变量pending_ids,它记录了当前已经被客户端读取的消息,但是还没有ack。如果客户端没有ack,这个变量里面的消息ID会越来越多,一旦某个消息被ack,它就开始减少。这个pending_ids变量在Redis官方被称之为PEL,也就是Pending Entries List,这是一个很核心的数据结构,它用来确保客户端至少消费了消息一次,而不会在网络传输的中途丢失了没处理。

  • 消息ID:消息ID的形式是timestampInMillis-sequence,例如1527846880572-5,它表示当前的消息在毫米时间戳1527846880572时产生,并且是该毫秒内产生的第5条消息。消息ID可以由服务器自动生成,也可以由客户端自己指定,但是形式必须是整数-整数,而且必须是后面加入的消息的ID要大于前面的消息ID。

  • 消息内容:消息内容就是键值对,形如hash结构的键值对,这没什么特别之处。

127.0.0.1:6379> XADD mystream * field1 value1 field2 value2 field3 value3
"1588491680862-0"
127.0.0.1:6379> XADD mystream * username lisi age 18
"1588491854070-0"
127.0.0.1:6379> xlen mystream
(integer) 2
127.0.0.1:6379> XADD mystream * username lisi age 18
"1588491861215-0"
127.0.0.1:6379> xrange mystream - + 
1) 1) "1588491680862-0"
   2) 1) "field1"
      2) "value1"
      3) "field2"
      4) "value2"
      5) "field3"
      6) "value3"
2) 1) "1588491854070-0"
   2) 1) "username"
      2) "lisi"
      3) "age"
      4) "18"
3) 1) "1588491861215-0"
   2) 1) "username"
      2) "lisi"
      3) "age"
      4) "18"
127.0.0.1:6379> xdel mystream 1588491854070-0 
(integer) 1
127.0.0.1:6379> xrange mystream - + 
1) 1) "1588491680862-0"
   2) 1) "field1"
      2) "value1"
      3) "field2"
      4) "value2"
      5) "field3"
      6) "value3"
2) 1) "1588491861215-0"
   2) 1) "username"
      2) "lisi"
      3) "age"
      4) "18"
127.0.0.1:6379> xlen mystream
(integer) 2

内部编码

streams底层的数据结构是radix tree:Radix Tree(基数树) 事实上就几乎相同是传统的二叉树。仅仅是在寻找方式上,以一个unsigned int类型数为例,利用这个数的每个比特位作为树节点的推断。能够这样说,比方一个数10001010101010110101010,那么依照Radix 树的插入就是在根节点,假设遇到0,就指向左节点,假设遇到1就指向右节点,在插入过程中构造树节点,在删除过程中删除树节点。如下是一个保存了7个单词的Radix Tree:

Redis的9种数据类型及使用场景_第27张图片

应用场景总结

实际上,所谓的应用场景,其实就是合理的利用Redis本身的数据结构的特性来完成相关业务功能

Redis的9种数据类型及使用场景_第28张图片

常用内存优化手段与参数

通过我们上面的一些实现上的分析可以看出 redis 实际上的内存管理成本非常高,即占用了过多的内存,作者对这点也非常清楚,所以提供了一系列的参数和手段来控制和节省内存,我们分别来讨论下。

首先最重要的一点是不要开启 Redis 的 VM 选项,即虚拟内存功能,这个本来是作为 Redis 存储超出物理内存数据的一种数据在内存与磁盘换入换出的一个持久化策略,但是其内存管理成本也非常的高,并且我们后续会分析此种持久化策略并不成熟,所以要关闭 VM 功能,请检查你的 redis.conf 文件中 vm-enabled 为 no。

其次最好设置下 redis.conf 中的 maxmemory 选项,该选项是告诉 Redis 当使用了多少物理内存后就开始拒绝后续的写入请求,该参数能很好的保护好你的 Redis 不会因为使用了过多的物理内存而导致 swap,最终严重影响性能甚至崩溃。

另外 Redis 为不同数据类型分别提供了一组参数来控制内存使用,我们在前面详细分析过 Redis Hash 是 value 内部为一个 HashMap,如果该 Map 的成员数比较少,则会采用类似一维线性的紧凑格式来存储该 Map,即省去了大量指针的内存开销,这个参数控制对应在 redis.conf 配置文件中下面2项:

hash-max-ziplist-entries 64
hash-max-zipmap-value    512

含义是当 value 这个 Map 内部不超过多少个成员时会采用线性紧凑格式存储,默认是64,即 value 内部有64个以下的成员就是使用线性紧凑存储,超过该值自动转成真正的 HashMap。hash-max-zipmap-value 含义是当 value 这个 Map 内部的每个成员值长度不超过多少字节就会采用线性紧凑存储来节省空间。以上2个条件任意一个条件超过设置值都会转换成真正的 HashMap,也就不会再节省内存了,那么这个值是不是设置的越大越好呢,答案当然是否定的,HashMap 的优势就是查找和操作的时间复杂度都是 O(1) 的,而放弃 Hash 采用一维存储则是 O(n) 的时间复杂度,如果成员数量很少,则影响不大,否则会严重影响性能,所以要权衡好这个值的设置,总体上还是最根本的时间成本和空间成本上的权衡。

同样类似的参数还有

list-max-ziplist-entries512

说明:list 数据类型多少节点以下会采用去指针的紧凑存储格式。

list-max-ziplist-value64

说明:list 数据类型节点值大小小于多少字节会采用紧凑存储格式。

set-max-intset-entries512

说明:set 数据类型内部数据如果全部是数值型,且包含多少节点以下会采用紧凑格式存储。

最后想说的是 Redis 内部实现没有对内存分配方面做过多的优化,在一定程度上会存在内存碎片,不过大多数情况下这个不会成为 Redis 的性能瓶 颈,不过如果在 Redis 内部存储的大部分数据是数值型的话,Redis 内部采用了一个 shared integer 的方式来省去分配内存的开销,即在系统启动时先分配一个从 1~n 那么多个数值对象放在一个池子中,如果存储的数据恰好是这个数值范围内的数据,则直接从池子里取出该对象,并且通过引用计数的方式来共享,这样在系统存储了大量数值下,也能一定程度上节省内存并且提高性能,这个参数值 n 的设置需要修改源代码中的一行宏定义 REDIS_SHARED_INTEGERS,该值 默认是 10000,可以根据自己的需要进行修改,修改后重新编译就可以了。

你可能感兴趣的:(测试开发,redis,redis,java,缓存)