一:需求背景
产品签到活动需求,对于日活量不高的程序,可以直接使用MySQL去处理,当然数据量不大的话,签到一次,数据库签到表保存一条记录,也是可以的。但是如果类似京东等商城,那种签到活动,日活好几亿,并且并发也很高,这样不断疯狂进行数据库读写操作,MySQL压力是很大的。
那怎么解决呢?
二:解决方案
可以利用MySQL,定义一个int类型的字段,4byte=32位,存储一个月的数据,每次签到,改变这个字段,程序代码进行二进制逻辑预算,数据库该值存储十进制,优化成一行数据存一个人一个月的签到记录。一般商城促销活动签到,一个月一个周期已经可以了。
三:代码实现
代码实现前,先来复习一下逻辑运算和签到逻辑:
1.数据库保存签到记录,int类型,4 byte = 32位可以存储一周或者一个月数据
周1:0000 0001 == 1
周2:0000 0010 == 2
周3:0000 0100 == 4
周4:0000 1000 == 8
周5:0001 0000 == 16
周6:0010 0000 == 32
周7:0100 0000 == 64
2.异或 ^ 、与 & 、或 | 逻辑运算:
异或 ^:两者不一样,eg 1^0,0^1 得出结果就是 1,其他1^1,0^0都是0
与 &: 只有两者都是是 1 ,eg 1&1 得出结果就是 1,其他都是0
或 |: 两者有一个是 1 ,eg 1^0,0^1,1^1 得出结果就是 1,其他都是0
所以根据上面分析,可以得出以下模拟每天签到的案例(ps:<< 表示左移):
第一天签到:
1 << 0 == 0000 0001 这时数据库存的是 1
第二天签到:
1 << 1 == 0000 0010,此时数据保存前一天签到 0000 0001,所以需要两个数据合成一个,可以用 异或 ^ 或者 或 |
0000 0010 | 0000 0001 = 0000 0011 或者 0000 0001 ^ 0000 0010 = 0000 0011
这时数据库存的是 0000 0011 = 3
第三天签到:
1 << 2 == 0000 0100, 数据库存的是 0000 0011
0000 0100 | 0000 0011 = 0000 0111 或者 0000 0100 ^ 0000 0011 = 0000 0111
这时数据库存的是 0000 0111 = 7
第四天没有签到
第五天签到:
1 << 4 == 0001 0000, 数据库存的是 0000 0111
0001 0000 | 0000 0111 = 0001 0111 或者 0001 0000 ^ 0000 0111 = 0001 0111
........
业务层代码实现,为了方便,代码模拟一周为一个周期进行签到:
/**
* 签到并领取奖励
* @param uid
* @param day 第几天
* @return 签到成功返回奖励
*/
public Integer rewardReceiveByDB(String uid, int day) {
// 查询当前一周,该用户是否签到记录
LocalDate now = LocalDate.now();
LocalDateTime startTime = LocalDateTime.of(now.with(DayOfWeek.MONDAY), LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(now.with(DayOfWeek.SUNDAY), LocalTime.MAX);
// 查询是否有本周签到记录,一周只有一条记录
QueryWrapper<SignLog> wrapper = new QueryWrapper<>();
wrapper.eq("uid", uid)
.ge("ctime", startTime)
.le("ctime", endTime);
SignLog signLog = signLogService.getOne(wrapper);
if (signLog == null){
signLog = new SignLog();
signLog.setUid(uid);
signLog.setFlag(0);
}
/*
1 << 2 == 0000 0100, 数据库存的是 0000 0011
0000 0100 | 0000 0011 = 0000 0111 或者 0000 0100 ^ 0000 0011 = 0000 0111
*/
int signFlag = signLog.getFlag();
// 二进制下标 0开始
signFlag = signFlag | (1 << (day - 1));
signLog.setFlag(signFlag);
signLogService.saveOrUpdate(signLog);
// Integer.bitCount直接统计,签到了几天,发奖励
long signInDays = Integer.bitCount(signFlag);
// todo 根据业务需求,发送奖励
return rewardMap.get(signInDays);
}
/**
* 查看用户在某一天是否签到了
* @param uid
* @param day 第几天
* @return
*/
@Override
public boolean getSignStatus(String uid, int day) {
/**
* 判断某一天 day 该用户是否签到
* 可用 某一天 day 签到的 二进制值 跟 当前数据的值 进行 与& 操作,也就是二进制位只有 day 位置是 1,其他都是0,
* 如果跟它进行 与& 操作 如果数据库值当天签到 就会 等于 1
*/
LocalDate now = LocalDate.now();
LocalDateTime startTime = LocalDateTime.of(now.with(DayOfWeek.MONDAY), LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(now.with(DayOfWeek.SUNDAY), LocalTime.MAX);
// 查询是否有本周签到记录,一周只有一条记录
QueryWrapper<SignLog> wrapper = new QueryWrapper<>();
wrapper.eq("uid", uid)
.ge("ctime", startTime)
.le("ctime", endTime);
SignLog signLog = signLogService.getOne(wrapper);
if (signLog == null){
return false;
}
Integer flag = signLog.getFlag();
// 当天签到的二进制值
int signFlag = 1 << (day - 1);
return (flag & signFlag) == 1;
}
四:总结
个人觉得,具体实现根据公司的业务体量来决定。但是条件允许的话,确实可以用位存储来实现。其实redis的一个数据类型bitmap也是根据位来实现。方便一点的,可以知己义工redis的bitmap来实现,这样完全就不用经过数据库,但是如果需要归因的话,数据库保存发送签到奖励的记录即可。