Redis - bitmap 使用案例
- 【基本操作】
- setbit KEY OFFSET VALUE
举例 : setbit month_au_202106 20210601 '1',这里需要注意的一点是offset是按照比特位设置的,与bitcount
- getbit KEY OFFSET
举例:getbit month_au_202106 20210601
- bitcount KEY
举例 :bitcount month_au_202106
- bitcount KEY START END
举例 : bitcount month_au_202106 20210601 20210602
这里要补充说明的一点是,Redis的setbit修改的是bit位置,而bitcount检查的是byte位置,1byte=8bit,因此在实际使用中需要统一offset的使用,需要再setbit时设置offset * 8,后面案例实现中注意
- 【案例应用】
- 统计用户活跃状态
- 如果允许有误差,可以考虑HyperLogLog类型而不使用BitMap
- 需要考虑以下内容 key、offset的设定,key的存活时间
场景 | key定义 | offset 计算 |
按年或月统计每个用户活跃天数 | AU_userid_year AU_userid_yearMonth AU_userid | DateTime.dayOfYear() DateTime.dayOfMonth() Days.daysBetween(BASE_DATE,time).getDays() |
按年或月统计用户活跃量 | AU_year AU_yearMonth AU_userid | calOffset(userId) 自定义一个合适的offset |
这里举例:key 设定格式为 month_au_userId_yearMonth ,offset = (当前时间 - 基准时间)的天数*8,代码如下:
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeFieldType;
import org.joda.time.DateTimeZone;
import org.joda.time.Days;
/**
* redis bitmap 统计用户月活样例代码
*/
public class BitMapMark {
/** 标记常量 */
private static final String ACTIVE_MARK = "1";
/** 基准日期 */
//private static final DateTime BASE_DATE;
/** 1字节=8bit */
private static final int BIT_LEN = 8;
/** 失效时间 */
int ACTIVE_USER_MONTH_COUNT_EXPIRE = 31*24*3600;
/**
* 如果key不按时间进行区分,在计算偏移值时可以指定一个起始时间,用于计算offset
* 如果key中不考虑用户信息 则可以考虑使用用户ID(如果是数字的话)经过一定运算后用于offset
static {
//基准时间20210601
BASE_DATE =
new DateTime()
.withZone(DateTimeZone.UTC)
.withYear(2021).withMonthOfYear(6).withDayOfMonth(1).withMillisOfDay(0);
}*/
/**
* 根据用户ID 标识当天状态
* @param userId
* @return
*/
public void incryDailyActive(String userId) {
Long result = 0L;
RedisConnect redisConnection = null;
try {
if(StringUtils.isNumeric(userId)){
// TODO conn = 获取redis链接
DateTime currTime = new DateTime();
//根据当前时间计算偏移量
//当前时间 - 基准时间(取当前时间较近的日期)
//计算偏移
// 1.是避免浪费空间,
// 2.offset * BIT_LEN 是为了在使用bitcount 按范围查询是能够正确对应
Long timeOffset = getDayOffset(currTime) * BIT_LEN;
//获取KEY值 "MAU_userID_yearMon"
String monthKey = getActiveUserMonthKey(userId,currTime);
redisConnection.setbit(monthKey,timeOffset,ACTIVE_MARK);
//设置失效时间 根据需要设置过期
//redisConnection.expireKey(monthKey,ACTIVE_USER_MONTH_COUNT_EXPIRE);
}
} catch(Exception e){
e.printStackTrace();
} finally {
if(null!=redisConnection){
//TODO 关闭链接
}
}
}
/**
* 查询当月累计月活
* @param userId
* @return
*/
public Long getCurrMonthActive(String userId) {
Long monthAuCount = 0L;
try {
DateTime curr = new DateTime();
//计算查询日期范围
Long end = calcOffset(curr);
curr = curr.withField(DateTimeFieldType.dayOfMonth(),1);
Long start = calcOffset(curr);
//TODO conn = getRedisConnection(); 获取redis链接
String monthKey = getActiveUserMonthKey(userId,curr);
monthAuCount = redisConnection.bitcountRange(monthKey,start,end);
if(null == monthAuCount){
monthAuCount = 0L;
}
} catch(Exception e){
e.printStackTrace();
} finally {
if(null!=redisConnection){
//TODO 关闭链接
}
}
return monthAuCount;
}
/**
* 获取单个用户月活统计key样例
* 指定用户指定月份的日活统计
* 格式:month_au_userId_yearMonth eg: month_au_100_202106
* @param userId
* @param curr
* @return
*/
private String getActiveUserMonthKey(String userId,DateTime curr) {
Integer yearMonth = curr.getYear()*100+curr.getMonthOfYear();
return String.format("month_au_%s_%d",userId,yearMonth);
}
/**
* 根据当前时间计算offset样例
* 要和上面的key 进行对应得到一个合适的偏移
* 避免空间浪费
* @param time
* @return
*/
private static Long calcOffset(DateTime time){
return time.getDayOfMonth();
//return Days.daysBetween(BASE_DATE,time).getDays()*1L;
}
}