【从0-1 千万级直播项目实战】从MVP到高质量代码:审查中的经验教训

背景

随着项目的迅猛发展和团队的扩张,前期版本的功能迭代速度非常快。众多的MVP版本陆续上线,当时的核心原则是确保代码能够顺利地通过测试并在生产环境中无故障地运行。但现在,随着项目和团队的持续成长,代码质量的重要性日益凸显,这也意味着我们的代码审核标准必须更加严格。在这里,我将分享一些代码审查中发现的常见问题,以及如何改进这些问题。

review原则

  1. 遵循约定,使用团队制定的标准
  2. 提交review请求时自我审查一遍
  3. 所有review的问题和修改,必须由提交review的人进行最终确认
  4. 基于 约定>可用性>性能>可读性 原则

review案例

案例一:数据多查、重复查

在循环中查询缓存或调用rpc接口是一种常见的效率降低的原因。在这种情境中,如果giftIduserId在列表中有多个相同的记录,会导致多次重复的I/O操作。

java 复制代码
for (RoomGiftRecordListVo vo : list) {

    //查询礼物缓存
    GiftVo giftVo = giftService.getGiftInfo(
            vo.getGiftId());
      //查询用户信息缓存
    UserCacheDto sendUser = userClient.loadUser(vo.getSender()), receiverUser = userClient.loadUser(vo.getReceiver());
    vo.setGiftName(giftVo.getName());
    vo.setGiftIcon(giftVo.getIcon());
    vo.setSenderName(sendUser.getNickName());
    vo.setReceiverName(receiverUser.getNickName());
    vo.setSenderAvatar(sendUser.getAvatar());
}

优化:使用局部的Map缓存来减少重复查询

java 复制代码
Map<Long, GiftVo> giftCache = new HashMap<>();
Map<Long, UserCacheDto> userCache = new HashMap<>();

list.forEach(vo -> {
    GiftVo giftVo = giftCache.computeIfAbsent(vo.getGiftId(), key -> giftService.getGiftInfo(key));

    UserCacheDto sendUser = userCache.computeIfAbsent(vo.getSender(), key -> userClient.loadUser(key));
    UserCacheDto receiverUser = userCache.computeIfAbsent(vo.getReceiver(), key -> userClient.loadUser(key));

    vo.setGiftName(giftVo.getName());
    vo.setGiftIcon(giftVo.getIcon());
    vo.setSenderName(sendUser.getNickName());
    vo.setReceiverName(receiverUser.getNickName());
    vo.setSenderAvatar(sendUser.getAvatar());
});

案例二:接口设计不合理

为了获取相似的业务数据而设计了多个接口,这不仅增加了系统的复杂性,还降低了效率。

java 复制代码
if (!CollectionUtils.isEmpty(giftList)) {
    //获取活动的在线礼物ID
    Long onlineGiftId = giftClient.getOnlineGiftId(),
            //获取活动的大礼物ID
            bigGiftId = giftClient.getBigGiftId();
    for (GiftVo giftVo : giftList) {
        if (giftVo.getId().equals(onlineGiftId)) {
            giftVo.setBannerUrl(ToastMessage.ONLINE_GIFT.getTranslateMessage(userCacheDto.getSystemLanguage()));
        }
        if (giftVo.getId().equals(bigGiftId)) {
            giftVo.setBannerUrl(ToastMessage.BIG_GIFT_BANNER.getTranslateMessage(userCacheDto.getSystemLanguage()));
        }
        build.addGiftList(GrpcBuildMessageUtil.buildGiftInfo(giftVo));
    }
}

优化:一个合理的接口应该能够返回所有需要的配置

java 复制代码
if (!CollectionUtils.isEmpty(giftList)) {
    //获取所有xxx活动配置
    GiftActivityDto giftActivityDto = giftClient.getGiftActivityConfig();
    //获取活动的在线礼物ID
    Long onlineGiftId = giftActivityDto.getOnlineGiftId(),
            //获取活动的大礼物ID
            bigGiftId = giftActivityDto.getBigGiftId();
    for (GiftVo giftVo : giftList) {
        if (giftVo.getId().equals(onlineGiftId)) {
            giftVo.setBannerUrl(ToastMessage.ONLINE_GIFT.getTranslateMessage(userCacheDto.getSystemLanguage()));
        }
        if (giftVo.getId().equals(bigGiftId)) {
            giftVo.setBannerUrl(ToastMessage.BIG_GIFT_BANNER.getTranslateMessage(userCacheDto.getSystemLanguage()));
        }
        build.addGiftList(GrpcBuildMessageUtil.buildGiftInfo(giftVo));
    }
}

案例三:IF-ELSE套娃

过于复杂的IF逻辑判断会降低代码的可读性和复用性。

java 复制代码
if (approve.getCurrentStatus()
            == CurrentStatusType.OPEN.getType()) {
        Boolean flag = false;
        SwitchBo switchBo = new SwitchBo();
        switchBo.setSwitchType(aSwitch.getSwitchType());
        // 判断提审时状态
        if (aSwitch.getApproveStatus() == ApproveStatusType.NOT.getType()) {
            flag = isSwitch(rbo, lbo);
        } else if (aSwitch.getApproveStatus() == ApproveStatusType.OPEN.getType()) {
            if (isStrNUll(approve.getAndroidBuildNumber()) && !isStrNUll(approve.getAndroidVersion()) && isStrNUll(rbo.getAndroidBuildNumber()) && approve.getAndroidBuildNumber().equals(rbo.getAndroidBuildNumber()) && rbo.getClientPlatformType() == ClientPlatformType.ANDROID.getType()) {
                flag = true;
            } else if (isStrNUll(approve.getAndroidVersion()) && !isStrNUll(approve.getAndroidBuildNumber()) && isStrNUll(rbo.getAndroidVersion()) && approve.getAndroidVersion().equals(rbo.getAndroidVersion().split("-")[0]) && rbo.getClientPlatformType() == ClientPlatformType.ANDROID.getType()) {
                flag = true;
            } else if (isStrNUll(approve.getIosBuildNumber()) && !isStrNUll(approve.getIosVersion()) && isStrNUll(rbo.getIosBuildNumber()) && approve.getIosBuildNumber().equals(rbo.getIosBuildNumber()) && rbo.getClientPlatformType() == ClientPlatformType.IOS.getType()) {
                flag = true;
            } else if (isStrNUll(approve.getIosVersion()) && !isStrNUll(approve.getIosBuildNumber()) && isStrNUll(rbo.getIosVersion()) && approve.getIosVersion().equals(rbo.getIosVersion().split("-")[0]) && rbo.getClientPlatformType() == ClientPlatformType.IOS.getType()) {
                flag = true;
            } else {
                flag = isSwitch(rbo, lbo);
            }
        } else if (aSwitch.getApproveStatus() == ApproveStatusType.CLOSE.getType()) {
            if (isStrNUll(approve.getAndroidBuildNumber()) && !isStrNUll(approve.getAndroidVersion()) && isStrNUll(rbo.getAndroidBuildNumber()) && approve.getAndroidBuildNumber().equals(rbo.getAndroidBuildNumber()) && rbo.getClientPlatformType() == ClientPlatformType.ANDROID.getType()) {
                flag = false;
            } else if (isStrNUll(approve.getAndroidVersion()) && !isStrNUll(approve.getAndroidBuildNumber()) && isStrNUll(rbo.getAndroidVersion()) && approve.getAndroidVersion().equals(rbo.getAndroidVersion().split("-")[0]) && rbo.getClientPlatformType() == ClientPlatformType.ANDROID.getType()) {
                flag = false;
            } else if (isStrNUll(approve.getIosBuildNumber()) && !isStrNUll(approve.getIosVersion()) && isStrNUll(rbo.getIosBuildNumber()) && approve.getIosBuildNumber().equals(rbo.getIosBuildNumber()) && rbo.getClientPlatformType() == ClientPlatformType.IOS.getType()) {
                flag = false;
            } else if (isStrNUll(approve.getIosVersion()) && !isStrNUll(approve.getIosBuildNumber()) && isStrNUll(rbo.getIosVersion()) && approve.getIosVersion().equals(rbo.getIosVersion().split("-")[0]) && rbo.getClientPlatformType() == ClientPlatformType.IOS.getType()) {
                flag = false;
            } else {
                flag = isSwitch(rbo, lbo);
            }
        }
        switchBo.setStatus(flag);
        list.add(switchBo);
    } else {
        boolean flag = isSwitch(rbo, lbo);
        SwitchBo switchBo = new SwitchBo();
        switchBo.setSwitchType(aSwitch.getSwitchType());
        switchBo.setStatus(flag);
        list.add(switchBo);
    }
}

优化:提取公共方法,减少IF逻辑嵌套。

java 复制代码
private Boolean shouldSwitchForApproveStatus(Approve approve, ASwitch aSwitch, RBO rbo, LBO lbo) {
    if (aSwitch.getApproveStatus() == ApproveStatusType.NOT.getType()) {
        return isSwitch(rbo, lbo);
    }
    if (aSwitch.getApproveStatus() == ApproveStatusType.OPEN.getType() || aSwitch.getApproveStatus() == ApproveStatusType.CLOSE.getType()) {
        boolean isApproveOpen = aSwitch.getApproveStatus() == ApproveStatusType.OPEN.getType();

        if (matchesAndroidCriteria(approve, rbo)) {
            return isApproveOpen;
        }
        if (matchesIosCriteria(approve, rbo)) {
            return isApproveOpen;
        }
        return isSwitch(rbo, lbo);
    }
    return false;
}

private Boolean matchesAndroidCriteria(Approve approve, RBO rbo) {
    return (isStrNull(approve.getAndroidBuildNumber()) && !isStrNull(approve.getAndroidVersion()) && isStrNull(rbo.getAndroidBuildNumber()) && approve.getAndroidBuildNumber().equals(rbo.getAndroidBuildNumber()) && rbo.getClientPlatformType() == ClientPlatformType.ANDROID.getType())
        || (isStrNull(approve.getAndroidVersion()) && !isStrNull(approve.getAndroidBuildNumber()) && isStrNull(rbo.getAndroidVersion()) && approve.getAndroidVersion().equals(rbo.getAndroidVersion().split("-")[0]) && rbo.getClientPlatformType() == ClientPlatformType.ANDROID.getType());
}

private Boolean matchesIosCriteria(Approve approve, RBO rbo) {
    return (isStrNull(approve.getIosBuildNumber()) && !isStrNull(approve.getIosVersion()) && isStrNull(rbo.getIosBuildNumber()) && approve.getIosBuildNumber().equals(rbo.getIosBuildNumber()) && rbo.getClientPlatformType() == ClientPlatformType.IOS.getType())
        || (isStrNull(approve.getIosVersion()) && !isStrNull(approve.getIosBuildNumber()) && isStrNull(rbo.getIosVersion()) && approve.getIosVersion().equals(rbo.getIosVersion().split("-")[0]) && rbo.getClientPlatformType() == ClientPlatformType.IOS.getType());
}

// Main Code
Boolean flag;
if (approve.getCurrentStatus() == CurrentStatusType.OPEN.getType()) {
    flag = shouldSwitchForApproveStatus(approve, aSwitch, rbo, lbo);
} else {
    flag = isSwitch(rbo, lbo);
}

SwitchBo switchBo = new SwitchBo();
switchBo.setSwitchType(aSwitch.getSwitchType());
switchBo.setStatus(flag);
list.add(switchBo);

案例四:不写注释+魔法值

不写注释和在代码中直接使用硬编码的值(也被称为"魔法值")会降低代码的可读性和维护性。

java 复制代码
if (lbo != null) {
    if (!isStrNUll(lbo.getAndroidVersionMin())) {
        lbo.setAndroidVersionMin("0");
    }
    if (!isStrNUll(lbo.getAndroidVersionMax())) {
        lbo.setAndroidVersionMax("100.0.0.0");
    }
    if (!isStrNUll(lbo.getIosVersionMin())) {
        lbo.setIosVersionMin("0");
    }
    if (!isStrNUll(lbo.getIosVersionMax())) {
        lbo.setIosVersionMax("100.0.0.0");
    }
}
if (rbo.getClientPlatformType() == ClientPlatformType.ANDROID.getType()) {
    if (!isVersionInRange(rbo.getAndroidVersion(), lbo.getAndroidVersionMin(), lbo.getAndroidVersionMax())) {
        return false;
    }
} else if (rbo.getClientPlatformType() == ClientPlatformType.IOS.getType()) {
    if (!isVersionInRange(rbo.getIosVersion(), lbo.getIosVersionMin(), lbo.getIosVersionMax())) {
        return false;
    }
}
if (lbo.getRelevatUser() == RelevatUserType.YES.getType()) {
    if (redisService.sIsMember(String.format(RedisConstant.User.USER_WHITE_SWITCH, lbo.getSwitchType()), rbo.getUserId())) {
        return true;
    } else {
        return false;
    }
}

if (redisService.sIsMember(String.format(RedisConstant.User.USER_BLACK_SWITCH, lbo.getSwitchType()), rbo.getUserId())) {
    return false;
}

if (redisService.sIsMember(String.format(RedisConstant.User.USER_WHITE_SWITCH, lbo.getSwitchType()), rbo.getUserId())) {
    return true;
}
if (!redisService.hasKey(getEntranceAreaKey())) {
    areaCache();
}

优化:避免魔法值,适当的地方加上注释。

java 复制代码
//边界判断
if (lbo != null) {
    if (!isStrNUll(lbo.getAndroidVersionMin())) {
        lbo.setAndroidVersionMin(SwitchConstant.MIN_ANDROID_VERSION);
    }
    if (!isStrNUll(lbo.getAndroidVersionMax())) {
        lbo.setAndroidVersionMax(SwitchConstant.MAX_ANDROID_VERSION);
    }
    if (!isStrNUll(lbo.getIosVersionMin())) {
        lbo.setIosVersionMin(SwitchConstant.MIN_IOS_VERSION);
    }
    if (!isStrNUll(lbo.getIosVersionMax())) {
        lbo.setIosVersionMax(SwitchConstant.MAX_IOS_VERSION);
    }
}
// 判断版本号
if (rbo.getClientPlatformType() == ClientPlatformType.ANDROID.getType()) {
    if (!isVersionInRange(rbo.getAndroidVersion(), lbo.getAndroidVersionMin(), lbo.getAndroidVersionMax())) {
        return false;
    }
} else if (rbo.getClientPlatformType() == ClientPlatformType.IOS.getType()) {
    if (!isVersionInRange(rbo.getIosVersion(), lbo.getIosVersionMin(), lbo.getIosVersionMax())) {
        return false;
    }
}
if (lbo.getRelevatUser() == RelevatUserType.YES.getType()) {
    if (redisService.sIsMember(String.format(RedisConstant.User.USER_WHITE_SWITCH, lbo.getSwitchType()), rbo.getUserId())) {
        return true;
    } else {
        return false;
    }
}

//判断黑名单
if (redisService.sIsMember(String.format(RedisConstant.User.USER_BLACK_SWITCH, lbo.getSwitchType()), rbo.getUserId())) {
    return false;
}

//判断白名单
if (redisService.sIsMember(String.format(RedisConstant.User.USER_WHITE_SWITCH, lbo.getSwitchType()), rbo.getUserId())) {
    return true;
}

//判断地区选项
if (!redisService.hasKey(getEntranceAreaKey())) {
    areaCache();
}

案例五:日志打印不合理、不规范

不规范的日志记录会使线上问题难以定位。

java 复制代码
public boolean submit(WithdrawSubmitBo bo) {
    log.info("用户提交提现操作");
    Long userId = bo.getUserId();
    int assetType = bo.getAssetType();
    BigDecimal withdrawAmount = bo.getAmount();

    String limitKey = String.format(RedisLimitConstant.UserPassword.ENTER_PASSWORD_ERROR_LIMIT, PasswordType.PURSE.getType(), userId);

    if (redisLimitUtils.isLimit(limitKey, userPasswordConfig.getEnterPasswordErrorLimitNum())) {
        throw new UserException(UserCode.PASSWORD_LOCK.getCode(),
                String.format(UserCode.PASSWORD_LOCK.getTranslateMap()
                                .get(userService.loadUserLang(userId)),
                        userPasswordConfig.getEnterPasswordErrorLimitNum()));
    }


    UserPassword userPassword = userPasswordMapper.selectOne(new LambdaQueryWrapper<UserPassword>()
            .eq(UserPassword::getUserId, userId)
            .eq(UserPassword::getType, PasswordType.PURSE.getType())
    );

    if (userPassword == null) {
        log.error("用户钱包密码不存在 | userId:{}", userId);
        throw new UserException(UserCode.NOT_SET_PASSWORD, userService.loadUserLang(userId));
    }


    if (!userPassword.getPassword().equals(bo.getPassword())) {
        log.error("用户钱包密码验证不一致 | userId:{}",userId);
        //输入错误,连续n次要锁住
        redisLimitUtils.tryLimit(limitKey, userPasswordConfig.getEnterPasswordErrorLimitNum(), 24, TimeUnit.HOURS);
        throw new UserException(UserCode.PASSWORD_ERROR.getCode(), String.format(UserCode.PASSWORD_ERROR.getTranslateMap().get(userService.loadUserLang(userId)),
                NumberUtils.getMaxValue(userPasswordConfig.getEnterPasswordErrorLimitNum() - redisLimitUtils.getLimitNum(limitKey), 0)));
    }

    redisLimitUtils.deleteLimit(limitKey);

    BankData bankData = bankDataService.getData(userId);
    if (null == bankData) {
        throw new IllegalArgumentException("no bank data.");
    }

    if (bankData.getStatus().intValue() != BankDataStatus.PASS.ordinal()) {
        throw new UserException(UserCode.NO_ACCESS, userService.loadUserLang(bo.getUserId()));
    }

    UserAsset userAsset = userAssetService.get(bo.getUserId(), bo.getAssetType());
    if (userAsset.getAmount().compareTo(bo.getAmount()) < 0) {
        log.error("用户提现余额不足 | userId:{}", userId);
        throw new UserException(UserCode.NOT_SUFFICIENT_FUNDS, userService.loadUserLang(bo.getUserId()));
    }

    ExchangeRateConfig exchangeRateConfig = exchangeRateConfigMapper.selectByBankType(bankData.getType());

    BigDecimal dayWithdrawAmount = withdrawRecordMapper.countUserDayWithdrawAmount(userId);
    if (dayWithdrawAmount.add(withdrawAmount).compareTo(NumberUtils.getDecimalValue(exchangeRateConfig.getDayCashHigh())) > 0) {
        if (getWhiteList().contains(userId)) {
            log.info("用户是白名单,不受限额控制 | userId:{}", userId);
        } else {
            log.error("用户提现超出今日限额 | userId:{}", userId);
            throw new UserException(UserCode.MAX_LIMIT, userService.loadUserLang(bo.getUserId()));
        }

    }


   ...
}

优化:日志内容应详细、清晰,并在适当的地方进行记录。

java 复制代码
public boolean submit(WithdrawSubmitBo bo) {
    log.info("用户提交提现操作 | {}", GsonUtil.GsonString(bo));
    Long userId = bo.getUserId();
    int assetType = bo.getAssetType();
    BigDecimal withdrawAmount = bo.getAmount();

    String limitKey = String.format(RedisLimitConstant.UserPassword.ENTER_PASSWORD_ERROR_LIMIT, PasswordType.PURSE.getType(), userId);

    if (redisLimitUtils.isLimit(limitKey, userPasswordConfig.getEnterPasswordErrorLimitNum())) {
        throw new UserException(UserCode.PASSWORD_LOCK.getCode(),
                String.format(UserCode.PASSWORD_LOCK.getTranslateMap()
                                .get(userService.loadUserLang(userId)),
                        userPasswordConfig.getEnterPasswordErrorLimitNum()));
    }


    UserPassword userPassword = userPasswordMapper.selectOne(new LambdaQueryWrapper<UserPassword>()
            .eq(UserPassword::getUserId, userId)
            .eq(UserPassword::getType, PasswordType.PURSE.getType())
    );

    if (userPassword == null) {
        log.error("用户钱包密码不存在 | userId:{}", userId);
        throw new UserException(UserCode.NOT_SET_PASSWORD, userService.loadUserLang(userId));
    }


    if (!userPassword.getPassword().equals(bo.getPassword())) {
        log.error("用户钱包密码验证不一致 | userId:{} |  originPassword:{} | inputPassword:{}", bo.getUserId(), userPassword.getPassword(), bo
                .getPassword());
        //输入错误,连续n次要锁住
        redisLimitUtils.tryLimit(limitKey, userPasswordConfig.getEnterPasswordErrorLimitNum(), 24, TimeUnit.HOURS);
        throw new UserException(UserCode.PASSWORD_ERROR.getCode(), String.format(UserCode.PASSWORD_ERROR.getTranslateMap().get(userService.loadUserLang(userId)),
                NumberUtils.getMaxValue(userPasswordConfig.getEnterPasswordErrorLimitNum() - redisLimitUtils.getLimitNum(limitKey), 0)));
    }

    redisLimitUtils.deleteLimit(limitKey);

    BankData bankData = bankDataService.getData(userId);
    if (null == bankData) {
        log.error("用户还未完善提现银行信息 | userId:{}", userId);
        throw new IllegalArgumentException("no bank data.");
    }

    if (bankData.getStatus().intValue() != BankDataStatus.PASS.ordinal()) {
        log.error("用户提现银行信息还未审核通过 | userId:{}", userId);
        throw new UserException(UserCode.NO_ACCESS, userService.loadUserLang(bo.getUserId()));
    }

    UserAsset userAsset = userAssetService.get(bo.getUserId(), bo.getAssetType());
    if (userAsset.getAmount().compareTo(bo.getAmount()) < 0) {
        log.error("用户提现余额不足 | userId:{} | assetType:{} |  withdraw amount:{} | fund amount:{}", userId, assetType, withdrawAmount, userAsset.getAmount());
        throw new UserException(UserCode.NOT_SUFFICIENT_FUNDS, userService.loadUserLang(bo.getUserId()));
    }

    ExchangeRateConfig exchangeRateConfig = exchangeRateConfigMapper.selectByBankType(bankData.getType());

    BigDecimal dayWithdrawAmount = withdrawRecordMapper.countUserDayWithdrawAmount(userId);
    if (dayWithdrawAmount.add(withdrawAmount).compareTo(NumberUtils.getDecimalValue(exchangeRateConfig.getDayCashHigh())) > 0) {
        if (getWhiteList().contains(userId)) {
            log.info("用户是白名单,不受限额控制 | userId:{} | assetType:{} |  withdraw amount:{} | fund amount:{}", userId, assetType, withdrawAmount, userAsset.getAmount());
        } else {
            log.error("用户提现超出今日限额 | userId:{} | assetType:{} |  withdraw amount:{} | fund amount:{}", userId, assetType, withdrawAmount, userAsset.getAmount());
            throw new UserException(UserCode.MAX_LIMIT, userService.loadUserLang(bo.getUserId()));
        }

    }


  ...
}

案例六:主次关联表数据操作时不加事务

当涉及到主次关联表的数据写入时,不使用事务可能会导致数据不一致。

java 复制代码
@Override
public boolean add(MomentReportBo bo) {

    MomentReport momentReport = ModelMapperUtil.MAPPER.map(bo, MomentReport.class);

    momentReport.setReportTime(new Date());
    momentReport.setReportState(MomentReportState.WAIT_APPROVE.getState());

    //主表
    momentReportMapper.insert(momentReport);

    List<String> sourceList = bo.getSourceList();
    if (!CollectionUtils.isEmpty(sourceList)) {
        AtomicInteger atomicInteger = new AtomicInteger();
        sourceList.forEach(url -> {
            MomentReportSource momentReportSource = new MomentReportSource(null, momentReport.getId(), url,
                    atomicInteger.incrementAndGet(),
                    new Date());
            //次表
            momentReportSourceMapper.insert(momentReportSource);
        });
    }

    return true;
}

优化:对于涉及到多个表的操作,应使用事务来确保数据的一致性。

java 复制代码
@Override
@Transactional
public boolean add(MomentReportBo bo) {

    MomentReport momentReport = ModelMapperUtil.MAPPER.map(bo, MomentReport.class);

    momentReport.setReportTime(new Date());
    momentReport.setReportState(MomentReportState.WAIT_APPROVE.getState());

    //主表
    momentReportMapper.insert(momentReport);

    List<String> sourceList = bo.getSourceList();
    if (!CollectionUtils.isEmpty(sourceList)) {
        AtomicInteger atomicInteger = new AtomicInteger();
        sourceList.forEach(url -> {
            MomentReportSource momentReportSource = new MomentReportSource(null, momentReport.getId(), url,
                    atomicInteger.incrementAndGet(),
                    new Date());
            //次表
            momentReportSourceMapper.insert(momentReportSource);
        });
    }

    return true;
}

案例7:Redis Key不设置过期时间

长时间不使用的Redis key会浪费内存资源。比如有个活动 用户做任务发放奖励的功能,在活动时间7天内完成首充算完成一个任务,发放任务奖励,此时我们设计了一个Redis Key 标记用户是否完成任务和完成任务的时间 在活动结束后根据Key去发放奖励,待活动结束这些Key就没有存在的意义了

java 复制代码
@Override
public void setFinishUser(Integer taskType, Integer taskItemId, Long userId) {
    // zSet记录完成任务用与完成时间
    redisService.zAdd(String.format("xxx:%s:%s", taskType, taskItemId), userId, System.currentTimeMillis());
}

优化:为Redis key设置合理的过期时间,并且过期时间需要避开临界值,尽量延长一些。

java 复制代码
@Override
public void setFinishUser(Integer taskType, Integer taskItemId, Long userId) {
    // zSet记录完成任务用与完成时间
    String key = String.format("xxx:%s:%s", taskType, taskItemId);
    redisService.zAdd(key, userId, System.currentTimeMillis());

    //设置过期时间
    long expireTime = redisService.getExpire(key);
    if (expireTime < 0) {
        //活动七天结束,但避免意外情况过期时间尽量延长一些
        redisService.expire(key, 7 + 3, TimeUnit.DAYS);
    }
}

案例8:Redis Bitmap滥用

执行 SETBIT key 7 1,Redis 只需要1个字节。但如果你执行 SETBIT key 8000000 1,会占用至少1000000个字节,尽管中间大部分位都没有使用,Redis 依然需要分配足够的内存来存储最大偏移量位置的位 这在使用上是需要严格避免的

java 复制代码
public void retentionReceipt(Long userId) {
    log.info("用户活跃埋点事件触发,userId:{}", userId);
    redisService.setBit("xxx:xxx", userId, true);
    try {
        redisService.expire("xxx:xxx", 3, TimeUnit.DAYS);
    } catch (Exception e) {
        log.info("留存事件设置过期时间异常,exception:{}", e);
    }
}

优化:分析key的使用范围,如果这个key是稀疏的,并且跨度很大,那使用Set会更加合适,如果Key是密集的,或者是连续的数据范围,使用bitmap更合适,用户活跃埋点事件很明显用户ID不是连续的数据范围,userId是8位数的,明显跨度也很大,使用Set会更合适

java 复制代码
public void retentionReceipt(Long userId) {
    log.info("用户活跃埋点事件触发,userId:{}", userId);
    redisService.sAdd("xxx:xxx", userId);
    try {
        redisService.expire("xxx:xxx", 3, TimeUnit.DAYS);
    } catch (Exception e) {
        log.info("留存事件设置过期时间异常,exception:{}", e);
    }
}

总结

代码审查是确保代码质量和项目成功的关键环节。通过分享这些案例,我们可以看到很多常见的问题和如何避免它们。当我们坚持高标准并不断地学习和改进时,我们的代码将变得更加健壮、高效和可维护。希望这些建议能帮助大家在日常的开发工作中避免这些常见的问题。

相关推荐
考虑考虑24 分钟前
JDK9中的dropWhile
java·后端·java ee
想躺平的咸鱼干32 分钟前
Volatile解决指令重排和单例模式
java·开发语言·单例模式·线程·并发编程
hqxstudying1 小时前
java依赖注入方法
java·spring·log4j·ioc·依赖
·云扬·1 小时前
【Java源码阅读系列37】深度解读Java BufferedReader 源码
java·开发语言
martinzh2 小时前
Spring AI 项目介绍
后端
Bug退退退1232 小时前
RabbitMQ 高级特性之重试机制
java·分布式·spring·rabbitmq
小皮侠2 小时前
nginx的使用
java·运维·服务器·前端·git·nginx·github
前端付豪2 小时前
20、用 Python + API 打造终端天气预报工具(支持城市查询、天气图标、美化输出🧊
后端·python
爱学习的小学渣2 小时前
关系型数据库
后端
武子康2 小时前
大数据-33 HBase 整体架构 HMaster HRegion
大数据·后端·hbase