文章目录
1. Bitmaps
现代计算机用二进制(位)作为信息的基础单位,1个字节等于8位,例如“big”字符串是由3个字节组成,但实际在计算机存储时将其用二进制表示,“big”分别对应的ASCII码分别是66、73、71,对应的二进制分别是100 0010、100 1001和 100 0111。
Bitmaps单独提供了一套命令,所以在Redis中使用Bitmaps和使用字符串的方法不太相同。可以把 Bitmaps想象成一个以位为单位的数组,数组的每个单元只能存储0和1,数组的下标在 Bitmaps 中叫做偏移量。
1.1 操作命令
1.1.1 setbit 设置值
命令用于对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit)。
1.1.2 getbit 获取值
命令用于对 key 所储存的字符串值,获取指定偏移量上的位(bit)。
1.1.3 bitcount 获取 key 的值中二进制是 1 的个数
1.1.4 bitop
BITOP operation resultKey key1 key2。operation 是位运算的操作,有 AND,OR,XOR,NOT。resultKey 是把运算结构存储在这个 key 中,key1 和 key2 是参与运算的 key,参与运算的 key 可以指定多个。
1.1.5 bitpos
bittops key bit [start] [end]
返回位第一个值为bit的二进制位的位置
1.2 Bitmaps的优缺点
- 效率高,不允许运算和比较,占用内存少。
- 不能对重复的数据进行排序和查找。
1.3 布隆过滤器
布隆过滤器的本质是一种数据结构,是一个概率型数据结构。特点是能够高速的插入与查询。相比于传统的Redis数据结构更加高效,占用内存更少。但是缺点是返回的结果具有概率性。
常用的应用:
- Redis通过布隆过滤器来预防缓存击穿,可以在访问Redis之前使用布隆过滤器来对请求的key进行过滤, 可以大大减少那些恶意攻击。
- 黑白名单功能,可以把黑名单放入布隆过滤器,对内容进行过滤。
1.4 SpringBoot 使用布隆过滤器
- 引入依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.6.3</version>
</dependency>
<!--引入Redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
<version>1.4.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.3</version>
</dependency>
- 添加配置
@Configuration
@PropertySource("classpath:application.properties")
public class RedisConfig {
@Value("${redis.host}")
private String host;
@Value("${redis.port}")
private int port;
@Value("${redis.password}")
private String passWord;
@Value("${redis.timeout}")
private int timeout;
@Value("${redis.maxIdle}")
private int maxIdle;
@Value("${redis.maxWaitMillis}")
private int maxWaitMillis;
@Value("${redis.blockWhenExhausted}")
private Boolean blockWhenExhausted;
@Value("${redis.JmxEnabled}")
private Boolean JmxEnabled;
@Bean
public JedisPool jedisPoolFactory() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
// 连接耗尽时是否阻塞, false报异常,true阻塞直到超时, 默认true
jedisPoolConfig.setBlockWhenExhausted(blockWhenExhausted);
// 是否启用pool的jmx管理功能, 默认true
jedisPoolConfig.setJmxEnabled(JmxEnabled);
JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout,passWord);
return jedisPool;
}
}
- 布隆过滤器工具类
/*仿Google的布隆过滤器实现,基于redis支持分布式*/
public class RedisBloomFilter {
public final static String RS_BF_NS = "rbf:";
private int numApproxElements; /*预估元素数量*/
private double fpp; /*可接受的最大误差*/
private int numHashFunctions; /*自动计算的hash函数个数*/
private int bitmapLength; /*自动计算的最优Bitmap长度*/
@Autowired
private JedisPool jedisPool;
/**
* 构造布隆过滤器
* @param numApproxElements 预估元素数量
* @param fpp 可接受的最大误差
* @return
*/
public RedisBloomFilter init(int numApproxElements,double fpp){
this.numApproxElements = numApproxElements;
this.fpp = fpp;
/*位数组的长度*/
this.bitmapLength = (int) (-numApproxElements*Math.log(fpp)/(Math.log(2)*Math.log(2)));
//this.bitmapLength=128;
/*算hash函数个数*/
this.numHashFunctions = Math.max(1, (int) Math.round((double) bitmapLength / numApproxElements * Math.log(2)));
//this.numHashFunctions=2;
return this;
}
/**
* 计算一个元素值哈希后映射到Bitmap的哪些bit上
* 用两个hash函数来模拟多个hash函数的情况
* * @param element 元素值
* @return bit下标的数组
*/
private long[] getBitIndices(String element){
long[] indices = new long[numHashFunctions];
/*会把传入的字符串转为一个128位的hash值,并且转化为一个byte数组*/
byte[] bytes = Hashing.murmur3_128().
hashObject(element, Funnels.stringFunnel(Charset.forName("UTF-8"))).
asBytes();
long hash1 = Longs.fromBytes(bytes[7],bytes[6],bytes[5],bytes[4],bytes[3],bytes[2],bytes[1],bytes[0]);
long hash2 = Longs.fromBytes(bytes[15],bytes[14],bytes[13],bytes[12],bytes[11],bytes[10],bytes[9],bytes[8]);
/*用这两个hash值来模拟多个函数产生的值*/
long combinedHash = hash1;
for(int i=0;i<numHashFunctions;i++){
//数组下标
indices[i]=(combinedHash&Long.MAX_VALUE) % bitmapLength;
combinedHash = combinedHash + hash2;
}
System.out.print(element+"数组下标");
for(long index:indices){
System.out.print(index+",");
}
System.out.println(" ");
return indices;
}
/**
* 插入元素
*
* @param key 原始Redis键,会自动加上前缀
* @param element 元素值,字符串类型
* @param expireSec 过期时间(秒)
*/
public void insert(String key, String element, int expireSec) {
if (key == null || element == null) {
throw new RuntimeException("键值均不能为空");
}
String actualKey = RS_BF_NS.concat(key);
try (Jedis jedis = jedisPool.getResource()) {
try (Pipeline pipeline = jedis.pipelined()) {
for (long index : getBitIndices(element)) {
pipeline.setbit(actualKey, index, true);
}
pipeline.syncAndReturnAll();
} catch (Exception ex) {
ex.printStackTrace();
}
jedis.expire(actualKey, expireSec);
}
}
/**
* 检查元素在集合中是否(可能)存在
*
* @param key 原始Redis键,会自动加上前缀
* @param element 元素值,字符串类型
*/
public boolean mayExist(String key, String element) {
if (key == null || element == null) {
throw new RuntimeException("键值均不能为空");
}
String actualKey = RS_BF_NS.concat(key);
boolean result = false;
try (Jedis jedis = jedisPool.getResource()) {
try (Pipeline pipeline = jedis.pipelined()) {
for (long index : getBitIndices(element)) {
pipeline.getbit(actualKey, index);
}
result = !pipeline.syncAndReturnAll().contains(false);
} catch (Exception ex) {
ex.printStackTrace();
}
}
return result;
}
@Override
public String toString() {
return "RedisBloomFilter{" +
"numApproxElements=" + numApproxElements +
", fpp=" + fpp +
", numHashFunctions=" + numHashFunctions +
", bitmapLength=" + bitmapLength +
'}';
}
}
- 使用
@Autowired
private RedisBloomFilter redisBloomFilter;
//时效一天
private static final int DAY_SEC = 60 * 60 * 24;
@Test
public void testInsert() throws Exception {
redisBloomFilter.insert("bloom:name", "ybh", DAY_SEC);
}
@Test
public void testMayExist() throws Exception {
System.out.println(redisBloomFilter.mayExist("bloom:name", "杨炳浩"));
}
2. HyperLogLog
Redis 在 2.8.9 版本添加了 HyperLogLog 结构(简介HLL),用于做基数统计,其使用算法HyperLogLog使得在数量级特别大的情况下占用空间很小。说白了就是在大数据量级的情况下能够在很小的空间中进行元素去重统计。如果使用我们平常的数据结构比如set,HashMap,等,虽然也可以实现去重统计的工作,但是当数据量上升到一定级别之后,其占用的空间也是非常的大。
需要注意的是HyperLogLog算法的去重计数方案并不精确,当然不是特别不精确,标准误差只有0.81%
当然HyperLogLog虽说占据空间小,但也不是不占空间,它需要占据一定12k存储空间,所以如果我们的统计量可能比较小,使用HyperLogLog可能就是大材小用了,但是如果百万级、千万级,那节省的空间就大的大了去了。
2.1 使用场景
- 统计网页日活UV
2.2 操作命令
-
PFADD: 添加一个或者多个元素。PFADD key1 v1 v2 v3.O(1).
-
PFCOUNT: 返回不重复的元素的个数,可以统计多个 key. 同时,返回值是有一定 (0.81%) 错误率的近似值。PFCOUNT key1 key2 key3.O(n).
-
PFMERGE: 将多个 key 的内容合并到一个 key 中。PFMERGE target key1 key2.O(n),n 是 key 的数量。
2.3 原理概述
基本原理:HyperLogLog基于概率论中伯努利试验并结合了极大似然估算方法,并做了分桶优化。
2.3.1 伯努利试验
举个例子,玩一个抛硬币的游戏,每抛一次硬币有可能是正面有可能是反面,正面代表1反面代表0,每回合一直抛一直抛,抛到正面游戏结束。这回合我抛了7次抛到了正面,请问我抛了多少次才能做到?
进行了N次实验,如图:
第一次:抛了三次才出现正面,此时k=3
第二次:抛了两次才出现正面,此时k=2
第三次:抛了四次才出现正面,此时k=4
…
第N次,抛了N次才出现正面,此时k=7
k是每回合抛到1所用的次数,我们已知的是最大的k值,可以用kmax表示。由于每次抛硬币的结果只有0和1两种情况,因此,能够推测出kmax在任意回合出现的概率 ,并由kmax结合极大似然估算的方法推测出n的次数n =2^(k_max) 。概率学把这种问题叫做伯努利实验。
但是这是一种概率型实验,如果你第一次就正常了概率不能说是100%,第二次成功了不能说是50%,得有一个平均值的概念。所以这种预估方法存在较大误差,为了改善误差情况,HLL中引入分桶平均的概念。
2.3.2 分桶平均
分桶平均的基本原理是将统计数据划分为m个桶,每个桶分别统计各自的kmax,并能得到各自的基数预估值,最终对这些基数预估值求平均得到整体的基数估计值。LLC中使用几何平均数预估整体的基数值,但是当统计数据量较小时误差较大;HLL在LLC基础上做了改进,采用调和平均数过滤掉不健康的统计值。
普通平均数:
假如一个人工资是3k/月,一个人工资是13k/月,那他们的平均工资是:(3000+13000)/2=8000
调和平均数
2/(1/3000 + 1/13000) ≈ 4875
可见调和平均数比平均数的好处就是不容易受到大的数值的影响,比平均数的效果是要更好的。
结合Redis去理解:
- pfadd
当我们执行pfadd操作,add进去的数据会被转化成64个bit的二进制比特串。
0010…0001 64位
然后在Redis中要分到16384个桶中(为什么是这么多桶:第一降低误判,第二,用到了14位二进制:2的14次方=16384)
怎么分?根据得到的比特串的后14位来做判断即可。
根据上述的规则,我们知道这个数据要分到 1号桶,同时从左往右(低位到高位)计算第1个出现的1的位置,这里是第4位,那么就往这个1号桶插入4的数据(转成二进制)
如果有第二个数据来了,按照上述的规则进行计算。
那么问题来了,如果分到桶的数据有重复了(这里比大小,大的替换小的):
规则如下,比大小(比出现位置的大小),比如有个数据是最高位才出现1,那么这个位置算出来就是50,50比4大,则进行替换。1号桶的数据就变成了50(二进制是110010)
所以这里可以看到,每个桶的数据一般情况下6位存储即可。
所以我们这里可以推算一下一个key的HyperLogLog只占据多少的存储。
16384*6 /8/1024=12k。并且这里最多可以存储多少数据,因为是64位吗,所以就是2的64次方的数据,这个存储的数据非常非常大的,一般用户用long来定义,最大值也只有这么多。
- pfcount
进行统计的时候,就是把16384桶,把每个桶的值拿出来,比如取出是 n,那么访问次数就是2的n次方。
然后把每个桶的值做调和平均数,就可以算出一个算法值。
同时,在具体的算法实现上,HLL还有一个分阶段偏差修正算法。
const和m都是Redis里面根据数据做的调和平均数。
3. GEO
Redis 3.2版本提供了GEO(地理信息定位)功能,支持存储地理位置信息用来实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能。
地图元素的位置数据使用二维的经纬度表示,经度范围(-180, 180],纬度范围(-90,
90],纬度正负以赤道为界,北正南负,经度正负以本初子午线(英国格林尼治天文台) 为界,东正西负。
在 Redis 里面,经纬度使用 52 位的整数进行编码,放进了 zset 里面,zset 的 value 是元素的 key,score 是 GeoHash 的 52 位整数值。
3.1操作命令
- geoadd:添加地理位置的坐标。
- geopos:获取地理位置的坐标。
- geodist:计算两个位置之间的距离。
- georadius:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
- georadiusbymember:根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合。
- geohash:返回一个或多个位置对象的 geohash 值。
3.2 工具类
@Component
public class RedisGEO {
public final static String RS_GEO_NS = "rg:";
@Autowired
private JedisPool jedisPool;
/**
*
* @param key
* @param longitude 经度
* @param latitude 纬度
* @param member 成员名
* @return
*/
public Long addLocation(String key, double longitude, double latitude, String member) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
return jedis.geoadd(RS_GEO_NS+key,longitude,latitude,member);
} catch (Exception e) {
return null;
} finally {
jedis.close();
}
}
/**
*
* @param key
* @param memberCoordinateMap 成员为key,经纬度为value的map
* @return
*/
public Long addLocations(String key, Map<String, GeoCoordinate> memberCoordinateMap) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
return jedis.geoadd(RS_GEO_NS+key,memberCoordinateMap);
} catch (Exception e) {
return null;
} finally {
jedis.close();
}
}
public List<GeoRadiusResponse> nearbyMore(String key, String member, double radius,
boolean withDist, boolean isASC) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
GeoRadiusParam geoRadiusParam = new GeoRadiusParam();
if (withDist) geoRadiusParam.withDist();
if(isASC) geoRadiusParam.sortAscending();
else geoRadiusParam.sortDescending();
return jedis.georadiusByMember(RS_GEO_NS+key, member, radius, GeoUnit.KM,geoRadiusParam);
} catch (Exception e) {
return null;
} finally {
jedis.close();
}
}
}