将Waline从LeanCloud迁移到MongoDB
LeanCloud 将于 2027 年 1 月 12 日停止对外提供服务,老苏简单记录了从 LeanCloud 迁移到 MongoDB Atlas 的过程。
LeanCloud 将于 2027 年 1 月 12 日停止对外提供服务,老苏简单记录了从 LeanCloud 迁移到 MongoDB Atlas 的过程。
不如直接说是Bug修复日记吧…就是一些小问题拖了很久一直没搞,现在终于倒出手了…
之前专门写过一篇关于博客重定向的文章,不过当时没有注意一个问题,自动翻译的PostName是大写,这样输入到浏览器又会自动转为小写,在Redirection就会再重定向一次,经常会由于多次重定向提示刷新次数过多无法打开网页。
![]()
解决方法也很简单,打开Wordpress数据库,以phpadmin为例,在SQL查询中执行:
UPDATE wp_posts
SET post_name = LOWER(post_name)
WHERE BINARY post_name != LOWER(post_name);
全都改好之后,记得去WordPress 后台的固定链接,点击底部的 “保存更改”,刷新数据库等待生效。
修复重定向问题时点开文章赫然发现了一个主题的bug(cc @bigfa 哈哈哈哈),开启主题自带的“Friend icon-Show icon when comment author url is in blogroll”,但没想到pingback的评论也会加上icon,就这样了……
![]()
找了一下代码应该是在modules/comment.php下,增加一个判断逻辑,完美解决。
function show_friend_icon($comment_author, $comment_id, $comment)
{
// 🛑 新增逻辑:检查评论类型
// 如果是 pingback 或 trackback,直接返回作者名,不加图标
if (in_array($comment->comment_type, ['pingback', 'trackback'])) {
return $comment_author;
}
$comment_author_url = $comment->comment_author_url;
// 如果 URL 为空(有些评论可能没有留网址),也直接返回,避免报错
if (empty($comment_author_url)) {
return $comment_author;
}
// get domain name
$comment_author_url = parse_url($comment_author_url, PHP_URL_HOST);
![]()
这条应该是刚才执行 SQL 语句,把 CSV 里的 PostName 更新到了数据库的 post_name 字段,导致带了重复的字段进去,执行这条 SQL定位一下问题 post_name :
SELECT ID, post_name FROM wp_hidezemberposts WHERE post_name LIKE '%/%';
果然找到了一些:
![]()
执行修复,SUBSTRING_INDEX的作用是“只取最后一个斜杠之后的内容”。
UPDATE wp_hidezemberposts
SET post_name = SUBSTRING_INDEX(post_name, '/', -1)
WHERE post_name LIKE '%/%';
回到 WordPress 后台的固定链接,保存更改等待生效。
最后,赞扬Gemini 3,我的新晋心头好,帮我搞定了很多代码小问题,等我下次再用热力图那个功能来挑战下。
最近我的 FreshRSS 阅读器出了一个怪现象:用来实现智能刷新订阅源的 AutoTTL 扩展在这个月初突然“罢工”了。具体表现为,我手动点击刷新后,它能按调整后的 TTL 时间更一次,之后就彻底“躺平”。所有订阅源的「下次更新时间」都卡在 pending,关掉 AutoTTL 反而能恢复正常自动刷新。
这问题有点意思,像是某个环节的状态机卡住了。作为一个喜欢刨根问底的人,我花了点时间深入排查,最终发现问题的根源竟是一个看似不相关的数据库警告。记录一下这次排查的全过程,给遇到类似问题的博友一个排故参考。
![]()
[toc]
![]()
问题的核心矛盾点很明确:
actualize_script.php 和网络连接本身没问题。最讨厌这种“时灵时不灵”的问题,因为手动刷新后,AutoTTL 扩展居然还能正常工作一次(其实并不是正常工作,只是当时我没发现而已。其实这次会在更新到一半时卡住,但因为会更新一部分订阅源所以我当时一直以为订源被全部更新了)
首先重新拉取一次镜像,并检查AutoTTL 扩展的实际版本,确保他们都是最新版,以防这个 bug 其实早就被修复了,只是我没更新,或者是两者某一方更新后,另一方没更新导致的兼容性问题。
经过检查,确认目前,FreshRSS、PostgreSQL、AutoTTL都是他们各自的最新版本了。
看眼日志里都有点啥问题,是不是某个订阅源有问题,导致卡死在它上边了
虽然日志中有很多类似报错
cURL error 28: Operation timed out
HTTP 503 Service Unavailable!
HTML+XPath Web scraping failed for
Error fetching content: HTTP code 0: Could not resolve host:
但这基本都是订阅源本身的问题,比如触发了源的抓取频率限制,源站服务器卡了。并没有发现会引起订阅源无法更新的故障。于是这时我感觉肯定是扩展的锅,于是就跑去 github 给 AutoTTL 发了个 issues。
扩展作者mgnsk的回复提醒了我“How often does your cron run? A pending status means that the time for updating the feed has arrived but cron has not run yet.(cron 每隔多久运行一次?挂起状态意味着更新 feed 的时间已到,但 cron 尚未运行。)”
FreshRSS 的自动更新依赖于容器内的 Cron 服务定时执行任务,既然自动更新卡住,那就先检查 cron 是不是正常工作。
FreshRSS本体容器名:freshrss-app
PostgreSQL数据库容器名:freshrss-db
PostgreSQL数据库用户名:freshrss
PostgreSQL数据库密码:freshrss
docker exec -it freshrss-app /bin/bash
service cron status,结果显示 cron is running.。嗯,系统级的 cron 在正常走,没问题。
查看定时任务:看看具体定时任务是什么
执行 crontab -l,看到了关键配置:
*/21 * * * * . /var/www/FreshRSS/Docker/env.txt; su www-data -s /bin/sh -c 'php /var/www/FreshRSS/app/actualize_script.php' 2>> /proc/1/fd/2 > /tmp/FreshRSS.log
这个配置设计得很周到:先加载环境变量文件,然后切换到 www-data 用户执行更新脚本,还把日志重定向了。
php /var/www/FreshRSS/app/actualize_script.php :结果直接罢工了,好吧看来环境变量是必须的。. /var/www/FreshRSS/Docker/env.txt; su www-data -s /bin/sh -c 'php /var/www/FreshRSS/app/actualize_script.php' 结果订阅源正确刷新了! 这说明Docker内,cron设置的更新命令本身和权限设置都是正确的,所以如果不使用 AutoTTL 时能正常更新是理所应当的。AutoTTL 的工作原理,其实就是
1. 先根据每个订阅源历史上的平均更新间隔,最短更新间隔,计算出每个不同的订阅源,最合适的刷新间隔。
2. 拦截系统的cron,让他不是刷新所有订阅源,而是改为触发 AutoTTL,由 AutoTTL 去判断本次 cron 应该去刷新哪些订阅源。
3. 就在这时,我注意到了一个事情:AutoTTL 会往数据库里写数据并计算排序他们 既然刚才手动执行系统级 Cron 任务能成功,为什么自动运行时 AutoTTL 就不行呢?差别就在于“手动”和“自动”之间的环境差异。我意识到,刚才的输出信息我还没仔细看。
再次手动执行 Cron 任务,但这次我紧紧盯着终端输出。果然,在一堆刷新成功的提示信息之间,发现了一条 WARNING:
WARNING: database "freshrss" has a collation version mismatch
DETAIL: The database was created using collation version 2.36, but the operating system provides version 2.41.
HINT: Rebuild all objects in this database that use the default collation and run ALTER DATABASE freshrss REFRESH COLLATION VERSION, or build PostgreSQL with the right library version.
这个警告来自于 PostgreSQL 数据库。大意是:数据库的排序规则版本和操作系统提供的版本不匹配。通常是因为系统底层库升级了,但数据库对象还用的是旧规则。
我想起来,月初时服务器宕机了一次,被我顺势维护了一番,当属将所有能更新的 docker 都手动更新了一次,而日常docker 的自动更新是由 Watchtower 做的,为了稳定性,我并不允许 Watchtower 去更新 docker 中的数据库版本,这次我看 PostgreSQL 只是一个小版本升级( 15.14 → 15.15 )更新日志中没改啥东西,就顺手也给升级了。
根据警告信息的提示,我们需要对 PostgreSQL 数据库进行操作。
# 进入 PostgreSQL 的容器,使用 psql 客户端连接
docker exec -it freshrss-db psql -U freshrss -d freshrss
REINDEX DATABASE freshrss;
这个过程可能会花费一些时间,取决于你数据库大小。
刷新数据库的排序规则版本:
重建完成后,重建完成后,执行 WARNING 提示中的命令,更新数据库的系统目录版本:
ALTER DATABASE freshrss REFRESH COLLATION VERSION;
我推测是这样的机制
pending。希望这篇记录能帮到遇到类似问题的朋友。如果你的 FreshRSS 或者其他使用了 PostgreSQL 的Docker 也出现了什么灵异现象,不妨先去检查一下数据库日志,说不定会有惊喜(或者说惊吓)。
具体这次 Docker 官方在 PostgreSQL 升级时做了什么,可以参考这篇文章《原地报废:不要在生产环境用Docker跑PostgreSQL!》
The post FreshRSS 自动更新订阅源失效排查:AutoTTL 扩展失效竟是 Docker 官方埋下的坑 appeared first on 秋风于渭水.
就像 Martin Fowler 说的那样,“分布式调用的第一原则就是不要分布式”,谈分布式锁也要先说,不要使用分布式锁。原因很简单,分布式系统是软件系统中复杂的一种形式,而分布式锁是分布式系统中复杂的一种形式,没有必要的复杂性就不要引入。
有的逻辑是没有副作用的(纯函数代码),那就可以无锁执行;有的数据经过合理的 sharding 之后,可以使用单线程(单节点)执行,那就单线程执行。
比如一种常见的模式就是使用 queue(比如 Kafka),任务全部放到队列中,然后根据 sharding 的逻辑,不同的 consumer 来处理不同的任务,互相之间不会干扰冲突。
还有一个例子是 Kotlin Coroutine,通过指定一个单线程的 dispatcher,也可以保证它执行的操作之间互相不会有多线程的冲突问题。
有了这样的原则以后,再来谈谈几种分布式锁。
分布式系统中,我觉得我们最常见的锁就是使用一个中心数据库来做的。
一种是悲观锁,就是 “select xxx … for update” 这样的,相应的数据行会被锁上,直到 commit/rollback 操作发生。如果被别人锁了,当前线程没得到锁的话就会等着。
还有一种是乐观锁,就是使用版本号,“update … where … version=A” 这样的。如果 update 成功,表示获取锁成功,并且操作也成功;否则就是 update 失败,需要重新获取状态再来操作一遍。
大多数情况下,后者要更高效一些,因为阻塞的时间通常更短,不过在锁竞争比较激烈的情况下,反而效率会反过来。另外一个,悲观锁代码写起来会容易一些,因为 select 语句执行和 commit/rollback 是两步操作,因此二者之间可以放置任意逻辑;而乐观锁则是需要把数据的写操作和 version 的比较放在一条语句里面。
这两种都很常见,基本上我接触过的一半以上的项目都用过两者。这个数据库不一定非得是关系数据库,但是强一致性必须是保证的。
使用 S3 来创建文件,让创建成功的节点得到锁,文件里面也可以放自定义的内容。我们去年的项目用到这个机制。这种方式是建立在 S3 2020 年 12 月 1 日,上线的 strong consistency 的 feature。
大致上,有这样两种思路:
使用这种方式,对于那些本来就需要使用 S3 文件系统来共享任意信息的情况很方便,但是需要自己处理超时的问题,还有 retention 策略(该不该/什么时候删掉文件)。
Redlock 就是 Redis 的锁机制。Martin Kleppmann(就是那个写《Design Data-Intensive Applications》的作者)几年前写过一篇文章,来吐槽 Redlock 在几种情况下是有问题的:

问题可以理解,可是仔细想想这个问题的本质是什么?它的本质其实就是消息延迟+重排序的问题,或者更本质地说,就是分布式系统不同节点保持 consistency 的问题,因为 lock service 和 client 就是不同的节点,lock service 认为之前的锁过期了,并重分配锁给了 client 2,并且 client 2 也是这样认为的,可是 client 1 却不是,它在 GC 之后认为它还持有者锁呢。
如果我们把数据的写操作和锁管理的操作彻底分开,这个问题就很难解决,因为两个节点不可能 “一直” 在通信,在不通信的时间段内,就可能会发生这种理解不一致的情况。但是如果我们把写操作和锁管理以某种方式联系上,那么这个问题还是可以被解决的。简单说,就是物理时钟不可靠,逻辑时钟可以解决这个问题。
之后 Martin Kleppmann 提出了解决方案,他的解决方案也就是按照这个思路进行的。他的方法很简单,就是在获取锁的时候,得到一个永远递增的 token(可以被称作 “fencing token”),在执行写操作的时候,必须带上这个 token。如果 storage 看到了比当前 token 更小的 token,那么那个写操作就要被丢弃掉。

Chubby 是 Google 的分布式锁系统,论文在这里可以找到,还有这个胶片,对于进一步理解论文很有帮助。从时间上看,它是比较早的。
Chubby 被设计成用于粗粒度的(coarse-grained)锁需求,而非细粒度(fine-grained,比如几秒钟以内的)的锁需求。对于这样一个系统,文中开始就提到 consistency 和 availablity 重要性要大过 performance,后面再次提到首要目标包括 reliability,能够应对较多数量的 clients,和易于理解的语义,而吞吐量和存储容量被列在了第二位。
Chubby 暴露一个文件系统接口,每一个文件或者文件夹都可以视作一个读写锁,文件系统和 Unix 的设计思路一致,包括命名、权限等等的设计都是基于它。这是一个很有意思的设计。
对于一致性的达成,它使用 Paxos,客户端寻找 master 和写数据都使用 quorum 的机制,保证写的时候大部分节点成功,而读的时候则是至少成功读取大部分节点(R+W>N,这个思路最早我记得是 Dynamo 的论文里面有写);如果 lock 有了变化,它还提供有通知机制,因为 poll 的成本太高。
内部实现上面,每一个 Chubby 的 cell 都是由作为 replica 的 5 个服务节点组成,它们使用 Paxos 来选举 master 和达成一致,读写都只在 master 上进行(这个看起来还是挺奢侈的,一个干活,四个看戏)。如果 master 挂掉了,在 master lease 过了以后,会重新选举。Client 根据 DNS 的解析,会访问到该 cell 里面的某一个节点,它不一定是 master,但是它会告知谁是 master。
分布式锁里面比较难处理的问题不是失败,而是无响应或者响应慢的超时问题。Chubby 采用一种租约的机制,在租约期内,不会轻易变动当前的 master 节点决定。在响应超时的时期,客户端的策略就是 “不轻举妄动”,耐心等待一段时间等服务端恢复,再不行才宣告失败:

这个图的大致意思是,第一次租约 C1 续订没有问题;第二次租约续订 C2 了之后,原来的 master 挂了,心跳请求无响应,这种情况客户端不清楚服务端的状况,就比较难处理,于是它只能暂时先阻塞所有的操作,等到 C2 过期了之后,有一个 grace period;接着再 grace period 之内,新的 master 被选举出来了,心跳就恢复了,之后租约续订的 C3 顺利进行。
这显然是一个异常情形,但是一旦这种情况发生,系统是被 block 住的,会有非常大的延迟问题。思考一下,这种情况其实就是从原来的 master 到新的 master 转换的选举和交接期间,锁服务是 “暂停” 的。再进一步,这个事情的本质,其实就是在分布式系统中,CAP 原理告诉我们,为了保证 Consistency 和 Partition Tolerance,这里的情形下牺牲掉了 Availability;同时,为了保证 consistency,很难去兼顾 performance(latency 和 throughput)。
此外,有一个有点反直觉的设计是,Chubby 客户端是设计有缓存的。通常来讲,我们设计一个锁机制,第一印象就是使用缓存会带来复杂性,因为缓存会带来一致性的难题。不过它的解决办法是,使用租约。在租约期内,服务端的锁数据不可以被修改,如果要修改,那么就要同步阻塞操作通知所有的客户端,以让缓存全部失效(如果联系不上客户端那就要等过期了)。很多分布式系统都是采用 poll 的方案——一堆 client 去 poll 一个核心服务(资源),但是 Chubby 彻底反过来了,其中一个原因也是低 throughput 的考虑,毕竟只有一个 master 在干活。
对于前面提到的 Martin Kleppmann 谈到的那个问题,Chubby 给了两个解决方法:
回过头思考 Chubby 的实现机制,我觉得有这样几个启发:
《四火的唠叨》文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接
本文介绍了 Neo4j Server 的不同部署方式,并以豆瓣电影图谱数据为例说明了不同的数据导入方式,并简单介绍了 Cypher 查询语言的使用。
Neo4j 是一个流行的、Java 编写的图数据库 —— 所谓图数据库是一种 NoSQL 数据库,相比于 MySQL 之类的关系数据库(RDBMS),能更灵活地表示数据,这种灵活性体现在多方面:
图数据库强调实体和关系两个基本概念,虽然说在关系数据库中也可以表示实体和关系,但如果关系的种类繁多且实体之间通过关系构成复杂的结构的时候,用图数据库可能会更合适一些。此外,图数据库会对一些常见的图操作进行支持,典型的比如查询最短路径,如果用关系数据库来做就很比较麻烦。
目前的图数据库有很多种,根据一些排行数据,Neo4j 应该是其中最流行、使用最多的了。
Neo4j 由一个商业公司在开发、维护,并提供 GPLv3 协议的开源社区版本,当然相比他们商业授权的闭源版本,开源版本缺少一些特性,但基本功能都是完整的。
最简单的办法是从 Neo4j 的下载中心下载 Neo4j Server,解压后运行即可。可以看到下载页有三个不同的版本
对于仅仅想了解一下 Neo4j 的人来说,不妨下载 Neo4j Desktop 体验一下,本文则仅讨论 Neo4j Community Server。
目前 Neo4j Server 的版本是 3.5.x,虽然更旧的版本也能用,但建议使用 3.5.0 之后的版本,因为更早的版本是不支持全文索引的。
以 Linux 为例,假如下载的是最新的 3.5.5 版本,那么解压运行即可
我的做法是解压放到 /opt 目录下,并把对应的目录加到环境变量 PATH 里
tar xzvf neo4j-community-3.5.5-unix.tar.gz mv neo4j-community-3.5.5 /opt/neo4j/ export PATH=$PATH:/opt/neo4j/bin
这样之后就能使用 neo4j start 来启动服务了。
另外一种办法是通过 docker 来启动服务,这个就更简单了,直接利用官方提供的镜像即可。
docker pull neo4j:3.5.5 mkdir $HOME/neo4j/data -p docker run -p 7474:7474 -p 7687:7687 -v $HOME/neo4j/data/:/data neo4j
这之后就可以通过 http://localhost:7474/browser/ 这个地址访问 Neo4j Server 的 WebUI,可以在上面查询、修改数据。
然后有一些 Server 设置,可以根据自己的情况适当地进行修改,完整的配置见文档,这里罗列一些个人认为重要的
认证方式设置
默认情况下启动的 neo4j,会要求在访问时通过用户名密码进行认证,初始的用户名密码为 neo4j/neo4j ,但是会在第一次认证之后要求更换密码,有点不太方便。
一个办法是彻底关闭用户名密码认证,如果是非 docker 模式部署的,直接改 /opt/neo4j/conf/neo4j.conf 这个文件,加上这行配置
dbms.security.auth_enabled=false
如果是 docker 模式部署的,则在启动容器时,设置环境变量 NEO4J_AUTH 为 none
docker run -p 7474:7474 \ -p 7687:7687 \ -v $HOME/neo4j/data/:/data \ -e NEO4J_AUTH=none \ neo4j
另外一个办法是主动设置好密码,如果是非 docker 模式部署,需要在初次启动通过 neo4j-admin 这个命令来设置
neo4j-admin set-initial-password neo4j_password
如果是 docker 模式部署,则在启动容器时通过环境变量 NEO4J_AUTH 来设置
docker run -p 7474:7474 \ -p 7687:7687 \ -v $HOME/neo4j/data/:/data \ -e NEO4J_AUTH=neo4j/neo4j_password \ neo4j
内存设置
这块有三项设置,分别是
前两者决定了查询语言运行时候可用的内存,第三个则用于缓存数据和索引以提高查询效率。
非 docker 模式部署的,可以直接在 /opt/neo4j/conf/neo4j.conf 里修改,比如说这样
dbms.memory.heap.initial_size=1G dbms.memory.heap.max_size=2G dbms.memory.pagecache.size=4G
docker 模式部署则还是在启动容器时通过环境变量来设置,如下所示
docker run -p 7474:7474 \ -p 7687:7687 \ -v $HOME/neo4j/data/:/data \ -e NEO4j_dbms_memory_heap_initial__size=1G \ -e NEO4j_dbms_memory_heap_max__size=2G \ -e NEO4j_dbms_memory_pagecache_size=4G \ neo4j
dbms.security.allow_csv_import_from_file_urls
设置为 true,这样在执行 LOAD CSV 语句时,可以使用远程而非本地的 csv 文件。
docker 的话这样:
docker run -d -p 7474:7474 \ -p 7687:7687 \ -e NEO4J_dbms_security_allow__csv__import__from__file__urls=true \ -v /home/emonster/data/neo4j/:/data \ neo4j
这个之后会具体再聊一下。
dbms.connectors.default_listen_address
这个不设置的话,部署起来的 server 就只能监听本地的请求,如果是在生产中用 Neo4j Server 的话,要设置成
dbms.connectors.default_listen_address=0.0.0.0
docker 的话默认已经设置好了,不用自己再单独设置。
所有的配置项及其值可以用如下查询语言查询
call dbms.listConfig()
如果要查询单独某项的值,比如 "dbms.connectors.default_listen_address",则这样
call dbms.listConfig("dbms.connectors.default_listen_address")
为方便说明,我准备了一份豆瓣电影的图谱数据(说是图谱其实结构很简单)放在 Github 上,可以先将其 clone 到本地
git clone https://github.com/Linusp/kg-example
在这个项目下的 movie 目录里有按照 Neo4j 支持的格式整理好的实体、关系数据
(shell) $ cd kg-example (shell) $ tree movie movie ├── actor.csv ├── composer.csv ├── Country.csv ├── director.csv ├── district.csv ├── Movie.csv └── Person.csv 0 directories, 7 files
上述数据包含三类实体数据:
| 实体类型 | 数据文件 | 数量 | 说明 |
|---|---|---|---|
| Movie | Movie.csv | 4587 | 电影实体 |
| Person | Person.csv | 22937 | 人员实体 |
| Country | Country.csv | 84 | 国家实体 |
此外还包含四类关系数据
| 关系类型 | 主语实体类型 | 宾语实体类型 | 数据文件 | 数量 | 说明 |
|---|---|---|---|---|---|
| actor | Movie | Person | actor.csv | 35257 | 电影的主演 |
| composer | Movie | Person | composer.csv | 8345 | 电影的编剧 |
| director | Movie | Person | director.csv | 5015 | 电影的导演 |
| district | Movie | Country | district.csv | 6227 | 电影的制片国家/地区 |
下图是这份数据加载到 Neo4j 后的部分可视化示例
使用 neo4j-import 命令行工具导入 csv 数据是几种数据加载方式中最快的一种,但它不能导入数据到已有的数据库中,每次执行都是产生一个全新的数据库,因此必须在一条命令里将数据库中要包含的数据全部都制定好。
可以用下面的命令来导入豆瓣电影图谱数据
neo4j-import --into graph.db --id-type string \ --nodes:Person movie/Person.csv \ --nodes:Movie movie/Movie.csv \ --nodes:Country movie/Country.csv \ --relationships:actor movie/actor.csv \ --relationships:composer movie/composer.csv \ --relationships:director movie/director.csv \ --relationships:district movie/district.csv
上述命令会在当前目录下生成一个 graph.db 目录,就是最终产生的一个全新的数据库。要启用这个数据库,必须将其放置到 Neo4j Server 的 data 目录下:
如果当前 Neo4j Server 正在运行,需要先停掉它
neo4j stop
删除或备份原有的数据库
mv /opt/neo4j/data/databases/graph.db /opt/neo4j/data/databases/graph.db.bak
将产生的 graph.db 放置到 server 的 data 目录下
cp graph.db /opt/neo4j/data/databases/ -r
重新启动 Neo4j Server
neo4j start
实体和关系一共 8 万多条,在我的个人电脑上一共花费 3s 多
IMPORT DONE in 3s 692ms. Imported: 27608 nodes 54844 relationships 91628 properties Peak memory usage: 524.24 MB
如果是以 docker 的方式来使用 Neo4j,则稍有不同,需要在执行的时候将 movie 目录和输出结果所在的目录都挂载到容器里。假设说我们希望最终输出结果到 $HOME/neo4j/data 目录下,那么,先创建这个目录
mkdir $HOME/neo4j/data/databases -p
然后执行
docker run -v $PWD/movie:/movie:ro -v $HOME/neo4j/data:/data/ \ neo4j neo4j-import --into /data/databases/graph.db --id-type string \ --nodes:Person /movie/Person.csv \ --nodes:Movie /movie/Movie.csv \ --nodes:Country /movie/Country.csv \ --relationships:actor /movie/actor.csv \ --relationships:composer /movie/composer.csv \ --relationships:director /movie/director.csv \ --relationships:district /movie/district.csv
然后再用 docker 启动 Neo4j Server,并让其使用刚刚产生的数据库
docker run -p 7474:7474 \ -p 7687:7687 \ -v $HOME/neo4j/data/:/data \ -e NEO4J_AUTH=neo4j/neo4j_password \ neo4j
用 LOAD CSV 语句同样可以加载 csv 数据,不过和 neo4j-import 不一样,本质上它只是负责从 csv 文件中读取数据,如果要将读取到的数据写入到数据库中,还必须通过 CREATE 语句。也正因如此,用 LOAD CSV 语句来加载数据,不需要将 Neo4j Server 停掉。
用 LOAD CSV 语句将豆瓣电影图谱加载到数据库中的做法是下面这样的
从 Movie.csv 中加载电影数据并创建 Movie 实体
USING PERIODIC COMMIT 1000 LOAD CSV with headers from 'https://raw.githubusercontent.com/Linusp/kg-example/master/movie/Movie.csv' as line CREATE (:Movie { id:line["id:ID"], title:line["title"], url:line["url"], cover:line["cover"], rate:line["rate"], category:split(line["category:String[]"], ";"), language:split(line["language:String[]"], ";"), showtime:line["showtime"], length:line["length"], othername:split(line["othername:String[]"], ";") })
其中 "using periodic commit 1000" 表示每读取 1000 行数据就写入一次。
从 Person.csv 中加载人员数据并创建 Person 实体
USING PERIODIC COMMIT 1000 LOAD CSV with headers from 'https://raw.githubusercontent.com/Linusp/kg-example/master/movie/Person.csv' as line CREATE (:Person {id:line["id:ID"], name:line["name"]})
从 Country.csv 中加载国家数据并创建 Country 实体
USING PERIODIC COMMIT 1000 LOAD CSV with headers from 'https://raw.githubusercontent.com/Linusp/kg-example/master/movie/Country.csv' as line CREATE (:Country {id:line["id:ID"], name:line["name"]})
创建关系
每个关系的 csv 文件都是如下格式(以 actor.csv 为例)
":START_ID",":END_ID" "5ec851a8b7b7bbf0c9f42bbee021be00","3a20ded16ebce312f56a562e1bef7f05" "5ec851a8b7b7bbf0c9f42bbee021be00","8101549e05e6c1afbea62890117c01c6" "5ec851a8b7b7bbf0c9f42bbee021be00","111a3c7f6b769688da55828f36bbd604" "5ec851a8b7b7bbf0c9f42bbee021be00","5cc5d969f42ce5d8e3937e37d77b89b5" "5ec851a8b7b7bbf0c9f42bbee021be00","a5e6012efc56f0ca07184b9b88eb2373" "5ec851a8b7b7bbf0c9f42bbee021be00","435c8172c14c24d6cd123c529a0c2a76" "5ec851a8b7b7bbf0c9f42bbee021be00","5dfb355a385bcfe9b6056b8d322bfecb" "5ec851a8b7b7bbf0c9f42bbee021be00","5076a2f7479462dcc4637b6fe3226095" "5ec851a8b7b7bbf0c9f42bbee021be00","c7103a9ad17cf56fd572657238e49fff"
在创建关系的时候实际上是根据两个 id 查询到对应的实体,然后再为其建立关系。虽然我在准备这份数据时,已经保证了每个实体的 id 都是全局唯一的,但在没有创建索引的情况下,用这个 id 来查询实体会以遍历的形式进行,效率很差,所以在创建关系前,先创建一下索引。
为 Movie 实体的 id 属性创建索引
CREATE INDEX ON :Movie(id)
为 Person 实体的 id 属性创建索引
CREATE INDEX ON :Person(id)
为 Country 实体的 id 属性创建索引
CREATE INDEX ON :Country(id)
然后继续用 LOAD CSV 来创建关系
从 actor.csv 中加载数据并创建 actor 关系
USING PERIODIC COMMIT 1000 LOAD CSV with headers from 'https://raw.githubusercontent.com/Linusp/kg-example/master/movie/actor.csv' as line MATCH (a:Movie {id:line[":START_ID"]}) MATCH (b:Person {id:line[":END_ID"]}) CREATE (a)-[:actor]->(b)
从 composer.csv 中加载数据创建 composer 关系
USING PERIODIC COMMIT 1000 LOAD CSV with headers from 'https://raw.githubusercontent.com/Linusp/kg-example/master/movie/composer.csv' as line MATCH (a:Movie {id:line[":START_ID"]}) MATCH (b:Person {id:line[":END_ID"]}) CREATE (a)-[:composer]->(b)
从 director.csv 中加载数据创建 director 关系
USING PERIODIC COMMIT 1000 LOAD CSV with headers from 'https://raw.githubusercontent.com/Linusp/kg-example/master/movie/director.csv' as line MATCH (a:Movie {id:line[":START_ID"]}) MATCH (b:Person {id:line[":END_ID"]}) CREATE (a)-[:director]->(b)
从 district.csv 中加载数据并创建 district 关系
USING PERIODIC COMMIT 1000 LOAD CSV with headers from 'https://raw.githubusercontent.com/Linusp/kg-example/master/movie/district.csv' as line MATCH (a:Movie {id:line[":START_ID"]}) MATCH (b:Country {id:line[":END_ID"]}) CREATE (a)-[:district]->(b)
严格来说,上面的 LOAD CSV 的方式,也是在用 Cypher 语句,不过说到底它还是要依赖一个外部的 CSV 文件,自由度没那么高。而 Neo4j Server 本身还提供 RESTful API,利用这个 API 就可以进行编程来完成更复杂的需求。
以创建实体为例来说明一下 Neo4j Server 的 RESTful API。假设说我们要创建三个 Person 实体,简单起见,我们假设每个 Person 实体需要有 id, name, age 三个属性,比如
[
{
"id": "person1",
"name": "张志昂",
"age": 23
},
{
"id": "person2",
"name": "刘文刀",
"age": 18
},
{
"id": "person3",
"name": "孙子小",
"age": 22
}
]
通过 RESTful API,可以一次性创建这三个 Person 实体
POST http://neo4j:neo4j_password@localhost:7474/db/data/cypher Content-Type: application/json { "query": "UNWIND {values} as data CREATE (:Person {id: data.id, name: data.name, age: data.age})", "params": { "values": [ {"id": "person1", "name": "张志昂", "age": 23}, {"id": "person2", "name": "刘文刀", "age": 18}, {"id": "person3", "name": "孙子小", "age": 22} ] } }
这种通过带参数的 query 进行批量写入的方式,和 MySQL 等数据库的接口很相似,不过在 Cypher 中可以通过 UNWIND 语句做一些复杂的事情。详见文档。
用 Python 来做的话大概是这个样子
import requests url = "http://neo4j:neo4j_password@localhost:7474/db/data/cypher" payload = { "query": ( "UNWIND {values} as data " "CREATE (:Person {id: data.id, name: data.name, age: data.age})" ), "params": { "values": [ {"id": "person1", "name": "张志昂", "age": 23}, {"id": "person2", "name": "刘文刀", "age": 18}, {"id": "person3", "name": "孙子小", "age": 22} ] } } requests.post(url, json=payload)
或者也可以使用 Neo4j 官方的 Python 客户端
import neo4j client = neo4j.GraphDatabase.driver( 'bolt://localhost:7687', auth=('neo4j', 'neo4j_password') ) with client.session() as session: query = ( "UNWIND {values} as data " "create (:Person {id: data.id, name: data.name, age: data.age})" ) values = [ {"id": "person1", "name": "张志昂", "age": 23}, {"id": "person2", "name": "刘文刀", "age": 18}, {"id": "person3", "name": "孙子小", "age": 22} ] session.run(query, {'values': values})
此处仅记录我个人认为常用或重要的部分,完整内容请参考官方文档。
在 Cypher 中,用小括号来表示一个实体,用中括号来表示关系,这个是 Cypher 语言中最基础的表示了。
实体的各种表示方式如下:
表示一个 Person 类型的实体,并记其名字为 a
(a:Person)
表示一个 id 值为 "person1" 的实体,并记其名字为 a
(a {id:"person1"})
表示任意一个实体,并记其名字为 a ,之后可以通过 WHERE 语句来对其进行约束
(a)
表示一个任意的匿名实体
()
关系的各种表示方式如下
表示一个 actor 类型的实体,并记其名字为 r
[r:actor]
表示任意一个实体,并记其名字为 r
[r]
表示一个任意的匿名实体
[]
在上面的基础之上,即可方便地表示图数据中的一条实际的边,比如说
表示命名为 m 的 Movie 类型实体到命名为 p 的 Person 类型实体、匿名的边
(m:Movie)-[]->(p:Person)
这里的 "->" 表示关系的方向是从 m 到 p 的
同上,但要求关系类型为 actor
(m:Movie)-[:actor]->(p:Person)
同上,并记关系的名字为 r
(m:Movie)-[r:actor]->(p:Person)
更复杂的表示:Person p 是 Movie m1 的主演,同时也是 Movie m2 的导演
(m1:Movie)-[r1:actor]->(p:Person)<-[r2:director]-(m2:Movie)
掌握上述表示方法后,就可以用其来进行数据的创建、查询、修改和删除操作也就是俗称的 CRUD 了。
查询实体
MATCH (p:Person {name:"黄渤"}) RETURN p
或者
MATCH (p:Person) WHERE p.name="黄渤" RETURN p
结果如下图所示
当然也可以不带筛选条件
MATCH (p:Person) RETURN p LIMIT 10
(没错,我非常心机地把结果排成了整齐的两排哈哈)
创建实体
语法类似这样
create (:Person {id:"ac1d6226", name:"王大锤"})
修改实体
MATCH (p:Person) WHERE p.id="ac1d6226" SET p.name="黄大锤"
删除实体
MATCH (p:Person) WHERE p.id="ac1d6226" DELETE p
注意,删除实体时,如果这个实体还有和其他实体有关联关系,那么会无法删除,需要先将其关联关系解除才可以。
查询关系
查询 actor 类型的关系,不对起点、终点做任何约束
MATCH (m)-[r:actor]->(p) RETURN * LIMIT 10
结果如下图所示:
查询 actor 类型的关系,对起点(或终点)做约束,比如说,查询主演是黄渤的所有电影
MATCH (m:Movie)-[r:actor]->(p:Person) WHERE p.name="黄渤" RETURN *
结果如下图所示:
创建关系
语法如下,要求涉及到的两个实体 a 和 b 是已经存在的。
MATCH (a:Person {id:"person_id_a"}), MATCH (b:Person {id:"person_id_b"}) CREATE (a)-[:KNOWS]->(b)
之前导入的豆瓣电影图谱其实缺少人和人之间的关系,比如说宁浩和黄渤彼此都认识,可以加上这个关系
MATCH (a:Person), (b:Person) WHERE a.name="黄渤" and b.name="宁浩" CREATE (a)<-[:knows]->(b), (b)-[:knows]->(a)
删除关系
先用 MATCH 语句进行查询,并为其中的关系命名,然后在 DELETE 语句中用这个关系的名字即可。
MATCH (a:Person)-[r:knows]-(b:Person) WHERE a.name="黄渤" and b.name="宁浩" DELETE r
查询两个节点之间的最短路径
查询黄渤和汤姆·克鲁斯之间的最短路径
MATCH (a:Person), (b:Person), p=shortestpath((a)-[:actor*]-(b)) WHERE a.name="黄渤" and b.name="汤姆·克鲁斯" RETURN p
结果如下图所示:
CRUD 之外,索引的创建也是很重要的,如果没有创建索引或者索引设计有问题,那么可能会导致查询效率特别差。我最早开始用 Neo4j 的时候,在批量导入数据时没有建索引,导致不到五十万的数据量(包括实体和关系)的导入需要近一个小时,而在正确设置了索引之后,十几秒就完成了。对于比较慢的查询,可以用 PROFILE 语句来检查性能瓶颈。
以本文用来做示例的豆瓣电影图谱来说,如果没有给 Person.name 建立索引,那么下面这个查询语句就会很慢
MATCH (p:Person) where p.name="黄渤" RETURN p
用 PROFILE 语句做一下分析,只需要再原来的 query 前加上 PROFILE 这个关键词即可。
PROFILE MATCH (p:Person) where p.name="黄渤" RETURN p
分析结果如下图所示:
从上图来看,这个查询语句的逻辑是遍历了一下所有 Person 实体,挨个比较哪个实体的 name 是「黄渤」,这无疑是极其低效的。而在创建了索引后,PROFILE 的结果是下图这个样子:
关于索引可以展开更多内容,准备另外写一篇,这里只是强调一下 PROFILE 语句的作用。
最近迁移服务器,并且搞了一个1P专业版玩玩,感觉效果很不错,想找一个管理数据库的服务,其中看了phpmyadmin等一些知名项目,但是都不太符合我的要求,最后经过筛选,找到了比较合适的,分享给大家!