文章目录

  • 前言
  • 一、分析设计
    • 1.分析
    • 2.设计
      • 2.1 整体执行流程
      • 2.2 相关表
      • 2.3 相关处理器
        • 2.3.1 DataFetchReconTaskProcessor
        • 2.3.2 DataCompareReconTaskProcessor
          • 1.查询交易表数据/临时表数据
          • 2. 构建比较模型
          • 3. 数据比对
          • 4. 差错入表
        • 2.3.3 DiffDataDealReconTaskProcessor
  • 总结

前言

对账是支付系统必不可少的一部分,可以及时发现交易环节存在的交易差异,运营人员可以及时修订交易差异,最重要的是可以发现支付系统存在的问题。一般对账主要分两部分,渠道对账、商户对账,渠道对账是支付平台交易数据和对接的各个支付机构数据进行比对,商户对账是给使用此交易平台的商户生成特定格式的交易账单文件,商户获取然后自己去核对。

本篇文章我们将探究如何设计一个能够支撑日交易量百万级的对账系统,并给出核心代码实现,仅供参考。

此设计实现已经在实际生产运行,总体效果还是不错的吧,100W笔交易,10min之内数据比对完成,当然还有很大的优化空间。真正的数据比对部分100W笔交易1s也用不到。时间耗费在哪里呢,有兴趣的接着往下看。

大小/耗时/配置
对账文件大小 180M
交易量 110W笔
文件下载时间 20s
文件解析入库 35s
数据查询 4min
数据比对 700ms
差错处理 /
应用 4核 8G
数据库 Oracle

一、分析设计

1.分析

首先我们得知道对账是在做什么?无非就是看同一笔交易数据,双方是否都存在,双方都存在的情况下再比较交易状态、交易金额是否一致。是的,对账就是那么简单,当然还有各种汇总需求,此处我们不做讨论,因为汇总是紧跟系统业务的,咱们只讨论数据比对核心部分。如何设计一个好的对账系统并不容易,对于交易类型多,交易量大的支付系统来说更不容易了。不仅要保证准确性,也要保证效率,一般只有在渠道对账完成后,才去给商户生成对账文件,商户对账依赖渠道对账,即我们的目标是快速、准确!

不同的支付公司有不同的对账方式,下面我们大概了解下常见的对账方案。

基于sql的,这种在我看来应该算是最低级的了吧,也就是将通道侧的数据入到一个临时表中,和自己平台的交易表进行连表操作,这种在交易量小的情况下还是可以用下的,当日交易和表交易存量很大的时候,你也不知道你的sql需要执行多久才能执行出结果了,这个自己很难把控了。

基于Redis的,利用Redis的Set类型的差集功能得出差异数据,使用Redis提供的并集/交集/差集等指令完成两个集合的数据比对,这个笔者暂时没有研究过,网上博客介绍此方法的还挺多,有兴趣的可以研究下,暂且不说效率,至少得费一套Redis集群。

基于应用进程内存的,利用Map数据结构进行数据比对,我们接下来主要讨论这个,这个是笔者在所做的几个支付系统里实际验证过的方式,完全可以扛起百万级别数据比对,中小型支付系统完全够用了,日交易量能达到千万级别的公司,应该确实不多吧。

基于大数据技术的,这个确实没有了解过,不过也是进行数据分片处理吧,这篇咱们是探究百万级别的,百万级别也用不到大数据相关技术,毕竟我们是普通Java开发攻城狮。

2.设计

2.1 整体执行流程

从上图中可以看到整体执行流程,是由两个定时任务触发的。

第一个定时任务用于生成对账任务,生成对账任务的元数据信息每个公司支付系统设计各异,具体问题具体分析吧,这里不在展开。

第二个定时任务用于对账任务的分发工作,查询表中需要对账的对账任务,将任务投递到MQ队列中,消费节点消费到任务后完成对账流程。

2.2 相关表

涉及到的三张表:recon_task、recon_trans、recon_diff

recon_task:对账任务表,每日凌晨,第一个对账任务触发,根据对接的渠道信息,生成对应的对账任务。
recon_trans:渠道交易数据临时表,用于存放渠道侧的交易数据。
recon_diff:差错表,用于存储比对出的差异数据。

2.3 相关处理器

涉及到的三个处理器:DataFetchReconTaskProcessor、DataCompareReconTaskProcessor、DiffDataDealReconTaskProcessor

DataFetchReconTaskProcessor:数据获取处理器,处理对账任务状态为INIT、DATA_FETCH_FAILED的任务;将对账文件数据解析入临时表。
DataCompareReconTaskProcessor:数据比对处理器,处理对账任务状态为DATA_FETCHED、FAILED_RECON的任务;将交易表和临时表数据查询出来构建数据比对模型进行数据比对,并将差错结果存入差错表。
DiffDataDealReconTaskProcessor:差错处理处理器,处理对账任务状态为DONE_RECON、DIFF_DEAL_FAILED的任务;根据公司实际业务处理。

消费节点获取到任务后判断需要哪个处理器进行处理。

    @RabbitListener(queues = MqConstant.Q_RECON_TASK_DISPATCH, containerFactory = "rabbitListenerContainerFactory")public void execute(ReconTask reconTask, Channel channel, Message messageSource) {logger.info("[机构({})对账码({})状态({})对账任务号({})] 开始对账任务处理", reconTask.getInstCode(), reconTask.getTransCode(), reconTask.getProcessStatus(), reconTask.getReconTaskNo());try {for (IReconTaskProcessor processor : processors) {if (processor.isSupport(reconTask.getProcessStatus())) {try {processor.process(reconTask);} catch (Exception e) {logger.error("对账任务处理异常", e);break;}}}} finally {try {channel.basicAck(messageSource.getMessageProperties().getDeliveryTag(), false);} catch (IOException e) {LoggerUtil.error(logger, "MQ消息监听-消息ACK-异常", e);}}logger.info("[对账任务号({})] 对账任务处理结束", reconTask.getReconTaskNo());}

下面我们来分析设计这个三个处理器具体都是做什么的。

2.3.1 DataFetchReconTaskProcessor

此处理器负责数据的获取工作,对接的支付机构以不同形式将前一天/小时的交易数据提供给接入系统,一般通过文件形式方式。但是每个支付机构提供的文件格式各异,所以要想做一个兼容各种形式、格式的通用的对账平台,还是挺不容易的吧,如何兼容不同形式、格式的处理方式有很多,使用脚本语言个人觉得是个比较好的处理方式,思路可借鉴支付网关设计-1,使用Groovy脚本方式,只需要编写个脚本类,每个支付渠道有自己特定的解析脚本,将标准流程下的不同处使用脚本处理,动态加载执行脚本,保证我们的标准化流程,此处就不在展开了。
此处就拿笔者近期为一个支付系统做的对账系统展开将吧,交易量还是可以的,此交易系统对接了4个支付渠道,并且渠道对账文件渠道根据公司要求生成了统一格式的对账文件,所以省却了很大工作量,拿到文件后只管解析就好了,不需要关注格式问题了。为了加快解析速度使用SpringBatch批处理框架,在入表时候使用Mybatis ExecutorType.BATCH模式,实际运行效果(110W笔交易,文件下载+解析入临时表)耗时60s左右。

2.3.2 DataCompareReconTaskProcessor

此处理器主要负责数据的比对,也是我们最重要的处理器了。
核心流程:
–>查询交易表数据/临时表数据
–>构建比较模型放入内存
–>数据比对
–>差错入表

1.查询交易表数据/临时表数据

第一部分,我们需要将交易表、临时表数据查询出来,因为对账文件给的一笔交易字段非常多,所以我们在第一个处理器中将文件解析入表,此处理器又将从表里查询出来我们所需的字段,没办法,文件数据全部放入内存的话将导致内存吃紧,所以多了一步看似脱裤子放屁的步骤,实际还是很有必要的,我们将整个流程划分为三个处理器处理,一方面使流程清晰,更重要的是一个任务在某个执行器流程出问题后,再次执行时候直接从失败的执行器执行就行了,不用从开始执行。
在查询交易表和临时表使用了不同的分片策略。交易表主键非自增,所以使用了交易完成时间作为分片策略(要在此字段创建索引),一个sql查询固定时间片的数据。

此片查询startTime:20221221224000,endTime:20221221225000,读出数据量:4137,耗时:858ms
此片查询startTime:20221221233000,endTime:20221221234000,读出数据量:2677,耗时:649ms
此片查询startTime:20221221235000,endTime:20221221235959,读出数据量:1603,耗时:300ms
此片查询startTime:20221221223000,endTime:20221221224000,读出数据量:3727,耗时:913ms
此片查询startTime:20221221234000,endTime:20221221235000,读出数据量:2325,耗时:476ms
此片查询startTime:20221221005000,endTime:20221221010000,读出数据量:2910,耗时:376ms
此片查询startTime:20221221000000,endTime:20221221001000,读出数据量:3573,耗时:710ms

此方法没有控制每个时间片交易量,可以优化为将一天的时间二分法根据控制的时间片交易量进行分片,没必要根据固定时间分片,固定时间分片无法控制每个时间片的交易量。

临时表自增主键,根据主键ID进行分片,可以准确的控制每个分片的交易量,所以首先要对前一天的交易进行分片,分片核心SQL如下:

--1.统计出待删除数据总量
SELECT count(1) FROM TEMP_RECON_TABLE where RECON_TASK_NO =?
--2.计算出此片最小ID
select Min(ID) from (select ID from TEMP_RECON_TABLE where RECON_TASK_NO =? and ID > ? order by ID) where rownum<= ?
--3.计算出此片最大ID
select Max(ID) from (select ID from TEMP_RECON_TABLE where RECON_TASK_NO =? and ID > ? order by ID) where rownum<= ?

分片核心代码

/*** @author kkk* @Description: 默认数据分片根据id进行分片*/
@Component
public class IdPartitioner implements Partitioner {private Logger logger = LoggerFactory.getLogger(IdPartitioner.class);@Overridepublic List<BatchTask> partition(BatchJob job) {List<BatchTask> tasks = new ArrayList<>();int sum;int slice = job.getSliceSize();sum = job.getRepository().selectBatch(job);logger.info("待分片数据量sum:{}",sum);long indexId = 0;for (int i = 0; i * slice < sum; i++) {int curSlice = (i + 1) * slice <= sum ? slice : sum % slice;BatchTask task = new BatchTask();//task任务分片计算task.setCurSlice(curSlice);task.setReconTaskNo(job.getReconTaskNo());long beginId = job.getRepository().selectByClause2Id(task, "beginId", indexId);long endId = job.getRepository().selectByClause2Id(task, "endId", indexId);indexId = endId;task.setRepository(job.getRepository());task.setStartId(beginId);task.setEndId(endId);//task任务集合tasks.add(task);logger.info("第{}片完成,beginId({}),endId({}),此片数据量:{}", i + 1, beginId, endId, curSlice);}return tasks;}
}@Overridepublic long selectByClause2Id(BatchTask task, String id, long indexId) {//定义临时辨别值String endId = "endId";String beginId = "beginId";Long idNo=0L;if (id.equals(endId)) {idNo=reconMapper.queryMaxIdLimitInfo(task.getReconTaskNo(),indexId,task.getCurSlice());} else if (id.equals(beginId)) {idNo=reconMapper.queryMinIdLimitInfo(task.getReconTaskNo(),indexId,task.getCurSlice());}logger.info("执行({})查询id结果idNo:{}", id,idNo);return idNo;}

分片执行结果:

查询数据开始分片…
待分片数据量sum:1168963
.
执行(beginId)查询id结果idNo:101218001
执行(endId)查询id结果idNo:101228000
第1片完成,beginId(101218001),endId(101228000),此片数量:10000
.
执行(beginId)查询id结果idNo:101228001
执行(endId)查询id结果idNo:101238000
第2片完成,beginId(101228001),endId(101238000),此片数量:10000
. . .
执行(beginId)查询id结果idNo:102378001
执行(endId)查询id结果idNo:102386963
第117片完成,beginId(102378001),endId(102386963),此片据量:8963

2. 构建比较模型

基于应用进程内存的方式,首先我们确定使用Map来进行数据比对,那么Map的Key、Value如何定义?同时值要尽量的小,我就直接说怎么定义吧,Key使用请求通道订单号,这个可以随意,但是要保证通道侧给的对账文件里和自己支付表中都要有的字段,当然也可以是若干个字段组合,目的是能唯一确定一笔交易!那么Value该怎么定义?我们定义CompareModel对象,CompareModel对象中属性定义如下:

/*** @author kkk* @Description: 比对实体*/
public class CompareModel extends Serializable {/** 唯一索引*/private String uniqueIndex;/** 值*/private String value;/** 主键*/private Long id;public CompareModel() {}public CompareModel(String uniqueIndex, String value) {this.uniqueIndex = uniqueIndex;this.value = value;}@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;CompareModel that = (CompareModel) o;if (uniqueIndex != null ? !uniqueIndex.equals(that.uniqueIndex) : that.uniqueIndex != null) return false;return value != null ? value.equals(that.value) : that.value == null;}@Overridepublic int hashCode() {int result = uniqueIndex != null ? uniqueIndex.hashCode() : 0;result = 31 * result + (value != null ? value.hashCode() : 0);return result;}
}

下面我们来简单介绍下,上面实体类中三个字段的用意:

uniqueIndex:交易唯一索引,同Map中的Key。
value:就是我们同一笔订单要比对的值,上面我们说过了,对账无非就是看同一笔订单在双方平台是否都存在,存在的话,那么再看交易状态和交易金额是否一致,所以我们将value值定义为:交易状态/交易金额的组合,如:SUCCESS_10。
id:交易表/临时表主键,用于差异数据原交易数据的查找,这句话可能有点绕,也就是对查差异来了,入差异表时候,我们要知道这笔差异的详情信息。

Map的数据格式为:Map<String,CompareModel>
map.put(compareModel.getUniqueIndex(), compareModel);
查询出的数据格式:

uniqueIndex, value, id
2022206580909023224135688, 1+9950, 1
2022010658090924388515840, 1+1000, 2
2022010658090915596992512, 1+2000, 3
2022000658090902112960512, 1+2100, 4
2022000658090909037232128, 1+9950, 5
2022010658090916209491968, 1+2176, 6

3. 数据比对

定义完数据比较实体,那么再定义数据比较器接口:

/*** @author kkk* @Description: 对账-比较器*/
public interface IComparator {IComparator putOrigins(List<CompareModel> origins);IComparator putTargets(List<CompareModel> targets);CompareResult compare();
}

基于本地内存方式的接口实现:

/*** @author kkk* @Description: 基于内存的比对实现*/
public class LocalCacheComparator implements IComparator{private static Logger logger = LoggerFactory.getLogger(LocalCacheComparator.class);//用于存储平台交易数据private Map<String, CompareModel> originMap = null;//用于存储渠道交易数据private Map<String, CompareModel> targetMap = null;/*** 存储原始交易数据*/@Overridepublic LocalCacheComparator putOrigins(List<CompareModel> origins) {if (originMap == null) {originMap = convert2Map(origins);}else {originMap.putAll(convert2Map(origins));}return this;}/*** 存储目标交易数据*/@Overridepublic LocalCacheComparator putTargets(List<CompareModel> targets) {if (targetMap == null) {targetMap = convert2Map(targets);}else {targetMap.putAll(convert2Map(targets));}return this;}/*** List-->Map*/private static Map<String, CompareModel> convert2Map(List<CompareModel> compareModels) {Map<String, CompareModel> map = new HashMap<>(Math.max((int) (compareModels.size() / 0.75f) + 1, 16));for (CompareModel compareModel : compareModels) {map.put(compareModel.getUniqueIndex(), compareModel);}return map;}/*** 数据比对*/public CompareResult compare() {if (originMap == null || targetMap == null) {throw new RuntimeException("compare交易数据或对账数据为空,无法比对");}CompareResult result = new CompareResult();Iterator<String> iterator = originMap.keySet().iterator();for (; iterator.hasNext();) {String originKey = iterator.next();CompareModel origin = originMap.get(originKey);if (targetMap.containsKey(originKey)) {CompareModel target = targetMap.get(originKey);if (!StringUtils.equals(origin.getValue(), target.getValue())) {result.addDiff(origin, target);}targetMap.remove(originKey);}else {result.addOriginAndNotTarget(origin);}}result.addTargetAndNotOrigins(targetMap.values());return result;}
}/*** @author kkk* @Description: 对账比对结果存储器*/
public class CompareResult {//用于存储平台有、支付渠道无的交易List<CompareModel> originsAndNotTargets = new ArrayList<>();//用于存储平台无、支付渠道有的交易List<CompareModel> targetAndNotOrigins = new ArrayList<>();//用于存储平台、支付渠道差异的交易数据Map<CompareModel, CompareModel> diffs = new HashMap<>();....
}

从基于本地内存的数据比对接口实现我们可以看到核心比对方法 compare(),即比对两个map,同时将比对过程产生的差异存入CompareResult对象集合中,

从上代码可以看出将两边数据各存入Map中,进行遍历比对,比对出三种差异:
originsAndNotTargets(平台多) 、targetAndNotOrigins(渠道多) 、diffs(状态/金额不一致) ,经过这轮比对后还需要再处理一轮,因为渠道一般只是给交易状态成功的交易,所以上面比对的时候我们也只是取了我们平台交易成功状态的交易数据,经过上面比对我们不确定 targetAndNotOrigins 渠道多数据我们平台是真的没有,还是交易状态为非成功状态,所以这时候需要将渠道侧多的数据集合查询我么交易表,此步数据已经很少了,所以也基本没几条数据到这步,所耗费性能微乎其微了,毕竟一个好的支付系统对账基本也对不出什么差异的。

        // 处理渠道存在我们不存在的数据:防止“渠道-(成功) <--> 平台-(失败|处理中)”的场景覆盖不了List<CompareModel> targetAndNotOrigins = result.getTargetAndNotOrigins();if (targetAndNotOrigins.size() > 0) {List<String> uniqueIndexList = Lists.transform(targetAndNotOrigins, new Function<CompareModel, String>() {@Overridepublic String apply(CompareModel input) {return input.getUniqueIndex();}});// 根据“渠道存在我们不存在”的关键字UniqueIndex搜索数据库,检查是否存在我们为失败|处理中的交易记录,如存在,则此差异需要转移到DIFF“渠道与平台不一致”中params.put("transStatus", getFailAndProcess());List<CompareModel> originFails = compareModelRepository.queryCompareModelWithCustomFieldsAndUniqueIndex(uniqueIndexFields, valueFields, uniqueIndexList, params, ReconCodeMapping.getByCode(reconTask.getTransCode()).getModelClazz());if (CollectionUtils.isNotEmpty(originFails)) {comparator = new LocalCacheComparator();comparator.putOrigins(originFails);comparator.putTargets(targetAndNotOrigins);CompareResult result1 = comparator.compare();result.getTargetAndNotOrigins().removeAll(result1.getDiffs().values());result.getDiffs().putAll(result1.getDiffs());}}

注意更严谨的话需要对如上List targetAndNotOrigins 集合进行分片,如果系统交易出问题,真的有个几万比渠道成功,平台处理中|失败,这里反查表不进行分片的话会出问题的。

4. 差错入表
    protected void buildAndSaveReconDiffs(ReconTask reconTask, CompareResult compareResult, ReconDataStorage<ReconDiff> storage) {BaseRepository originRepository = ReconCodeMapping.getByCode(reconTask.getTransCode()).getRepository();List<CompareModel> originsAndNotTargets = compareResult.getOriginsAndNotTargets();List<CompareModel> targetAndNotOrigins = compareResult.getTargetAndNotOrigins();Map<CompareModel, CompareModel> diffMap = compareResult.getDiffs();LoggerUtil.info(logger, "[对账任务({})]  对账数据完整补全开始", reconTask.getReconTaskNo());for (CompareModel compareModel : originsAndNotTargets) {Object originData = originRepository.findOne(compareModel.getTransId());ReconDiff diff = new ReconDiff();//...storage.saveAsyn(diff);}for (CompareModel compareModel : targetAndNotOrigins) {ReconTrans reconTrans = reconTransRepository.findOne(compareModel.getTransId());ReconDiff diff = new ReconDiff();//...storage.saveAsyn(diff);}Iterator<CompareModel> iterator = diffMap.keySet().iterator();for (; iterator.hasNext(); ) {CompareModel origin = iterator.next();CompareModel target = diffMap.get(origin);ReconDiff diff = new ReconDiff();Object originData = originRepository.findOne(origin.getTransId());//...ReconTrans reconTrans = reconTransRepository.findOne(target.getTransId());//...storage.saveAsyn(diff);}LoggerUtil.info(logger, "[对账任务({})]  对账数据完整补全及异步入库结束", reconTask.getReconTaskNo());}

核心代码如上了,核心逻辑即将我们比对出的差异根据我们比较模型中的Id查询交易表/临时表填充详细数据入差错表。
如上我们就完成了我们的数据比对了,整体实现思路还是挺简单的,但是写起来还是有点费劲的,时刻要想到我么的一条SQL会涉及到多少条数据,比如临时表数据的清理不能一条SQL delete掉,数据比对完成后交易表一般会有个对账状态字段,需要将前一天的数据对账状态置为已对账时候也不能一条SQL update,还有差异日、日切非整点的,一大堆头疼的问题。

2.3.3 DiffDataDealReconTaskProcessor

此步骤省略了,没啥说的,就是有的公司要做自动差错处理(将渠道状态/金额覆盖平台状态/金额),或者差错推送出去等,具体问题具体分析吧。

总结

拙技蒙斧正,不胜雀跃。

对账系统设计~百万级数据秒级对账相关推荐

  1. C#中使用SqlBulk百万级数据秒级插入

    本文转自这篇文章,提供了一种较快的数据插入的思路,转过来做个记录. #region static void Insert() {Console.WriteLine("使用Bulk插入的实现方 ...

  2. destoon7.0对mysql5..7优化,实现单台几百万数据下秒级速度

    destoon7.0对mysql5..7优化,实现单台几百万数据下秒级速度,可以缓解吃内存的情况,希望对大家有帮助 记得要备份数据,以防万一,代码附上 ALTER TABLE `destoon_sel ...

  3. QCon大会实录:PB级数据秒级分析-腾讯云原生湖仓DLC架构揭秘

    导语 ‍‍‍‍文章整理了全球软件开发大会QCon<PB级数据秒级分析-腾讯云原生湖仓DLC架构揭秘>.大数据基于海量数据的分析,硬件.存储.计算资源尽量都可以用廉价的资源完成,如何在廉价资 ...

  4. 万级数据秒级新增到数据库中---java mybatis中使用LOAD DATA LOCAL INFILE

    在linux上使用的Shell命令 [root@java-test ~]# mysql -u**** -p****** databaseName --local-infile=1 -e "L ...

  5. 一秒级接收20W+消息落库比Mysql快1000倍

    项目地址  flowback: 亿级消息落库,大数据收集,秒级10w+数据落库,亿级数据检索秒级响应解决方案 最近有大数据落库需求秒级达到10w+于是写了个开源项目 总体使用Netty 与 堆外内存 ...

  6. 揭秘阿里秒级百万TPS平台架构实现

    转载自  揭秘阿里秒级百万TPS平台架构实现 导读:搜索离线数据处理是一个典型的海量数据批次/实时计算结合的场景,阿里搜索中台团队立足内部技术结合开源大数据存储和计算系统,针对自身业务和技术特点构建了 ...

  7. Java 百万数据秒级导出到Excel中

    出自: 腾讯课堂 700多分钟干货实战Java多线程高并发高性能实战全集 , 我学习完了之后, 我给 老师在课上说的话做了个笔记,以及视频的内容,还有代码敲了一遍,然后添加了一些注释,把执行结果也整理 ...

  8. 亿级数据多条件组合查询——秒级响应解决方案

    1 概述 组合查询为多条件组合查询,在很多场景下都有使用.购物网站中通过勾选类别.价格.销售量范围等属性来对所有的商品进行筛选,筛选出满足客户需要的商品,这是一种典型的组合查询.在小数据量的情况下,后 ...

  9. 《阿里如何实现秒级百万TPS?搜索离线大数据平台架构解读》阅读笔记

    什么是搜索离线? 何谓离线?在阿里搜索工程体系中我们把搜索引擎.在线算分.SearchPlanner等ms级响应用户请求的服务称之为"在线"服务:与之相对应的,将各种来源数据转换处 ...

最新文章

  1. SQL Server 2008 下载地址(微软官方网站)
  2. WR:Tetrasphaera PAO 代谢中的储能物质与微生物多样性及除磷效能之间的关系
  3. BZOJ 2793: [Poi2012]Vouchers(调和级数)
  4. windows+sublime text3+MINGW编译运行c
  5. 配置数据库引擎BDE(Borland DataBase Engine)
  6. 在做技术面试官时,我是这样甄别大忽悠的——如果面试时你有这样的表现,估计悬
  7. HarmonyOS之sdkmgr命令的使用
  8. java常用的框架介绍
  9. ai背景合成_AI设计制作万圣节夜景插画
  10. js 浅拷贝直接赋值_JS中实现浅拷贝和深拷贝的代码详解
  11. Javascript中的null、undefined、NaN
  12. mysql索引选择_MySQL 索引选择原则
  13. AOP面向切面编程 淘宝京东网络处理
  14. 4道过滤菜鸟的iOS面试题
  15. 计算机制谱软件finale+2011应用教程,Finale2014(打谱软件)
  16. 已经有些跑偏的“学术会议文化”!
  17. c语言float m1 m2什么意思,M0、M1、M2的涵义及其作用
  18. dynamic_cast用法总结
  19. 基于JavaSwing的雷电游戏(附论文)
  20. uni-app实战教程

热门文章

  1. PyTorch中squeeze()和unsqueeze()详解
  2. SSM框架学习(3)CRM项目核心业务
  3. 计算机英语教学教案模板,关于英语教学设计模板
  4. Google diff-match-patch源代码解析:听说比GNU diff-patch更厉害?(一)
  5. everything设置同时搜索路径
  6. 【Qt一骚操作】Qt 开发中触发鼠标悬停事件
  7. 16条思想精华之《人性的弱点》读书笔记
  8. git删除远程的分支
  9. “亮剑”埃塞俄比亚,智能交通进入“中国时刻”
  10. 安卓WebView无法显示百度地图网页版的解决办法