# Redis 数据类型
> Redis 提供了多种数据类型,每种数据类型有丰富的命令支持。
>
> 使用 Redis ,不仅要了解其数据类型的特性,还需要根据业务场景,灵活的、高效的使用其数据类型来建模。
- [一、Redis 基本数据类型](#一redis-基本数据类型)
- [STRING](#string)
- [HASH](#hash)
- [LIST](#list)
- [SET](#set)
- [ZSET](#zset)
- [通用命令](#通用命令)
- [二、Redis 高级数据类型](#二redis-高级数据类型)
- [BitMap](#bitmap)
- [HyperLogLog](#hyperloglog)
- [GEO](#geo)
- [三、Redis 数据类型应用](#三redis-数据类型应用)
- [案例-最受欢迎文章](#案例-最受欢迎文章)
- [案例-管理令牌](#案例-管理令牌)
- [案例-购物车](#案例-购物车)
- [案例-页面缓存](#案例-页面缓存)
- [案例-数据行缓存](#案例-数据行缓存)
- [案例-网页分析](#案例-网页分析)
- [案例-记录日志](#案例-记录日志)
- [案例-统计数据](#案例-统计数据)
- [案例-查找 IP 所属地](#案例-查找-ip-所属地)
- [案例-服务的发现与配置](#案例-服务的发现与配置)
- [案例-自动补全](#案例-自动补全)
- [案例-广告定向](#案例-广告定向)
- [案例-职位搜索](#案例-职位搜索)
- [参考资料](#参考资料)
## 一、Redis 基本数据类型

| 数据类型 | 可以存储的值 | 操作 |
| -------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------- |
| STRING | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作 对整数和浮点数执行自增或者自减操作 |
| LIST | 列表 | 从两端压入或者弹出元素 读取单个或者多个元素 进行修剪,只保留一个范围内的元素 |
| SET | 无序集合 | 添加、获取、移除单个元素 检查一个元素是否存在于集合中 计算交集、并集、差集 从集合里面随机获取元素 |
| HASH | 包含键值对的无序散列表 | 添加、获取、移除单个键值对 获取所有键值对 检查某个键是否存在 |
| ZSET | 有序集合 | 添加、获取、删除元素 根据分值范围或者成员来获取元素 计算一个键的排名 |
> [What Redis data structures look like](https://redislabs.com/ebook/part-1-getting-started/chapter-1-getting-to-know-redis/1-2-what-redis-data-structures-look-like/)
### STRING
**适用场景:缓存、计数器、共享 Session**
命令:
| 命令 | 行为 |
| ------ | ---------------------------------------------------- |
| `GET` | 获取存储在给定键中的值。 |
| `SET` | 设置存储在给定键中的值。 |
| `DEL` | 删除存储在给定键中的值(这个命令可以用于所有类型)。 |
| `INCR` | 为键 `key` 储存的数字值加一 |
| `DECR` | 为键 `key` 储存的数字值减一 |
> 更多命令请参考:[Redis String 类型命令](https://redis.io/commands#string)
示例:
```shell
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> get hello
"world"
127.0.0.1:6379> del hello
(integer) 1
127.0.0.1:6379> get hello
(nil)
```
### HASH
**适用场景:存储结构化数据**,如一个对象:用户信息、产品信息等。
命令:
| 命令 | 行为 |
| --------- | ------------------------------------------ |
| `HSET` | 在散列里面关联起给定的键值对。 |
| `HGET` | 获取指定散列键的值。 |
| `HGETALL` | 获取散列包含的所有键值对。 |
| `HDEL` | 如果给定键存在于散列里面,那么移除这个键。 |
> 更多命令请参考:[Redis Hash 类型命令](https://redis.io/commands#hash)
示例:
```shell
127.0.0.1:6379> hset hash-key sub-key1 value1
(integer) 1
127.0.0.1:6379> hset hash-key sub-key2 value2
(integer) 1
127.0.0.1:6379> hset hash-key sub-key1 value1
(integer) 0
127.0.0.1:6379> hset hash-key sub-key3 value2
(integer) 0
127.0.0.1:6379> hgetall hash-key
1) "sub-key1"
2) "value1"
3) "sub-key2"
4) "value2"
127.0.0.1:6379> hdel hash-key sub-key2
(integer) 1
127.0.0.1:6379> hdel hash-key sub-key2
(integer) 0
127.0.0.1:6379> hget hash-key sub-key1
"value1"
127.0.0.1:6379> hgetall hash-key
1) "sub-key1"
2) "value1"
```
### LIST
**适用场景:用于存储列表型数据**。如:粉丝列表、商品列表等。
命令:
| 命令 | 行为 |
| -------- | ------------------------------------------ |
| `LPUSH` | 将给定值推入列表的右端。 |
| `RPUSH` | 将给定值推入列表的右端。 |
| `LPOP` | 从列表的左端弹出一个值,并返回被弹出的值。 |
| `RPOP` | 从列表的右端弹出一个值,并返回被弹出的值。 |
| `LRANGE` | 获取列表在给定范围上的所有值。 |
| `LINDEX` | 获取列表在给定位置上的单个元素。 |
| `LREM` | 从列表的左端弹出一个值,并返回被弹出的值。 |
| `LTRIM` | 只保留指定区间内的元素,删除其他元素。 |
> 更多命令请参考:[Redis List 类型命令](https://redis.io/commands#list)
示例:
```shell
127.0.0.1:6379> rpush list-key item
(integer) 1
127.0.0.1:6379> rpush list-key item2
(integer) 2
127.0.0.1:6379> rpush list-key item
(integer) 3
127.0.0.1:6379> lrange list-key 0 -1
1) "item"
2) "item2"
3) "item"
127.0.0.1:6379> lindex list-key 1
"item2"
127.0.0.1:6379> lpop list-key
"item"
127.0.0.1:6379> lrange list-key 0 -1
1) "item2"
2) "item"
```
### SET
**适用场景:用于存储去重的列表型数据**。
命令:
| 命令 | 行为 |
| ----------- | ---------------------------------------------- |
| `SADD` | 将给定元素添加到集合。 |
| `SMEMBERS` | 返回集合包含的所有元素。 |
| `SISMEMBER` | 检查给定元素是否存在于集合中。 |
| `SREM` | 如果给定的元素存在于集合中,那么移除这个元素。 |
> 更多命令请参考:[Redis Set 类型命令](https://redis.io/commands#set)
示例:
```shell
127.0.0.1:6379> sadd set-key item
(integer) 1
127.0.0.1:6379> sadd set-key item2
(integer) 1
127.0.0.1:6379> sadd set-key item3
(integer) 1
127.0.0.1:6379> sadd set-key item
(integer) 0
127.0.0.1:6379> smembers set-key
1) "item"
2) "item2"
3) "item3"
127.0.0.1:6379> sismember set-key item4
(integer) 0
127.0.0.1:6379> sismember set-key item
(integer) 1
127.0.0.1:6379> srem set-key item2
(integer) 1
127.0.0.1:6379> srem set-key item2
(integer) 0
127.0.0.1:6379> smembers set-key
1) "item"
2) "item3"
```
### ZSET
适用场景:由于可以设置 score,且不重复。**适合用于存储各种排行数据**,如:按评分排序的有序商品集合、按时间排序的有序文章集合。
命令:
| 命令 | 行为 |
| --------------- | ------------------------------------------------------------ |
| `ZADD` | 将一个带有给定分值的成员添加到有序集合里面。 |
| `ZRANGE` | 根据元素在有序排列中所处的位置,从有序集合里面获取多个元素。 |
| `ZRANGEBYSCORE` | 获取有序集合在给定分值范围内的所有元素。 |
| `ZREM` | 如果给定成员存在于有序集合,那么移除这个成员。 |
> 更多命令请参考:[Redis ZSet 类型命令](https://redis.io/commands#sorted_set)
示例:
```shell
127.0.0.1:6379> zadd zset-key 728 member1
(integer) 1
127.0.0.1:6379> zadd zset-key 982 member0
(integer) 1
127.0.0.1:6379> zadd zset-key 982 member0
(integer) 0
127.0.0.1:6379> zrange zset-key 0 -1 withscores
1) "member1"
2) "728"
3) "member0"
4) "982"
127.0.0.1:6379> zrangebyscore zset-key 0 800 withscores
1) "member1"
2) "728"
127.0.0.1:6379> zrem zset-key member1
(integer) 1
127.0.0.1:6379> zrem zset-key member1
(integer) 0
127.0.0.1:6379> zrange zset-key 0 -1 withscores
1) "member0"
2) "982"
```
### 通用命令
#### 排序
Redis 的 `SORT` 命令可以对 `LIST`、`SET`、`ZSET` 进行排序。
| 命令 | 描述 |
| ------ | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `SORT` | `SORT source-key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC | DESC] [ALPHA] [STORE dest-key]`—根据给定选项,对输入 `LIST`、`SET`、`ZSET` 进行排序,然后返回或存储排序的结果。 |
示例:
```shell
127.0.0.1:6379[15]> RPUSH 'sort-input' 23 15 110 7
(integer) 4
127.0.0.1:6379[15]> SORT 'sort-input'
1) "7"
2) "15"
3) "23"
4) "110"
127.0.0.1:6379[15]> SORT 'sort-input' alpha
1) "110"
2) "15"
3) "23"
4) "7"
127.0.0.1:6379[15]> HSET 'd-7' 'field' 5
(integer) 1
127.0.0.1:6379[15]> HSET 'd-15' 'field' 1
(integer) 1
127.0.0.1:6379[15]> HSET 'd-23' 'field' 9
(integer) 1
127.0.0.1:6379[15]> HSET 'd-110' 'field' 3
(integer) 1
127.0.0.1:6379[15]> SORT 'sort-input' by 'd-*->field'
1) "15"
2) "110"
3) "7"
4) "23"
127.0.0.1:6379[15]> SORT 'sort-input' by 'd-*->field' get 'd-*->field'
1) "1"
2) "3"
3) "5"
4) "9"
```
#### 键的过期时间
Redis 的 `EXPIRE` 命令可以指定一个键的过期时间,当达到过期时间后,Redis 会自动删除该键。
| 命令 | 描述 |
| ----------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `PERSIST` | `PERSIST key-name`—移除键的过期时间 |
| `TTL` | `TTL key-name`—查看给定键距离过期还有多少秒 |
| `EXPIRE` | `EXPIRE key-name seconds`—让给定键在指定的秒数之后过期 |
| `EXPIREAT` | `EXPIREAT key-name timestamp`—将给定键的过期时间设置为给定的 UNIX 时间戳 |
| `PTTL` | `PTTL key-name`—查看给定键距离过期时间还有多少毫秒(这个命令在 Redis 2.6 或以上版本可用) |
| `PEXPIRE` | `PEXPIRE key-name milliseconds`—让给定键在指定的毫秒数之后过期(这个命令在 Redis 2.6 或以上版本可用) |
| `PEXPIREAT` | `PEXPIREAT key-name timestamp-milliseconds`—将一个毫秒级精度的 UNIX 时间戳设置为给定键的过期时间(这个命令在 Redis 2.6 或以上版本可用) |
示例:
```shell
127.0.0.1:6379[15]> SET key value
OK
127.0.0.1:6379[15]> GET key
"value"
127.0.0.1:6379[15]> EXPIRE key 2
(integer) 1
127.0.0.1:6379[15]> GET key
(nil)
```
## 二、Redis 高级数据类型
### BitMap
BitMap 即位图。BitMap 不是一个真实的数据结构。而是 STRING 类型上的一组面向 bit 操作的集合。由于 STRING 是二进制安全的 blob,并且它们的最大长度是 512m,所以 BitMap 能最大设置 $$2^{32}$$ 个不同的 bit。
Bitmaps 的最大优点就是存储信息时可以节省大量的空间。例如在一个系统中,不同的用户被一个增长的用户 ID 表示。40 亿($$2^{32}$$ = $$4*1024*1024*1024$$ ≈ 40 亿)用户只需要 512M 内存就能记住某种信息,例如用户是否登录过。
#### BitMap 命令
- [SETBIT](http://redisdoc.com/bitmap/setbit.html) - 对 `key` 所储存的字符串值,设置或清除指定偏移量上的位(bit)。
- [GETBIT](http://redisdoc.com/bitmap/getbit.html) - 对 `key` 所储存的字符串值,获取指定偏移量上的位(bit)。
- [BITCOUNT](http://redisdoc.com/bitmap/bitcount.html) - 计算给定字符串中,被设置为 `1` 的比特位的数量。
- [BITPOS](http://redisdoc.com/bitmap/bitpos.html)
- [BITOP](http://redisdoc.com/bitmap/bitop.html)
- [BITFIELD](http://redisdoc.com/bitmap/bitfield.html)
#### BitMap 示例
```shell
# 对不存在的 key 或者不存在的 offset 进行 GETBIT, 返回 0
redis> EXISTS bit
(integer) 0
redis> GETBIT bit 10086
(integer) 0
# 对已存在的 offset 进行 GETBIT
redis> SETBIT bit 10086 1
(integer) 0
redis> GETBIT bit 10086
(integer) 1
redis> BITCOUNT bit
(integer) 1
```
#### BitMap 应用
Bitmap 对于一些特定类型的计算非常有效。例如:使用 bitmap 实现用户上线次数统计。
假设现在我们希望记录自己网站上的用户的上线频率,比如说,计算用户 A 上线了多少天,用户 B 上线了多少天,诸如此类,以此作为数据,从而决定让哪些用户参加 beta 测试等活动 —— 这个模式可以使用 [SETBIT key offset value](http://redisdoc.com/bitmap/setbit.html#setbit) 和 [BITCOUNT key [start\] [end]](http://redisdoc.com/bitmap/bitcount.html#bitcount) 来实现。
比如说,每当用户在某一天上线的时候,我们就使用 [SETBIT key offset value](http://redisdoc.com/bitmap/setbit.html#setbit) ,以用户名作为 `key`,将那天所代表的网站的上线日作为 `offset` 参数,并将这个 `offset` 上的为设置为 `1` 。
> 更详细的实现可以参考:
>
> [一看就懂系列之 详解 redis 的 bitmap 在亿级项目中的应用](https://blog.csdn.net/u011957758/article/details/74783347)
>
> [Fast, easy, realtime metrics using Redis bitmaps](http://blog.getspool.com/2011/11/29/fast-easy-realtime-metrics-using-redis-bitmaps/)
### HyperLogLog
HyperLogLog 是用于计算唯一事物的概率数据结构(从技术上讲,这被称为估计集合的基数)。如果统计唯一项,项目越多,需要的内存就越多。因为需要记住过去已经看过的项,从而避免多次统计这些项。
#### HyperLogLog 命令
- [PFADD](http://redisdoc.com/hyperloglog/pfadd.html) - 将任意数量的元素添加到指定的 HyperLogLog 里面。
- [PFCOUNT](http://redisdoc.com/hyperloglog/pfcount.html) - 返回 HyperLogLog 包含的唯一元素的近似数量。
- [PFMERGE](http://redisdoc.com/hyperloglog/pfmerge.html) - 将多个 HyperLogLog 合并(merge)为一个 HyperLogLog , 合并后的 HyperLogLog 的基数接近于所有输入 HyperLogLog 的可见集合(observed set)的并集。合并得出的 HyperLogLog 会被储存在 `destkey` 键里面, 如果该键并不存在, 那么命令在执行之前, 会先为该键创建一个空的 HyperLogLog 。
示例:
```shell
redis> PFADD databases "Redis" "MongoDB" "MySQL"
(integer) 1
redis> PFCOUNT databases
(integer) 3
redis> PFADD databases "Redis" # Redis 已经存在,不必对估计数量进行更新
(integer) 0
redis> PFCOUNT databases # 元素估计数量没有变化
(integer) 3
redis> PFADD databases "PostgreSQL" # 添加一个不存在的元素
(integer) 1
redis> PFCOUNT databases # 估计数量增一
4
```
### GEO
这个功能可以将用户给定的地理位置(经度和纬度)信息储存起来,并对这些信息进行操作。
#### GEO 命令
- [GEOADD](http://redisdoc.com/geo/geoadd.html) - 将指定的地理空间位置(纬度、经度、名称)添加到指定的 key 中。
- [GEOPOS](http://redisdoc.com/geo/geopos.html) - 从 key 里返回所有给定位置元素的位置(经度和纬度)。
- [GEODIST](http://redisdoc.com/geo/geodist.html) - 返回两个给定位置之间的距离。
- [GEOHASH](http://redisdoc.com/geo/geohash.html) - 回一个或多个位置元素的标准 Geohash 值,它可以在http://geohash.org/使用。
- [GEORADIUS](http://redisdoc.com/geo/georadius.html)
- [GEORADIUSBYMEMBER](http://redisdoc.com/geo/georadiusbymember.html)
## 三、Redis 数据类型应用
### 案例-最受欢迎文章
选出最受欢迎文章,需要支持对文章进行评分。
#### 对文章进行投票
(1)使用 HASH 存储文章
使用 `HASH` 类型存储文章信息。其中:key 是文章 ID;field 是文章的属性 key;value 是属性对应值。

操作:
- 存储文章信息 - 使用 `HSET` 或 `HMGET` 命令
- 查询文章信息 - 使用 `HGETALL` 命令
- 添加投票 - 使用 `HINCRBY` 命令
(2)使用 `ZSET` 针对不同维度集合排序
使用 `ZSET` 类型分别存储按照时间排序和按照评分排序的文章 ID 集合。

操作:
- 添加记录 - 使用 `ZADD` 命令
- 添加分数 - 使用 `ZINCRBY` 命令
- 取出多篇文章 - 使用 `ZREVRANGE` 命令
(3)为了防止重复投票,使用 `SET` 类型记录每篇文章 ID 对应的投票集合。

操作:
- 添加投票者 - 使用 `SADD` 命令
- 设置有效期 - 使用 `EXPIRE` 命令
(4)假设 user:115423 给 article:100408 投票,分别需要高更新评分排序集合以及投票集合。

当需要对一篇文章投票时,程序需要用 ZSCORE 命令检查记录文章发布时间的有序集合,判断文章的发布时间是否超过投票有效期(比如:一星期)。
```java
public void articleVote(Jedis conn, String user, String article) {
// 计算文章的投票截止时间。
long cutoff = (System.currentTimeMillis() / 1000) - ONE_WEEK_IN_SECONDS;
// 检查是否还可以对文章进行投票
// (虽然使用散列也可以获取文章的发布时间,
// 但有序集合返回的文章发布时间为浮点数,
// 可以不进行转换直接使用)。
if (conn.zscore("time:", article) < cutoff) {
return;
}
// 从article:id标识符(identifier)里面取出文章的ID。
String articleId = article.substring(article.indexOf(':') + 1);
// 如果用户是第一次为这篇文章投票,那么增加这篇文章的投票数量和评分。
if (conn.sadd("voted:" + articleId, user) == 1) {
conn.zincrby("score:", VOTE_SCORE, article);
conn.hincrBy(article, "votes", 1);
}
}
```
#### 发布并获取文章
发布文章:
- 添加文章 - 使用 `INCR` 命令计算新的文章 ID,填充文章信息,然后用 `HSET` 命令或 `HMSET` 命令写入到 `HASH` 结构中。
- 将文章作者 ID 添加到投票名单 - 使用 `SADD` 命令添加到代表投票名单的 `SET` 结构中。
- 设置投票有效期 - 使用 `EXPIRE` 命令设置投票有效期。
```java
public String postArticle(Jedis conn, String user, String title, String link) {
// 生成一个新的文章ID。
String articleId = String.valueOf(conn.incr("article:"));
String voted = "voted:" + articleId;
// 将发布文章的用户添加到文章的已投票用户名单里面,
conn.sadd(voted, user);
// 然后将这个名单的过期时间设置为一周(第3章将对过期时间作更详细的介绍)。
conn.expire(voted, ONE_WEEK_IN_SECONDS);
long now = System.currentTimeMillis() / 1000;
String article = "article:" + articleId;
// 将文章信息存储到一个散列里面。
HashMap articleData = new HashMap();
articleData.put("title", title);
articleData.put("link", link);
articleData.put("user", user);
articleData.put("now", String.valueOf(now));
articleData.put("votes", "1");
conn.hmset(article, articleData);
// 将文章添加到根据发布时间排序的有序集合和根据评分排序的有序集合里面。
conn.zadd("score:", now + VOTE_SCORE, article);
conn.zadd("time:", now, article);
return articleId;
}
```
分页查询最受欢迎文章:
使用 `ZINTERSTORE` 命令根据页码、每页记录数、排序号,根据评分值从大到小分页查出文章 ID 列表。
```java
public List