简介
一个SpringBoot的社区项目。
redis使用场景:网站数据统计、缓存用户信息、验证码、登录凭证、点赞、关注。
redis配置类
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 设置key的序列化方式
template.setKeySerializer(RedisSerializer.string());
// 设置value的序列化方式
template.setValueSerializer(RedisSerializer.json());
// 设置hash的key的序列化方式
template.setHashKeySerializer(RedisSerializer.string());
// 设置hash的value的序列化方式
template.setHashValueSerializer(RedisSerializer.json());
template.afterPropertiesSet();//使上面这些template的设置生效
return template;
}
}
自定义 RedisTemplate 进行序列化
使用 StringRedisTemplate 进行序列化
RedisTemplate 的两种序列化方式_华仔仔coding的博客-CSDN博客_redistemplate 序列化
功能描述:初始化RedisTemplate的一些参数设置
使用场景:主要是在RedisTemplate初始化时进行调用,如果不执行此方法,可能会报一些莫名其妙的错误,那应该就是部分参数没有初始化造成的。
网站数据统计
UV(Unique Visitor)
网站的独立访客,将访问网站的IP地址去重后统计数量,使用redis中的HyperLogLog记录。
HyperLogLog是用来做基数统计的算法,它提供不精确的去重计数方案(这个不精确并不是非常不精确),标准误差是0.81%
每个HyperLogLog键只需要花费12KB内存,就可以计算接近2^64个不同的基数
HyperLogLog只能统计基数的大小(也就是数据集的大小,集合的个数),他不能存储元素的本身,不能向set集合那样存储元素本身,也就是说无法返回元素。
1. 写拦截器
类DataInterceptor实现HandlerInterceptor接口,重写preHandle方法。
preHandle:预处理,在业务处理器处理请求之前被调用,可以进行登录拦截,编码处理、安全控制、权限校验等处理;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 记录UV
String ip = request.getRemoteHost();
dataService.recordUV(ip);
return true;
}
以当前日期为key,将访问网站的IP地址添加到HyperLogLog中。
private SimpleDateFormat df=new SimpleDateFormat("yyyyMMdd");
// 将指定的IP计入UV
public void recordUV(String ip) {
String redisKey = RedisKeyUtil.getUVKey(df.format(new Date()));
redisTemplate.opsForHyperLogLog().add(redisKey, ip);
}
Redis Pfadd 命令_添加指定元素到 HyperLogLog 中。
单日的UV的key为uv:date
// 单日UV
public static String getUVKey(String date) {
return PREFIX_UV + SPLIT + date;
}
2. 配置拦截器
实现WebMvcConfigurer接口。
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private DataInterceptor dataInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(dataInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
}
}
静态文件不需要被拦截,使用excludePathPatterns()排除。
3. 统计指定日期范围内的UV
public long calculateUV(Date start, Date end) {
if (start == null || end == null) {
throw new IllegalArgumentException("参数不能为空!");
}
// 整理该日期范围内的key
List<String> keyList = new ArrayList<>();
Calendar calendar = Calendar.getInstance();
calendar.setTime(start);
while (!calendar.getTime().after(end)) {
String key = RedisKeyUtil.getUVKey(df.format(calendar.getTime()));
keyList.add(key);
calendar.add(Calendar.DATE, 1);
}
// 合并这些数据
String redisKey = RedisKeyUtil.getUVKey(df.format(start), df.format(end));
redisTemplate.opsForHyperLogLog().union(redisKey, keyList.toArray());
// 返回统计的结果
return redisTemplate.opsForHyperLogLog().size(redisKey);
}
Redis Pgmerge 命令_将多个 HyperLogLog 合并为一个 HyperLogLog
Redis Pfcount 命令_返回给定 HyperLogLog 的基数估算值。
区间的UV的key为uv:startDate:endDate
// 区间UV
public static String getUVKey(String startDate, String endDate) {
return PREFIX_UV + SPLIT + startDate + SPLIT + endDate;
}
DAU(Daily Active User)
网站日活跃用户,用户只要在一天内访问过网站,就是活跃用户,使用redis中的Bitmap记录。
Bitmap实际上就是String类型,通过最小的单位bit来进行0或者1的设置,表示某个元素对应的值或者状态。 一个bit的值,或者是0,或者是1
redis 字符串最大值为512M,所以bigmap最大值为:4294967295
1. 写拦截器
统计UV的时候我们已经写过拦截器DataInterceptor了,而且配置了拦截器,现在我们只需要在preHandle方法中加入记录DAU的代码。
只有登录过的用户才需要被DAU统计。
// 记录DAU
User user = hostHolder.getUser();
if (user != null) {
dataService.recordDAU(user.getId());
}
以当前日期为key,将登录用户的id设置为true。
// 将指定用户计入DAU
public void recordDAU(int userId) {
String redisKey = RedisKeyUtil.getDAUKey(df.format(new Date()));
redisTemplate.opsForValue().setBit(redisKey, userId, true);
}
Redis Setbit 命令_对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit)。
单日DAU的key为dau:date
// 单日活跃用户
public static String getDAUKey(String date) {
return PREFIX_DAU + SPLIT + date;
}
2. 统计指定日期范围内的DAU
只要用户在给定的日期范围内有一天达到了活跃用户的标准,就算是活跃用户。
public long calculateDAU(Date start, Date end) {
if (start == null || end == null) {
throw new IllegalArgumentException("参数不能为空!");
}
// 整理该日期范围内的key
List<byte[]> keyList = new ArrayList<>();
Calendar calendar = Calendar.getInstance();
calendar.setTime(start);
while (!calendar.getTime().after(end)) {
String key = RedisKeyUtil.getDAUKey(df.format(calendar.getTime()));
keyList.add(key.getBytes());
calendar.add(Calendar.DATE, 1);
}
// 进行OR运算
return (long) redisTemplate.execute(new RedisCallback() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
String redisKey = RedisKeyUtil.getDAUKey(df.format(start), df.format(end));
connection.bitOp(RedisStringCommands.BitOperation.OR, redisKey.getBytes(), keyList.toArray(new byte[0][0]));
return connection.bitCount(redisKey.getBytes());
}
});
}
execute() 需要 RedisConnection 对象,通过 RedisConnection 操作 Redis
使用 RedisTemplate excute()与opsFor_bingguang1993的博客-CSDN博客_redistemplate.execute()的用法
BITOP — Redis 命令参考 (redisfans.com)
BITCOUNT — Redis 命令参考 (redisfans.com)
区间DAU的key为dau:startDate:endDate
// 区间活跃用户
public static String getDAUKey(String startDate, String endDate) {
return PREFIX_DAU + SPLIT + startDate + SPLIT + endDate;
}
缓存用户信息
使用redis临时存储用户的信息。
重构UserService中的getUser(int id)方法,不直接从DB中查询,而是从redis中查询。
如果从redis中查询不到用户信息,就从DB中查询数据,并且存储到redis中,这样下次就能直接从redis中查询到用户数据了。
public User getUser(int id) {
// return userDao.getUser(id);
User user = getCache(id);
if(user==null){
user = initCache(id);
}
return user;
}
getCache
优先从缓存中取数据。
private User getCache(int userId){
String key = RedisKeyUtil.getUserKey(userId);
return (User) redisTemplate.opsForValue().get(key);
}
用户信息在redis中的key为user:userId
public static String getUserKey(int userId) {
return PREFIX_USER + SPLIT + userId;
}
initCache
缓存取不到时,从DB中获取数据,并初始化缓存数据。注意redis中序列化的问题。
private User initCache(int userId) {
User user = userDao.getUser(userId);
String redisKey = RedisKeyUtil.getUserKey(userId);
redisTemplate.opsForValue().set(redisKey, user, 3600, TimeUnit.SECONDS);//设置过期时间为1小时
return user;
}
缓存在redis中的用户信息不需要一直保存,设置1小时过期时间,过期后缓存数据从redis中自动清除。
clearCache
数据变更时清除缓存数据,eg:用户更换头像时,用户激活成功时。
private void clearCache(int userId) {
String redisKey = RedisKeyUtil.getUserKey(userId);
redisTemplate.delete(redisKey);
}
验证码
以前的验证码存储在session中,在分布式系统中存在session共享问题。
验证码不需要永久存储,在redis中存储可以方便的对key的过期时间进行设置。
1. 生成验证码
我们使用kaptcha生成验证码,使用springboot整合kaptcha。
String text = kaptchaProducer.createText();
BufferedImage image = kaptchaProducer.createImage(text);
Kaptcha图片验证码工具_BUG弄潮儿的技术博客_51CTO博客
2. 存储验证码的归属
通过UUID生存随机字符串kaptchaOwner,用于将用户与验证码联系起来。
将kaptchaOwner存入cookie中。
String kaptchaOwner = CommunityUtil.generateUUID();
Cookie cookie = new Cookie("kaptchaOwner", kaptchaOwner);
cookie.setMaxAge(60);//设置cookie过期时间为60s
cookie.setPath(contextPath);
response.addCookie(cookie);
3. 验证码存入redis
注意下kaptchaOwner。
String key = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
redisTemplate.opsForValue().set(key, text, 60, TimeUnit.SECONDS);//失效时间60s
redis中key为kaptcha:kaptchaOwner,value为验证码。
public static String getKaptchaKey(String owner) {
return PREFIX_KAPTCHA + SPLIT + owner;
}
4. 返回验证码图片给浏览器
5. 校验验证码
-
从cookie中拿到
kaptchaOwner,根据kaptchaOwner从reids中拿到正确的验证码。 -
用正确的验证码与用户输入的验证码进行对比。
登录凭证
处理每次请求时,都要查询用户的登录凭证,访问的频率非常高。
-
登陆时将登录凭证存入redis中,ticket通过cookie存在浏览器中
-
退出时将redis中登录凭证的状态改为删除状态
- 取出登录凭证
- 修改状态
- 存入reids
-
通过cookie获取ticket,通过ticket从reids中获取登录凭证,登录凭证中有用户信息。
点赞&关注
- 点赞使用set
- 关注使用sorted set