Redis实现日榜|直播间榜单|排行榜|Redis实现日榜01

前言

直播间贡献榜是一种常见的直播平台功能,用于展示观众在直播过程中的贡献情况。它可以根据观众的互动行为和贡献值进行排名,并实时更新,以鼓励观众积极参与直播活动。

在直播间贡献榜中,每个观众都有一个对应的贡献值,贡献值用来衡量观众在直播过程中的贡献程度。观众的贡献值可以通过多种途径获得,比如送礼物、打赏主播等。

首先,我们需要创建一个贡献榜单,可以使用Redis的有序集合 (Sorted Set)结构来实现。在有序集合中,每个观众对应一个唯一的ID作为成员,而成员的分数表示观众的贡献值。可以根据观众每次送出礼物增加相应的贡献值。

当有新的观众参与直播并进行互动时,我们可以使用ZADD命令将其用户ID添加到贡献榜单中,并更新相应的贡献值。可以根据贡献值对观众进行排序,从而得到当前排名靠前的观众。

要实时更新贡献榜单,可以使用ZINCRBY命令增加观众的贡献值。当观众进行互动行为时,我们可以调用ZINCRBY命令增加相应观众的贡献值,并确保贡献榜单及时反映观众的最新贡献情况。

Redis实现命令

用户ID为Test1000的得到价值为1314的礼物时,以及获取排行榜时,命令如下。比如

# 增加排行榜用户数据ZINCRBY ROUND_LIST_CACHE_20221222 1314 Test1000# 展示用户榜单ZRANGE ROUND_LIST_CACHE_20221222 0 -1 WITHSCORES

JAVA简单逻辑代码实现 

1.Spring boot的yml配置文件,配置礼物队列

       
#yml配置文件配置队列 
GiftFlowOutput: 
  content-type: application/json
  destination: gift_all_flow
GiftFlowInput: #礼物队列
  content-type: application/json
  group: GiftAllFlowGroup

2.redis使用lua脚本增加榜单,保证多机并发原子性

//redis lua脚本配置
@Slf4j
@Configuration
public class RedisConfig {

    @Autowired
    private JdkCacheHandler jdkCacheHandler;


    @Bean("zsetScoreScript")
    public RedisScript<Long> zsetScoreScript() {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/zadd.lua")));
        redisScript.setResultType(Long.class);
        return redisScript;
    }
}

3。LUA脚本具体实现,保留3位有效礼物小数位,后面小数位用于同个时间刷礼物进行排序,目前这里只精确到了秒

local key=KEYS[1]
local member=KEYS[2]
local newValue=tonumber(string.format("%.16f",ARGV[1]))
local oldValue=redis.call('ZSCORE',key,member)
if type(oldValue) == 'boolean' then
    redis.call('ZADD',key,newValue,member)
    return 1
else
    redis.call('ZADD',key,tonumber(string.format("%.3f",oldValue))+newValue,member)
    return 1
end
return 0

 4.调用lua脚本,增加排行榜积分

@Component
@Slf4j
public class RankScoreUtilManager {

    private final static DecimalFormat format = new DecimalFormat(ActivityBase.TOTAL_FORMAT);

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private ActivityTimeCache activityTimeCache;

    @Resource(name = "zsetScoreScript")
    private RedisScript<Long> zaddScript;

    /**
     * 添加分数到排行榜,可以并发的
     */
    public void addScoreToRank(String cacheKey, String anchorId, BigDecimal integral, Date eventTime) {
        try {
            BigDecimal bigDecimal = dealScore(integral, activityTimeCache.getActivityDTO().getEndTime(), eventTime);
            String score = format.format(bigDecimal.doubleValue());
            Long execute = redisTemplate.execute(zaddScript, Arrays.asList(cacheKey, anchorId), score);
            log.warn("增加积分到排行榜integral={},anchorId={},score={},execute",integral,anchorId,score,execute);
        } catch (Exception e) {
            log.error("增加异常", e);
        }
    }

    private static BigDecimal dealScore(BigDecimal newScore, LocalDateTime activityEndTime, Date eventDate) {
        DecimalFormat format = new DecimalFormat(ActivityBase.VALID_FORMAT);
        String formatStr = format.format(EeBigDecimalUtil.scale(newScore, ActivityBase.VALID_SCALE, RoundingMode.DOWN).doubleValue());
        StringBuilder sb = new StringBuilder(32);
        //后面补个0,避免lua进1出错
        sb.append(formatStr).append('0');
        long n = EeDateUtil.getMilli(activityEndTime) - eventDate.getTime();
        String s = Long.toString(Math.abs(n) / 1000);
        for (int i = s.length(); i < ActivityBase.TIME_SCALE; i++) {
            sb.append('0');
        }
        sb.append(s);
        return new BigDecimal(sb.toString()).setScale(ActivityBase.TOTAL_SCALE, RoundingMode.DOWN);
    }

}

5.配置礼物队列名称 

/**
* 监听礼物流水队列
*/
public interface AllGiftFlowProcessor {

    String OUTPUT = "GiftFlowOutput";

    @Output(OUTPUT)
    MessageChannel output();

    String INPUT = "GiftFlowInput";

    @Input(INPUT)
    SubscribableChannel input();
}

 6.监听礼物队列的listener,前面做了一些活动时间校验的判断,最关键的是最下面roundListBusiness.dealAnchorRoundList(dto);的方法


//监听礼物队列,处理相关业务逻辑,榜单的处理在最下面

@Slf4j
@Service
public class AllGiftFlowListener {


    @Autowired
    private RedisTemplate<String, String> redisTemplate;


    @Autowired
    private AnchorLevelBusiness anchorLevelBusiness;

    private static final String cacheKey = "GIFT:TASK:INTER:EVENT:";

    @Autowired
    private EeEnvironmentHolder eeEnvironmentHolder;


    @Autowired
    private ActivityRoundDao activityRoundDao;

    @Autowired
    private ActivityTimeCache activityTimeCache;

    @Autowired
    private GiftConfigCache giftConfigCache;
    @Autowired
    private GiftFlowProcessor giftFlowProcessor;

    @Autowired
    private AnchorCache anchorCache;

    @Autowired
    private RoundListBusiness roundListBusiness;

    @Autowired
    private EeLog eeLog;



    @StreamListener(AllGiftFlowProcessor.INPUT)
    public void onReceive(ActivityGiftEventDTO dto) {
        MqConsumeRunner.run(dto.getEventId().toString(), dto, o -> dealMsgEvent(o), "TaskIntegralProcessor [{}]", dto);
    }

    private void dealMsgEvent(ActivityGiftEventDTO dto) {

        // 过滤非活动时间礼物
        ActivityDTO activityDTO = activityTimeCache.getActivityDTO();
        if (null == activityDTO) {
            return;
        }
        if (EeDateUtil.toLocalDateTime(dto.getEventDate()).isBefore(activityDTO.getStartTime())) {
            eeLog.info("礼物时间小于活动开始时间,丢弃礼物");
            return;
        }

        // 判断活动时间
        if (ActivityStatusEnum.NO_START == activityRoundDao.getActivityStatus()) {
            return;
        }

        // 过滤活动礼物
        if (giftConfigCache.getData().stream().noneMatch(o -> o.getGiftId().equals(dto.getGiftId()))) {
            eeLog.info("礼物id:{}不计算", dto.getGiftId());
            return;
        }

        Integer region = anchorCache.getRegionById(dto.getTarget());
        // 是否为签区域主播
        if (null == region || !ActivityBase.AnchorRegion.contains(region)) {
            eeLog.warn("该主播非签约或非参赛区域:{}", dto.getTarget());
            return;
        }

        // 是否重复消费礼物
        Boolean success = redisTemplate.opsForValue().setIfAbsent(cacheKey + dto.getEventId(), "", 15, TimeUnit.DAYS);
        if (success != null && !success) {
            eeLog.info("升级事件已处理:" + dto);
            return;
        }

        try {
            //监听礼物并且处理榜单(最主要的代码就这一句)
            roundListBusiness.dealAnchorRoundList(dto);
        } catch (Exception e) {
            log.error("处理榜单 fail.[" + dto + "]", e);
        }


    }

}

 7.榜单的具体实现逻辑

@Component
@Slf4j
public class RoundListBusiness {

    //平台主播榜单
    private final static String CHRISTMAS_ROUND_ANCHOR_LIST = "CHRISTMAS:ROUND:ANCHOR:LIST";
    

    private final static String CHRISTMAS_ROUND_LIST_LOCK = "CHRISTMAS:ROUND:LIST:LOCK";


    @Autowired
    private RankScoreUtilManager rankScoreUtilManager;

    @Autowired
    private ActivityTimeCache activityTimeCache;

    @Autowired
    RedisTemplate<String, String> redisTemplate;

    @Autowired
    private AllGiftFlowProcessor allGiftFlowProcessor;
    

    /**
    * 处理榜单加分逻辑
    */
    public void dealAnchorRoundList(ActivityGiftEventDTO dto) {

        ActivityDTO activityDTO = activityTimeCache.getActivityDTO();
        if (EeDateUtil.toLocalDateTime(dto.getEventDate()).isBefore(activityDTO.getStartTime())) {
            return;
        }

        if (!EeDateUtil.toLocalDateTime(dto.getEventDate()).isBefore(activityDTO.getEndTime())) {
            return;
        }


        //记录总的榜单流水
        try {
            //插入总的流水
            allGiftFlowProcessor.output().send(MessageBuilder.withPayload(dto).build());
        } catch (Exception e) {
            log.error("插入总的礼物流水异常dto={}", dto, e);
        }

        LocalDateTime now = LocalDateTime.now();
        if (!now.isBefore(activityDTO.getEndTime())) {
            //2.判断是否符合处理上一轮榜单的逻辑
            if (isThrowAwayBeforeGift(dto.getEventId(), now, activityDTO.getEndTime())) {
                log.warn("这里跳出了dto={},now={}", dto, EeDateUtil.format(now));
                return;
            }
        }

        dealRoundList(dto, dto.getTotalStarAmount());
    }

   

    /**
    * 处理主播榜单加分逻辑
    */
    private void dealRoundList(ActivityGiftEventDTO dto, BigDecimal value) {
        //增加平台主播榜单
        incrAnchorListValue(CHRISTMAS_ROUND_ANCHOR_LIST, dto.getTarget(), value, dto.getEventDate());
    }

    
    /**
    * 具体加分方法
    */
    public void incrAnchorListValue(String listCacheKey, String userId, BigDecimal value, Date eventTime) {
        if (EeStringUtil.isNotEmpty(listCacheKey)) {
            //增加榜单分数
            rankScoreUtilManager.addScoreToRank(listCacheKey, userId, value, eventTime);
        }
    }
    
    /**
    * 判断是否已经超过结算时间
    */
    private boolean isThrowAwayBeforeGift(String eventId, LocalDateTime now, LocalDateTime endTime) {
        //如果当前时间超过了结算时间,直接丢弃礼物
        if (!now.isBefore(endTime.plusSeconds(ActivityBase.PROCESS_TS))) {
            log.error("主播榜单-当前时间超过了结算时间,直接丢弃礼物: {}", eventId);
            return true;
        }

        //如果上一轮的榜单已经锁定,丢弃礼物
        if (checkBlockRankList(CHRISTMAS_ROUND_ANCHOR_LIST)) {
            log.error("主播榜单-榜单被锁定后丢弃礼物: {}, {}", eventId, EeDateUtil.format(LocalDateTime.now()));
            return true;
        }
        return false;
    }

    /**
    * 判断结算时榜单是否已经被锁定
    */
    public boolean checkBlockRankList(String listCacheKey) {
        Boolean cache = redisTemplate.opsForHash().hasKey(CHRISTMAS_ROUND_LIST_LOCK, listCacheKey);
        return null != cache && cache;
    }

    /**
     * 锁定榜单,把锁定的榜单都放入一个hash中
     */
    public void setBlockRankList(String cacheKey) {
        redisTemplate.opsForHash().put(CHRISTMAS_ROUND_LIST_LOCK, cacheKey, EeDateUtil.format(LocalDateTime.now()));
    }
}

总结:目前这段代码只是实现了简单的日榜逻辑,还有一段结算的代码我没有复制出来,结算榜单无非就是在每天0点的时候结算前一天的榜单,对榜单前几名的主播进行礼物发放,后续将会更新几种复杂榜单的实现方式,包括:晋级榜单,积分晋级榜单,滚动日榜,滚动周榜,滚动月榜的一些实现方式

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/264800.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux---优先级+并发+进程调度队列

目录 一、优先级 二、并发 三、Linux2.6内核进程调度队列 一、优先级 我们发现操作系统中有很多等待队列&#xff0c;也就是说进程需要排队&#xff0c;而排队的本质就是确认优先级&#xff0c;优先级高的排在前面&#xff0c;低的排在后面 为什么要有优先级&#xff1f; 本…

msyql 24day 数据库主从 主从复制 读写分离 master slave 有数据如何增加

目录 环境介绍读写分离纵向扩展横向扩展 数据库主从准备环境主库环境(master)从库配置(slave)状态分析重新配置问题分析 报错解决从库验证 有数据的情况下 去做主从清理环境环境准备数据库中的锁的机制主库配置从库配置最后给主库解锁常见错误 环境介绍 将一个数据库的数据 复…

数据库开发之SQL简介以及DDL的详细解析

1.3 SQL简介 SQL&#xff1a;结构化查询语言。一门操作关系型数据库的编程语言&#xff0c;定义操作所有关系型数据库的统一标准。 在学习具体的SQL语句之前&#xff0c;先来了解一下SQL语言的语法。 1.3.1 SQL通用语法 1、SQL语句可以单行或多行书写&#xff0c;以分号结尾…

80x86汇编—指令系统

顺序是按照我们老师教的顺序&#xff0c;仅仅作为复习笔记。 汇编入门真的简单&#xff0c;深入难&#xff0c;毕竟学过计组CPU都只寄组的难处&#xff0c;指令系统不在话下了。 MOV 下图说明了一个MOV指令能够从哪里传到哪里&#xff0c;总结成一句话就是&#xff1a;立即数不…

【贪心算法】之 摆动序列(中等题)

实际操作上&#xff0c;其实连删除的操作都不用做&#xff0c;因为题目要求的是最长摆动子序列的长度&#xff0c;所以只需要统计数组的峰值数量就可以了&#xff08;相当于是删除单一坡度上的节点&#xff0c;然后统计长度&#xff09; 这就是贪心所贪的地方&#xff0c;让峰…

使用StableDiffusion进行图片Inpainting原理

论文链接&#xff1a;RePaint: Inpainting using Denoising Diffusion Probabilistic Models代码链接&#xff1a;RePaint Inpainting任务是指在任意一个二进制的掩码指定的图片区域上重新生成新的内容&#xff0c;且新生成的内容需要和周围内容保持协调。当前SOTA模型用单一类…

as安装后第一次创建项目,出现gradle下载错误,或无法创建run/debug的启动

大概报错Could not resolve com.android.tools.build:gradle:8.0.1 原因两种第一种就是刚创建好后没有等待他自动下载完成就做了其他操作导致异常&#xff0c;第二组就是瞎几把乱改改错了 我就属于第二种 修改回来的方式&#xff1a; 就这个地方我改成了jdk1.8&#xff0c;然…

java智慧工地 人脸识别终端,智慧工地解决方案源码

智慧工地即施工现场全面数字化过程&#xff0c;使用IOT、云、移动、大数据、AI等关键技术,进行生产要素、管理过程、建筑物实体的数据采集、数据治理&#xff0c;最终通过大数据和人工智能帮助项目实现精益管理。 智慧工地围绕工程现场人、机、料、法、环及施工过程中质量、安全…

git分支解析

1、概述和优点 在版本控制过程中&#xff0c;同时会推进多个任务&#xff0c;为此&#xff0c;就可以为每个任务创建单独的分支。开发人员可以把自己的任务和主线任务分离开来&#xff0c;在开发自己的分支的时候不会影响主分支。 分支的好处&#xff1a; 同时推进多个功能开发…

【GitHub精选项目】短信系统测试工具:SMSBoom 操作指南

前言 本文为大家带来的是 OpenEthan 开发的 SMSBoom 项目 —— 一种用于短信服务测试的工具。这个工具能够发送大量短信&#xff0c;通常用于测试短信服务的稳定性和处理能力。在合法和道德的范畴内&#xff0c;SMSBoom 可以作为一种有效的测试工具&#xff0c;帮助开发者和系统…

WebAssembly 的魅力:高效、安全、跨平台(下)

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

项目应用多级缓存示例

前不久做的一个项目&#xff0c;需要在前端实时展示硬件设备的数据。设备很多&#xff0c;并且每个设备的数据也很多&#xff0c;总之就是数据很多。同时&#xff0c;设备的刷新频率很快&#xff0c;需要每2秒读取一遍数据。 问题来了&#xff0c;我们如何读取数据&#xff0c…

一文读懂Java中的设计模式——代理模式,以翻译场景举例,特别通俗易懂!

代理模式概念 在代理模式&#xff08;Proxy Pattern&#xff09;中&#xff0c;一个类代表另一个类的功能。这种类型的设计模式属于结构型模式。在代理模式中&#xff0c;我们创建具有现有对象的对象&#xff0c;以便向外界提供功能接口。目的&#xff1a;为其他对象提供一种代…

浅谈Guava Cache的参数使用

CacheLoader 用于数据加载方式比较固定且统一的场景&#xff0c;在缓存容器创建的时候就需要指定此具体的加载逻辑。通常开发中使用时我们需要继承CacheLoader类或写一个匿名实现类实现其load方法和reload方法 load方法 当执行get操作没有命中缓存或者判断缓存已经超出expir…

论文阅读——llava

Visual Instruction Tuning LLaVA 指令智能体分为两类&#xff1a;端到端的&#xff0c;通过LangChain[1]/LLM[35]协调各种模型的系统。 数据集生成用GPT辅助生成的&#xff0c;具体不写了。 模型结构&#xff1a; input image Xv LLM&#xff1a;Vicuna visual encoder&a…

芯片到底是怎么访问外设

微型计算机的组成&#xff1a;CPURAM硬盘等 什么是FLASH&#xff1f; FLASH存储器又称闪存&#xff0c;它结合了ROM和RAM的长处&#xff0c;不仅具备电子可擦除可编程&#xff08;EEPROM&#xff09;的性能&#xff0c;还不会断电丢失数据同时可以快速读取数据&#xff08;NV…

系列一、GitHub搜索技巧

一、GitHub搜索技巧 1.1、概述 作为程序员&#xff0c;GitHub大家应该都再熟悉不过了&#xff0c;很多时候当我们需要使用某一项技能而又无从下手时&#xff0c;通常会在百度&#xff08;面向百度编程&#xff09;或者在GitHub上通过关键字寻找相关案例&#xff0c;比如我想学…

nodejs+vue+ElementUi医院预约挂号系统3e3g0

本医院预约挂号系统有管理员&#xff0c;医生和用户。该系统将采用B/S结构模式&#xff0c;使用Vue和ElementUI框架搭建前端页面&#xff0c;后端使用Nodejs来搭建服务器&#xff0c;并使用MySQL&#xff0c;通过axios完成前后端的交互 管理员功能有个人中心&#xff0c;用户管…

springMVC-异常处理

一、四种异常形式 在springmvc中&#xff0c;处理异常有四种形式 1&#xff0e;局部异常 2.全局异常 3.自定义异常 4.统一异常(统一提示异常&#xff09; 作用&#xff1a;可以使浏览器不出现丑陋的500错误提示&#xff0c;而跳转到另外的错误提示页面 另外&#xff0c;自定义…

Linux——缓冲区

我在上篇博客留下了一个问题&#xff0c;那个问题就是关于缓冲区的问题&#xff0c;我们发现 文件有缓冲区&#xff0c;语言有用户级缓冲区&#xff0c;那么缓冲区到底是什么&#xff1f;&#xff0c;或者该怎 么认识缓冲区&#xff1f;这篇文章或许会让你有所认识&#xff0c;…