当前位置: 首页 > 编程日记 > 正文

完美数据迁移-MongoDB Stream的应用

目录

  • 一、背景介绍
  • 二、常见方案
    • 1. 停机迁移
    • 2. 业务双写
    • 3. 增量迁移
  • 三、Change Stream 介绍
    • 监听的目标
    • 变更事件
  • 四、实现增量迁移
  • 五、后续优化
  • 小结
  • 附参考文档

一、背景介绍

最近微服务架构火的不行,但本质上也只是风口上的一个热点词汇。
作为笔者的经验来说,想要应用一个新的架构需要带来的变革成本是非常高的。

尽管如此,目前还是有许多企业踏上了服务化改造的道路,这其中则免不了"旧改"的各种繁杂事。
所谓的"旧改",就是把现有的系统架构来一次重构,拆分成多个细粒度的服务后,然后找时间
升级割接一把,让新系统上线。这其中,数据的迁移往往会成为一个非常重要且繁杂的活儿。

拆分服务时数据迁移的挑战在哪?

  1. 首先是难度大,做一个迁移方案需要了解项目的前身今世,评估迁移方案、技术工具等等;

  2. 其次是成本高。由于新旧系统数据结构是不一样的,需要定制开发迁移转化功能。很难有一个通用的工具能一键迁移;

  3. 再者,对于一些容量大、可靠性要求高的系统,要能够不影响业务,出了问题还能追溯,因此方案上还得往复杂了想。

二、常见方案

按照迁移的方案及流程,可将数据迁移分为三类:

1. 停机迁移

最简单的方案,停机迁移的顺序如下:

242916-20180726230935975-1287608598.png

采用停机迁移的好处是流程操作简单,工具成本低;然而缺点也很明显,
迁移过程中业务是无法访问的,因此只适合于规格小、允许停服的场景。

2. 业务双写

业务双写是指对现有系统先进行改造升级,支持同时对新库和旧库进行写入。
之后再通过数据迁移工具对旧数据做全量迁移,待所有数据迁移转换完成后切换到新系统。

示意图:

242916-20180726230955258-172559890.png

业务双写的方案是平滑的,对线上业务影响极小;在出现问题的情况下可重新来过,操作压力也会比较小。

笔者在早些年前尝试过这样的方案,整个迁移过程确实非常顺利,但实现该方案比较复杂,
需要对现有的代码进行改造并完成新数据的转换及写入,对于开发人员的要求较高。
在业务逻辑清晰、团队对系统有足够的把控能力的场景下适用。

3. 增量迁移

增量迁移的基本思路是先进行全量的迁移转换,待完成后持续进行增量数据的处理,直到数据追平后切换系统。

示意图:

242916-20180726231004210-1520103142.png

关键点

  • 要求系统支持增量数据的记录。
    对于MongoDB可以利用oplog实现这点,为避免全量迁移过程中oplog被冲掉,
    在开始迁移前就必须开始监听oplog,并将变更全部记录下来。
    如果没有办法,需要从应用层上考虑,比如为所有的表(集合)记录下updateTime这样的时间戳,
    或者升级应用并支持将修改操作单独记录下来。

  • 增量数据的回放是持续的。
    在所有的增量数据回放转换过程中,系统仍然会产生新的增量数据,这要求迁移工具
    能做到将增量数据持续回放并将之追平,之后才能做系统切换。

MongoDB 3.6版本开始便提供了Change Stream功能,支持对数据变更记录做监听。
这为实现数据同步及转换处理提供了更大的便利,下面将探讨如何利用Change Stream实现数据的增量迁移。

三、Change Stream 介绍

Chang Stream(变更记录流) 是指collection(数据库集合)的变更事件流,应用程序通过db.collection.watch()这样的命令可以获得被监听对象的实时变更。
在该特性出现之前,你可以通过拉取 oplog达到同样的目的;但 oplog 的处理及解析相对复杂且存在被回滚的风险,如果使用不当的话还会带来性能问题。
Change Stream 可以与aggregate framework结合使用,对变更集进行进一步的过滤或转换。

由于Change Stream 利用了存储在 oplog 中的信息,因此对于单进程部署的MongoDB无法支持Change Stream功能,
其只能用于启用了副本集的独立集群或分片集群

监听的目标

名称说明
单个集合除系统库(admin/local/config)之外的集合,3.6版本支持
单个数据库除系统库(admin/local/config)之外的数据库集合,4.0版本支持
整个集群整个集群内除去系统库( (admin/local/config)之外的集合 ,4.0版本支持

变更事件

一个Change Stream Event的基本结构如下所示:

{_id : { <BSON Object> },"operationType" : "<operation>","fullDocument" : { <document> },"ns" : {"db" : "<database>","coll" : "<collection"},"documentKey" : { "_id" : <ObjectId> },"updateDescription" : {"updatedFields" : { <document> },"removedFields" : [ "<field>", ... ]}"clusterTime" : <Timestamp>,"txnNumber" : <NumberLong>,"lsid" : {"id" : <UUID>,"uid" : <BinData>}
}

字段说明

名称说明
_id变更事件的Token对象
operationType变更类型(见下面介绍)
fullDocument文档内容
ns监听的目标
ns.db变更的数据库
ns.coll变更的集合
documentKey变更文档的键值,含_id字段
updateDescription变更描述
updateDescription.updatedFields变更中更新字段
updateDescription.removedFields变更中删除字段
clusterTime对应oplog的时间戳
txnNumber事务编号,仅在多文档事务中出现,4.0版本支持
lsid事务关联的会话编号,仅在多文档事务中出现,4.0版本支持

Change Steram支持的变更类型有以下几个:

类型说明
insert插入文档
delete删除文档
replace替换文档,当执行replace操作指定upsert时,可能是insert事件
update更新文档,当执行update操作指定upsert时,可能是insert事件
invalidate失效事件,比如执行了collection.drop或collection.rename

利用以下的shell脚本,可以打印出集合 T_USER上的变更事件:

watchCursor=db.T_USER.watch()
while (!watchCursor.isExhausted()){if (watchCursor.hasNext()){printjson(watchCursor.next());}
}

下面提供一些样例,感受一下

insert 事件

{"_id": {"_data": "825B5826D10000000129295A10046A31C593902B4A9C9907FC0AB1E3C0DA46645F696400645B58272321C4761D1338F4860004"},"operationType": "insert","clusterTime": Timestamp(1532503761, 1),"fullDocument": {"_id": ObjectId("5b58272321c4761d1338f486"),"name": "LiLei","createTime": ISODate("2018-07-25T07:30:43.398Z")},"ns": {"db": "appdb","coll": "T_USER"},"documentKey": {"_id": ObjectId("5b58272321c4761d1338f486")}
}

update事件

{"_id" : {"_data" : "825B5829DF0000000129295A10046A31C593902B4A9C9907FC0AB1E3C0DA46645F696400645B582980ACEC5F345DB998EE0004"},"operationType" : "update","clusterTime" : Timestamp(1532504543, 1),"ns" : {"db" : "appdb","coll" : "T_USER"},"documentKey" : {"_id" : ObjectId("5b582980acec5f345db998ee")},"updateDescription" : {"updatedFields" : {"age" : 15},"removedFields" : [ ]}
}

replace事件

{"_id" : {"_data" : "825B58299D0000000129295A10046A31C593902B4A9C9907FC0AB1E3C0DA46645F696400645B582980ACEC5F345DB998EE0004"},"operationType" : "replace","clusterTime" : Timestamp(1532504477, 1),"fullDocument" : {"_id" : ObjectId("5b582980acec5f345db998ee"),"name" : "HanMeimei","age" : 12},"ns" : {"db" : "appdb","coll" : "T_USER"},"documentKey" : {"_id" : ObjectId("5b582980acec5f345db998ee")}
}

delete事件

{"_id" : {"_data" : "825B5827A90000000229295A10046A31C593902B4A9C9907FC0AB1E3C0DA46645F696400645B58272321C4761D1338F4860004"},"operationType" : "delete","clusterTime" : Timestamp(1532503977, 2),"ns" : {"db" : "appdb","coll" : "T_USER"},"documentKey" : {"_id" : ObjectId("5b58272321c4761d1338f486")}
}

invalidate 事件
执行db.T_USER.drop() 可输出

{"_id" : {"_data" : "825B582D620000000329295A10046A31C593902B4A9C9907FC0AB1E3C0DA04"},"operationType" : "invalidate","clusterTime" : Timestamp(1532505442, 3)
}

更多的Change Event 信息可以参考这里

四、实现增量迁移

本次设计了一个简单的论坛帖子迁移样例,用于演示如何利用Change Stream实现完美的增量迁移方案。
背景如下:
现有的系统中有一批帖子,每个帖子都属于一个频道(channel),如下表

频道名英文简称
美食Food
情感Emotion
宠物Pet
家居House
征婚Marriage
教育Education
旅游Travel

新系统中频道字段将采用英文简称,同时要求能支持平滑升级。
根据前面篇幅的叙述,我们将使用Change Stream 功能实现一个增量迁移的方案。

相关表的转换如下图:

242916-20180726231027808-588452509.png

原理
topic 是帖子原表,在迁移开始前将开启watch任务持续获得增量数据,并记录到 topic_incr表中;
接着执行全量的迁移转换,之后再持续对增量表数据进行迁移,直到无新的增量为止。

接下来我们使用Java程序来完成相关代码,mongodb-java--driver 在 3.6 版本后才支持 watch 功能
需要确保升级到对应版本:

<dependency><groupId>org.mongodb</groupId><artifactId>mongo-java-driver</artifactId><version>3.6.4</version>
</dependency>
  1. 定义Channel频道的转换表
public static enum Channel {Food("美食"),Emotion("情感"),Pet("宠物"),House("家居"),Marriage("征婚"),Education("教育"),Travel("旅游");private final String oldName;public String getOldName() {return oldName;}private Channel(String oldName) {this.oldName = oldName;}/*** 转换为新的名称* * @param oldName* @return*/public static String toNewName(String oldName) {for (Channel channel : values()) {if (channel.oldName.equalsIgnoreCase(oldName)) {return channel.name();}}return "";}/*** 返回一个随机频道* * @return*/public static Channel random() {Channel[] channels = values();int idx = (int) (Math.random() * channels.length);return channels[idx];}
}
  1. 为 topic 表预写入1w条记录
private static void preInsertData() {MongoCollection<Document> topicCollection = getCollection(coll_topic);// 分批写入,共写入1w条数据int current = 0;int batchSize = 100;while (current < 10000) {List<Document> topicDocs = new ArrayList<Document>();for (int j = 0; j < batchSize; j++) {Document topicDoc = new Document();Channel channel = Channel.random();topicDoc.append(field_channel, channel.getOldName());topicDoc.append(field_nonce, (int) (Math.random() * nonce_max));topicDoc.append("title", "This is the tilte -- " + UUID.randomUUID().toString());topicDoc.append("author", "LiLei");topicDoc.append("createTime", new Date());topicDocs.add(topicDoc);}topicCollection.insertMany(topicDocs);current += batchSize;logger.info("now has insert {} records", current);}
}

上述实现中,每个帖子都分配了随机的频道(channel)

  1. 开启监听任务,将topic上的所有变更写入到增量表
MongoCollection<Document> topicCollection = getCollection(coll_topic);
MongoCollection<Document> topicIncrCollection = getCollection(coll_topic_incr);// 启用 FullDocument.update_lookup 选项
cursor = topicCollection.watch().fullDocument(FullDocument.UPDATE_LOOKUP).iterator();
while (cursor.hasNext()) {ChangeStreamDocument<Document> changeEvent = cursor.next();OperationType type = changeEvent.getOperationType();logger.info("{} operation detected", type);if (type == OperationType.INSERT || type == OperationType.UPDATE || type == OperationType.REPLACE|| type == OperationType.DELETE) {Document incrDoc = new Document(field_op, type.getValue());incrDoc.append(field_key, changeEvent.getDocumentKey().get("_id"));incrDoc.append(field_data, changeEvent.getFullDocument());topicIncrCollection.insertOne(incrDoc);}
}

代码中通过watch 命令获得一个MongoCursor对象,用于遍历所有的变更。
FullDocument.UPDATE_LOOKUP选项启用后,在update变更事件中将携带完整的文档数据(FullDocument)。

watch()命令提交后,mongos会与分片上的mongod(主节点)建立订阅通道,这可能需要花费一点时间。

  1. 为了模拟线上业务的真实情况,启用几个线程对topic表进行持续写操作;
private static void startMockChanges() {threadPool.submit(new ChangeTask(OpType.insert));threadPool.submit(new ChangeTask(OpType.update));threadPool.submit(new ChangeTask(OpType.replace));threadPool.submit(new ChangeTask(OpType.delete));
}

ChangeTask 实现逻辑如下:

while (true) {logger.info("ChangeTask {}", opType);if (opType == OpType.insert) {doInsert();} else if (opType == OpType.update) {doUpdate();} else if (opType == OpType.replace) {doReplace();} else if (opType == OpType.delete) {doDelete();}sleep(200);long currentAt = System.currentTimeMillis();if (currentAt - startAt > change_during) {break;}
}

每一个变更任务会不断对topic产生写操作,触发一系列ChangeEvent产生。

  • doInsert:生成随机频道的topic后,执行insert
  • doUpdate:随机取得一个topic,将其channel字段改为随机值,执行update
  • doReplace:随机取得一个topic,将其channel字段改为随机值,执行replace
  • doDelete:随机取得一个topic,执行delete

doUpdate为例,实现代码如下:

private void doUpdate() {MongoCollection<Document> topicCollection = getCollection(coll_topic);Document random = getRandom();if (random == null) {logger.info("update skip");return;}String oldChannel = random.getString(field_channel);Channel channel = Channel.random();random.put(field_channel, channel.getOldName());random.put("createTime", new Date());topicCollection.updateOne(new Document("_id", random.get("_id")), new Document("$set", random));counter.onChange(oldChannel, channel.getOldName());
}
  1. 启动一个全量迁移任务,将 topic 表中数据迁移到 topic_new 新表
final MongoCollection<Document> topicCollection = getCollection(coll_topic);
final MongoCollection<Document> topicNewCollection = getCollection(coll_topic_new);Document maxDoc = topicCollection.find().sort(new Document("_id", -1)).first();
if (maxDoc == null) {logger.info("FullTransferTask detect no data, quit.");return;
}ObjectId maxID = maxDoc.getObjectId("_id");
logger.info("FullTransferTask maxId is {}..", maxID.toHexString());AtomicInteger count = new AtomicInteger(0);topicCollection.find(new Document("_id", new Document("$lte", maxID))).forEach(new Consumer<Document>() {@Overridepublic void accept(Document topic) {Document topicNew = new Document(topic);// channel转换String oldChannel = topic.getString(field_channel);topicNew.put(field_channel, Channel.toNewName(oldChannel));topicNewCollection.insertOne(topicNew);if (count.incrementAndGet() % 100 == 0) {logger.info("FullTransferTask progress: {}", count.get());}}});
logger.info("FullTransferTask finished, count: {}", count.get());

在全量迁移开始前,先获得当前时刻的的最大 _id 值(可以将此值记录下来)作为终点。
随后逐个完成迁移转换。

  1. 在全量迁移完成后,便开始最后一步:增量迁移

注:增量迁移过程中,变更操作仍然在进行

final MongoCollection<Document> topicIncrCollection = getCollection(coll_topic_incr);
final MongoCollection<Document> topicNewCollection = getCollection(coll_topic_new);ObjectId currentId = null;
Document sort = new Document("_id", 1);
MongoCursor<Document> cursor = null;// 批量大小
int batchSize = 100;
AtomicInteger count = new AtomicInteger(0);try {while (true) {boolean isWatchTaskStillRunning = watchFlag.getCount() > 0;// 按ID增量分段拉取if (currentId == null) {cursor = topicIncrCollection.find().sort(sort).limit(batchSize).iterator();} else {cursor = topicIncrCollection.find(new Document("_id", new Document("$gt", currentId))).sort(sort).limit(batchSize).iterator();}boolean hasIncrRecord = false;while (cursor.hasNext()) {hasIncrRecord = true;Document incrDoc = cursor.next();OperationType opType = OperationType.fromString(incrDoc.getString(field_op));ObjectId docId = incrDoc.getObjectId(field_key);// 记录当前IDcurrentId = incrDoc.getObjectId("_id");if (opType == OperationType.DELETE) {topicNewCollection.deleteOne(new Document("_id", docId));} else {Document doc = incrDoc.get(field_data, Document.class);// channel转换String oldChannel = doc.getString(field_channel);doc.put(field_channel, Channel.toNewName(oldChannel));// 启用upsertUpdateOptions options = new UpdateOptions().upsert(true);topicNewCollection.replaceOne(new Document("_id", docId),incrDoc.get(field_data, Document.class), options);}if (count.incrementAndGet() % 10 == 0) {logger.info("IncrTransferTask progress, count: {}", count.get());}}// 当watch停止工作(没有更多变更),同时也没有需要处理的记录时,跳出if (!isWatchTaskStillRunning && !hasIncrRecord) {break;}sleep(200);}
} catch (Exception e) {logger.error("IncrTransferTask ERROR", e);
}

增量迁移的实现是一个不断 tail 的过程,利用 **_id 字段的有序特性 ** 进行分段迁移;
即记录下当前处理的 _id 值,循环拉取在 该 _id 值之后的记录进行处理。

增量表(topic_incr)中除了DELETE变更之外,其余的类型都保留了整个文档,
因此可直接利用 replace + upsert 追加到新表。

  1. 最后,运行整个程序
[2018-07-26 19:44:16] INFO ~ IncrTransferTask progress, count: 2160
[2018-07-26 19:44:16] INFO ~ IncrTransferTask progress, count: 2170
[2018-07-26 19:44:27] INFO ~ all change task has stop, watch task quit.
[2018-07-26 19:44:27] INFO ~ IncrTransferTask finished, count: 2175
[2018-07-26 19:44:27] INFO ~ TYPE 美食:1405
[2018-07-26 19:44:27] INFO ~ TYPE 宠物:1410
[2018-07-26 19:44:27] INFO ~ TYPE 征婚:1428
[2018-07-26 19:44:27] INFO ~ TYPE 家居:1452
[2018-07-26 19:44:27] INFO ~ TYPE 教育:1441
[2018-07-26 19:44:27] INFO ~ TYPE 情感:1434
[2018-07-26 19:44:27] INFO ~ TYPE 旅游:1457
[2018-07-26 19:44:27] INFO ~ ALLCHANGE 12175
[2018-07-26 19:44:27] INFO ~ ALLWATCH 2175

查看 topic 表和 topic_new 表,发现两者数量是相同的。
为了进一步确认一致性,我们对两个表的分别做一次聚合统计:

topic表

db.topic.aggregate([{"$group":{"_id":"$channel","total": {"$sum": 1}}},{"$sort": {"total":-1}}])

topic_new表

db.topic_new.aggregate([{"$group":{"_id":"$channel","total": {"$sum": 1}}},{"$sort": {"total":-1}}])

前者输出结果:

242916-20180726231105165-685257136.png

后者输出结果:

242916-20180726231154007-1117844618.png

前后对比的结果是一致的!

五、后续优化

前面的章节演示了一个增量迁移的样例,在投入到线上运行之前,这些代码还得继续优化:

  • 写入性能,线上的数据量可能会达到亿级,在全量、增量迁移时应采用合理的批量化处理;
    另外可以通过增加并发线程,添置更多的Worker,分别对不同业务库、不同表进行处理以提升效率。
    增量表存在幂等性,即回放多次其最终结果还是一致的,但需要保证表级有序,即一个表同时只有一个线程在进行增量回放。

  • 容错能力,一旦 watch 监听任务出现异常,要能够从更早的时间点开始(使用startAtOperationTime参数),
    而如果写入时发生失败,要支持重试。

  • 回溯能力,做好必要的跟踪记录,比如将转换失败的ID号记录下来,旧系统的数据需要保留,
    以免在事后追究某个数据问题时找不着北。

  • 数据转换,新旧业务的差异不会很简单,通常需要借助大量的转换表来完成。

  • 一致性检查,需要根据业务特点开发自己的一致性检查工具,用来证明迁移后数据达到想要的一致性级别。

BTW,数据迁移一定要结合业务特性、架构差异来做考虑,否则还是在耍流氓。

示例代码

小结

服务化系统中扩容、升级往往会进行数据迁移,对于业务量大,中断敏感的系统通常会采用平滑迁移的方式。
MongoDB 3.6 版本后提供了 Change Stream 功能以支持应用订阅数据的变更事件流,
本文使用 Stream 功能实现了增量平滑迁移的例子,这是一次尝试,相信后续这样的应用场景会越来越多。
欢迎关注"美码师的公众号" -- 唯美食与技术不可辜负" ,期待更多精彩内容^-^

附参考文档

百亿级数据迁移-58沈剑
MongoDB-ChangeStream
Use-ChangeStream To Handle Temperature

转载于:https://www.cnblogs.com/littleatp/p/9375045.html

相关文章:

Tslib移植与分析【转】

转自&#xff1a;http://blog.csdn.net/water_cow/article/details/7215308 目标平台&#xff1a;LOONGSON-1B开发板&#xff08;mips32指令集&#xff09;编译平台&#xff1a;x86PC--VMware6.5--Ubuntu10.04&#xff08;下面简称“ubuntu系统”&#xff09; 或&am…

用 Python 写一个天天酷跑,在线摸鱼不烦恼

来源丨Python小二写出来的效果图就是这样了&#xff1a;下面就更新一下全部的代码吧~还是老样子先定义import pygame,sys import random写一下游戏配置width 1200 #窗口宽度 height 508 #窗口高度 size width, height scoreNone #分数…

php fsockopen解决办法

最近研究php多线程的问题&#xff0c;发现中文资源少的可怜&#xff0c;仅有的几篇文章被转了又转&#xff0c;但文中内容价值有限。搜索过程中发现国外很多网站引用的一篇文章写的不错&#xff0c;所以翻译过来。版权声明&#xff1a;可以任意转载&#xff0c;转载时请务必以超…

深入浅出JVM的锁优化案例

锁优化适应性自旋&#xff08;Adaptive Spinning&#xff09;线程阻塞的时候&#xff0c;让等待的线程不放弃cpu执行时间&#xff0c;而是执行一个自旋(一般是空循环)&#xff0c;这叫做自旋锁。自旋等待本身虽然避免了线程切换的开销&#xff0c;但它是要占用处理器时间的&…

DOMContentLoaded 与onload区别以及使用

一、何时触发这两个事件&#xff1f; 1、当 onload 事件触发时&#xff0c;页面上所有的DOM&#xff0c;样式表&#xff0c;脚本&#xff0c;图片&#xff0c;flash都已经加载完成了。 2、当 DOMContentLoaded 事件触发时&#xff0c;仅当DOM加载完成&#xff0c;不包括样式表&…

php-fpm – 配置详解

http://duyongguang.blogbus.com/logs/156375484.html php5.3自带php-fpm /usr/local/php/etc/php-fpm.conf pid run/php-fpm.pidpid设置&#xff0c;默认在安装目录中的var/run/php-fpm.pid&#xff0c;建议开启 error_log log/php-fpm.log错误日志&#xff0c;默认在安装目…

真香!Vision Transformer 快速实现 Mnist 识别

作者 | 李秋键出品 | AI科技大本营&#xff08;ID:rgznai100&#xff09;引言&#xff1a;基于深度学习的方法在计算机视觉领域中最典型的应用就是卷积神经网络CNN。CNN中的数据表示方式是分层的&#xff0c;高层特征表示依赖于底层特征&#xff0c;由浅入深抽象地提取高级特征…

(二十一)数组的初始化

class Demo3 {public static void main(String[] args) {//数组的初始化int[] a new int[] {12,13,14,15};int[] b {12,13,14,15};//数组的便利for(int i 0;i<4;i) {System.out.println(a[i]);}for(int i 0;i<b.length;i) {System.out.println(b[i]);}} }转载于:http…

深入探讨PHP中的内存管理问题

一、 内存在PHP中&#xff0c;填充一个字符串变量相当简单&#xff0c;这只需要一个语句"&#xff1c;?php $str hello world ; ?&#xff1e;"即可&#xff0c;并且该字符串能够被自由地修改、拷贝和移动。而在C语言中&#xff0c;尽管你能够编写例如"char …

介绍一个效率爆表的数据采集框架

作者 | 俊欣来源丨关于数据分析与可视化今天我们来聊一下如何用协程来进行数据的抓取&#xff0c;协程又称为是微线程&#xff0c;也被称为是用户级线程&#xff0c;在单线程的情况下完成多任务&#xff0c;多个任务按照一定顺序交替执行。那么aiohttp模块在Python中作为异步的…

最多显示6行并且最多显示三条文本

为什么80%的码农都做不了架构师&#xff1f;>>> private void setCommentContent(ViewHolder vh, String feedId, int commentNum, ArrayList<CommentItem> comment_lists){if(commentNum < 0 || comment_lists null || comment_lists.isEmpty()){for(in…

【刷算法】LeetCode- 两数之和

题目描述 给定一个整数数组和一个目标值&#xff0c;找出数组中和为目标值的两个数。 你可以假设每个输入只对应一种答案&#xff0c;且同样的元素不能被重复利用。 示例: 给定 nums [2, 7, 11, 15], target 9因为 nums[0] nums[1] 2 7 9 所以返回 [0, 1] 复制代码分析 第…

栈区和堆区内存分配区别

一直以来总是对这个问题的认识比较朦胧&#xff0c;我相信很多朋友也是这样的&#xff0c;总是听到内存一会在栈上分配&#xff0c;一会又在堆上分配&#xff0c;那么它们之间到底是怎么的区别呢&#xff1f;为了说明这个问题&#xff0c;我们先来看一下内存内部的组织情况&…

高精度进制转换

高精度进制转换&#xff1a; 对于普通的不是非常大的数的相互转换&#xff0c;我们一般採用不断模取余的方法&#xff0c;比如&#xff1a;将10进制数m转换成n进制数&#xff0c;则对m模n取余就可以。可是&#xff0c;假设是一个有几百、几千或者很多其它位的大数呢&#xff1f…

远程办公,你希望在家工作几天?

受疫情影响&#xff0c;员工的工作方式不得不发生改变。在过去短短的几个月内&#xff0c;远程办公从偶然一次变成了常态化。随着疫情的反复&#xff0c;远程办公再次成为了许多企业的选择。3月份携程正式启动了“32”混合办公模式&#xff0c;即每周有1-2天&#xff0c;员工可…

python爬虫日志(9)爬取代理

2019独角兽企业重金招聘Python工程师标准>>> 话不多说&#xff0c;直接上代码&#xff0c;很简单&#xff0c;很容易看懂 import requests from bs4 import BeautifulSoup import randomdef get_ip_list():print("正在获取代理列表...")ip_url http://ww…

使php支持mbstring库以及使用

1.执行yum install php-mbstring2. 修改php.ini (这一步非常重要, 部分lxadmin版本无法自动修改)echo ‘extensionmbstring.so’ >>/etc/php.ini #更具php安装目录而定3. 重启web service如果是apache: service httpd restart方法二&#xff1a;php 5.36安装目录&#xf…

仿余额宝数字跳动效果 TextCounter

1、TextCounter 效果 2、TextCounter 说明 每次打开余额宝第一件事情就去看看有多少钱&#xff0c;最炫的就是看着钱在跳动相当的舒服&#xff0c;今天放出这个效果。 温馨提示&#xff1a;支持的Android版本最低的是Android 4.0.0 IceCreamSandwich &#xff08; API等级14 &a…

年仅 16 岁的黑客少年,竟是搅乱 IT 巨头的幕后主使?

整理 | 郑丽媛出品 | CSDN近来&#xff0c;黑客组织 Lapsus$ 活跃在各大科技网站&#xff1a;窃取英伟达近 1TB 的数据、泄露三星近 190GB 的机密数据、公布微软 Bing 和 Cortana 源码…不同于大部分黑客组织&#xff0c;Lapsus$ 没有刻意隐藏自己&#xff0c;反而行事非常高调…

使用硬盘,安装双系统,Win7+CentOS

我用那个U盘装了很多次都不行&#xff0c;都是说找不到文件。最后就找了一篇博客看如何安装双系统&#xff0c;最后发现原来可以用硬盘安装的。经过5个多小时终于完成了。^-^。 1.首先是分区&#xff0c;可以使用Window7自带的磁盘管理程序进行分区。&#xff08;PS 我是用Cent…

Linux 文件系统剖析

Linux 文件系统剖析 按照分层结构讨论 Linux 文件系统 M. Tim Jones, 顾问工程师, Emulex Corp. 简介&#xff1a; 在文件系统方面&#xff0c;Linux 可以算得上操作系统中的 “瑞士军刀”。Linux 支持许多种文件系统&#xff0c;从日志型 文件系统到集群文件系统和加密文件系统…

Docker构建Nginx+Tomcat动静分离架构

随着主流Nginx WEB服务器的发展&#xff0c;现在基于Nginx的WEB服务器已广泛应用于各大互联网企业。今天我们来使用docker构建我们的LinuxNginxTomcat动静分离服务器。1) 启动docker镜像查看当前系统存在的镜像&#xff0c;我这里为CentOS6.6&#xff0c;大家可以参考我第一…

硬核!Python 四种变量的代码对象和反汇编分析

作者 | 大奎整理 | 阳哥来源丨Python数据之道在Python基础的学习过程中&#xff0c;对变量和参数的理解有助于我们从更基础层面了解Python语言的运行。在这个过程中&#xff0c;还是有不少冷门和细节的地方需要进一步熟悉。今天我们来分享Python四种变量的代码对象和反汇编分析…

Python--数据存储:pickle模块的使用讲解

在机器学习中&#xff0c;我们常常需要把训练好的模型存储起来&#xff0c;这样在进行决策时直接将模型读出&#xff0c;而不需要重新训练模型&#xff0c;这样就大大节约了时间。Python提供的pickle模块就很好地解决了这个问题&#xff0c;它可以序列化对象并保存到磁盘中&…

Linux虚拟内存和物理内存精华【美】

原文地址&#xff1a; 《Playing with Virtual Memory》 http://www.snailinaturtleneck.com/blog/2011/08/30/playing-with-virtual-memory/ 扩展阅读&#xff1a; 《Understanding Memory》 http://www.ualberta.ca/CNS/RESEARCH/LinuxClusters/mem.html 《Understanding Vir…

留不住客户?该从你的系统上找找原因了

本篇文章暨 CSDN《中国 101 计划》系列数字化转型场景之一。 《中国 101 计划——探索企业数字化发展新生态》为 CSDN 联合《新程序员》、GitCode.net 开源代码仓共同策划推出的系列活动&#xff0c;寻访一百零一个数字化转型场景&#xff0c;聚合呈现并开通评选通道&#xff0…

系统配置文件备份比较

客户的系统出各种问题&#xff0c;这次出了问题整整一天都没找出原因&#xff0c;都红脸了&#xff0c;最后发现是系统配置文件被改掉了&#xff0c;简直不能忍&#xff0c;所以写了这个脚本&#xff0c;放到定时任务里面&#xff0c;每天备份比较配置文件import difflib impor…

RPC是什么?为什么要学习RPC?

随着近几年分布式、微服务架构的火热&#xff0c;RPC在开发工作中使用的越来越多&#xff0c;也变的越来越重要。 今天我们来看RPC是什么&#xff0c;为什么要了解RPC,通过学习RPC我们能掌握什么内容&#xff1f; 什么是「RPC」 RPC 全称 Remote Procedure Call, wikipedia的部…

Lua学习笔记6:C++和Lua的相互调用

曾经一直用C写代码。话说近期刚换工作。项目组中的是cocos2dx-lua&#xff0c;各种被虐的非常慘啊有木有。新建cocos2dx-lua项目。打开class能够发现&#xff0c;事实上就是C项目啦&#xff0c;只是为什么仅仅有一类Appdelegate类呢&#xff1f;哈哈,我相信聪明的你一定猜到了&…

Redis消息通知系统的实现

Redis消息通知系统的实现Posted on 2012-02-29by 老王 http://huoding.com/2012/02/29/146最近忙着用Redis实现一个消息通知系统&#xff0c;今天大概总结了一下技术细节&#xff0c;其中演示代码如果没有特殊说明&#xff0c;使用的都是PhpRedis扩展来实现的。内存比如要推送一…