redis排行榜

104 阅读6分钟

一,实现原理

 有序集合 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/> <!-- lookup parent from repository -->
	</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 -->
		<mybatis-plus-boot-starter.version>3.5.5</mybatis-plus-boot-starter.version>
		<!-- mybatis代码生成器 -->
		<mybatis-plus-generator.version>3.5.5</mybatis-plus-generator.version>
		<!-- mybatis代码生成器,所需要的“velocity模板引擎” -->
		<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>
		<!--数据库mysql驱动-->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>${mysql-connector.version}</version>
		</dependency>

		<!-- mybatis plus -->
		<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>

		<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
		<dependency>
			<groupId>org.mybatis</groupId>
			<artifactId>mybatis-spring</artifactId>
			<version>3.0.3</version>
		</dependency>

		<!-- mybatis代码生成器 -->
		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-generator</artifactId>
			<version>${mybatis-plus-generator.version}</version>
		</dependency>

		<!-- mybatis代码生成器,所需要的“velocity模板引擎” -->
		<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://192.168.8.230:3306/springcloudalibaba?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2b8&allowMultiQueries=true&useSSL=false&allowPublicKeyRetrieval=true
    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 # 空闲连接回收的时间间隔,与test-while-idle一起使用
      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 user
 */
@TableName(value ="user")
@Data
public class UserDO implements Serializable {
    /**
     * 用户id
     */
    @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;

/**
 * @Description:
 * @Author: 爱吃橙子的
 * @Date: 2024/3/7
 **/

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;

/**
* @author Administrator
* @description 针对表【user】的数据库操作Mapper
* @createDate 2024-03-07 10:58:12
* @Entity com.majinwen.demo.filterinterceptoraop.entity.UserDO
*/
@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;

/**
* @author Administrator
* @description 针对表【user】的数据库操作Service
* @createDate 2024-03-07 10:58:12
*/
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;

/**
 * @author Administrator
 * @description 针对表【user】的数据库操作Service实现
 * @createDate 2024-03-07 10:58:12
 */
@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;

/**
 * @Description:
 * @Author: 爱吃橙子的
 * @Date: 2024/3/7
 **/
@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;

/**
 * @Description: 排行榜
 * @Author: 爱吃橙子的
 * @Date: 2024/3/7
 **/
public interface ActivityScoreService {
    /**
     * @param activityUser
     * @description 更新排行榜
     * @author 爱吃橙子的
     * @date 2024/03/07 14:11
     */
    void addActivityScore(UserOperationBO activityUser);


    /**
     * @return java.util.List<com.majinwen.demo.filterinterceptoraop.entity.UserActivityBO>
     * @description 排行榜查询
     * @author 爱吃橙子的
     * @date 2024/03/07 14:39
     */
    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;


/**
 * @Description: 排行榜
 * @Author: 爱吃橙子的
 * @Date: 2024/3/7
 **/
@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; // 若分数为null(即默认情况),则不进行后续操作
        }
        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);
        // 如果ttl为-1,表示key没有设置过期时间;如果为0,则表示key已经过期;其他正数则是剩余的生存时间(秒)
        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;
    }

    /**
     * @param optionType
     * @param objectId
     * @return java.lang.String
     * @description 生成不同操作类型对应的操作对象的key
     * @author 爱吃橙子的
     * @date 2024/03/08 10:31
     */
    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; // 默认情况返回null
        }
    }

    /**
     * @param activityUser
     * @return java.lang.Double
     * @description 生成操作分数
     * @author 爱吃橙子的
     * @date 2024/03/08 10:31
     */
    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; // 默认情况返回null
        }
    }

    /**
     * @param userId
     * @param field
     * @param score
     * @param todayRankKey
     * @param monthRankKey
     * @description 更新排行榜
     * @author 爱吃橙子的
     * @date 2024/03/08 10:32
     */
    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();
            // 可以根据需要添加更多的异常处理或日志记录
        }
    }

    /**
     * @param todayRankKey
     * @param monthRankKey
     * @param userId
     * @param score
     * @description 具体的更新排行榜操作
     * @author 爱吃橙子的
     * @date 2024/03/08 10:32
     */
    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();
        });
    }

    /**
     * @return java.lang.String
     * @description 生成今日排行榜的key
     * @author 爱吃橙子的
     * @date 2024/03/08 10:32
     */
    public String todyRankKey() {
        return ACTIVITY_RANK_KEY_ + DateUtil.format(new Date(), DateTimeFormatter.ofPattern("yyyyMMdd"));
    }

    /**
     * @return java.lang.String
     * @description 生成本月排行榜的key
     * @author 爱吃橙子的
     * @date 2024/03/08 10:33
     */
    public String monthRankKey() {
        return ACTIVITY_RANK_KEY_ + DateUtil.format(new Date(), DateTimeFormatter.ofPattern("yyyyMM"));
    }
}