SpringBoot继承Redis实现排行榜
项目文件结构
1、修改maven文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.wu</groupId>
<artifactId>rank</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rank</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.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-test</artifactId>
<scope>test</scope>
</dependency>
<!--redis的依赖包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.38</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2、redis配置文件
spring.application.name=spring-boot-redis
# 配置redis
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=foobared
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=200 # 这个地方需要
这里上面的redis的密码需要根据实际情况进行修改
3、RedisConfig文件
package com.wu.rank.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate redis = new StringRedisTemplate();
redis.setConnectionFactory(redisConnectionFactory);
// 设置redis的String/value的默认序列化方式
DefaultSerializer stringRedisSerializer = new DefaultSerializer();
redis.setKeySerializer(stringRedisSerializer);
redis.setValueSerializer(stringRedisSerializer);
redis.setHashKeySerializer(stringRedisSerializer);
redis.setHashValueSerializer(stringRedisSerializer);
redis.afterPropertiesSet();
return redis;
}
}
4、DefaultSerializer文件
package com.wu.rank.config;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.util.Assert;
import java.nio.charset.Charset;
public class DefaultSerializer implements RedisSerializer<Object> {
private final Charset charset;
public DefaultSerializer() {
this(Charset.forName("UTF8"));
}
public DefaultSerializer(Charset charset) {
Assert.notNull(charset, "Charset must not be null!");
this.charset = charset;
}
@Override
public byte[] serialize(Object o) throws SerializationException {
return o == null ? null : String.valueOf(o).getBytes(charset);
}
@Override
public Object deserialize(byte[] bytes) throws SerializationException {
return bytes == null ? null : new String(bytes, charset);
}
}
component文件夹
5、RedisComponent代码
package com.wu.rank.component;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;
import java.util.Set;
@Component
public class RedisComponent {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 添加一个元素, zset与set最大的区别就是每个元素都有一个score,因此有个排序的辅助功能; zadd
*
* @param key
* @param value
* @param score
*/
public void add(String key, String value, double score) {
redisTemplate.opsForZSet().add(key, value, score);
}
/**
* 删除元素 zrem
*
* @param key
* @param value
*/
public void remove(String key, String value) {
redisTemplate.opsForZSet().remove(key, value);
}
/**
* score的增加or减少 zincrby
*
* @param key
* @param value
* @param score
*/
public Double incrScore(String key, String value, double score) {
return redisTemplate.opsForZSet().incrementScore(key, value, score);
}
/**
* 查询value对应的score zscore
*
* @param key
* @param value
* @return
*/
public Double score(String key, String value) {
return redisTemplate.opsForZSet().score(key, value);
}
/**
* 判断value在zset中的排名 zrank
*
* 积分小的在前面
*
* @param key
* @param value
* @return
*/
public Long rank(String key, String value) {
return redisTemplate.opsForZSet().rank(key, value);
}
/**
* 查询集合中指定顺序的值, 0 -1 表示获取全部的集合内容 zrange
*
* 返回有序的集合,score小的在前面
*
* @param key
* @param start
* @param end
* @return
*/
public Set<String> range(String key, long start, long end) {
return redisTemplate.opsForZSet().range(key, start, end);
}
/**
* 查询集合中指定顺序的值和score,0, -1 表示获取全部的集合内容
*
* @param key
* @param start
* @param end
* @return
*/
public Set<ZSetOperations.TypedTuple<String>> rangeWithScore(String key, long start, long end) {
return redisTemplate.opsForZSet().rangeWithScores(key, start, end);
}
/**
* 查询集合中指定顺序的值 zrevrange
*
* 返回有序的集合中,score大的在前面
*
* @param key
* @param start
* @param end
* @return
*/
public Set<String> revRange(String key, long start, long end) {
return redisTemplate.opsForZSet().reverseRange(key, start, end);
}
/**
* 根据score的值,来获取满足条件的集合 zrangebyscore
*
* @param key
* @param min
* @param max
* @return
*/
public Set<String> sortRange(String key, long min, long max) {
return redisTemplate.opsForZSet().rangeByScore(key, min, max);
}
/**
* 返回集合的长度
*
* @param key
* @return
*/
public Long size(String key) {
return redisTemplate.opsForZSet().zCard(key);
}
}
6、RankListComponent
package com.wu.rank.component;
import com.wu.rank.model.RankDO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
@Component
public class RankListComponent {
@Autowired
private RedisComponent redisComponent;
private static final String RANK_PREFIX = "global_rank";
private List<RankDO> buildRedisRankToBizDO(Set<ZSetOperations.TypedTuple<String>> result, long offset) {
List<RankDO> rankList = new ArrayList<>(result.size());
long rank = offset;
for (ZSetOperations.TypedTuple<String> sub : result) {
rankList.add(new RankDO(rank++, Math.abs(sub.getScore().floatValue()), Long.parseLong(sub.getValue())));
}
return rankList;
}
/**
* 获取前n名的排行榜数据
*
* @param n
* @return
*/
public List<RankDO> getTopNRanks(int n) {
Set<ZSetOperations.TypedTuple<String>> result = redisComponent.rangeWithScore(RANK_PREFIX, 0, n - 1);
return buildRedisRankToBizDO(result, 1);
}
/**
* 获取用户所在排行榜的位置,以及排行榜中其前后n个用户的排行信息
*
* @param userId
* @param n
* @return
*/
public List<RankDO> getRankAroundUser(Long userId, int n) {
// 首先是获取用户对应的排名
RankDO rank = getRank(userId);
if (rank.getRank() <= 0) {
// fixme 用户没有上榜时,不返回
return Collections.emptyList();
}
// 因为实际的排名是从0开始的,所以查询周边排名时,需要将n-1
Set<ZSetOperations.TypedTuple<String>> result =
redisComponent.rangeWithScore(RANK_PREFIX, Math.max(0, rank.getRank() - n - 1), rank.getRank() + n - 1);
return buildRedisRankToBizDO(result, rank.getRank() - n);
}
/**
* 获取用户的排行榜位置
*
* @param userId
* @return
*/
public RankDO getRank(Long userId) {
// 获取排行, 因为默认是0为开头,因此实际的排名需要+1
Long rank = redisComponent.rank(RANK_PREFIX, String.valueOf(userId));
if (rank == null) {
// 没有排行时,直接返回一个默认的
return new RankDO(-1L, 0F, userId);
}
// 获取积分
Double score = redisComponent.score(RANK_PREFIX, String.valueOf(userId));
return new RankDO(rank + 1, Math.abs(score.floatValue()), userId);
}
/**
* 更新用户积分,并获取最新的个人所在排行榜信息
*
* @param userId
* @param score
* @return
*/
public RankDO updateRank(Long userId, Float score) {
// 因为zset默认积分小的在前面,所以我们对score进行取反,这样用户的积分越大,对应的score越小,排名越高
redisComponent.add(RANK_PREFIX, String.valueOf(userId), -score);
Long rank = redisComponent.rank(RANK_PREFIX, String.valueOf(userId));
return new RankDO(rank + 1, score, userId);
}
}
Model文件夹
7、RankDO
package com.wu.rank.model;
import java.io.Serializable;
public class RankDO implements Serializable {
private static final long serialVersionUID = 4804922606006935590L;
/**
* 排名
*/
private Long rank;
/**
* 积分
*/
private Float score;
/**
* 用户id
*/
private Long userId;
public RankDO(Long rank, Float score, Long userId) {
this.rank = rank;
this.score = score;
this.userId = userId;
}
public static long getSerialVersionUID() {
return serialVersionUID;
}
public Long getRank() {
return rank;
}
public void setRank(Long rank) {
this.rank = rank;
}
public Float getScore() {
return score;
}
public void setScore(Float score) {
this.score = score;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
}
8、controller文件夹
RankAction
package com.wu.rank.controller;
import com.wu.rank.component.RankListComponent;
import com.wu.rank.model.RankDO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class RankAction {
@Autowired
private RankListComponent rankListComponent;
@GetMapping(path = "/topn")
public List<RankDO> showTopN(int n) {
return rankListComponent.getTopNRanks(n);
}
@GetMapping(path = "/update")
public RankDO updateScore(long userId, float score) {
return rankListComponent.updateRank(userId, score);
}
@GetMapping(path = "/rank")
public RankDO queryRank(long userId) {
return rankListComponent.getRank(userId);
}
@GetMapping(path = "/around")
public List<RankDO> around(long userId, int n) {
return rankListComponent.getRankAroundUser(userId, n);
}
}
主程序代码
package com.wu.rank;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RankApplication {
public static void main(String[] args) {
SpringApplication.run(RankApplication.class, args);
}
}
测试
1、先运行redis服务器
2、打开postman进行测试
参考:
1、https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-case/120-redis-ranklist