一,实现原理
有序集合 Zset 与 普通集合 Set 类似 , 都是 没有重复元素的集合
有序集合 Zset 中的 元素排序 , 是 根据 评分 进行排序 , 每个成员 都关联了一个 评分 , 在该 有序集合 中 , 根据 评分 由低到高 进行排序
Zset 中的元素 是 不可重复的 , 但是 元素 关联 的 评分 是可以重复的 , 也就是说 存在 两个不同的元素 关联着 相同的 评分
Zset 中的元素 是 有序 的 , 根据 排序的索引 或者 元素的评分 可以获取 指定范围 的 成员
二,实现步骤
1,基本项目搭建,依赖引入
这里数据库用的是mysql,版本是8.0,orm框架是mybatisplus,数据库连接池是druid,jdk版本是21,springboot版本是3.x
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.3</version>
<relativePath/>
</parent>
<properties>
<java.version>21</java.version>
<mysql-connector.version>8.0.11</mysql-connector.version>
<druid-spring-boot-starter.version>1.2.11</druid-spring-boot-starter.version>
<druid.version>1.2.11</druid.version>
<mybatis-plus-boot-starter.version>3.5.5</mybatis-plus-boot-starter.version>
<mybatis-plus-generator.version>3.5.5</mybatis-plus-generator.version>
<velocity-engine-core.version>2.3</velocity-engine-core.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus-boot-starter.version}</version>
<exclusions>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis-plus-generator.version}</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>${velocity-engine-core.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid-spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
</dependencies>
application配置
server:
port: 8080
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
username: root
password: 123456
url: jdbc:mysql://localhost:3306/learnnote?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
druid:
initial-size: 10
min-idle: 10
max-active: 40
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: select 1
validationQueryTimeout: 3
test-while-idle: true
test-on-borrow: false
test-on-return: false
keep-alive: true
data:
redis:
port: 6379
host: localhost
mybatis:
configuration:
map-underscore-to-camel-case: true
type-aliases-package: com.majinwen.springbootmybatisplusdruidredis.*.dao
mapper-locations:
- classpath*:mapper/**/*.xml
2,具体的代码实现
建表语句
CREATE TABLE `user` (
`userid` varchar(64) NOT NULL COMMENT '用户id',
`username` varchar(64) DEFAULT NULL COMMENT '用户登录名',
`nickname` varchar(64) DEFAULT NULL COMMENT '用户中文名',
PRIMARY KEY (`userid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
UserDO
package com.majinwen.demo.filterinterceptoraop.entity;
import java.io.Serializable;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@TableName(value ="user")
@Data
public class UserDO implements Serializable {
@TableId
private String userid;
private String username;
private String nickname;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
@Override
public boolean equals(Object that) {
if (this == that) {
return true;
}
if (that == null) {
return false;
}
if (getClass() != that.getClass()) {
return false;
}
UserDO other = (UserDO) that;
return (this.getUserid() == null ? other.getUserid() == null : this.getUserid().equals(other.getUserid()))
&& (this.getUsername() == null ? other.getUsername() == null : this.getUsername().equals(other.getUsername()))
&& (this.getNickname() == null ? other.getNickname() == null : this.getNickname().equals(other.getNickname()));
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((getUserid() == null) ? 0 : getUserid().hashCode());
result = prime * result + ((getUsername() == null) ? 0 : getUsername().hashCode());
result = prime * result + ((getNickname() == null) ? 0 : getNickname().hashCode());
return result;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(" [");
sb.append("Hash = ").append(hashCode());
sb.append(", userid=").append(userid);
sb.append(", username=").append(username);
sb.append(", nickname=").append(nickname);
sb.append(", serialVersionUID=").append(serialVersionUID);
sb.append("]");
return sb.toString();
}
}
排行榜类型枚举
package com.majinwen.springbootmybatisplusdruidredis.enums;
public enum ActivityRankTimeEnum {
DAY,
MONTH;
}
UserDOMapper
package com.majinwen.springbootmybatisplusdruidredis.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.majinwen.springbootmybatisplusdruidredis.entity.UserDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserDOMapper extends BaseMapper<UserDO> {
}
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.majinwen.springbootmybatisplusdruidredis.dao.UserDOMapper">
<resultMap id="BaseResultMap" type="com.majinwen.springbootmybatisplusdruidredis.entity.UserDO">
<id property="userid" column="userid" jdbcType="VARCHAR"/>
<result property="username" column="username" jdbcType="VARCHAR"/>
<result property="nickname" column="nickname" jdbcType="VARCHAR"/>
</resultMap>
<sql id="Base_Column_List">
userid
,username,nickname
</sql>
</mapper>
UserDOService
package com.majinwen.springbootmybatisplusdruidredis.service.common;
import com.baomidou.mybatisplus.extension.service.IService;
import com.majinwen.springbootmybatisplusdruidredis.entity.UserDO;
public interface UserDOService extends IService<UserDO> {
}
UserDOServiceImpl
package com.majinwen.springbootmybatisplusdruidredis.service.common.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.majinwen.springbootmybatisplusdruidredis.dao.UserDOMapper;
import com.majinwen.springbootmybatisplusdruidredis.entity.UserDO;
import com.majinwen.springbootmybatisplusdruidredis.service.common.UserDOService;
import org.springframework.stereotype.Service;
@Service
public class UserDOServiceImpl extends ServiceImpl<UserDOMapper, UserDO> implements UserDOService {
}
配置redistemplate
package com.majinwen.springbootmybatisplusdruidredis.componet;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;
@Component
public class RedisTemplateConf {
@Autowired
private RedisProperties properties;
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(d0redisConnectionFactory(properties));
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
public RedisConnectionFactory d0redisConnectionFactory(RedisProperties properties) {
RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration();
redisConfig.setHostName(properties.getHost());
redisConfig.setPort(properties.getPort());
redisConfig.setPassword(properties.getPassword());
redisConfig.setDatabase(properties.getDatabase());
final LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisConfig);
lettuceConnectionFactory.afterPropertiesSet();
return lettuceConnectionFactory;
}
}
排行榜的核心接口
package com.majinwen.springbootmybatisplusdruidredis.service.rank;
import com.majinwen.springbootmybatisplusdruidredis.entity.UserActivityBO;
import com.majinwen.springbootmybatisplusdruidredis.entity.UserOperationBO;
import com.majinwen.springbootmybatisplusdruidredis.enums.ActivityRankTimeEnum;
import java.util.List;
public interface ActivityScoreService {
void addActivityScore(UserOperationBO activityUser);
List<UserActivityBO> getActivityRank(ActivityRankTimeEnum activityRankTimeEnum, int size);
}
接口实现
package com.majinwen.springbootmybatisplusdruidredis.service.rank.impl;
import cn.hutool.core.date.DateUtil;
import com.majinwen.springbootmybatisplusdruidredis.entity.UserActivityBO;
import com.majinwen.springbootmybatisplusdruidredis.entity.UserDO;
import com.majinwen.springbootmybatisplusdruidredis.entity.UserOperationBO;
import com.majinwen.springbootmybatisplusdruidredis.enums.ActivityRankTimeEnum;
import com.majinwen.springbootmybatisplusdruidredis.service.rank.ActivityScoreService;
import com.majinwen.springbootmybatisplusdruidredis.service.common.UserDOService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.TimeUnit;
@Service
public class ActivityScoreServiceImpl implements ActivityScoreService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
UserDOService userDOService;
public final String ACTIVITY_SCORE_HISTORY_LIKE_KEY_PREFIX = "activity_score_like_";
public final String ACTIVITY_SCORE_HISTORY_COMMENT_KEY_PREFIX = "activity_score_comment_";
public final String ACTIVITY_SCORE_HISTORY_PUBLISH_KEY_PREFIX = "activity_score_like_publishArticle_";
public final String ACTIVITY_RANK_KEY_ = "activity_rank_";
public final String LIKE = "like";
public final String COMMENT = "comment";
public final String PUBLISH_ARTICLE = "publishArticle";
@Override
public void addActivityScore(UserOperationBO activityUser) {
Assert.notNull(activityUser, "用户操作信息不能为空");
Assert.notNull(activityUser.getOptionType(), "操作类型不能为空");
Assert.notNull(activityUser.getUserid(), "用户id不能为空");
Assert.notNull(activityUser.getObjectId(), "操作对象不能为空");
Double score = calculateScore(activityUser);
if (score == null) {
return;
}
String field = getField(activityUser.getOptionType(), activityUser.getObjectId());
final String todayRankKey = todyRankKey();
final String monthRankKey = monthRankKey();
updateScoreAndRank(activityUser.getUserid(), field, score, todayRankKey, monthRankKey);
if (isKeyExpired(todayRankKey)) {
redisTemplate.expireAt(todayRankKey, DateUtil.endOfDay(new Date()));
}
if (isKeyExpired(monthRankKey)) {
redisTemplate.expireAt(monthRankKey, DateUtil.endOfMonth(new Date()));
}
}
public boolean isKeyExpired(String key) {
Long ttl = redisTemplate.getExpire(key, TimeUnit.SECONDS);
return ttl == null || ttl <= 0;
}
@Override
public List<UserActivityBO> getActivityRank(ActivityRankTimeEnum activityRankTimeEnum, int size) {
List<UserActivityBO> list = new ArrayList<>();
String rankKey = activityRankTimeEnum == ActivityRankTimeEnum.DAY ? todyRankKey() : monthRankKey();
final Set<Object> range = redisTemplate.opsForZSet().reverseRange(rankKey, 0, -1);
for (Object o : range) {
final UserDO userDO = userDOService.getById(String.valueOf(o));
if (userDO != null) {
list.add(new UserActivityBO() {{
setUserid(userDO.getUserid());
setUsername(userDO.getUsername());
setNickname(userDO.getNickname());
setScore(redisTemplate.opsForZSet().score(rankKey, o));
}});
}
}
if (!CollectionUtils.isEmpty(list)) {
list.sort(Comparator.comparing(UserActivityBO::getScore).reversed());
for (int i = 0; i < list.size(); i++) {
list.get(i).setRank(i + 1);
}
}
return list;
}
private String getField(String optionType, String objectId) {
switch (optionType) {
case LIKE:
return ACTIVITY_SCORE_HISTORY_LIKE_KEY_PREFIX + objectId;
case COMMENT:
return ACTIVITY_SCORE_HISTORY_COMMENT_KEY_PREFIX + objectId;
case PUBLISH_ARTICLE:
return ACTIVITY_SCORE_HISTORY_PUBLISH_KEY_PREFIX + objectId;
default:
return null;
}
}
private Double calculateScore(UserOperationBO activityUser) {
switch (activityUser.getOptionType()) {
case LIKE:
return activityUser.getLike() ? 1.0D : -1.0D;
case COMMENT:
return activityUser.getComment() ? 2.0D : -2.0D;
case PUBLISH_ARTICLE:
return activityUser.getPublishArticle() ? 10.0D : -10.0D;
default:
return null;
}
}
private void updateScoreAndRank(String userId, String field, Double score, String todayRankKey, String monthRankKey) {
try {
final Double oldScore = (Double) redisTemplate.opsForHash().get(userId, field);
Double newScore = oldScore == null ? 0.0D : oldScore + score;
if (newScore >= 0) {
redisTemplate.opsForHash().put(userId, field, score);
incrementRank(todayRankKey, monthRankKey, userId, score);
} else {
redisTemplate.opsForHash().delete(userId, field);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void incrementRank(String todayRankKey, String monthRankKey, String userId, Double score) {
redisTemplate.execute((RedisCallback<Object>) connection -> {
connection.multi();
connection.zIncrBy(todayRankKey.getBytes(), score, userId.getBytes());
connection.zIncrBy(monthRankKey.getBytes(), score, userId.getBytes());
return connection.exec();
});
}
public String todyRankKey() {
return ACTIVITY_RANK_KEY_ + DateUtil.format(new Date(), DateTimeFormatter.ofPattern("yyyyMMdd"));
}
public String monthRankKey() {
return ACTIVITY_RANK_KEY_ + DateUtil.format(new Date(), DateTimeFormatter.ofPattern("yyyyMM"));
}
}