需求分析
- 添加积分:在上述签到的基础上添加用户积分(签到1天送10积分,续签到2天送20积分,3天送30积分,4天以上均送50积分)
- 积分排行榜设计
设计思路
数据库解决
利用关系型数据库保存积分记录数据,然后进行统计
CREATE TABLE `t_diner_points` (
`id` int(11) NOT NULL AUTO_INCREMENT ,
`fk_diner_id` int(11) NULL DEFAULT NULL ,
`points` int(11) NULL DEFAULT NULL ,
`types` int(11) NULL DEFAULT NULL COMMENT '积分类型:0=签到,1=关注好友,2=添加评论,
3=点赞商户' ,
`is_valid` int(11) NULL DEFAULT NULL ,
`create_date` datetime NULL DEFAULT NULL ,
`update_date` datetime NULL DEFAULT NULL ,
PRIMARY KEY (`id`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8mb4 COLLATE=utf8mb4_general_ci
AUTO_INCREMENT=1
ROW_FORMAT=COMPACT
;
字段名 | desc |
---|---|
id | 数据表主键(AUTO_INCREMENT) |
fk_diner_id | 用户id |
points | 积分 |
types | 积分类型:0=签到,1=关注好友,2=添加评论,3=点赞商户 |
is_valid | 是否有效 |
create_date | 添加日期 |
update_date | 修改日期 |
其实这个类似于一张日志表,因此数据量是非常庞大的,当我们想要统计用户积分做排行榜的的时候:
编写统计SQL是:(如果重复排名,那么后一位就靠后)
-- 开窗函数
select
t1.fk_diner_id as id,
sum(t1.points) as total,
rank () over (order by sum(t1.points) desc) as ranks,
t2.nickname,
t2.avatar_url
from
t_diner_points t1
left join t_diners t2 on t1.fk_diner_id = t2.id
where
t1.is_valid = 1
and t2.is_valid = 1
group by
t1.fk_diner_id
order by
total desc
limit 20
如果数据量小的话运行应该也没有什么大问题,但如果当数据量超过一定量以后,就会出现很大的延迟,毕竟MySQL查询是要消耗大量的IO的。
建模块
建 fs_points 模块
pom
<?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">
<parent>
<artifactId>food_social</artifactId>
<groupId>com.itkaka</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>fs_points</artifactId>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<!-- eureka client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- spring web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- spring data redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!-- commons 公共项目 -->
<dependency>
<groupId>com.itkaka</groupId>
<artifactId>fs_commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- test 单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- 集中定义项目所需插件 -->
<build>
<plugins>
<!-- spring boot maven 项目打包插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
配置文件
server:
port: 8096 # 端口
spring:
application:
name: fs_points # 应用名
# 数据库
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
url: jdbc:mysql://127.0.0.1:3306/db_lezijie_food_social?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useUnicode=true&useSSL=false
# Redis
redis:
port: 6379
host: 192.168.10.101
timeout: 3000
password: 123456
database: 3
# Swagger
swagger:
base-package: com.lezijie.points
title: 美食社交积分API接口文档
# 配置 Eureka Server 注册中心
eureka:
instance:
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
client:
service-url:
defaultZone: http://localhost:8090/eureka/
service:
name:
fs_oauth-server: http://fs_oauth/
fs_diners-server: http://fs_diners/
mybatis:
configuration:
map-underscore-to-camel-case: true
logging:
pattern:
console: '%d{2100-01-01 13:14:00.666} [%thread] %-5level %logger{50} - %msg%n'
配置类以及全局异常处理
Rest配置类和Redis配置类和全局异常处理
新增积分接口
编写实体对象
package com.itkaka.points.model.pojo;
import com.itkaka.commons.model.base.BaseModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class DinerPoints extends BaseModel {
@ApiModelProperty("关联食客ID")
private Integer fkDinerId;
@ApiModelProperty("积分")
private Integer points;
@ApiModelProperty(name = "积分类型", example = "0=签到,1=关注好友,2=添加Feed,3=添加商户评论")
private Integer types;
}
编写Mapper方法
package com.itkaka.points.mapper;
import com.itkaka.points.model.pojo.DinerPoints;
import org.apache.ibatis.annotations.Insert;
/**
* 积分服务 Mapper
*/
public interface DinerPointsMapper {
/**
* 添加积分
*/
@Insert("INSERT INTO t_diner_points (fk_diner_id, points, types, is_valid, create_date, update_date) " +
"VALUES (#{fkDinerId}, #{points}, #{types}, 1, NOW(), NOW())")
int save(DinerPoints dinerPoints);
}
编写Service新增方法
package com.itkaka.points.service;
import com.itkaka.commons.constant.RedisKeyConstant;
import com.itkaka.commons.utils.AssertUtil;
import com.itkaka.points.mapper.DinerPointsMapper;
import com.itkaka.points.model.pojo.DinerPoints;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
/**
* 积分服务 Service
*/
@Service
public class DinerPointsService {
@Value("${service.name.fs_oauth-server}")
private String oauthServerName;
@Value("${service.name.fs_diners-server}")
private String dinersServerName;
@Resource
private RestTemplate restTemplate;
@Resource
private RedisTemplate redisTemplate;
@Resource
private DinerPointsMapper dinerPointsMapper;
// 排行榜前 20
private static final int TOPN = 20;
/**
* 添加积分
*
* @param dinerId
* @param points
* @param types 0=签到,1=关注好友,2=添加Feed,3=添加商户评论
*/
@Transactional(rollbackFor = Exception.class)
public void addPoints(Integer dinerId, Integer points, Integer types) {
// 基本参数校验
AssertUtil.isTrue(dinerId == null || dinerId < 1, "食客不能为空");
AssertUtil.isTrue(points == null || points < 1, "积分不能为空");
AssertUtil.isTrue(types == null, "请选择对应的积分类型");
// 插入数据库
DinerPoints dinerPoints = new DinerPoints();
dinerPoints.setFkDinerId(dinerId);
dinerPoints.setPoints(points);
dinerPoints.setTypes(types);
dinerPointsMapper.save(dinerPoints);
// 将积分保存到 Redis 的 Sorted Set 中
redisTemplate.opsForZSet().incrementScore(
RedisKeyConstant.diner_points.getKey(), dinerId, points
);
}
}
编写Controller API接口方法
package com.itkaka.points.controller;
import com.itkaka.commons.model.domain.ResultInfo;
import com.itkaka.commons.utils.ResultInfoUtil;
import com.itkaka.points.service.DinerPointsService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
/**
* 积分服务 Controller
*/
@RestController
public class DinerPointsController {
@Resource
private DinerPointsService dinerPointsService;
@Resource
private HttpServletRequest request;
/**
* 添加积分
*
* @param dinerId
* @param points
* @param types 0=签到,1=关注好友,2=添加Feed,3=添加商户评论
* @return
*/
@PostMapping
public ResultInfo<Integer> addPoints(@RequestParam(required = false) Integer dinerId,
@RequestParam(required = false) Integer points,
@RequestParam(required = false) Integer types) {
dinerPointsService.addPoints(dinerId, points, types);
return ResultInfoUtil.buildSuccess(request.getServletPath(), points);
}
}
积分类型枚举
package com.itkaka.commons.constant;
import lombok.Getter;
/**
* 积分类型
*/
@Getter
public enum PointTypesConstant {
sign(0), // 签到
follow(1), // 关注
feed(2), // 添加Feed
review(3), // 添加商户评论
;
private int type;
PointTypesConstant(int key) {
this.type = type;
}
}
修改fs-diners的签到的业务逻辑
- 添加积分逻辑
- 修改返回结果是签到后的积分
/**
* 添加用户积分
*
* @param count 连续签到次数
* @param dinerId 登录用户 ID
* @return 获取的积分
*/
private int addPoints(int count, Integer dinerId) {
// 签到 1 天送 10 积分,连续签到 2 天送 20 积分,3 天送 30 积分
// 4 天以及以上均送 50 积分
int points = 10;
if (count == 2) {
points = 20;
} else if (count == 3) {
points = 30;
} else if (count >= 4) {
points = 50;
}
// 调用积分服务添加积分
// 构建请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
// 构建请求体(请求参数)
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("dinerId", dinerId);
body.add("points", points);
body.add("types", PointTypesConstant.sign.getType());
HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
// 发送请求
ResponseEntity<ResultInfo> result = restTemplate.postForEntity(pointsServerName, entity, ResultInfo.class);
AssertUtil.isTrue(result.getStatusCode() != HttpStatus.OK, "登录失败");
ResultInfo resultInfo = result.getBody();
if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
// 失败了, 事物要进行回滚
throw new ParameterException(resultInfo.getCode(), resultInfo.getMessage());
}
return points;
}
/**
* 用户签到/可以补签
*
* @param accessToken 登录用户 token
* @param dateStr 日期,默认当天
* @return 连续签到次数
*/
@Transactional(rollbackFor = Exception.class)
public int doSign(String accessToken, String dateStr) {
// 获取登录用户信息
SignInDinerInfo dinerInfo = loadSignInDinerInfo(accessToken);
// 获取日期
Date date = getDate(dateStr);
// 获取日期对应的天数,多少号
int offset = DateUtil.dayOfMonth(date) - 1; // 从 0 开始
// 构建 Redis Key
String signKey = buildSignKey(dinerInfo.getId(), date);
// 查看指定日期是否已签到
boolean isSigned = redisTemplate.opsForValue().getBit(signKey, offset);
AssertUtil.isTrue(isSigned, "当前日期已完成签到,无需再签");
// 签到
redisTemplate.opsForValue().setBit(signKey, offset, true);
// 统计连续签到次数
// 根据当前日期统计
date = new Date();
int count = getContinuousSignCount(dinerInfo.getId(), date);
// 用户签到成功添加对应积分
int points = addPoints(count, dinerInfo.getId());
return points;
}
Postman测试
访问:http://localhost/diners/sign?access_token=116af711-645c-4ce9-bc32-cd304bdda020
编写积分排行榜 TOPN 接口
- 读取数据库中积分,排行榜取TopN,显示字段有:用户id、用户昵称、头像、总积分以及排行榜
- 需要标记当前登录用户的排行情况
视图对象
package com.itkaka.points.model.vo;
import com.itkaka.commons.model.vo.ShortDinerInfo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
@ApiModel(description = "用户积分总排行榜")
@Getter
@Setter
public class DinerPointsRankVO extends ShortDinerInfo {
@ApiModelProperty("总积分")
private int total;
@ApiModelProperty("排名")
private int ranks;
@ApiModelProperty(value = "是否是自己", example = "0=否,1=是")
private int isMe;
}
编写Mapper方法(⭐[开窗函数])
package com.itkaka.points.mapper;
import com.itkaka.points.model.pojo.DinerPoints;
import com.itkaka.points.model.vo.DinerPointsRankVO;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 积分服务 Mapper
*/
public interface DinerPointsMapper {
/**
* 添加积分
*/
@Insert("INSERT INTO t_diner_points (fk_diner_id, points, types, is_valid, create_date, update_date) " +
"VALUES (#{fkDinerId}, #{points}, #{types}, 1, NOW(), NOW())")
int save(DinerPoints dinerPoints);
//查询积分排行榜 topN
@Select("select " +
" t1.fk_diner_id id," +
"sum(t1.points) total," +
"RANK() over ( order by sum(t1.points) desc ) ranks," +
"t2.nickname," +
"t2.avatar_url " +
" from t_diner_points t1 " +
" left join t_diners t2 on t1.fk_diner_id = t2.id " +
" where t1.is_valid = 1 and t2.is_valid = 1 " +
" group by t1.fk_diner_id " +
" order by total desc limit #{top}")
List<DinerPointsRankVO> findTopN(@Param("top") int top);
// 根据食客 ID 查询当前食客积分排名
@Select("SELECT id, total, ranks, nickname, avatar_url " +
" FROM ( " +
" SELECT " +
" t1.fk_diner_id id, " +
" sum(t1.points) total, " +
" RANK() over ( ORDER BY sum(t1.points) DESC ) ranks, " +
" t2.nickname, " +
" t2.avatar_url " +
" FROM t_diner_points t1 " +
" LEFT JOIN t_diners t2 ON t1.fk_diner_id = t2.id " +
" WHERE t1.is_valid = 1 AND t2.is_valid = 1 " +
" GROUP BY t1.fk_diner_id " +
" ORDER BY total DESC " +
" ) t " +
" WHERE id = #{dinerId}")
DinerPointsRankVO findDinerRank(@Param("dinerId") Integer dinerId);
}
Service读取方法
- 从数据库中查询前20的用户排行榜
- 判断当前登录用户是否在这20名
- 获取当前登录用户的排名
/**
* 积分服务 Service
*/
@Service
public class DinerPointsService {
@Value("${service.name.fs_oauth-server}")
private String oauthServerName;
@Value("${service.name.fs_diners-server}")
private String dinersServerName;
@Resource
private RestTemplate restTemplate;
@Resource
private RedisTemplate redisTemplate;
@Resource
private DinerPointsMapper dinerPointsMapper;
// 排行榜前 20
private static final int TOPN = 20;
//查询前 20 积分排行榜,显示个人排民 -- MySQL
public List<DinerPointsRankVO> findDinerPointRank(String accessToken){
// 获取用户的登录信息
SignInDinerInfo dinerInfo = loadSignInDinerInfo(accessToken);
// 统计积分排行榜
List<DinerPointsRankVO> ranks =dinerPointsMapper.findTopN(TOPN);
if (ranks == null || ranks.isEmpty()){
return Lists.newArrayList();
}
// 根据 Key:食客 ID,Value:积分信息 构建一个 Map
Map<Integer,DinerPointsRankVO> ranksMap = new LinkedHashMap<>();
for (int i = 0; i < ranks.size(); i++) {
ranksMap.put(ranks.get(i).getId(),ranks.get(i));
}
DinerPointsRankVO myRank = null;
// 判断当前登录用户是否在 排行榜 中,在的话添加标记直接返回
if (ranksMap.containsKey(dinerInfo.getId())){
myRank = ranksMap.get(dinerInfo.getId());
myRank.setIsMe(1);
return Lists.newArrayList(ranksMap.values());
}
// 如果不在.获取个人排名追加到最后
myRank = dinerPointsMapper.findDinerRank(dinerInfo.getId());
myRank.setIsMe(1);
ranks.add(myRank);
return ranks;
}
// 获取登录用户信息
private SignInDinerInfo loadSignInDinerInfo(String accessToken) {
// 是否有 accessToken
AssertUtil.mustLogin(accessToken);
// 拼接远程请求 url
String url = oauthServerName + "user/me?access_token={accessToken}";
// 发送请求
ResultInfo resultInfo = restTemplate.getForObject(url, ResultInfo.class, accessToken);
if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
throw new ParameterException(resultInfo.getCode(), resultInfo.getMessage());
}
SignInDinerInfo dinerInfo = BeanUtil.fillBeanWithMap((LinkedHashMap) resultInfo.getData(), new SignInDinerInfo(), false);
return dinerInfo;
}
}
DinerPointsController的读取接口方法
/**
* 查询 TOPN 积分排行榜,同时显示个人排名
*/
@GetMapping
public ResultInfo<List<DinerPointsRankVO>> dinerPointRank(String access_token) {
List<DinerPointsRankVO> ranks = dinerPointsService.findDinerPointRank(access_token);
return ResultInfoUtil.buildSuccess(request.getServletPath(), ranks);
}
网关配置
- id: fs-points
uri: lb://fs-points
predicates:
- Path=/points/**
filters:
- StripPrefix=1
测试
操作数据库问题
因为t_diner_points本质上是一张日志表,记录了所有用户积分记录,因此直接去数据库统计会有如下问题:
- SQL编写复杂
- 数据量大,执行统计SQL慢
- 高并发下会拖累其他业务表的操作,导致系统变慢
Sorted Sets 优化性能
使用Sorted Sets保存用户的积分总数,因为Sorted Sets有score属性,能够方便保存与读取,使用指令:
# 添加元素的分数,如果member不存在就会自动创建
ZINCRBY key increment member
# 按分数从大到小进行读取
zrevrange key
# 根据分数从大到小获取member排名
zrevrank key member
RedisKeyConstant
diner_points("diner:points", "diner用户的积分"),
修改添加积分方法
当将用户积分记录插入数据库后,同时利用 ZINCRBY 指令,将数据存入Redis中,这里不使用 ZADD 的原因是当食客不存在记录要插入,而且存在时需要将分数累加
/**
* 添加积分
*
* @param dinerId
* @param points
* @param types 0=签到,1=关注好友,2=添加Feed,3=添加商户评论
*/
@Transactional(rollbackFor = Exception.class)
public void addPoints(Integer dinerId, Integer points, Integer types) {
// 基本参数校验
AssertUtil.isTrue(dinerId == null || dinerId < 1, "食客不能为空");
AssertUtil.isTrue(points == null || points < 1, "积分不能为空");
AssertUtil.isTrue(types == null, "请选择对应的积分类型");
// 插入数据库
DinerPoints dinerPoints = new DinerPoints();
dinerPoints.setFkDinerId(dinerId);
dinerPoints.setPoints(points);
dinerPoints.setTypes(types);
dinerPointsMapper.save(dinerPoints);
//todo
//将积分查询出来累加以后再重新插入
// 将积分保存到 Redis 的 Sorted Set 中
redisTemplate.opsForZSet().incrementScore(
RedisKeyConstant.diner_points.getKey(), dinerId, points
);
}
添加查询TOP20的Service方法
- 排行榜:从Redis中根据diner:points的key按照score的排序进行读取,这里使用Redis的ZREVRANGE 指令,但在 ZREVRANGE 指令只返回member,不返回score,在RedisTemplate的ZSetOperations中有一个一个API方法叫 reverseRangeWithScores(key, start, end) 其中start从0开始,返回的是member和score,底层是将ZREVRANGE 与 ZSCORE 指令进行组装。因此使用起来非常方便。
- 个人排名:时使用 REVRANK 和 ZSCORE 操作进行读取
//查询前 20 积分排行榜,并显示个人排名 -- Redis
public List<DinerPointsRankVO> findDinerPointRankFromRedis(String accessToken){
// 获取登录用户信息
SignInDinerInfo dinerInfo = loadSignInDinerInfo(accessToken);
// 统计积分排行榜
Set<ZSetOperations.TypedTuple<Integer>> ranks = redisTemplate.opsForZSet().reverseRangeWithScores(
RedisKeyConstant.diner_points.getKey(),0,19
);
if (ranks == null || ranks.isEmpty()){
return Lists.newArrayList();
}
// 初始化食客 ID 集合
List<Integer> rankDinerIds = Lists.newArrayList();
// 根据 Key:食客 ID,Value:积分信息 构建一个 Map
Map<Integer, DinerPointsRankVO> ranksMap = new LinkedHashMap<>();
// 初始化排名
int rank = 1;
// 循环处理排行榜,添加排名信息
for (ZSetOperations.TypedTuple<Integer> rangeWithScore:ranks
) {
// 食客 ID
Integer dinerId = rangeWithScore.getValue();
// 积分
int point = rangeWithScore.getScore().intValue();
// 将食客 ID 添加至食客 ID 集合
rankDinerIds.add(dinerId);
DinerPointsRankVO dinerPointsRankVO = new DinerPointsRankVO();
dinerPointsRankVO.setId(dinerId);
dinerPointsRankVO.setRanks(rank);
dinerPointsRankVO.setTotal(point);
// 将 VO 对象添加至 Map 中
ranksMap.put(dinerId, dinerPointsRankVO);
// 排名++
rank++;
}
// 获取 Diners 用户信息
ResultInfo resultInfo = restTemplate.getForObject(dinersServerName +
"findByIds?access_token={accessToken}&ids={ids}",ResultInfo.class,
accessToken, StrUtil.join(",",rankDinerIds));
if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE){
throw new ParameterException(resultInfo.getCode(),resultInfo.getMessage());
}
List<LinkedHashMap> dinerInfoMaps = (List<LinkedHashMap>) resultInfo.getData();
// 完善食客昵称和头像
for (LinkedHashMap dinerInfoMap : dinerInfoMaps) {
ShortDinerInfo shortDinerInfo = BeanUtil.fillBeanWithMap(dinerInfoMap,
new ShortDinerInfo(), false);
DinerPointsRankVO rankVO = ranksMap.get(shortDinerInfo.getId());
rankVO.setNickname(shortDinerInfo.getNickname());
rankVO.setAvatarUrl(shortDinerInfo.getAvatarUrl());
}
DinerPointsRankVO myRank = null;
// 判断当前登录用户是否在 ranks 中,如果在,添加标记直接返回
if (ranksMap.containsKey(dinerInfo.getId())) {
myRank = ranksMap.get(dinerInfo.getId());
myRank.setIsMe(1);
return Lists.newArrayList(ranksMap.values());
}
// 如果不在 ranks 中,获取个人排名追加在最后
Long r = redisTemplate.opsForZSet().reverseRank(
RedisKeyConstant.diner_points.getKey(), dinerInfo.getId()
);
if (r != null) {
myRank = new DinerPointsRankVO();
BeanUtils.copyProperties(dinerInfo, myRank);
myRank.setRanks(r.intValue() + 1); // 排名是从 0 开始
myRank.setIsMe(1);
// 获取积分
Double point = redisTemplate.opsForZSet().score(
RedisKeyConstant.diner_points.getKey(), dinerInfo.getId()
);
myRank.setTotal(point.intValue());
ranksMap.put(dinerInfo.getId(), myRank);
}
return Lists.newArrayList(ranksMap.values());
}
为了方便对比,编写controller的方法
// 查询 TOPN 积分排行榜,同时显示个人排名
@GetMapping
public ResultInfo<List<DinerPointsRankVO>> dinerPointRank(String access_token){
List<DinerPointsRankVO> ranks = dinerPointsService.findDinerPointRankFromRedis(access_token);
return ResultInfoUtil.buildSuccess(request.getServletPath(),ranks);
}
使用JMeter压测对比
MySQL 接口压测结果:
Redis 接口压测结果:
使用Sorted Sets优势
- Redis本身内存数据库,读取性能高
- Sorted Sets底层是SkipList + ZipList既能保证有序又能对数据进行压缩存储
- Sorted Sets操作简单,几个命令搞定
拓展:
思考 :在好友关注, 新增 Feed 接口中添加积分
写在最后
积分功能
这个功能中我们实现了添加积分、获取积分排行榜功能。
这个功能中 Redis 主要用于存储积分信息,使用了 Sorted Set 数据类型
:::
下篇博客我将讲解基于 Redis 的 GEO 实现 附近的人 功能。
本文内容到此结束了,
如有收获欢迎点赞👍收藏💖关注✔️,您的鼓励是我最大的动力。
如有错误❌疑问💬欢迎各位指出。