前言

需求背景

雪球近几年来用户量和产品线激增。为了更加贴切的迎合公司业务发展和用户个性化需求,实现以下目标:

  • 满足用户对信息把控的时效性
  • 增加用户终端机型的覆盖率
  • 提升用户满意度和产品体验

雪球统一推送平台应运而生,推送作为 APP 运营中一个关键渠道,通过对它的合理运用,可以很好的促进目标实现。目前已在:关注发帖、回复评论、股价提醒、个股公告、组合调仓等多个业务场景服务用户,同时帮助运营人员将7*24小时资讯、行情速递等精选内容第一时间投递到目标用户。

产品设计

雪球早期自建推送是基于自建长连接和第三方依赖,前期由于历史业务冗余和代码可维护性差,存在以下问题:

问题 原因
缺乏ACK机制 推送是异步的无法得知是否送达,第三方受其他推送方的影响,可能造成延迟和丢失
缺乏消息的持久化 消息来一条推一条无法追溯历史和状态
缺乏幂等重传机制 推送环节过多,任何环节出问题都会造成消息丢失,再无法收到
客户端接入逻辑复杂 每接入一个新的APP都需要进行重复工作,代码无法复用
客户端与推送服务的 SDK 强耦合 推送端提供的接口不统一,如果需要替换Client就要重写
缺乏数据监控和统计 每天推送了多少,成功到达了多少,失败了多少

基于以上问题,通过调研比对业内实现方案,立足于雪球现状设计实现了基于各大主流厂商的自建推送通道,从根本上解决推送场景面临的实际困难。

通道能力建设

Android通道

安卓手机厂商众多,推送服务不可避免需要面临碎片化问题,目前雪球推送已集成华为、小米、OPPO、VIVO、魅族原生手机厂商通道,其余设备接入依托第三方友盟通道。

在推送内容审核、额度限制和流量控制多方面,各大手机厂商都有自己不同的平台规则。面对这些共性问题,从平台搭建至今一直跟进由中国信通院推动的统一推送联盟,目前来看想要结合雪球当前情况实施落地还不是合适的时机。

以下是雪球推送平台的优化方案,其中未提及厂商为当前阶段尚未触达或未发现类似问题。

单日推送总量限制

手机厂商的推送总数限制,如下表:

通道 状态码 官方简述
小米 200001 推送数量超过当日限制时,会调用请求失败,返回错误码200001
OPPO 33 消息条数超过日限额,接口返回:The number of messages exceeds the daily limit
VIVO 10070 可发送的单推和群推消息指定的用户量不得超过每日限制的推送总量

解决方案:

  1. 优化业务内容推送逻辑,对各厂商通道内容下发制定不同策略,保障关键内容下发
  2. 根据APP应用的类型和厂商的规则提交申请,可以增加不同的推送额度

根据消息下发标识出归属的业务种类,区分优先级来保障关键内容下发

message Message {int64 messageId = 1; // 推送消息批次号string title = 2; // 推送消息标题string payload = 3; // 推送内容主体string description = 4; // 通知栏上方描述(摘要)string callback = 5; // "eg:http://example.com" 回调地址string summary_callback = 6; // "eg:http://img.com" 通知图片地址 Type type = 7; // 业务类型,根据业务类型划分下发优先级Application app = 8; // 推送的客户端,由同一平台下发多端APPrepeated int64 target = 9; // 推送目标用户(详细推送用户,数组格式)int64 created = 10; // 消息创建时间int32 ttl = 11; // 消息的过期时间(单位ms)map<string, string> ext = 12; // 其他自定义字段map<string, string> version_filter = 13; // 版本过滤,漏斗模式Application targetType = 14; // 推送目标用户的id类型
}

实时推送速率限制

雪球作为一个财富管理类应用,其中交易、行情和内容资讯一直为用户关心的首要内容。对于推送的实时性要求高,推送服务面临数据量和QPS等众多问题,其中流控限制如下表:

通道 状态码 官方简述
小米 200002 小米推送对推送速率(QPS)的分配主要依据App的MIUI日联网设备数进行分级计算,QPS超限时会返回错误码200002
华为 HTTP-503 推送次数限制:每天向某个设备上某个应用发送消息数量不超过3000条,超过3000条进行限流(限流24小时后恢复)
VIVO 10072 推送QPS根据SDK订阅数自动调整,默认值为3000条/秒

解决方案:

  1. 类似限额解决思路,优化下发逻辑,保障关键内容下发
  2. 充分利用各大厂商提供的批量下发接口
  • 小米和华为限制的QPS为接口访问频次,因此在数据到达厂商通道前,根据用户发送渠道提前聚合,尽可能多的使用批量发送。(例如:根据小米官方描述:1个请求中最多可以携带1000个目标设备。例如:3000QPS时,1秒内最多可推送 300万设备。最高可以实现 300w/秒的下发。
  • OPPO和VIVO的批量下发接口和单条下发接口有不同的访问频次限制,在进行数据下发时,根据消息内容标识,当批量下发接口触发上限后,切换到单条下发接口。
  1. 制定消息有效时间,通道层在触发厂商QPS上限后,再次进入推送下发队列
//小米推送通道触发流控限制,根据状态码判断进行回传重试...String responseBody = URLDecoder.decode(response.body().string(), "UTF-8");
JsonNode obj = MAPPER.readTree(responseBody);...if ("200002".equals(obj.get("code").asText())) {// 200002限速,稍后重试limitCounter.increment();LOGGER.warn("小米api接口调用触发频控限制,重传的用户uid列表:{} | 返回的消息体:{} | 推送的APP:{} | 该批消息的messageId:{}", uidList, responseBody, message.getApp().name(), message.getMessageId());pushStatusProducer.sendMessageRetry(message.toBuilder().clearTarget().addAllTarget(uidList).build());return;}

此方案需注意:

  • 需要有消息的 deadline,否则最后下发成功,内容的时效性在用户体验上也会打折扣
  • 消息重传需要考虑幂等性,在弱网和其他边界情况下重传会导致推送重复,影响用户体验,对于消息幂等各大厂商给出的解决方案如下表:
通道 幂等参数 描述
小米 notify_id 如果通知栏要显示多条推送消息,需要针对不同的消息设置不同的notify_id(相同notify_id的通知栏消息会覆盖之前的),且要求notify_id为取值在0~2147483647的整数
华为 notify_id Push NC自动为给每条消息生成一个唯一标识;不同的通知栏消息可以拥有相同的notifyId,实现新的消息覆盖上一条消息功能。
OPPO app_message_id API推送请检查app_message_id是否自定义,API单推相同的app_message_id只推送一次。

上述厂商给出的解决方案除了表格中的,其实还有相似的其他手段解决。例如:小米的 ‘extra.jobkey’ 字段或华为的 ‘group’ 字段可以实现消息折叠,也可以改善用户体验上相关问题。

//小米API接口请求体封装,利用notify_id参数保障消息幂等下发
RequestBody requestBody = new FormBody.Builder().add("payload", MAPPER.valueToTree(messageTemplate).toString()).add("restricted_package_name", packageName).add("description", (messageTemplate.getDescription().length() > 120 ? messageTemplate.getDescription().substring(0, 120) + CutString.SUB_TAIL: messageTemplate.getDescription())).add("extra.notification_large_icon_uri", StringUtils.trimToEmpty(message.getSummaryCallback())).add("title", messageTemplate.getTitle().length() > 50 ? messageTemplate.getTitle().substring(0, 50) : messageTemplate.getTitle()).add("pass_through", "0").add("notify_type", "-1")// 开发者在发送消息时可以设置消息的组ID(JobKey), 带有相同的组ID的消息会被聚合为一个消息组.add("extra.jobkey", String.valueOf(messageTemplate.getMessageId() & Integer.MAX_VALUE)).add("registration_id", StringUtils.join(deviceTokens, ","))//默认情况下, 通知栏只显示一条推送消息, 如果通知栏要显示多条推送消息, 需要针对不同的消息设置不同的notify_id.add("notify_id", String.valueOf(messageTemplate.getMessageId() & Integer.MAX_VALUE)).build();

iOS & 其他通道

苹果厂商的通道下发根据官方提供的APNs实现,早期是采用了基于JDK实现,由于性能较差目前采用了开源的第三方SDK:pushy

使用过程中偶尔也有问题,但大部分是网路链路环境原因。通过调研得到个方案:将 iOS 推送任务所在服务节点就近部署至APNs服务器。但是基于实际使用现状及目前 iOS 业务需求,在此只作讨论。

魅族通道根据官方API文档接入,可以满足当前的QPS和总量使用在此不做过多讲述。

友盟通道或极光等其他第三方通道在上述的各大厂商通道接入的前提下可以优化通道的两方面能力:

  1. 其他手机用户的接入,提高推送下发的覆盖率
  2. 在系统的构建中承担一个 fallback 的角色,保障系统的健壮性

平台能力建设

目前推送平台在提供通道能力基础上,更加丰富平台的系统、数据和业务能力

系统能力

推送平台目前由8台4vCPU 8GiB服务器:实现80+w/s的消息总数下发、满足10+亿/天的业务指标当前性能瓶颈全在厂商侧的限制)。如何保障系统自身的高可用和稳定性,除了良好的初期架构设计,还需要对系统进行持久的优化迭代和跟踪指标体系便于提前告警和分析问题。

推送通道优化下发期间面临众多问题,贴出两个代表型的问题在此简述下:

厂商通道调用选型

通道下发的选型最初采用各厂商提供的SDK进行集成,其中大部分的包与公司基础架构设施依赖冲突,在性能优化和业务兼容中也存在众多问题。例如:日志组件冲突、SDK线程池调整和版本升级兼容困难、HTTP接口数据返回内容不全等。因此最终选用API接口进行封装,多通道报文协议自行解析,统一推送通道连接标准。

基于以上原因利用消息总线和OkHttp的异步请求,数据格式、代码模型和性能目标统一。

//call_before,OkHttp下发前统一格式封装
public static RequestBody requestBodyFormat(MessageProto.Message message, String packageName, List<String> deviceTokens, boolean channelSwitch) throws UnsupportedEncodingException {MessageTemplate messageTemplate = MessageTemplate.messageConvert(message, MessageProto.Platform.XIAOMI);messageTemplate.setTitle(StringUtils.isEmpty(messageTemplate.getTitle()) ? PushTitleUtils.getTitleFromAPP(message.getApp()) : messageTemplate.getTitle());RequestBody requestBody = new FormBody.Builder().add("payload", MAPPER.valueToTree(messageTemplate).toString()).add("restricted_package_name", packageName).add("description", (messageTemplate.getDescription().length() > 120 ? messageTemplate.getDescription().substring(0, 120) + CutString.SUB_TAIL: messageTemplate.getDescription())).add("extra.notification_large_icon_uri", StringUtils.trimToEmpty(message.getSummaryCallback())).add("title", messageTemplate.getTitle().length() > 50 ? messageTemplate.getTitle().substring(0, 50) : messageTemplate.getTitle()).add("pass_through", "0").add("notify_type", "-1")// 开发者在发送消息时可以设置消息的组ID(JobKey), 带有相同的组ID的消息会被聚合为一个消息组.add("extra.jobkey", String.valueOf(messageTemplate.getMessageId() & Integer.MAX_VALUE))//使用批量接口下发,单次最大1000个deviceToken充分利用批量机制提高系统吞吐率.add("registration_id", StringUtils.join(deviceTokens, ","))//默认情况下, 通知栏只显示一条推送消息, 如果通知栏要显示多条推送消息, 需要针对不同的消息设置不同的notify_id.add("notify_id", String.valueOf(messageTemplate.getMessageId() & Integer.MAX_VALUE)).build();return requestBody;
}//call,OkHttp进行通道消息下发
public void send(List<UserStateProto.Device> deviceList, MessageProto.Message message, RequestBody requestBody) {List<Long> uidList_GE = deviceList.stream().map(m -> m.getUid()).collect(Collectors.toList());try {LOGGER.info("小米api接口调用前,将要发送的用户uid列表:{} | 发送的消息报文:{} | 推送的APP:{} | 该批消息的messageId:{}", uidList_GE, OkHttp3ConvertUtils.requestBodyURLToString(requestBody), message.getApp().name(), message.getMessageId());Request request = new Request.Builder().url(xiaomiSendUrl).addHeader("Authorization", String.format("key=%s", accessToken)).post(requestBody).build();Call call = okHttpClient.newCall(request);call.enqueue(new XiaomiResponseCall(deviceList, message, pushStatusProducer));} catch (Exception e) {exceptionCounter.increment(deviceList.size());LOGGER.error("小米api接口调用过程异常,失败的用户uid列表:{} | 失败的原因:{} | 推送的APP:{} | 该批消息的messageId:{}", uidList_GE, e.getMessage(), message.getApp().name(), message.getMessageId(), e);pushStatusProducer.sendByDeviceList(PushResultEnum.FAIL, PushFailedTypeEnum.SYSTEM_ERROR, e.getMessage(), deviceList, message);}
}//call_back,OkHttp异步结果回调
public void onResponse(Call call, Response response) throws IOException {String responseBody = URLDecoder.decode(response.body().string(), "UTF-8");if (response.isSuccessful()) {JsonNode obj = MAPPER.readTree(responseBody);if ("0".equals(obj.get("code").asText())) {JsonNode jsonNode = obj.findPath("data").findPath("bad_regids");if (jsonNode.isMissingNode()) {successCounter.increment(deviceList.size());LOGGER.info("小米api接口调用返回全部成功,成功的用户uid列表:{} | 返回的消息体:{} | 推送的APP:{} | 该批消息的messageId:{}", uidList, responseBody, message.getApp().name(), message.getMessageId());pushStatusProducer.sendByDeviceList(PushResultEnum.SUCCESS, PushFailedTypeEnum.NULL, "SUCCESS", deviceList, message);} else {List<String> failedTokenList = new ArrayList<>();for (String objNode : jsonNode.textValue().split(",")) {failedTokenList.add(objNode);}List<UserStateProto.Device> failedList = deviceList.stream().filter(f -> failedTokenList.contains(f.getDeviceToken())).collect(Collectors.toList());failedCounter.increment(failedList.size());LOGGER.info("小米api接口调用返回部分失败,失败的用户uid列表:{} | 返回的消息体:{} | 推送的APP:{} | 该批消息的messageId:{}", failedList.stream().map(m -> m.getUid()).collect(Collectors.toList()), responseBody, message.getApp().name(), message.getMessageId());pushStatusProducer.sendByDeviceList(PushResultEnum.IGNORE, PushFailedTypeEnum.CHANNEL_ERROR, responseBody, failedList, message);List<UserStateProto.Device> successedList = deviceList.stream().filter(f -> !failedTokenList.contains(f.getDeviceToken())).collect(Collectors.toList());successCounter.increment(successedList.size());LOGGER.info("小米api接口调用返回部分成功,成功的用户uid列表:{} | 返回的消息体:{} | 推送的APP:{} | 该批消息的messageId:{}", successedList.stream().map(m -> m.getUid()).collect(Collectors.toList()), responseBody, message.getApp().name(), message.getMessageId());pushStatusProducer.sendByDeviceList(PushResultEnum.SUCCESS, PushFailedTypeEnum.NULL, "SUCCESS", successedList, message);}} else if ("200002".equals(obj.get("code").asText())) {// 200002限速,稍后重试limitCounter.increment();LOGGER.warn("小米api接口调用触发频控限制,重传的用户uid列表:{} | 返回的消息体:{} | 推送的APP:{} | 该批消息的messageId:{}", uidList, responseBody, message.getApp().name(), message.getMessageId());pushStatusProducer.sendMessageRetry(message.toBuilder().clearTarget().addAllTarget(uidList).build());return;} else {failedCounter.increment(deviceList.size());LOGGER.warn("小米api接口调用返回全部失败,失败的用户uid列表:{} | 返回的消息体:{} | 推送的APP:{} | 该批消息的messageId:{}", uidList, responseBody, message.getApp().name(), message.getMessageId());pushStatusProducer.sendByDeviceList(PushResultEnum.IGNORE, PushFailedTypeEnum.CHANNEL_ERROR, responseBody, deviceList, message);}} else {failedCounter.increment(deviceList.size());LOGGER.error("小米api接口调用返回异常,失败的用户uid列表:{} | 返回的消息体:{} | 推送的APP:{} | 该批消息的messageId:{}", uidList, responseBody, message.getApp().name(), message.getMessageId());pushStatusProducer.sendByDeviceList(PushResultEnum.IGNORE, PushFailedTypeEnum.CHANNEL_ERROR, responseBody, deviceList, message);}
}

推送消息全链跟踪

由于离线推送不是由自建长连接通道下发,如何定位每个用户的每条推送消息当前状态是个不可忽视的问题。各厂商推送后台都集成的有对应的问题Debug工具,因此在推送平台数据埋点中API接口的返回数据需要记录厂商对应的 trace_id,以便问题定位和数据分析。

例如:小米厂商需要IMEI和接口返回的批次ID,通过小米后台查询就可知道厂商下发链路状态

数据能力

完成消息推送的下一步是进一步地对不同业务、场景进行闭环管理和效果跟踪,通过数据大盘量化推送效果。数据大盘目前已经涵盖三个APP的几十种业务场景,提供实时数据和离线数据分析。

在数据能力建设时,架构上直接将系统链路上所有的数据层通过消息总线的方式传输。细化每条消息的报文格式,规定由 msg_id + uid 作为唯一标识,应用端统一采用 event_tracking 作为推送平台埋点字段,实现了数据指标体系的规范和接入标准。

//消息总线实时推送数据格式规范public void sendByDevice(PushResultEnum pushResultEnum, PushFailedTypeEnum pushFailedTypeEnum, String reason, UserStateProto.Device device, MessageProto.Message message) {MessageAck messageAck = new MessageAck();messageAck.setUploadTime(System.currentTimeMillis());messageAck.setMsgId(message.getMessageId());messageAck.setUid(device.getUid());messageAck.setChannel(device.getDeviceChannel());messageAck.setResult(pushResultEnum.getTypeName());messageAck.setFailedType(pushFailedTypeEnum.getTypeName());messageAck.setFailedReason(reason);messageAck.setAppVersion(device.getAppVersion());messageAck.setToken(device.getDeviceToken());messageAck.setDescription(message.getDescription());messageAck.setApp(message.getApp().name());messageAck.setBizType(message.getExtMap().get(TrackingExtKey.BIZ_TYPE));//扩展字段K/V,应对临时变更性需求messageAck.setExt(message.getExtMap());messageAck.setCallback(message.getCallback());sendMessageACK(messageAck);
}

依托推送数据能力可以做到:APP卸载率分析(依赖于厂商推送token,数据可以用作参考)、推送内容热度标签、厂商通道送达率指标优化,优化推送业务对用户的体验等。

业务能力

一个强大的推送运营中台,除了基础推送下发功能,还为运营提供了推送效果分析,对每一条推送消息记录推送各阶段明细数据,形成漏斗分析。运营人员通过运营中台了解一条消息的生命周期,量化推送效果,优化后续选题和人群。

运营侧

运营决策千变万化,除了定时任务下发之类的基础功能,推动平台在架构设计上对功能层面和数据层面均做了隔离,方便配合大数据和算法实现动态的目标圈选和算法个性化千人千面。

审核侧

厂商对推送内容有各自严苛的标准,国内运营的监管环境同时对用户数据有严格管理,推送平台在平台搭建中模块化数据流转处理,以满足审核内容动态调整。

回顾总结

以上主要是分享在推送平台搭建和优化过程中面临和解决的一些问题,重点是架构技术选型和厂商通道优化,主要是以下两点:

  • 架构上尽量将业务功能和数据体系解耦合,可以使用消息总线的方式将业务逻辑和数据分析分开
  • 通道下发在选型上统一使用API接口进行交互,方便后续维护、性能优化和业务个性化需求接入

基于以上方案和技巧对于文章开始的问题都通过如下方式得到解决:

问题 实现
缺乏ACK机制 利用HTTP接口调用的 callback 返回结果,实时反馈厂商通道ACK状态
缺乏消息的持久化 对每条消息采用 msg_id + uid 机制,通过数据能力搭建消息追踪截机制
缺乏重传机制 直接采用厂商提供的幂等参数,做到异常消息重传下发
客户端接入逻辑复杂 配合前端基础设施,沉淀基础能力和组件做到复用与快速接入
客户端与推送服务的 SDK 强耦合 规范所有厂商接口的数据埋点字段,轻量化前端代码同时达到标准化数据流程
缺乏数据监控和统计 丰富系统整他的监控和链路追踪,同时将数据和功能代码拆分便于量化指标

未来展望

智能频控免打扰设计

提升平台整体资源的利用效率,降低用户不必要的打扰,将资源给到用户最关心部分。

站内站外推送同步设计

配合站内瀑布提醒,做到厂商离线与长连接在线推送组合下发,降低推送平台压力。

SMS和PUSH互补下发设计

配合短信提醒,提高关键类信息的到达率,提升用户产品体验。

参考链接

APNs / MiPush / HMS / Opush / Vpush / meizu push


作者简介

贺矿省、王文文,来自雪球社区平台/基础组件。

招聘信息

雪球业务正在突飞猛进的发展,工程师团队期待牛人的加入。如果你对「做中国人首选的在线财富管理平台」感兴趣,希望你能一起来添砖加瓦,点击「阅读原文」查看热招职位,就等你了。

统一推送平台搭建与优化相关推荐

  1. 魅族推送平台架构及优化

    魅族推送平台架构及优化 内容简介 平台从支撑魅族内部业务到对外能力开放过程中一系列的系统架构优化及扩张, 支撑亿级高并发消息实时推送,包括服务高可用.监控.容灾.流量调度.海量存储等方面的实践与探讨. ...

  2. 运维企业专题(2)HTTP加速器——Varnish缓存机制后篇(后端服务器集群、负载均衡与CDN推送平台搭建)

    1.实验一:配置后端服务器集群 1)实验目的:定义不同域名站点的后端服务器,通过域名会访问不同的后端主机 2)实验过程: <1>在调度器server1上编写Varnish的配置文件 vim ...

  3. 京东大规模消息推送平台搭建实践

    背景 每个app或者业务都有将信息推送到用户客户端的需求.作为中台的推送平台,需要为公司内部许多个不同app同时提供可用,稳定的推送服务,因此我们消息推送平台应运而生. 推送平台架构 名词解释: dt ...

  4. 基于SpringBoot、RabbitMQ的Android消息推送平台搭建

    消息推送,类似于微信来新消息时出现在通知栏那种情景.很多APP都有这个功能.现在有很多第三方平台可以实现这个需要,但是有的公司对所要推送的消息保密要求比较高,不希望被第三方看到,可以使用此种方式进行消 ...

  5. android 统一推送平台,工信部实验室成立安卓统一推送联盟:推送服务将实现统一...

    据微信公众号" 泰尔终端实验室"7月19日消息,移动互联网时代,消息推送是移动应用(APP)的一项重要功能,目前中国安卓系统生态环境尚不成熟,设备碎片化现象严重,导致不同应用与操作 ...

  6. 极光为华硕 ROG 游戏手机 3 搭建符合统一推送联盟标准的推送系统

    2020年7月23日,华硕 ROG 游戏手机3正式发布.该款手机为 ROG 玩家深度定制,旨在提供专属沉浸式游戏体验.华硕在发布会上同时宣布,由其合作伙伴.中国领先的移动开发者服务提供商极光(Auro ...

  7. 快速搭建企业内部信息推送平台

    为了解决企业内部的信息推送问题,及时.准确地将报表.订单等各种重要消息推送给员工,很多IT经理人可谓操碎了心.有没有一种可以灵活配置,能够快速与企业现有系统整合,并且费用相对低廉的企业内部信息推送解决 ...

  8. android手机功耗优化,安卓统一推送实测:待机功耗降30%

    IT之家3月18日消息 据泰尔终端实验室官方微信公众号消息,2020年3月16日,OPPO Find X2 通过"移动终端高性能长连接"测试.经检测,OPPO Find X2手机在 ...

  9. Redis 在 vivo 推送平台的应用与优化实践

    一.推送平台特点 vivo推送平台是vivo公司向开发者提供的消息推送服务,通过在云端与客户端之间建立一条稳定.可靠的长连接,为开发者提供向客户端应用实时推送消息的服务,支持百亿级的通知/消息推送,秒 ...

最新文章

  1. ccf 最优灌溉(prime模板)
  2. 函数对象、 函数对象与容器、函数对象与算法
  3. java 实验4 异常
  4. 美团外卖商家端视频探索之旅
  5. PAT甲级题目翻译+答案 AcWing(动态规划)
  6. vue商城项目开发:底部导航样式、顶部导航矩阵和轮播图
  7. 阮一峰react demo代码研究的学习笔记 - demo4 debug - create element and Render
  8. linux操作系统之读写锁
  9. Java有快速打好基础的方法?
  10. 浅谈C#中virtual和abstract的区别
  11. 6 二十五项反措--防止锅炉事故
  12. 大胜凭德--入行选领导(转载分析)
  13. Spring文件上传接口学习(MultipartFile,MultiparHttpservletRequest,MultipartResolver)
  14. python自然语言分析_Python自然语言处理-分析句子结构
  15. AMM引入无限网格策略,变无常损失为阿尔法收益
  16. 基于滴滴云安装 Docker 并上传镜像到滴滴云 Docker 仓库
  17. @Scheduled注解与参数
  18. 计算机网络和信息安全-网络安全
  19. 基于babylon.js的3D网页游戏从零教程
  20. 【正则】Lua中的正则表达式

热门文章

  1. AIX日志型文件系统的nbpi
  2. 正则密码验证,包含数字、字母、特殊符号
  3. 灰色预测GM(1,1)代码
  4. 服务器上搭建git仓库
  5. QVE音乐剪辑器制作手机铃声的方法
  6. Android 第三方应用接入微信平台(2)
  7. 台式计算机如何双屏显示,一个电脑主机怎样接两个显示器_一个电脑连接两个显示器如何操作-win7之家...
  8. svn恢复到指定版本
  9. 【计算机网络】数据流简单分析
  10. 投资的心里按摩(一):远离噪声