优酷投票系统设计和重构

本文涉及的产品
云数据库 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端预览或下载

重构后的业务图

 

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
23天前
|
自然语言处理 安全
线下陪玩游戏系统开发多语言/海外版/成熟技术/方案项目/源码功能
Continuing to develop an offline companion game system may involve the following aspects:
|
9月前
|
区块链
Jogger/Stepn跑鞋零撸项目系统开发实现技术案例/逻辑方案/案例介绍/源码运营版
 区块链的可追溯性来源于区块链数据结构的特殊性。在区块链系统中,它的链式结构是从创世区块开始的,其后系统产生的所有区块都通过父区块的哈希值前后相连,并最终能追溯到创世区块。
三三复制互助拆分公排双轨系统开发(开发案例)丨DAPP互助三三复制公排拆分双轨模式系统开发运营版/成熟技术/源码详细
 DAPP是去中心化应用程序(Decentralized Application),它是建立在区块练技术之上的应用程序,具有去中心化、开放性、透明性、安全性等特点,DAPP可以实现各种功能
|
10月前
|
新零售 人工智能 供应链
东郊到家系统开发(规则及玩法)/方案详解/案例设计/成熟技术,东郊到家APP开发源码
 新零售就是企业借助互联网,通过大数据、人工智能等一些手段,对产品的生产、流通以及销售的过程俩进行升级改造,从而可以把线上服务、线下服务以及现代的物流进行深度的融合的新零售模式。
|
10月前
|
存储 算法 安全
Jogger跑鞋链游开发详情丨Jogger链游跑鞋系统开发方案详细/项目逻辑/功能分析/案例设计/源码平台
  区块链就是把加密数据(区块)按照时间顺序进行叠加(链)生成的永久、不可逆向修改的记录。某种意义上说,区块链技术是互联网时代一种新的“信息传递”技术,
|
10月前
|
存储 安全 区块链
Jogger慢跑者跑鞋/链游项目系统开发(开发方案),Jogger跑鞋NFT链游模式系统开发详细案例及源码技术
  区块链是一种将数据区块按照时间顺序组合成的链式结构,是去中心化系统中各节点共享且共同维护的分布式数据账本,具体的:各节点由P2P组网方式相互连通和交互,受激励机制激励贡献自身算力,
|
10月前
|
安全
DAPP公排互助拆分系统开发详情原理丨DAPP拆分互助公排系统开发玩法功能/方案设计/案例分析/成熟技术/源码版
The lifecycle of smart contracts can be summarized into six stages based on their operational mechanisms: negotiation, development, deployment, operation and maintenance, learning, and self destruction. The development stage includes contract testing before contract chaining, while the learning sta
|
12月前
|
存储 安全 算法
TechFinger游戏搬砖平台系统开发方案详细丨TechFinger搬砖游戏系统开发案例项目/源码功能/成熟技术
去中心化:以分布式网络为基础结构,对数据进行验证、记账、存储、维护和传输等操作,利用纯数学方法建立节点之间的交互信任关系,进而形成去中心化、可信任的分布式系统;
|
运维 监控 小程序
淘宝小游戏背后的质量保障方案
2022年4月,淘宝开启了小程序游戏项目,旨在从互动公域和店铺私域引入了大量的三方游戏服务商入淘 ,初步构建淘宝游戏的三方生态。对于开放质量团队来说,“游戏生态管控 & 游戏容器测试”是一个新的命题。
821 1
淘宝小游戏背后的质量保障方案

热门文章

最新文章