实体分为很多类,实体的确定要通过实体类型和实体id两个属性同时确定。牛客论坛中使用到了3类实体:
1 登录
使用到的Redis命令:
bash
set key value // 设置指定key的值为value
get key // 获取指定key的值
1.1 存储/获取验证码
验证码文本,登录前,用户根据验证码图片输入验证码文本,用户提交登录,服务器会将用户输入的验证码文本于缓存在Redis中的验证码文本做字符串比较。
1、将验证码存入Redis
java
String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
redisTemplate.opsForValue().set(redisKey, text, 60, TimeUnit.SECONDS);
2、获取验证码
java
String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
kaptcha = (String) redisTemplate.opsForValue().get(redisKey);
1.2 存储/获取登录凭证
登录成功后,用户获得登录凭证,里面存储了用户id、凭证过期时间、状态,用于登录和登出功能。
1、存储登录凭证:
java
String redisKey = RedisKeyUtil.getTicketKey(loginTicket.getTicket());
redisTemplate.opsForValue().set(redisKey, loginTicket);
2、获取登录凭证:
java
String redisKey = RedisKeyUtil.getTicketKey(ticket);
LoginTicket loginTicket = (LoginTicket) redisTemplate.opsForValue().get(redisKey);
2 点赞
使用到的Redis命令:
bash
// 以下是集合的命令
sadd key element [element ...] // 向集合key添加一个或多个元素
srem key element [element ...] // 移除集合key中的一个或多个元素
sismember key memeber // 用于判断元素 member是否集合的成员。
scard key // 获取集合的成员数
// 以下是字符串的命令
incr key //将key中储存的字符串数值增一
decr key //将key中储存的字符串数值减一
get key // 获取字符串数值
2.1 用户给实体点赞 / 取消点赞
java
public void like(int userId, int entityType, int entityId, int entityUserId) {
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
String userLikeKey = RedisKeyUtil.getUserLikeKey(entityUserId);
boolean isMember = operations.opsForSet().isMember(entityLikeKey, userId);
// 开始一个Redis事务
operations.multi();
if (isMember) {
operations.opsForSet().remove(entityLikeKey, userId);
operations.opsForValue().decrement(userLikeKey);
} else {
operations.opsForSet().add(entityLikeKey, userId);
operations.opsForValue().increment(userLikeKey);
}
// 执行Redis事务,并返回结果
return operations.exec();
}
});
}
2.2 查询某实体点赞的数量
java
public long findEntityLikeCount(int entityType, int entityId) {
String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
return redisTemplate.opsForSet().size(entityLikeKey);
}
2.3 查询某人对某实体的点赞状态
java
public int findEntityLikeStatus(int userId, int entityType, int entityId) {
String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
return redisTemplate.opsForSet().isMember(entityLikeKey, userId) ? 1 : 0;
}
2.4 查询某个用户获得的赞
java
public int findUserLikeCount(int userId) {
String userLikeKey = RedisKeyUtil.getUserLikeKey(userId);
Integer count = (Integer) redisTemplate.opsForValue().get(userLikeKey);
return count == null ? 0 : count.intValue();
}
3 关注
使用到的Redis命令
bash
zadd key score member [score member ...] // 向有序集合key添加一个或多个成员,或者更新已存在成员的分数
zrem key member [member ...] // 移除有序集合key中的一个或多个成员
zcard key // 获取有序集合key的成员数
zscore key member //返回有序集合key中,成员member的分数
zrange key start end [withscores] //返回有序集合key中,指定区间内的成员
zrevrange key start end [withscores] //返回有序集合key中,指定区间内的成员,通过索引,分数从高到低
3.1 用户关注某个实体
某个用户关注的实体,有序集合里面存放的是entity_id,分数是时间,实体类型在key里。
可以关注某个用户,entity_type=3
可以关注某个帖子,entity_type=1
可以关注某条评论,entity_type=2
java
public void follow(int userId, int entityType, int entityId) {
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);
operations.multi();
operations.opsForZSet().add(followeeKey, entityId, System.currentTimeMillis());
operations.opsForZSet().add(followerKey, userId, System.currentTimeMillis());
return operations.exec();
}
});
}
3.2 用户取消关注
java
public void unfollow(int userId, int entityType, int entityId) {
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);
operations.multi();
operations.opsForZSet().remove(followeeKey, entityId);
operations.opsForZSet().remove(followerKey, userId);
return operations.exec();
}
});
}
3.3 查询关注的实体的数量
java
public long findFolloweeCount(int userId, int entityType) {
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
return redisTemplate.opsForZSet().zCard(followeeKey);
}
3.4 查询实体的粉丝的数量
java
public long findFollowerCount(int entityType, int entityId) {
String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);
return redisTemplate.opsForZSet().zCard(followerKey);
}
3.5 查询当前用户是否已关注该实体
由于有序集合没有类似集合那样sismember key memeber // 用于判断元素 member是否集合的成员。
所以只能用获取分数,判断分数是不是为空
java
public boolean hasFollowed(int userId, int entityType, int entityId) {
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
return redisTemplate.opsForZSet().score(followeeKey, entityId) != null;
}
3.6 查询某用户关注的人(关注列表) ,分页展示
java
public List<Map<String, Object>> findFollowees(int userId, int offset, int limit) {
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, ENTITY_TYPE_USER);
Set<Integer> targetIds = redisTemplate.opsForZSet().reverseRange(followeeKey, offset, offset + limit - 1);
3.7 查询某用户的粉丝 (粉丝列表),分页展示
java
public List<Map<String, Object>> findFollowers(int userId, int offset, int limit) {
String followerKey = RedisKeyUtil.getFollowerKey(ENTITY_TYPE_USER, userId);
Set<Integer> targetIds = redisTemplate.opsForZSet().reverseRange(followerKey, offset, offset + limit - 1);
4 网站数据统计
使用到的Redis命令:
bash
// 以下是HyperLogLog的命令
pfadd key element [element ...] // 向HyperLogLog添加元素
pfcount key [key ..] // 计算独立用户数
pfmerge destkey sourcekey [sourcekey ...] // 求出多个HyperLogLog的并集并复制给destkey
// 以下是bitmaps的命令
setbit key offset value // 设置第offset个位置的值
getbit key offset // 获取第offset个位置的值
bitop or destkey key [key ...] //多个bitmaps做Or运算
bitcount key [start end] // 获取bitmaps指定范围值为1的个数
4.1 统计独立访客
独立访客,union
1、将指定的IP计入UV
java
public void recordUV(String ip) {
String redisKey = RedisKeyUtil.getUVKey(df.format(new Date()));
redisTemplate.opsForHyperLogLog().add(redisKey, ip);
}
2、统计指定日期范围内的UV
java
// 合并这些数据
String redisKey = RedisKeyUtil.getUVKey(df.format(start), df.format(end));
redisTemplate.opsForHyperLogLog().union(redisKey, keyList.toArray());
// 返回统计的结果
return redisTemplate.opsForHyperLogLog().size(redisKey);
4.2 统计日活跃用户
日活跃用户,or位运算
1、 将指定用户计入DAU
java
public void recordDAU(int userId) {
String redisKey = RedisKeyUtil.getDAUKey(df.format(new Date()));
redisTemplate.opsForValue().setBit(redisKey, userId, true);
}
2、 统计指定日期范围内的DAU
java
// 进行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());
}
});
5 缓存设计
使用到的Redis命令:
bash
set key value // 设置指定key的值为value
get key // 获取指定key的值
del key [key ...] // 删除一个或多个key
5.1 优先从缓存中取值
java
private User getCache(int userId) {
String redisKey = RedisKeyUtil.getUserKey(userId);
return (User) redisTemplate.opsForValue().get(redisKey);
}
5.2 取不到时初始化缓存数据
java
private User initCache(int userId) {
User user = userMapper.selectById(userId);
String redisKey = RedisKeyUtil.getUserKey(userId);
redisTemplate.opsForValue().set(redisKey, user, 3600, TimeUnit.SECONDS);
return user;
}
5.3 数据变更时清除缓存数据
java
private void clearCache(int userId) {
String redisKey = RedisKeyUtil.getUserKey(userId);
redisTemplate.delete(redisKey);
}
6 热帖排行
使用到的Redis命令
bash
sadd key element [element ...] // 向集合key添加一个或多个元素
spop key // 移除并返回集合中的一个随机元素
里面存放的是帖子id,用于启动定时任务计算帖子分数,有哪些帖子要计算分数。
6.1 记录要计算分数的帖子
java
//
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey, postId);
## 6.1 获取所有要计算分数的帖子
java
while (operations.size() > 0) {
this.refresh((Integer) operations.pop());
}