优酷投票系统设计和重构

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介:      介绍优酷投票系统,承载了优酷暑假战役、寒假战役、双十一促销,以及平时各自营销套路活动,系统设计相对合理,实现一塌糊涂,有代码为证(参考:列举一些神奇的代码)。重构的理由:代码僵化,难以改动,新增一个字段改动多出代码代码脆弱,改动会出现意想不到的情况,改一个bug引发无数bug代码晦涩,代码难以阅读理解,多位前任改bug的代码实现套娃式的修复投票系统的设计基础名词:投票活动,投票渠道,投票

     介绍优酷投票系统,承载了优酷暑假战役、寒假战役、双十一促销,以及平时各自营销套路活动,系统设计相对合理,实现一塌糊涂,有代码为证(参考:列举一些神奇的代码)。

重构的理由:

  1. 代码僵化 ,难以改动,新增一个字段改动多出代码
  2. 代码脆弱 ,改动会出现意想不到的情况,改一个bug引发无数bug
  3. 代码晦涩 ,代码难以阅读理解,多位前任改bug的代码实现套娃式的修复

投票系统的设计

基础名词:投票活动,投票渠道,投票选项

动态名词:用户账户,用户流水,选项账户

备注:选项是抽象的投票对象,可以是用户,节目,CP等

         选项账户汇总表,有个日期关键字,是为了把榜单可以按照时间维度切割,设计实现支持到小时级别,就是每个选项在指定渠道下没小时一条记录,一天最多24条,考虑到一个投票活动大概一个月左右,每个选项产生720条记录,既能保障业务等灵活性又能兼顾性能

动词流程:投票,加票,查询(选项排行榜,粉丝排行榜)

 

投票系统的重构

遇到的问题:

  1. 表结构问题,只有用户id,没有用户类型,在二三方的合作中,用户流水傻傻分不清,用户账户相关的数据和接口不能给合作方使用,一切都是基于优酷id设计
  2. 性能问题,黄色的三张表:用户账户,用户流水,选项账户都是单表
  3. 流程问题,变更中存在大量加锁,既要读写ldb,redis,又要查询数据库,为了数据一致和前期实现不合理补丁改bug,写了大量锁和读数据刷缓存的逻辑
  4. 质量问题,实现不合理,每个方法单独看貌似挺合理的,多个方法组合到一起看,为了打补丁修正之前的不合理实现,多处加锁还是数据不一致,忍无可忍

这些问题都是站在技术视角看的,换成业务视角,做个投票活动都提心吊胆的,各种小问题不断,天天做消防:刚得的票丢失,每日免费票没帐,活动结束了排行榜票数还在变化等等

列举一些神奇的代码:

  1. 票数过期逻辑,得票后n天票失效:chargeOffTicket方法,类似这种缓存和数据库放在一起的代码在投票环节也有
 Date currDate = new Date();
			//这个set集合,放入的是需要清票的时间戳,记录需要清票的时间
       //这边看着没啥大问题,就是记录用户得票当天所得票数,以及把过期时间记录下,实现起来如此复杂
			//简化后: 得票时间+过期设置 = 过期时间:票数
      //我用了一个zset集合,member记录过期时间,value为所得票数
      //这样到期后要失效多少票,在set集合里面都可以计算出来
        Set<String> chargeOffSet = redisFactory.getRedisCacheService(votingDTO.getId()).smembers(getUserChargeOffCacheKey(votingDTO.getId(), ytid));
        if (chargeOffSet == null || chargeOffSet.size() == 0) {
            StringBuilder sb = new StringBuilder();
            sb.append("redisCache.smember get fail, chargeOffSet=")
                .append((chargeOffSet == null) ? "" : JSON.toJSONString(chargeOffSet));
            GlobalLogger.info("getUserAccountInfo", sb.toString());
            return;
        }
        String chargeOffKey = BaseUtil.buildKey("chargeOffTicket", votingDTO.getId(), ytid,
            DateUtil.getDefaultFormatDate(new Date()));
        int count = ldbTairManager.incrAndGet(chargeOffKey, 1, 86400);
        if (count > 1) {
//            logArgs.put("info", "今日已核销");
//            logArgs.put("ytid", ytid);
//            GlobalLogger.info("chargeOffTicket", logArgs.toJSONString());
            return;
        }

        List<String> chargeOffList = new ArrayList<>();
        for (String chargeOff : chargeOffSet) {
            chargeOffList.add(chargeOff);
        }
        List<String> sortChargeOffList = chargeOffList.stream().sorted(Comparator.reverseOrder()).collect(
            Collectors.toList());
        int index = -1;
        boolean chargeOffFlag = false;
				//这里是对时间戳的判断
        for (String str : sortChargeOffList) {
            index++;
            Long chargeOffTime = Long.valueOf(str);
            if (chargeOffTime <= currDate.getTime()) {
                chargeOffFlag = true;
                break;
            }
        }
        //这步可以防止用户每次进来都走核销的逻辑,这样每天最多走一次核销的逻辑
        if (!chargeOffFlag) {
            //不需要核销
            return;
        }

//这些看着好像也没啥问题,在看下面
 if (voteLimitDTO.getTicketExp() == 1) {
 //当天有效直接将用户账号清零,看看这段神奇的代码,删除用户账户缓存,再去查数据,再写缓存,
//其中这个getUserRemainTicketCount实现我放在2,看下面
                ldbTairManager.delete(getRemainTicketCacheKey(votingDTO.getId(), ytid));
                long chargeOffTicket = getUserRemainTicketCount(ytid, votingDTO.getId());

                if (chargeOffTicket < 0) {
                    logArgs.put("info", "核销票数小于0");
                    logArgs.put("chargeOffTicket", String.valueOf(chargeOffTicket));
                    logArgs.put("ytid", String.valueOf(ytid));
                    logArgs.put("voteId", String.valueOf(votingDTO.getId()));
                    GlobalLogger.errorArgs("chargeOffTicket", logArgs.toJSONString());
                    userTicketAccountMapper.resetTicket(votingDTO.getId(), ytid, currDate);
                } else {
                    userTicketAccountMapper.decreaseTicket(votingDTO.getId(), ytid, chargeOffTicket, currDate);
                }
                //核销票后删除用户票数信息缓存
                ldbTairManager.delete(getRemainTicketCacheKey(votingDTO.getId(), ytid));
} else {
//这段查询数据库,然后删除1天以上过去的票
UserTicketDetailDO userTicketDetailDO = userTicketDetailCustomMapper.getUserTicketCountByCondition(
                votingDTO.getId(), ytid, currDate);
            if (userTicketDetailDO == null) {
                return;
            }
            long userRemainTicket = getUserRemainTicketCount(ytid, votingDTO.getId());
            long chargeOffTicket;
            if (userRemainTicket >= userTicketDetailDO.getTicketCount()) {
                chargeOffTicket = userTicketDetailDO.getTicketCount();
            } else {
                chargeOffTicket = userRemainTicket;
            }
            userTicketAccountMapper.decreaseTicket(votingDTO.getId(), ytid, chargeOffTicket, currDate);

            TicketStatementDTO statementDTO = new TicketStatementDTO();
            statementDTO.setCurrDate(currDate);
            statementDTO.setVoteId(votingDTO.getId());
            statementDTO.setBalance(userTicketDetailDO.getTicketCount().longValue());
            statementDTO.setTicketCount((int)chargeOffTicket);
            statementDTO.setTicketType(VoteConstants.CHARGE_OFF_TICKET);
            statementDTO.setYtid(ytid);
            recordTicketStatement(statementDTO, votingDTO);
            //核销票后删除用户票数信息缓存
            ldbTairManager.delete(getRemainTicketCacheKey(votingDTO.getId(), ytid));
}
  1. 获取用户账户信息:getUserRemainTicketCount,这个神奇的循环加锁
//如果不加锁当多个请求同时调该方法时可能会导致用户缓存中的票比实际的多,导致超投
        String lockKey = BaseUtil.buildKey("userRemainTicketLock", String.valueOf(voteId), String.valueOf(ytid));
        try {
            int value = ldbTairManager.incrAndGet(lockKey, 1, 60);
            int index = 0;
            while (value > 1 && index < 10) {
                index++;
                JSONObject args = new JSONObject();
                args.put("info", "出现锁竞争");
                args.put("ytid", ytid);
                args.put("voteId", voteId);
                GlobalLogger.info("possibleError,getUserRemainTicketCount", args.toJSONString());
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                }
                value = ldbTairManager.incrAndGet(lockKey, 1, 60);
            }
            if (index == 10) {
                JSONObject args = new JSONObject();
                args.put("info", "严重隐患,可能导致超投");
                args.put("ytid", ytid);
                args.put("voteId", voteId);
                GlobalLogger.info("possibleError,getUserRemainTicketCount", args.toJSONString());
            }
            long returnTicketCount;
            userVoteCount = ldbTairManager.get(getRemainTicketCacheKey(voteId, ytid));
            if (userVoteCount == null || !(userVoteCount instanceof Integer)) {
                UserTicketAccountExample example = new UserTicketAccountExample();
                example.createCriteria().andVoteIdEqualTo(voteId).andYtidEqualTo(ytid);
                List<UserTicketAccountDO> accountDO = userTicketAccountMapper.selectByExample(example);
                Long ticketCount;
                if (accountDO == null || accountDO.size() == 0) {
                    ticketCount = 0L;
                } else {
                    ticketCount = accountDO.get(0).getBalance();
                }
                ldbTairManager.incr(getRemainTicketCacheKey(voteId, ytid), ticketCount.intValue(), DateUtil.getHour());
                returnTicketCount = ticketCount;
            } else {
                returnTicketCount = (Integer)userVoteCount;
            }

            return returnTicketCount;
        } finally {
            ldbTairManager.delete(lockKey);
        }

梳理投票系统:

  1. 整体设计思路没啥大问题可以保留,这起神奇的实现需要重新规划设计
  2. 用了一周的时间梳理每个方法,了解业务的本意,画出各个方法直接的连线
  3. 整体重构的思路: 同步操作全部在缓存中完成,持久化全部异步完成,用户数据和选项账户汇总分库分表

梳理后的业务实现

重构后的业务实现

[文件: 投票.xmind] 请在PC端预览或下载

[文件: 投票-优化.xmind] 请在PC端预览或下载

重构后的业务图

 

相关文章
|
负载均衡 算法 数据安全/隐私保护
|
机器学习/深度学习 数据采集 数据挖掘
Python | 机器学习之数据清洗
Python | 机器学习之数据清洗
488 0
|
5月前
|
人工智能 数据库 开发工具
通过阿里云 Milvus 和 Dify 平台构建RAG系统
本文介绍了如何结合阿里云 Milvus 向量数据库与低代码 AI 平台 Dify,快速构建企业级检索增强生成(RAG)应用。通过该方案,可有效解决大语言模型的知识局限与“幻觉”问题,提升 AI 应用的回答准确性与可靠性。
|
机器学习/深度学习 人工智能 自然语言处理
Tokenformer:基于参数标记化的高效可扩展Transformer架构
本文是对发表于arXiv的论文 "TOKENFORMER: RETHINKING TRANSFORMER SCALING WITH TOKENIZED MODEL PARAMETERS" 的深入解读与扩展分析。主要探讨了一种革新性的Transformer架构设计方案,该方案通过参数标记化实现了模型的高效扩展和计算优化。
587 0
|
Linux 网络安全 Windows
VScode远程开发之remote 远程开发(二)
VScode远程开发之remote 远程开发(二)
165 0
|
开发框架 .NET API
在IIS上部署ASP.NET Core Web API和Blazor Wasm详细教程
在IIS上部署ASP.NET Core Web API和Blazor Wasm详细教程
792 3
|
缓存 UED 网络架构
网站404该怎么解决
网站404错误通常表示用户尝试访问的网页不存在或无法找到
1746 0
|
设计模式 前端开发 数据库
微服务架构谈(4) plus:DDD 分层架构如何推动架构演进
微服务架构谈(4) plus:DDD 分层架构如何推动架构演进
1344 0
微服务架构谈(4) plus:DDD 分层架构如何推动架构演进
|
C++
gRPC 四模式之 服务器端流RPC模式
gRPC 四模式之 服务器端流RPC模式
371 0
|
自然语言处理 前端开发 Java
网上投票系统的设计与实现(论文+源码)_kaic
随着全球Internet的迅猛发展和计算机应用的普及,特别是近几年无线网络的广阔覆盖以及无线终端设备的爆炸式增长,使得人们能够随时随地的访问网络,以获取最新信息、参与网络活动、和他人在线互动。为了能及时地了解民情民意,把握人们近期关注的内容,政府机构以及各大门户网站等单位会将一些热点话题以投票的形式发布到他们的网站上面,供人们在线投票。因此,网络在线投票系统应运而生。 本文在此情况下设计了一款网上线投票系统。首先,结合实际的应用开发情况,对该系统做了详细的需求分析。然后给出该系统的结构和各功能模块的分析,通过详细的结构和数据库表的设计,最终构建出一个基于Web的、以Struts2框架和MySQ