背景
接口实现思路:
- 通过 userId 和当前年份从 Redis 中获取对应的 Bitmap
- 获取当前年份的总天数
- 循环天数拼接日期,根据日期去 Bitmap 中判断是否有签到记录,并记录到数组中
- 最后,将拼接好的、一年的签到记录返回给前端
@Override
public Map<LocalDate, Boolean> getUserSignInRecord(long userId, Integer year) {
if (year == null) {
LocalDate date = LocalDate.now();
year = date.getYear();
}
String key = RedisConstant.getUserSignInRedisKey(year, userId);
RBitSet signInBitSet = redissonClient.getBitSet(key);
// LinkedHashMap 保证有序
Map<LocalDate, Boolean> result = new LinkedHashMap<>();
// 获取当前年份的总天数
int totalDays = Year.of(year).length();
// 依次获取每一天的签到状态
for (int dayOfYear = 1; dayOfYear <= totalDays; dayOfYear++) {
// 获取 key:当前日期
LocalDate currentDate = LocalDate.ofYearDay(year, dayOfYear);
// 获取 value:当天是否有刷题
boolean hasRecord = signInBitSet.get(dayOfYear);
// 将结果放入 map
result.put(currentDate, hasRecord);
}
return result;
}
性能优化
思路一、判断每天是否刷题逻辑优化
循环内部需要判断当天是否有刷题,这种写法效率非常低,因为需要循环365次,每次循环都需要和 Redis 交互。
具体来说,signInBitSet 通过 Redisson 客户端与 Redis 交互的 RBitSet 对象,但是 RBitSet.get() 方法会触发一次 Redis 请求来获取对应位的值。
解决办法:在循环外缓存一下 Bitmap 中的数据
// 加载 BitSet 到内存中,避免后续读取时发送多次请求
BitSet bitSet = signInBitSet.asBitSet();
之后在循环内使用 bitSet.get即可:
// 获取 value:当天是否有刷题
boolean hasRecord = bitSet.get(dayOfYear);
这样一次连接就获取全部 Redis 的数据,之后再在缓存中找就快一些。
思路二、刷题记录返回值优化
我们最开始用一个 Map 存储记录,但是我们不需要获取完全组组装好的数据,这样传输的数据多、计算时间耗时、带宽占用多、效率低。我们只需要告诉用户哪天有刷题就行。
@Override
public List<Integer> getUserSignInRecord(long userId, Integer year) {
if (year == null) {
LocalDate date = LocalDate.now();
year = date.getYear();
}
String key = RedisConstant.getUserSignInRedisKey(year, userId);
RBitSet signInBitSet = redissonClient.getBitSet(key);
// 加载 BitSet 到内存中,避免后续读取时发送多次请求
BitSet bitSet = signInBitSet.asBitSet();
// 统计签到的日期
List<Integer> dayList = new ArrayList<>();
// 获取当前年份的总天数
int totalDays = Year.of(year).length();
// 依次获取每一天的签到状态
for (int dayOfYear = 1; dayOfYear <= totalDays; dayOfYear++) {
// 获取 value:当天是否有刷题
boolean hasRecord = bitSet.get(dayOfYear);
if (hasRecord) {
dayList.add(dayOfYear);
}
}
return dayList;
}
将 Map 换为 List 数据结构,只存储刷题的记录。
思路三、计算优化
一般遇到 循环 要注意,因为循环需要消耗 CPU 计算资源。
在 java 的 BitSet 类中,可以使用 nextSetBit(int fromIndex) 和nextClearBit(int fromIndex) 方法来获取从指定索引开始的下一个 已设置(即为1)或未设置(即为0)。
使用 nextSetBit ,可以跳过无意义的循环检测,通过 位运算来获取值设置为 1 的位置。
@Override
public List<Integer> getUserSignInRecord(long userId, Integer year) {
if (year == null) {
LocalDate date = LocalDate.now();
year = date.getYear();
}
String key = RedisConstant.getUserSignInRedisKey(year, userId);
RBitSet signInBitSet = redissonClient.getBitSet(key);
// 加载 BitSet 到内存中,避免后续读取时发送多次请求
BitSet bitSet = signInBitSet.asBitSet();
// 统计签到的日期
List<Integer> dayList = new ArrayList<>();
// 从索引 0 开始查找下一个被设置为 1 的位
int index = bitSet.nextSetBit(0);
while (index >= 0) {
dayList.add(index);
// 查找下一个被设置为 1 的位
index = bitSet.nextSetBit(index + 1);
}
return dayList;
}
总结
接口性能优化思路:
- 减少网络请求或调用次数。
- 减少接口传输数据的体积
- 减少循环和计算
- 通过客户端计算减少服务的压力