本文参考原文
Redis中的BitMap同我们所使用的算法当中的bitmap是相同的道理。简单来说就是使用二进制位,每一位的0/1状态表示信息。
二进制表示的优势:
大大节省空间。如两位二进制位00、01、10、11可以表示四种状态
Redis当中提供了bitmap的数据结构,并有相关的操作命令。通过这命令的搭配,可以实现不同的逻辑应用。
Redis提供了SETBIT、GETBIT、BITCOUNT、BITOP四个常用命令用于处理二进制位数组。
SETBIT:为位数组指定偏移量上的二进制位设置值,偏移量从0开始计数,二进制位的值只能为0或1。返回原位置值。 GETBIT:获取指定偏移量上二进制位的值。 BITCOUNT:统计位数组中值为1的二进制位数量。 BITOP:对多个位数组进行按位与、或、异或、取反运算(对应具体的操作为:ADD、OR、XOR,还有NOT)。
具体例子如下:
127.0.0.1:6379> SETBIT first 0 1 # 0000 0001
(integer) 0
127.0.0.1:6379> SETBIT first 3 1 # 0000 1001
(integer) 0
127.0.0.1:6379> SETBIT first 0 0 # 0000 1000
(integer) 1
127.0.0.1:6379> GETBIT first 0
(integer) 0
127.0.0.1:6379> GETBIT first 3
(integer) 1
127.0.0.1:6379> BITCOUNT first # 0000 1000
(integer) 1
127.0.0.1:6379> SETBIT first 0 1 # 0000 1001
(integer) 0
127.0.0.1:6379> BITCOUNT first # 0000 1001
(integer) 2
127.0.0.1:6379> SETBIT first 1 1 # 0000 1011
(integer) 0
127.0.0.1:6379> BITCOUNT first # 0000 1011
(integer) 3
127.0.0.1:6379> SETBIT x 3 1
(integer) 0
127.0.0.1:6379> SETBIT x 1 1
(integer) 0
127.0.0.1:6379> SETBIT x 0 1 # 0000 1011
(integer) 0
127.0.0.1:6379> SETBIT y 2 1
(integer) 0
127.0.0.1:6379> SETBIT y 1 1 # 0000 0110
(integer) 0
127.0.0.1:6379> SETBIT z 2 1
(integer) 0
127.0.0.1:6379> SETBIT z 0 1 # 0000 0101
(integer) 0
127.0.0.1:6379> BITOP AND andRes x y z #0000 0000
(integer) 1
127.0.0.1:6379> BITOP OR orRes x y z #0000 1111
(integer) 1
127.0.0.1:6379> BITOP XOR x y z #0000 1000
(integer) 1
# 对给定的位数组进行按位取反
127.0.0.1:6379> SETBIT value 0 1
(integer) 0
127.0.0.1:6379> SETBIT value 3 1 #0000 1001
(integer) 0
127.0.0.1:6379> BITOP NOT notValue value #1111 0110
(integer) 1
具体每种命令的详解可见参考原文。
功能分析:
GitHub当中的打卡签到为一年,每天提交的次数决定了当前天签到颜色的深浅。
向bitmap的结构中思考。
1、我们对于每个用户,都有一个打卡表,所以可以将用户的唯一标识作为bitmap的key,当前是哪一天作为bitmap的offset,提交次数作为bit。
2、按照以上思考的大概是没问题的,但是我们的提交次数肯定不能只限定在0次或1次。
3、所以解决此问题的方法,就是我们多使用几位bit来表示提交次数,并且,因为多使用了几位bit,需要我们在设置offset也就是日期,做相应的逻辑转换。
整体的逻辑为:set userID Date times
使用一个字节(8bit)表示提交次数,所以offset(date)每次移动8位
如:0000 0011 0000 1011,后八位表示一天(如11月26日),提交次数为11次,前八位表示相邻一天(11月27日)提交次数3次
代码实现:
public void genTestData() {
if(redisUtils.isExist(CommonConstant.KEY)){
return;
}
// 获取当前年的总天数
int days = getDays();
for (int i = 0; i < days; i++) {
int random = ThreadLocalRandom.current().nextInt(64);
// 生成随机数表示每天的PR次数
String binaryString = Integer.toBinaryString(random);
if (binaryString.length() < 8) {
// 填充0
if(binaryString.length() == 0){binaryString = "00000000";}
else if(binaryString.length() == 1){binaryString = "0000000"+binaryString;}
else if(binaryString.length() == 2){binaryString = "000000"+binaryString;}
else if(binaryString.length() == 3){binaryString = "00000"+binaryString;}
else if(binaryString.length() == 4){binaryString = "0000"+binaryString;}
else if(binaryString.length() == 5){binaryString = "000"+binaryString;}
else if(binaryString.length() == 6){binaryString = "00"+binaryString;}
else if(binaryString.length() == 7){binaryString = "0"+binaryString;}
}
char[] chars = binaryString.toCharArray();
for (int j = 0; j < chars.length; j++) {
// 设置BitMap
redisUtils.setBit(CommonConstant.KEY,i*8+j,chars[j]);
}
}
}
/**
* 获取当前年的总天数
* @return days 总天数
*/
private int getDays(){
Calendar calOne = Calendar.getInstance();
int year = calOne.get(Calendar.YEAR);
System.out.println(year);
Calendar calTwo = new GregorianCalendar(year, 11, 31);
return calTwo.get(Calendar.DAY_OF_YEAR);
}
public List<String> getPushData() {
List<String> res = new ArrayList<>(366);
// 没有数据就先造数据
genTestData();
int days = getDays();
for(long i=0;i<days;i++){
StringBuilder sb = new StringBuilder();
for (int j = 0; j < 8; j++) {
String bit = redisUtils.getBit(CommonConstant.KEY, i * 8 + j);
sb.append(bit);
}
// 直接返回二进制串,前端转换为十进制
res.add(sb.toString());
}
return res;
}
<script type="text/javascript">
var chartDom = document.getElementById('main');
var myChart = echarts.init(chartDom);
var option;
function getVirtulData(year) {
var date = +echarts.number.parseDate(year + '-01-01');
var end = +echarts.number.parseDate(+year + 1 + '-01-01');
var dayTime = 3600 * 24 * 1000;
var data = [];
$.ajax({
"url":'http://localhost:8080/test/getPushData',
"async":false, // ajax同步获取
success:function (res){
for (let time = date,k=0; time < end && k < res.data.length; time += dayTime,k++) {
data.push([
echarts.format.formatTime('yyyy-MM-dd', time),
parseInt(res.data[k],2)//客户端完成进制转换,不放在服务端完成
]);
}
}
})
return data;
}
option = {
title: {
top: 30,
left: 'left',
text: 'BitMap Demo'
},
tooltip: {},
visualMap: {
min: 0,
max: 32,
type: 'piecewise',
orient: 'horizontal',
left: 'right',
top: 220,
pieces: [
{min: 0, max: 0,label:"less"},
{min: 1, max: 10,label:" "},
{min: 1, max: 20,label:" "},
{min: 21, max: 40,label:" "},
{min: 41, max: 64,label:"more"},
],
inRange: {
color: [ '#EAEDF0', '#9AE9A8', '#41C363', '#31A14E', '#206D38' ],//颜色设置
colorAlpha: 0.9,//透明度
}
},
calendar: {
top: 120,
left: 30,
right: 30,
cellSize: 13,
range: '2022',
splitLine: { show: false },//不展示边线
itemStyle: {
borderWidth: 0.5
},
yearLabel: { show: false }
},
series: {
type: 'heatmap',
coordinateSystem: 'calendar',
data: getVirtulData('2022')
}
};
option && myChart.setOption(option);
</script>