接上一篇 重学redis系列之string,本来这个案例原计划是在上一篇做的,结果由于篇幅问题挪到了本篇介绍,废话少说,开始进入正题。
回忆一下上一篇的需求:需要对我们app的用户做一个活跃度统计,我们app的用户量不算太大也不是很小(3000多w),需求提到需要统计任意时间段内的用户活跃度(一个用户在一定时间段内登录我们app的天数,一定时间段内登录天天数达到xx的用户数等等),要求要快速响应。上一篇我们分析了,通过传统的关系型数据库,例如mysql也可以处理这个需求,但是响应速度可能相对较慢,同时由于数据量较大,传统关系型数据库使用的空间会快速增加,而且随着数据量增加,数据库的响应速度会进一步变慢,因此,传统关系型数据库并不是最优选择。
有了上一节的基础之后,我们知道redis对位图操作的支持,可以精细的操作每一个位,每一个位由0或1表示,刚好我们的需求针对某一个用户来说一天只有登录了(1)和未登录(0)两总情况。一个字节有8位,可以表示一个用户8天是否登录的信息,一年按366天计算也只需要 366 / 8向上取整等于46个字节就可以存储一个用户一年的登录信息。3000W用户一年的登录信息只需要 30000000 * 46 / 1024 / 1024 / 1024 = 1.285G。1.3G空间储存3000w用户一年的登录数据,应该说已经做到了空间利用的最大化。redis储存1.3G数据还是蛮多的,但是我们完全可以通过hash(username)之类的方式把数据分布到不同的redis。数据存完了,统计方便吗?redis提供了大量的对应api来方便我们做一些或、与之类的逻辑运算。因此,类似需求选择redis即可以满足统计的及时性需求,还节省了资源,是类似需求的不二选择。说了这么多,直接上代码,看看怎么实现。
快速搭建一个springboot项目,引入相关jar:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
编写controller,模拟接收前端的用户登录,同时提供统计接口
package com.info.loginstatistics.controller;
import com.info.loginstatistics.dto.ResultCode;
import com.info.loginstatistics.service.StaticsService;
import com.info.loginstatistics.util.RedisUtil;
import org.apache.commons.lang3.time.DateUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.text.ParseException;
import java.util.Date;
@RestController
public class IndexController {
private final StaticsService staticsService;
public IndexController(StaticsService staticsService) {
this.staticsService = staticsService;
}
@GetMapping("/login")
public ResultCode login(HttpServletRequest request) {
String username = request.getParameter("username");
if (StringUtils.isEmpty(username)) {
return ResultCode.fail("登录用户用户名不能为空");
}
String date = request.getParameter("date");
Date loginDate = null;
if (!StringUtils.isEmpty(date)) {
try {
loginDate = DateUtils.parseDate(date, "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd");
} catch (ParseException e) {
e.printStackTrace();
}
}
if (loginDate == null) {
loginDate = new Date();
}
staticsService.login(username, loginDate);
return ResultCode.success();
}
@GetMapping("/login-count")
public ResultCode loginCount(HttpServletRequest request) {
String username = request.getParameter("username");
if (StringUtils.isEmpty(username)) {
return ResultCode.fail("查询的用户名不能为空");
}
String startDate = request.getParameter("startDate");
String endDate = request.getParameter("endDate");
Date end = null;
Date start = null;
if (!StringUtils.isEmpty(startDate) && !StringUtils.isEmpty(endDate)) {
try {
start = DateUtils.parseDate(startDate, "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd");
end = DateUtils.parseDate(endDate, "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd");
} catch (ParseException e) {
e.printStackTrace();
}
}
long count;
if (start != null && end != null) {
count = staticsService.count(username, start, end);
} else {
count = staticsService.count(username);
}
return ResultCode.success(count);
}
@GetMapping("/active-user-count")
public ResultCode activeUserCount(HttpServletRequest request) {
String startDate = request.getParameter("startDate");
String endDate = request.getParameter("endDate");
return ResultCode.success(staticsService.activeUserCount(startDate, endDate));
}
}
servie层实现具体的业务逻辑操作
package com.info.loginstatistics.service.impl;
import com.info.loginstatistics.service.StaticsService;
import com.info.loginstatistics.util.RedisUtil;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
/**
* @date 2020/9/25 14:40
*/
@Service
public class StaticsServiceImpl implements StaticsService {
private static final String REDIS_KEY_PREFIX = "count:";
@Override
public void login(String username, Date date) {
LocalDateTime dateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
int year = dateTime.getYear();
int day = dateTime.getDayOfYear();
String key = REDIS_KEY_PREFIX + username;
RedisUtil.setBit(key, day - 1, true);
String formatedDate = DateFormatUtils.format(date, "yyyy-MM-dd");
// 便于后期统计用户一定时间段内的活跃度
RedisUtil.setBit(REDIS_KEY_PREFIX + formatedDate, username.hashCode(), true);
}
@Override
public long count(String key) {
return RedisUtil.bitCount(REDIS_KEY_PREFIX + key);
}
@Override
public long count(String key, Date startDate, Date endDate) {
LocalDateTime startTime = LocalDateTime.ofInstant(startDate.toInstant(), ZoneId.systemDefault());
LocalDateTime endTime = LocalDateTime.ofInstant(endDate.toInstant(), ZoneId.systemDefault());
long dayOfYearStart = (long) startTime.getDayOfYear();
long dayOfYearEnd = (long) endTime.getDayOfYear();
return RedisUtil.bitCount(key, dayOfYearStart - 1, dayOfYearEnd - 1);
}
@Override
public long count(String[] keys) {
return RedisUtil.OpOrCount(keys);
}
@Override
public long activeUserCount(String startDate, String endDate) {
long count;
if (StringUtils.isEmpty(startDate) && StringUtils.isEmpty(endDate)) {
Set<String> keys = RedisUtil.getKeys(REDIS_KEY_PREFIX + "*");
if (CollectionUtils.isEmpty(keys)) {
return 0L;
}
count = RedisUtil.OpOrCount(keys.toArray(new String[keys.size()]));
} else if (!StringUtils.isEmpty(startDate) && StringUtils.isEmpty(endDate)) {
List<String> dateList = getBetweenDate(startDate, DateFormatUtils.format(new Date(), "yyyy-MM-dd"));
count = RedisUtil.OpAndCount(dateList.toArray(new String[dateList.size()]));
} else if (StringUtils.isEmpty(startDate) && !StringUtils.isEmpty(endDate)) {
List<String> dateList = getBetweenDate(DateFormatUtils.format(getFirstDayOfYear(new Date()), "yyyy-MM-dd"), endDate);
count = RedisUtil.OpAndCount(dateList.toArray(new String[dateList.size()]));
} else {
List<String> dateList = getBetweenDate(startDate, endDate);
count = RedisUtil.OpAndCount(dateList.toArray(new String[dateList.size()]));
}
return count;
}
/**
* 获取一定时间段内每一天的日期
*
* @param start
* @param end
* @return {@link List}
* @date 2020/9/30 11:18
*/
private static List<String> getBetweenDate(String start, String end) {
List<String> list = new ArrayList<>();
LocalDate startDate = LocalDate.parse(start);
LocalDate endDate = LocalDate.parse(end);
long distance = ChronoUnit.DAYS.between(startDate, endDate);
if (distance < 1) {
return list;
}
Stream.iterate(startDate, d -> d.plusDays(1)).limit(distance + 1).forEach(f -> list.add(f.toString()));
return list;
}
/**
* 获取某一年的第一天日期
* @date 2020/9/30 12:31
* @param date
* @return {@link Date}
*/
private static Date getFirstDayOfYear(Date date) {
LocalDateTime localDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
LocalDate localDate = localDateTime.withDayOfMonth(1).withDayOfYear(1).toLocalDate();
return Date.from(localDate.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant());
}
}
另外封装了一个redis工具类,提供对bitmap的操作
package com.info.loginstatistics.util;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtil {
private static RedisTemplate<String, String> redisTemplate;
public RedisUtil(RedisTemplate redisTemplate) {
RedisUtil.redisTemplate = redisTemplate;
}
public static void set(String key, String value) {
redisTemplate.opsForValue().set(key, value);
}
public static void set(String key, String value, long expire) {
redisTemplate.opsForValue().set(key, value, expire, TimeUnit.SECONDS);
}
public static String get(String key) {
return redisTemplate.opsForValue().get(key);
}
/**
* 设置key字段第offset位bit数值
*
* @param key 字段
* @param offset 位置
* @param value 数值
*/
public static void setBit(String key, long offset, boolean value) {
redisTemplate.execute((RedisCallback) con -> con.setBit(key.getBytes(), offset, value));
}
/**
* 判断该key字段offset位否为1
*
* @param key 字段
* @param offset 位置
* @return 结果
*/
public static boolean getBit(String key, long offset) {
return (boolean) redisTemplate.execute((RedisCallback) con -> con.getBit(key.getBytes(), offset));
}
/**
* 统计key字段value为1的总数
*
* @param key 字段
* @return 总数
*/
public static Long bitCount(String key) {
return (Long) redisTemplate.execute((RedisCallback<Long>) con -> con.bitCount(key.getBytes()));
}
/**
* 统计key字段value为1的总数,从start开始到end结束
*
* @param key 字段
* @param start 起始
* @param end 结束
* @return 总数
*/
public static Long bitCount(String key, Long start, Long end) {
return (Long) redisTemplate.execute((RedisCallback) con -> con.bitCount(key.getBytes(), start, end));
}
/**
* 取多个key并集并计算总数
*
* @param key key
* @return 总数
*/
public static Long OpOrCount(String... key) {
byte[][] keys = new byte[key.length][];
for (int i = 0; i < key.length; i++) {
keys[i] = key[i].getBytes();
}
redisTemplate.execute((RedisCallback) con -> con.bitOp(RedisStringCommands.BitOperation.OR, (key[0] + "To" + key[key.length - 1]).getBytes(), keys));
redisTemplate.expire(key[0] + "To" + key[key.length - 1], 10, TimeUnit.SECONDS);
return RedisUtil.bitCount(key[0] + "To" + key[key.length - 1]);
}
/**
* 取多个key的交集并计算总数
*
* @param key key
* @return 总数
*/
public static Long OpAndCount(String... key) {
byte[][] keys = new byte[key.length][];
for (int i = 0; i < key.length; i++) {
keys[i] = key[i].getBytes();
}
redisTemplate.execute((RedisCallback) con -> con.bitOp(RedisStringCommands.BitOperation.AND, (key[0] + "To" + key[key.length - 1]).getBytes(), keys));
redisTemplate.expire(key[0] + "To" + key[key.length - 1], 10, TimeUnit.SECONDS);
return RedisUtil.bitCount(key[0] + "To" + key[key.length - 1]);
}
/**
* 取多个key的补集并计算总数
*
* @param key key
* @return 总数
*/
public static Long OpXorCount(String... key) {
byte[][] keys = new byte[key.length][];
for (int i = 0; i < key.length; i++) {
keys[i] = key[i].getBytes();
}
redisTemplate.execute((RedisCallback) con -> con.bitOp(RedisStringCommands.BitOperation.XOR, (key[0] + "To" + key[key.length - 1]).getBytes(), keys));
redisTemplate.expire(key[0] + "To" + key[key.length - 1], 10, TimeUnit.SECONDS);
return RedisUtil.bitCount(key[0] + "To" + key[key.length - 1]);
}
/**
* 取多个key的否集并计算总数
*
* @param key key
* @return 总数
*/
public static Long OpNotCount(String... key) {
byte[][] keys = new byte[key.length][];
for (int i = 0; i < key.length; i++) {
keys[i] = key[i].getBytes();
}
redisTemplate.execute((RedisCallback) con -> con.bitOp(RedisStringCommands.BitOperation.NOT, (key[0] + "To" + key[key.length - 1]).getBytes(), keys));
redisTemplate.expire(key[0] + "To" + key[key.length - 1], 10, TimeUnit.SECONDS);
return RedisUtil.bitCount(key[0] + "To" + key[key.length - 1]);
}
}
至此,主要的代码就是这些了,可以通过postman进行测试了。
模拟登录
统计用户的登录情况
今天的分享就先到这里了,有时间再做一些其余情况的测试,感谢大家的观看,如若有误,欢迎指正,谢谢!