第六章--美食服务--积分服务

需求分析

  • 添加积分:在上述签到的基础上添加用户积分(签到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修改日期

其实这个类似于一张日志表,因此数据量是非常庞大的,当我们想要统计用户积分做排行榜的的时候:
image.png
编写统计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;
    }

image.png

    /**
     * 用户签到/可以补签
     *
     * @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
image.png
image.png

编写积分排行榜 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读取方法

  1. 从数据库中查询前20的用户排行榜
  2. 判断当前登录用户是否在这20名
  3. 获取当前登录用户的排名
/**
 * 积分服务 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 接口压测结果:
image.png
Redis 接口压测结果:
image.png

使用Sorted Sets优势

  • Redis本身内存数据库,读取性能高
  • Sorted Sets底层是SkipList + ZipList既能保证有序又能对数据进行压缩存储
  • Sorted Sets操作简单,几个命令搞定

拓展:

思考 :在好友关注, 新增 Feed 接口中添加积分

写在最后

积分功能
这个功能中我们实现了添加积分、获取积分排行榜功能。
这个功能中 Redis 主要用于存储积分信息,使用了 Sorted Set 数据类型

:::

下篇博客我将讲解基于 Redis 的 GEO 实现 附近的人 功能。
本文内容到此结束了,
如有收获欢迎点赞👍收藏💖关注✔️,您的鼓励是我最大的动力。
如有错误❌疑问💬欢迎各位指出。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值