前言
在娱乐直播中,用户最为重要的消费方式就是赠送虚拟礼物,这也代表着平台最核心的业务就是送礼,需要很高的安全性和性能要求,同时,在送礼的基础上会有很多的玩法、限制等等要求。
在公司的三年多,送礼相关的已经从几百行代码发展到了两千行代码,而且接入的模块越来越多。
在开发的过程中,往往为了安全性和快速开发,只是在原有代码上面进行堆积,造成了业务的冗杂,无法有效的扩展,同时,如果有其他的消费模式,更加难以进行开拓,比如说想从原有的消费逻辑中,只需要部分的业务,组成一个新的送礼逻辑,就很难做到,只能重新写代码或者抽出功能代码作为内部方法。
在现有问题下,推进了送礼的重构。
存在问题
1.校验逻辑冗杂
现有代码,所有判断逻辑顺序执行,各种判断冗杂一起,虽然一起抽出方法checkParam()
方法,但是方法中代码量大,逻辑不清。
2.核心逻辑不清晰
赠送虚拟礼物的核心代码就是扣除库存/虚拟货币、增加主播/用户收益、送礼记录等。
此处说的核心代码并不代码其他的不重要,而是作为主播或者用户最在意的其实就是收益方面的问题。
这部分的代码和后续的次重要的代码夹杂在一起,而且送礼完之后,其他业务端会根据接口返回处理自己的业务,这也导致了,其实核心业务完成之后,次重要业务出现问题,导致其他业务也出现异常。
3.次核心业务
次核心业务几个功能点之间没有关联,但是代码是顺序运行,这也导致了,前面业务失败的情况下,后面的业务也出现异常。
同步运行也降低了接口的吞吐量。
抽成方法之后,为了减少外部调用,需要将前面方法中调用的外部结果传递或者在开始各业务逻辑之前提前准备好数据,造成了业务复杂度,以及不便于维护。
解决方式
以下的解决方式并不一定是最完美的方式,仅仅是在我的知识范围内,更适合公司业务场景等方面综合考虑的解决方式,欢迎大家提供自己的想法与意见。
1.前置校验处理
解决思路:
- 1.将各种校验逻辑抽出方法
- 2.通过校验器模式显式组合校验方法。
- 3.通过future异步调用,组合结果。
public abstract class AbsVerify implements Verify {
protected SendGiftParam sendGiftParam;
public AbsVerify(SendGiftParam sendGiftParam) {
this.sendGiftParam = sendGiftParam;
}
}
public interface Verify {
/**
* 校验
*
* @return 是否成功
*/
ResultCode doVerify();
}
public class VerifyManger {
//校验器集合
private List<Verify> verifies = new ArrayList<>();
//添加校验器
public VerifyManger addVerify(Verify verify) {
verifies.add(verify);
return this;
}
//执行校验器
public ResultCode execute() {
if (verifies.isEmpty()) {
return ResultCode.SUCCESS;
}
int size = verifies.size();
ExecutorService exs = Executors.newFixedThreadPool(size);
ResultCode resultCode = ResultCode.SUCCESS;
try {
List<Future<ResultCode>> futureList = new ArrayList<>();
for (Verify verify : verifies) {
futureList.add(exs.submit(new CallableTask(verify)));
}
while (futureList.size() > 0) {
Iterator<Future<ResultCode>> iterable = futureList.iterator();
//遍历一遍
while (iterable.hasNext()) {
Future<ResultCode> future = iterable.next();
//如果任务完成取结果,否则判断下一个任务是否完成
if (future.isDone() && !future.isCancelled()) {
//获取结果
ResultCode resultCodeInFuture = future.get();
if (resultCodeInFuture != ResultCode.SUCCESS && resultCode == ResultCode.SUCCESS) {
resultCode = resultCodeInFuture;
}
//任务完成移除任务
iterable.remove();
} else {
Thread.sleep(1);//避免CPU高速运转,这里休息1毫秒,CPU纳秒级别
}
}
}
} catch (Exception e) {
e.printStackTrace();
}finally {
exs.shutdown();
}
return resultCode;
}
static class CallableTask implements Callable<ResultCode> {
Verify verify;
public CallableTask(Verify verify) {
super();
this.verify = verify;
}
@Override
public ResultCode call() throws Exception {
return verify.doVerify();
}
}
}
private ResultCode checkParam(SendGiftParam sendGiftParam) {
return new VerifyManger()
//入参、配置校验
.addVerify(new ParamVerify(sendGiftParam))
.addVerify(new GiftVersionVerify(sendGiftParam))
//数据有效性,限制校验
.addVerify(new GiftVerify(sendGiftParam))
.addVerify(new ForbiddenGiftVerify(sendGiftParam))
.execute();
}
ps:后续发现,还可针对返回结果进行完善,就是如果遇到resultcode不为success,直接跳出。
如果后续会针对不同的业务场景,出针对性的校验逻辑,可以新开接口,调用不同的校验方法或者使用策略模式,针对不同业务场景分别调用已经归结好的校验方法。
2.核心逻辑
针对核心逻辑,因为可能存在多种送礼模式,此处用了策略模式进行归类调用。
通过使用策略模式,使客户端与具体的策略进行解耦,由策略上下文来决定具体使用哪种策略方式。
此处区分了三种策略:虚拟货币赠送、库存赠送、系统赠送。
后续如果需要扩展,则可以在此处进行扩展。
因为涉及到具体的业务,就不贴代码展示,就是简单的策略模式,网上有很多的模板可以参考。
同时,为了数据的获取与传输,这里用到了线程上下文来存储共用的内容参数,需要注意的是,使用ThreadLocal的时候,再最后需要将内容移除,防止内存溢出,具体的原因网上也有很多的介绍,其实就是弱引用没有gc的问题。
3.次核心逻辑
因为次核心逻辑可以通过监听送礼的mq或者redis消息,来将原本在送礼模块中的功能迁移出去,现为改动较小,暂时先不迁移。
通过启用线程的模式,将次核心逻辑异步执行。
@Component
public class GiftConsumeDirector {
ConsumeBuilder consumeBuilder = new GiftConsumeBuilder();
private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
.setNameFormat("consume-pool-%d").build();
private static ExecutorService service = new ThreadPoolExecutor(25, 200,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(1024), namedThreadFactory,
new ThreadPoolExecutor.AbortPolicy());
......
}
同时,通过构造器模式,更加显式的展示当前的调用逻辑。
public abstract class ConsumeBuilder {
public abstract ConsumeBuilder doDivide();
public abstract ConsumeBuilder doPublish();
public abstract ConsumeBuilder doBigEventGift();
}
consumeBuilder = consumeBuilder
.doDivide()
.doPublish();
以上,为了数据的传递,也使用了线程上下文的模式来处理,当然也要注意oom问题。
其他修改
- 1.增加redis消息发布,为特定场景下,需要实时性要求高的需求。
结语
通过测试服的压测,基本达到上线的要求,也提升了接口的吞吐量,增加了代码的复杂度与阅读代码的难度,但是提高了扩展性,有些地方也尽量让阅读代码更简单点,毕竟重构一方面也是为了更好的阅读与扩展。
如果读者有更好的建议,可留言一起讨论。
我始终坚持,重构是改善既有代码设计的最好方式,重构并不是只有一种方式,也没有所谓的最佳解决方案,只有最合适当前的方案。
网友评论