Redis + son 意思很明显,自诩 Redis 的儿子,不过也确实好用
这篇文章来介绍下可能不那么常见的,但是却非常好用的 3 个东西:BitSet/Bloomfilter/HperLogLog
BitMap 可以用作签到、统计、用户状态等的处理
BitMap
做And
和OR
等操作除了最后一个功能,Set
无法完全支持,其他都是可以支持的。那么为什么还需要BitSet
了?原因是空间占用。
BitSet 不是 Redis 中的数据结构,其本质是String 的内部数据结构 Bit。我们可以理解为:一连串 Bit构成了String,每一个 Bit只能存储 0 和 1,同时有自己的偏移量,而Redis 可以根据偏移量对任何一个Bit进行 位操作
。
Redis中 String 最大可以达到512M
,根据这个公式:($offset/8/1024/1024)MB
可以算出,offset最大为4,294,967,296
,也就是2的32次方
,所以 offset也不能大于这个值。
场景 1:会议签到
会议key
为id
,offset
是用户的 id
(不能大于 2^32次方,建议自增的 id)
下面我们通过 代码来了解下这 3 个作用,在此之前我们先来创建一个redisson
对象
companion object {
private val config = Config().apply {
this.useSingleServer()
.setAddress("redis://192.168.0.88:6379")
.setDatabase(9)
.setPassword("23457687")
this.codec = StringCodec()
}
val redisson: RedissonClient = Redisson.create(config)
}
下面的直接上代码,细节都在注释里面
@Test
fun signInTest() {
//创建 bitset 的时候,通过名字来确定`key`
val bitSet = redisson.getBitSet("sign-0927")
//其中 998 23 为模拟的用户 id
bitSet.set(998)
bitSet.set(23)
bitSet.set(98432)
//获取签到的人数
val cardinality = bitSet.cardinality()
println("签到人数:$cardinality")
//判定某个具体的用户是否签到了
println("98432签到结果为 ${bitSet.get(98432)}")
println("123签到结果为 ${bitSet.get(123)}")
}
其结果如下所示。
签到人数:3
98432签到结果为 true
123签到结果为 false
场景 2:APP 的消息提醒
假如 APP
有消息提示框,用户有消息的时候,需要进行标记提醒。此时就可以使用BitSet
直接存储有消息的标志,待用户点击消息标志的时候,才在数据库查询具体的消息,这样很节省资源。
假如某个消息提示框为notice:1
,那么可以通过这个key
创建一个 bitset
,某用户有消息的时候,用户的ID
作为offset
并设置为 1
,用户点击了消息提示框后,再通过这个ID
的offset
将其设置为0
场景 3:BitMap的与运算和或运算
待补充。。。
最佳实践:偏移量不能一个小,一个大,这样非常耗费性能。举个例子,你一上来就给便宜量设置为最大值,那么 521M的内存就直接占用了。所以更推荐通过自增的 ID来完成这个操作。
这个就是鼎鼎大名的布隆过滤器,可以在数据量很大的时候,在一定偏差下,判定准确的不是或者不准确的是。
我们简单通过原理来理解下这个是或者不是的问题:将一个数通过多次 hash 处理后,算出一个偏移量,在一个数组上将对应的 index设置为 1,这个就是其原理。
接下来我们通过例子来判定学习下这个布隆过滤器用法。
例子:判定是否是新用户
假如业务要求:某个活动只能新用户能参与,此时你想怎么实现?
其中一个方法是采取上述提到的BitSet
:参与过的用户的ID
对应的偏移量设置为1
。但是BitSet
有限制,offset
存在一个最大值限制,假如用户的 ID
是通过SnowFlake
生成的,那么就不能直接使用了。
此时可以考虑使用BloomFilter
,来精准判定某个用户不是新用户,当是新用户的时候,再辅以其他手段来判定。
@Test
fun testBloomFilter() {
val bloomFilter = redisson.getBloomFilter<Long>("new-user").apply {
//初始化:会存储的数据容量(4294967294),期待的错误率
this.tryInit(1000000,0.03)
}
val id1 = 1156040301344284674
val id2 = 1156040301344284675
val id3 = 1156040301344284676
val id4 = 1156040301407199234
bloomFilter.add(id1)
bloomFilter.add(id2)
bloomFilter.add(id3)
assert(bloomFilter.contains(id1))
assert(bloomFilter.contains(id2))
assert(bloomFilter.contains(id3))
println(bloomFilter.contains(id4))
}
上述就是一个简单的使用bloomfilter
的例子,注意几点
4294967294
缺点
同 BitSet 对比
当进行统计的时候,小数量可以采用Set,没有超过2^32
的数据时候可以使用Bitset
,那么当数据超级大的时候,如何进行统计?
此时就需要这个HyperLogLog
了,这个数据结构的原理很复杂,这文章只讲其使用场景、特点。
使用场景:大流量网站的 UV 统计。
特点:存在一定偏差,不过再超大流量下,这个完全可以忽略不计了。
@Test
fun testHyperLogLog() {
val hyperLogLog = redisson.getHyperLogLog<String>("uv-20200911")
hyperLogLog.add("1156040301344284674")
hyperLogLog.add("1156040301344284675")
val uv = hyperLogLog.count()
println(uv)
}
数据统计:Set -> BitSet -> HyperLogLog
是与否判定:Set -> BitSet ->BloomFilter