本书作者,老钱。
在没有读这本书之前,我对redis的认知范围 只有五种数据结构的基础使用。
系统的学习一个东西,才能对它有个全面的认识。
通过这本书我学到了 五种数据结构的高级用法,如:批量存取、延时队列等、redis的其他特性,如:节省空间的BitMap、四两拨千斤的HyperLogLog、布隆过滤器、漏斗限流、GeoHash、Scan、Stream等以及源码等。
redis指令:mget / mset [key ...]
localhost:6379> mget "key - 1" "key - 2"
1) "1"
2) "2"
localhost:6379> mset key0 value0 k2 v2
OK
Java 应用:
for (Integer i = 0 ; i < 20 ; i++) {
list.add("key - " + i);
}
List<String> multValues = redisTemplate.opsForValue().multiGet(list);
使用场景:
当我们根据 pageSize 和 pageNum 缓存列表时候,如果要求倒序展示,并且不断有新的数据生成,那么
缓存的列表很快就失效了,需要删除大量缓存,效率低,可以:
这样有新的数据进来不需要删除缓存,缓存变动小,其他地方也可以重复使用,复用率高,空间利用合理。
实验及分析: mget 20 条使用时间大概是 get 时间的 2 倍,批量相比一次一条循环 20 次减少 19 次网络开销。
Redis指令: 利用 阻塞指令 BLPOP 阻塞等待 list 出现新数据消费。
localhost:6379> LPUSH list1 v1 v2 v3
(integer) 3
localhost:6379> BLPOP list1 10
1) "list1"
2) "v3"
(5.26s)
使用场景: 尽量不要使用,不是专业的消息队列,消息的状态过于简单(没有状态),且没有ack机制,消息取出后消费失败依赖于client记录日志或者重新push到队列里面。
利用 zset 实现,将消息序列化成一个字符串作为 zset 的 value, 这个消息的到期处理时间为 score,然后用多个先生轮询 zset 获取到期的任务进行处理,多线程保障可用性。为了防止多次消费, zrem 方法是关键,根据返回值决定由谁来消费。
reids 指令: zrangebysocre, zrem
使用场景:同上。
把一个字节的 8 个位当八个空间使用,节省空间。
字符串值指定偏移量上原来储存的位(bit)。
redis指令:
设置:SETBIT key offset value
查询:SETBIT key offset value
返回值:偏移量原来的值。
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 值,如:用户一年中的签到情况。
可以粗略计算去重后元素的数量,不需要记录所有的元素,这个数据结构需要占据 12 KB的存储空间。
在技术比较小时,采用稀疏矩阵存储,占用空间很小,仅仅在计数慢慢变大,稀疏矩阵占用空间渐渐超过了阈值,才会一次性转变成稠密矩阵,占用 12 KB 空间。
Redis 指令:
PFADD key [element ...]
PFCOUNT [key ...] 多个key 合并的数量
PFMERGE destkey [key ...] 多个key合并生成新的 key : destkey
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 应用:
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));
Out:
size:1999
union result:2993
使用场景: 计算网页 UV; 多个网页 UV 相加统计。
可以理解为一个不怎么精确的 set 结构
特点:
1 当布隆过滤器说某个值存在时,这个值可能不存在;当它说某个值不存在时,一定不存在。存在一定的误判,但是误判率可以设置。
2 节省空间,不需要存储元素即可判断元素是否在布隆过滤器中。
Java应用:
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 + "%");
Out:
wrong count : 10
right count : 1000
wrong rate : 0.1%
地理模块,通过设置各个位置的经纬度,通过 GeoHash 算法,可以快速得到一个点附近 X 距离内有哪些点。
Java 应用:
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));
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
}
}
]
}
应用场景: 附近的人。
Redis 5.0 增加了一个数据结构 Stream, 它是一个新的强大的支持多播的可持久化消息队列。作者坦言极大的借鉴了 Kafka 的设计。
Redis 指令:
xadd:
xdel:
xrange:
xlen:
del:
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 删除。