前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >《Redis深度历险》

《Redis深度历险》

原创
作者头像
用户7481820
修改2021-01-27 18:09:38
5050
修改2021-01-27 18:09:38
举报

本书作者,老钱。

在没有读这本书之前,我对redis的认知范围 只有五种数据结构的基础使用。

系统的学习一个东西,才能对它有个全面的认识。

通过这本书我学到了 五种数据结构的高级用法,如:批量存取延时队列等、redis的其他特性,如:节省空间的BitMap、四两拨千斤的HyperLogLog布隆过滤器漏斗限流GeoHashScanStream等以及源码等。

进阶使用 & 使用场景

1 批量存取

redis指令:mget / mset [key ...]

代码语言:javascript
复制
localhost:6379> mget "key - 1" "key - 2"
1) "1"
2) "2"

localhost:6379> mset key0 value0 k2 v2 
OK

Java 应用:

代码语言:javascript
复制
for (Integer i = 0 ; i < 20 ; i++) {
    list.add("key - " + i);
}
List<String> multValues = redisTemplate.opsForValue().multiGet(list);

使用场景:

当我们根据 pageSize 和 pageNum 缓存列表时候,如果要求倒序展示,并且不断有新的数据生成,那么

缓存的列表很快就失效了,需要删除大量缓存,效率低,可以:

  1. 根据条件查到数据的 id
  2. 根据ID批量取缓存中已有的数据
  3. 没有的去查数据库
  4. 批量缓存到

这样有新的数据进来不需要删除缓存,缓存变动小,其他地方也可以重复使用,复用率高,空间利用合理。

实验及分析: mget 20 条使用时间大概是 get 时间的 2 倍,批量相比一次一条循环 20 次减少 19 次网络开销。

2 异步消息队列

Redis指令: 利用 阻塞指令 BLPOP 阻塞等待 list 出现新数据消费。

代码语言:javascript
复制
localhost:6379> LPUSH list1 v1 v2 v3
(integer) 3
代码语言:javascript
复制
localhost:6379> BLPOP list1 10
1) "list1"
2) "v3"
(5.26s)

使用场景: 尽量不要使用,不是专业的消息队列,消息的状态过于简单(没有状态),且没有ack机制,消息取出后消费失败依赖于client记录日志或者重新push到队列里面。

3 延迟队列

利用 zset 实现,将消息序列化成一个字符串作为 zset 的 value, 这个消息的到期处理时间为 score,然后用多个先生轮询 zset 获取到期的任务进行处理,多线程保障可用性。为了防止多次消费, zrem 方法是关键,根据返回值决定由谁来消费。

reids 指令: zrangebysocre, zrem

使用场景:同上。

高级特性

1 位图

把一个字节的 8 个位当八个空间使用,节省空间。

返回值:

字符串值指定偏移量上原来储存的位(bit)。

redis指令:

设置:SETBIT key offset value

查询:SETBIT key offset value

返回值:偏移量原来的值。

代码语言:javascript
复制
localhost:6379> SETBIT dw 2 1
(integer) 0
localhost:6379> getbit dw  1
(integer) 0
localhost:6379> getbit dw  2
(integer) 1
localhost:6379> SETBIT dw 2 0
(integer) 1

Java应用: Java中没有 API 操作bitmap。

使用场景:同一个 key 存大量 boolean 值,如:用户一年中的签到情况。

2 HyperLogLog

可以粗略计算去重后元素的数量,不需要记录所有的元素,这个数据结构需要占据 12 KB的存储空间。

在技术比较小时,采用稀疏矩阵存储,占用空间很小,仅仅在计数慢慢变大,稀疏矩阵占用空间渐渐超过了阈值,才会一次性转变成稠密矩阵,占用 12 KB 空间。

Redis 指令:

PFADD key [element ...]

PFCOUNT [key ...] 多个key 合并的数量

PFMERGE destkey [key ...] 多个key合并生成新的 key : destkey

代码语言:javascript
复制
localhost:6379> PFADD pfkey 1 2 3 4 5 6 7 8 9 1 2 3  
(integer) 0
localhost:6379> PFCOUNT pfkey
(integer) 9
localhost:6379> PFADD pfkey2 7 8 9 10 11 
(integer) 1
localhost:6379> PFCOUNT pfkey2
(integer) 5
localhost:6379> PFCOUNT pfkey2 pfkey
(integer) 11
localhost:6379> PFMERGE pfkey3 pfkey pfkey2
OK
localhost:6379> PFCOUNT pfkey3
(integer) 11

Java 应用:

代码语言:javascript
复制
String key = "hyperloglog";
for (Integer i = 0 ; i < 2000 ; i++) {
    String value = "key - " + i;
    redisTemplate.opsForHyperLogLog().add(key, value);
}

String key2 = "hyperloglog-2";
for (Integer i = 1000 ; i < 3000 ; i++) {
    String value = "key - " + i;
    redisTemplate.opsForHyperLogLog().add(key2, value);
}

System.out.println("size:" + redisTemplate.opsForHyperLogLog().size(key));
System.out.println("union result:" + redisTemplate.opsForHyperLogLog().union(key, key2));
代码语言:javascript
复制
Out:
size:1999
union result:2993

使用场景: 计算网页 UV; 多个网页 UV 相加统计。

3 布隆过滤器

可以理解为一个不怎么精确的 set 结构

特点:

1 当布隆过滤器说某个值存在时,这个值可能不存在;当它说某个值不存在时,一定不存在。存在一定的误判,但是误判率可以设置。

2 节省空间,不需要存储元素即可判断元素是否在布隆过滤器中。

Java应用:

代码语言:javascript
复制
int insertions = 1000000;

//初始化一个存储String数据的布隆过滤器,初始化大小为100w, 误判率 0.001
BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), insertions, 0.001);
//初始化一个存储String数据的set,初始化大小为100w,做验证参考
Set<String> sets = new HashSet<String>(insertions);
//初始化一个存储String数据额list,初始化大小为100w
List<String> lists = new ArrayList<String>(insertions);

//向三个容器中初始化100w个随机唯一的字符串
for (int i = 0; i < insertions; i++) {
    String uuid = UUID.randomUUID().toString();
    bloomFilter.put(uuid);
    sets.add(uuid);
    lists.add(uuid);
}

//布隆过滤器误判的次数: 说有,其实没有
int wrongCount = 0;

// 取1000个存在的,9900个不存在的,数据做验证
for (int i = 0; i < 10000; i++) {
    String test = i % 10 == 0 ? lists.get(i / 10) : UUID.randomUUID().toString();
    //布隆过滤器验证通过
    if (bloomFilter.mightContain(test) && !sets.contains(test)) {
            wrongCount++;
    }
}

System.out.println("wrong count : " + wrongCount);
System.out.println("right count : " + rightCount);
System.out.println("wrong rate : " + Double.valueOf(wrongCount * 100) / 10000 + "%");
代码语言:javascript
复制
Out:
wrong count : 10
right count : 1000
wrong rate : 0.1%

4 GeoHash

地理模块,通过设置各个位置的经纬度,通过 GeoHash 算法,可以快速得到一个点附近 X 距离内有哪些点。

Java 应用:

代码语言:javascript
复制
String key = "key-geo";
Map<String, Point> memberCoordinateMap = new HashMap<>();

for (Integer i = 1 ; i < 20 ; i++) {  memberCoordinateMap.put("abc" + i, new Point(i.doubleValue(), 2d)); }
redisTemplate.opsForGeo().add(key, memberCoordinateMap);

// 求 点 ’abc1‘ 200 公里以内的点的信息,限50个、倒序列表、 返回距离信息
RedisGeoCommands.GeoRadiusCommandArgs geoRadiusCommandArgs =  RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().sortDescending().limit(50).includeCoordinates().includeDistance();
GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults = redisTemplate.opsForGeo().geoRadiusByMember(key, "abc1", new Distance(200, Metrics.KILOMETERS), geoRadiusCommandArgs);

System.out.println(new ObjectMapper().writeValueAsString(geoResults));
代码语言:javascript
复制
Out:
{
    "averageDistance": {
        "value": 55.5794,
        "metric": "KILOMETERS",
        "unit": "km",
        "normalizedValue": 0.008714049259211586
    },
    "content": [
        {
            "content": {
                "name": "abc2",
                "point": {
                    "x": 2.0000025629997253,
                    "y": 2.000000185646549
                }
            },
            "distance": {
                "value": 111.1588,
                "metric": "KILOMETERS",
                "unit": "km",
                "normalizedValue": 0.017428098518423172
            }
        },
        {
            "content": {
                "name": "abc1",
                "point": {
                    "x": 0.9999999403953552,
                    "y": 2.000000185646549
                }
            },
            "distance": {
                "value": 0.0,
                "metric": "KILOMETERS",
                "unit": "km",
                "normalizedValue": 0.0
            }
        }
    ]
}

应用场景: 附近的人。

5 Stream

Redis 5.0 增加了一个数据结构 Stream, 它是一个新的强大的支持多播的可持久化消息队列。作者坦言极大的借鉴了 Kafka 的设计。

Redis 指令:

xadd:

xdel:

xrange:

xlen:

del:

代码语言:javascript
复制
localhost:6379> del streamKey
(integer) 1
localhost:6379> XADD sk * name dewu age 5     // a) 创建一个stream
"1604059483892-0"
localhost:6379> xadd sk * sk sv     // b) 添加数据
"1604059500291-0"
localhost:6379> xadd sk * sk1 sv1
"1604059520653-0"
localhost:6379> xread count 1 streams sk 0-0   // c) 读取数据, ”0-0“ 从头开始读
1) 1) "sk"
   2) 1) 1) "1604059483892-0"
         2) 1) "name"
            2) "dewu"
            3) "age"
            4) "5"
localhost:6379> xread count 1 streams sk $ // 从末尾开始读
(nil)
localhost:6379> XGROUP create sk gr1 0-0
OK
localhost:6379> XGROUP create sk gr2 $
OK
localhost:6379> XINFO stream sk
 1) "length"
 2) (integer) 3
 3) "radix-tree-keys"
 4) (integer) 1
 5) "radix-tree-nodes"
 6) (integer) 2
 7) "last-generated-id"
 8) "1604059520653-0"
 9) "groups"
10) (integer) 2
11) "first-entry"
12) 1) "1604059483892-0"
    2) 1) "name"
       2) "dewu"
       3) "age"
       4) "5"
13) "last-entry"
14) 1) "1604059520653-0"
    2) 1) "sk1"
       2) "sv1"
  
localhost:6379> XREADGROUP GROUP gr1 c1 count 1 streams sk > // 创建消费者消费,一次一个,然后移动消息ID
1) 1) "sk"
   2) 1) 1) "1604059483892-0"
         2) 1) "name"
            2) "dewu"
            3) "age"
            4) "5"
localhost:6379> XREADGROUP GROUP gr1 c1 count 1 streams sk >
1) 1) "sk"
   2) 1) 1) "1604059500291-0"
         2) 1) "sk"
            2) "sv"
localhost:6379> XREADGROUP GROUP gr1 c1 count 1 streams sk >
1) 1) "sk"
   2) 1) 1) "1604059520653-0"
         2) 1) "sk1"
            2) "sv1"
localhost:6379> XREADGROUP GROUP gr1 c1 count 1 streams sk >
(nil)
localhost:6379> XINFO consumers sk gr1  // 消费者读到消息后,对应消息 ID 会进入消费者的 PEL(正在处理的消息)
1) 1) "name"
   2) "c1"
   3) "pending"
   4) (integer) 3
   5) "idle"
   6) (integer) 50580
localhost:6379> XACK sk gr1 1604059483892-0 1604059500291-0 // ack 后从 PEL 中删除
(integer) 2
localhost:6379> XINFO consumers sk gr1
1) 1) "name"
   2) "c1"
   3) "pending"
   4) (integer) 1
   5) "idle"
   6) (integer) 129701
localhost:6379> xlen sk // 队列长度
(integer) 3

使用场景:不建议使用,消息队列有专门的中间件。

总结

实际上redis并不适合任何有保障数据持久性的场景。它适合做cache,不重要的存储,或者是可以反复重来的批处理计算任务的临时存储等。

再见

源码和集群部分下期分析,敬请期待。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 进阶使用 & 使用场景
    • 1 批量存取
      • 2 异步消息队列
        • 3 延迟队列
        • 高级特性
          • 1 位图
            • 2 HyperLogLog
              • 3 布隆过滤器
                • 4 GeoHash
                  • 5 Stream
                  • 总结
                  • 再见
                  相关产品与服务
                  云数据库 Redis
                  腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档