Neo4J部署
官网提供 搭建教程 涵盖了所有平台, 主要是硬件设备满足即可, 最低需要双核4G配置才能获取到较高效率.
这里采用系统 apt|yum 按照部署:
# 注意目前官方对于Java版本有需求, 所以可能需要按照官方处理
# 配置官网源
wget -O - https://debian.neo4j.com/neotechnology.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/neotechnology.gpg
echo 'deb [signed-by=/etc/apt/keyrings/neotechnology.gpg] https://debian.neo4j.com stable 5' | sudo tee -a /etc/apt/sources.list.d/neo4j.list
sudo apt-get update
# 查看安装版本
apt list -a neo4j
# Ubuntu的话需要启用 universe 库, 否则可能安装失败
sudo add-apt-repository universe
# 这里默认采用社区版, 付费版本需要另外配置
sudo apt-get install neo4j=1:5.26.0 // 社区版
# sudo apt-get install neo4j-enterprise=1:5.26.0 // 企业版
安装完成之后配置跟随启动和启动:
# 启动之前最好重置下密码
sudo neo4j-admin dbms set-initial-password 密码
# 首次启动数据库前,建议使用 neo4j-admin 的 set-initial-password 命令定义本地用户 neo4j 的密码
sudo systemctl enable neo4j
sudo systemctl start neo4j
# 默认会启动WebGUI来提供测试:
curl http://localhost:7474/
# 开发环境可以通过配置修改为全网访问, 正式环境别这样处理
# 修改全网访问: server.default_listen_address=0.0.0.0
sudo vim /etc/neo4j/neo4j.conf
其他关于按照配置可以查看官网文档处理, 后续访问 7474 端口可以通过账号 neo4j 和自己重置的密码登录,
确认没问题就可以.
日常概念语法
neo4j 主要采用数据结构为 节点(Node)|标签(Label)|属性(Property) 组成,
按照常见关系型数据库来看就是 库(Database)|表(Table)|字段(Fields) 对应.
这里以玩家 player 表为样例做样本转化:
# 切换服务器1分库
USE game_1;
# 玩家表
CREATE TABLE `player`
(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '玩家ID',
`sid` INT(11) NOT NULL COMMENT '虚拟服务器ID|特殊服务器ID',
`role_name` VARCHAR(20) NOT NULL COMMENT '角色名' COLLATE 'utf8mb4_general_ci',
`account_name` VARCHAR(128) NOT NULL COMMENT '账号' COLLATE 'utf8mb4_general_ci',
`passwd` VARCHAR(128) NOT NULL DEFAULT '1' COMMENT '密码' COLLATE 'utf8mb4_general_ci',
`level` INT(11) NOT NULL DEFAULT '1' COMMENT '等级',
`ip` CHAR(20) NULL DEFAULT '' COMMENT '登陆ip' COLLATE 'utf8mb4_general_ci',
`is_pay` INT(11) UNSIGNED NULL DEFAULT '0' COMMENT '是否充值0为未充值,1为已充值',
PRIMARY KEY (`id`) USING BTREE,
INDEX `account_name` (`account_name`) USING BTREE,
INDEX `role_name` (`role_name`) USING BTREE
) COLLATE = 'utf8mb4_general_ci'
ENGINE = InnoDB;
只有数据关联性强且复杂的字段才允许被加入 Neo4j 处理, player 需要提取的关联数据:
id: 玩家ID, 最容易和子服务器ID组合查询数据sid: 子服务器ID, 注意外部已经分库比如DB_1, 游戏内部实际上还有子服务器相关( 渠道服务器分发下一级渠道 )account_name: 账号名, 代表玩家账号名account_create_time: 账号创建时间
这里就是基础的玩家关联数据, 其他字段都是会被频繁修改所以暂时不需要用到, 那么就需要按照这种方式开始对玩家信息进行建模.
这里将MySQL数据导出到Neo4j处理:
// 创建简单节点
CREATE (Game)
// 创建多个节点
CREATE (Game),(Log)
// 创建带标签和属性的节点并返回节点
CREATE (Game:player {id:1001, sid:1, account_name:'MeteorCat',account_create_time:1734761940}) RETURN Game
// 查询并且追加内部属性并返回节点
MATCH (Game:player)
SET Game.server_id = 1
RETURN Game
// 遍历所有节点并删除
MATCH (n)
WHERE NOT (n)--()
DELETE n
如果单纯写入数据节点, 直接采用以下方式处理:
// 在 Game 节点之中创建 player 数据, 注意多次重复执行会生成相同数据, 后续会介绍去重写入
CREATE (Game:player {id:1001, sid:1, server_id:1, account_name:'MeteorCat',account_create_time:1734761940})
// 另外追加数据
CREATE (Game:player {id:1002, sid:1, server_id:1, account_name:'dev1234',account_create_time:1734767940})
之后就是传统的 CURD 处理:
// 查询整个节点的玩家信息: SELECT * FROM player
MATCH (Game:player) RETURN Game
// 限制数量查询: SELECT * FROM player LIMIT 10
MATCH (Game:player) RETURN Game LIMIT 10
// 数据分页处理: SELECT * FROM player LIMIT 10,10
MATCH (Game:player) RETURN Game SKIP 10 LIMIT 10
// 条件查询: SELECT * FROM player WHERE server_id = 1
MATCH (Game:player) WHERE Game.server_id = 1 RETURN Game
// 也可以采用简洁条件查询
MATCH (Game:player{ account_name:'dev1234' }) RETURN Game
// 删除节点: 注意删除必须节点不存在关系, 也就是必须先删除关系
MATCH (Game:player{id: 1001}) DELETE Game
// 属性排序处理: SELECT * FROM player ORDER BY account_create_time DESC
MATCH (Game:player) RETURN Game ORDER BY Game.account_create_time DESC
// 字段筛选返回: SELECT id,sid,server_id FROM player
// 注意这里 id(xxx) 代表导出索引id, 可以看作数据内部的主键id
MATCH (Game:player) RETURN id(Game), Game.sid, Game.server_id,Game.account_name
// 数据数量
MATCH (Game:player) RETURN COUNT(Game:player)
// 数据求和
MATCH (Game:player) RETURN SUM(Game.id)
// 数据求和和数据数量附带条件混合查询
MATCH (Game:player{ server_id:1 }) RETURN SUM(Game.id),COUNT(Game:player)
查询数据格式:
MATCH (节点:标签{ 属性 }) WHERE 条件 RETURN 节点(可以单独提取节点字段, 也可以通过id(节点)获取主键ID)
这就是比较简单关系型数据日常使用, 之后就是重要 neo4j 关键的特性: 关联.
数据关联
neo4j 强大关联特性十分适合在后台运营当中做数据筛选统计情况, 这里生成些玩家数据:
// 在 Game 节点之中创建 player 数据, 确认是否存在重复节点
MERGE (Game:player {id:1001})
ON CREATE SET
Game.id=1001,
Game.sid=1,
Game.server_id = 1,
Game.account_name = 'MeteorCat1',
Game.account_create_time = 1734761940
RETURN Game;
// 之后再生成两个账号用于匹配查询: id=1002
MERGE (Game:player {id:1002})
ON CREATE SET
Game.id=1002,
Game.sid=1,
Game.server_id = 1,
Game.account_name = 'MeteorCat2',
Game.account_create_time = 1734761940
RETURN Game;
// id=1003
MERGE (Game:player {id:1003})
ON CREATE SET
Game.id=1003,
Game.sid=1,
Game.server_id = 1,
Game.account_name = 'MeteorCat3',
Game.account_create_time = 1734761940
RETURN Game;
之后就是生成订单数据:
// 批量插入生成 Order:recharge 订单信息, 这里重新创建 Order 节点避免混淆
// 202412220001 - 1003
MERGE (Order:recharge {order_id:202412220001})
ON CREATE SET
Order.order_id = 202412220001,
Order.player_id=1003,
Order.sid=1,
Order.server_id = 1,
Order.amount = 10000,
Order.created_at = 1734768940
RETURN Order;
// 202412220002 - 1001
MERGE (Order:recharge {order_id:202412220002})
ON CREATE SET
Order.order_id = 202412220002,
Order.player_id=1001,
Order.sid=1,
Order.server_id = 1,
Order.amount = 700,
Order.created_at = 1734762940
RETURN Order;
// 202412220003 - 1001
MERGE (Order:recharge {order_id:202412220003})
ON CREATE SET
Order.order_id = 202412220003,
Order.player_id=1001,
Order.sid=1,
Order.server_id = 1,
Order.amount = 900,
Order.created_at = 1734762940
RETURN Order;
// 202412220004 - 1003
MERGE (Order:recharge {order_id:202412220004})
ON CREATE SET
Order.order_id = 202412220004,
Order.player_id=1003,
Order.sid=1,
Order.server_id = 1,
Order.amount = 2400,
Order.created_at = 1734762940
RETURN Order;
之后就是将对应 玩家(Game:player.id) 关联到 订单(Order:recharge.player_id):
// 对1001玩家订单绑定关联, 这里生成关联: (Game) - [返回:关系名] - (Order)
// 这里的创建从 Game -> Order 节点单向的 PAY 关系
MATCH (Game:player {id: 1001, sid:1, server_id:1}),
(Order:recharge {player_id: 1001, sid:1, server_id:1})
CREATE (Game) - [r:PAY] -> (Order)
RETURN r;
// 后续也是获取所有的节点关联
MATCH (Game:player {id: 1002, sid:1, server_id:1}),
(Order:recharge {player_id: 1002, sid:1, server_id:1})
CREATE (Game) - [r:PAY] -> (Order)
RETURN r;
MATCH (Game:player {id: 1003, sid:1, server_id:1}),
(Order:recharge {player_id: 1003, sid:1, server_id:1})
CREATE (Game) - [r:PAY] -> (Order)
RETURN r;
// 删除指定关联
MERGE (Game:player {id: 1001, sid:1, server_id:1 }) - [r:PAY]
- (Order:recharge {player_id: 1001, sid:1, server_id:1})
DELETE r;
// 删除对应所有关联
MATCH ()-[r:PAY]->() DELETE r;
// 当让如果两个节点需要互相反查就需要建立双向关系
// 一般来说如果只需要 Game 查 Order 单向就只需要单向关系, 否则就需要建立双向
MATCH (Game:player {id: 1001, sid:1, server_id:1}),
(Order:recharge {player_id: 1001, sid:1, server_id:1})
CREATE (Game) - [r1:PAY] -> (Order)
CREATE (Order) - [r2:PAY] -> (Game)
RETURN r1,r2;
// 后续也是获取所有的节点关联
MATCH (Game:player {id: 1002, sid:1, server_id:1}),
(Order:recharge {player_id: 1002, sid:1, server_id:1})
CREATE (Game) - [r1:PAY] -> (Order)
CREATE (Order) - [r2:PAY] -> (Game)
RETURN r1,r2;
MATCH (Game:player {id: 1003, sid:1, server_id:1}),
(Order:recharge {player_id: 1003, sid:1, server_id:1})
CREATE (Game) - [r1:PAY] -> (Order)
CREATE (Order) - [r2:PAY] -> (Game)
RETURN r1,r2;
现在已经完成双向关联, 这时候就能采用 关联数据库 的 JOIN 操作提高效率:
// 关联查询数据, 首先就是查询所有玩家对应支付订单
MATCH (Game:player)-[r:PAY]-(Order:recharge)
RETURN Order;
// 查询出内部关于 1001 玩家ID数据
MATCH (Game:player{ id:1001 })-[r:PAY]-(Order:recharge)
RETURN Order;
这里就是简单的数据关联, 这种简单一级的数据并不能体现出效率, 所以数据不复杂的情况并不需要引入 neo4j.
复杂关联
现在需求已经不再那么简单, 后台运营目前需要做更高级查询:
- 筛选推广渠道(
admin_id->platform_id) - 筛选推广员ID(
platform_id->channel_id) - 查找关联绑定的玩家(
channel_id->player_id) - 查找关联玩家的充值金额(
player_id->recharge)
一般还包含时间|服务器ID筛选, 这里先不加入否则加深复杂不好理解
这里就是标准通过后台管理员推广账号反查到最后关联玩家充值金额的流程, 试想下对于 MySQL 处理应该怎么优化?
需要注意: 后台数据库 | 玩家数据库 | 支付订单库 三者可能是不同连接云数据库, 也就是需要跨库查询
这里模拟下分开查询的步骤:
# 假设 admin 是后台数据库, 查询出后台推广员id
# 这里假设后台运营准备查渠道下面推广员所属玩家准备定下推广员绩效
SELECT admin_id, platform_id
FROM admin
WHERE username = "游戏运营";
# 假设后台运营选择某个平台, 比如微信小游戏的推广组
# 这里导出该推广组当中的所有后台关联的推广管理员id列表
SELECT channel_id
FROM admin
WHERE platform_id = 1;
# 已经拿到推广组的所有后台管理员渠道标识id, 这时候需要去玩家库查询绑定的玩家
SELECT player_id, channel_id
FROM player
WHERE channel_id IN (10004, 10006, 10053);
# 这时候要去充值数据库查询最后这些玩家充值数额
SELECT DISTINCT player_id, SUM(recharge) as amount
FROM order
WHERE player_id IN (1001, 1002, 1003);
如果加上按照充值金额排序的情况, 用来自主排序直接查看最差绩效.
最后呈现的表格效果如下:
| 渠道ID | 推广员ID | 充值数量 | 充值金额 | … | |
|---|---|---|---|---|---|
| 1 - 微信小游戏的推广组 | 10004 - 推广员A | 0 | 0 | … | |
| 1 - 微信小游戏的推广组 | 10006 - 推广员B | 1 | 648 | … | |
| 1 - 微信小游戏的推广组 | 10053 - 推广员C | 4 | 1024 | … |
这样就能在月度盘点时候对后台推广账号计算绩效核算, 而对于 neo4j 来说只需要导入数据并建立关系即可,
这里导入测试数据处理:
// 后台推广员A
MERGE (Admin:user {admin_id:1})
ON CREATE SET
Admin.admin_id = 1,
Admin.channel_id = 0,
Admin.platform_id = 0
RETURN Admin;
// 后台推广员A
MERGE (Admin:user {admin_id:2})
ON CREATE SET
Admin.admin_id = 2,
Admin.channel_id = 10001,
Admin.platform_id = 1
RETURN Admin;
// 后台推广员B
MERGE (Admin:user {admin_id:3})
ON CREATE SET
Admin.admin_id = 3,
Admin.channel_id = 10002,
Admin.platform_id = 1
RETURN Admin;
// 后台推广员C
MERGE (Admin:user {admin_id:4})
ON CREATE SET
Admin.admin_id = 4,
Admin.channel_id = 10003,
Admin.platform_id = 1
RETURN Admin;
// 玩家信息库
MERGE (Game:player {id:1001})
ON CREATE SET
Game.id=1001,
Game.sid=1,
Game.server_id = 1,
Game.channel_id = 10001
RETURN Game;
// 之后再生成账号用于匹配查询: id=1002
MERGE (Game:player {id:1002})
ON CREATE SET
Game.id=1002,
Game.sid=1,
Game.server_id = 1,
Game.channel_id = 10001
RETURN Game;
// 之后再生成两个账号用于匹配查询: id=1003
MERGE (Game:player {id:1003})
ON CREATE SET
Game.id=1003,
Game.sid=1,
Game.server_id = 1,
Game.channel_id = 10003
RETURN Game;
// 最后充值订单数据
// 202412220001 - 1003
MERGE (Order:recharge {order_id:202412220001})
ON CREATE SET
Order.order_id = 202412220001,
Order.player_id=1003,
Order.sid=1,
Order.server_id = 1,
Order.amount = 10000,
Order.created_at = 1734768940
RETURN Order;
// 202412220002 - 1001
MERGE (Order:recharge {order_id:202412220002})
ON CREATE SET
Order.order_id = 202412220002,
Order.player_id=1001,
Order.sid=1,
Order.server_id = 1,
Order.amount = 700,
Order.created_at = 1734762940
RETURN Order;
// 202412220003 - 1001
MERGE (Order:recharge {order_id:202412220003})
ON CREATE SET
Order.order_id = 202412220003,
Order.player_id=1001,
Order.sid=1,
Order.server_id = 1,
Order.amount = 900,
Order.created_at = 1734762940
RETURN Order;
// 202412220004 - 1003
MERGE (Order:recharge {order_id:202412220004})
ON CREATE SET
Order.order_id = 202412220004,
Order.player_id=1003,
Order.sid=1,
Order.server_id = 1,
Order.amount = 2400,
Order.created_at = 1734762940
RETURN Order;
现在就是建立数据关联:
// 处理推广员和渠道关联
MATCH (Admin:platform),(Admin:user)
WHERE Admin.admin_id = 2
CREATE (Admin) - [r:PLATFROM] -> (Admin)
RETURN r;
MATCH (Admin:platform {admin_id:3}),
(Admin:user {admin_id:3})
CREATE (Admin) - [r:PLATFROM] -> (Admin)
RETURN r;
MATCH (Admin:platform {admin_id:4}),
(Admin:user {admin_id:4})
CREATE (Admin) - [r:PLATFROM] -> (Admin)
RETURN r;
// 处理玩家和渠道ID绑定
MATCH (Game:player {channel_id:10001}),
(Admin:user {channel_id:10001})
CREATE (Game) - [r1:CHANNEL] -> (Admin)
CREATE (Admin) - [r2:CHANNEL] -> (Game)
RETURN r1,r2;
MATCH (Game:player {channel_id:10002}),
(Admin:user {channel_id:10002})
CREATE (Game) - [r1:CHANNEL] -> (Admin)
CREATE (Admin) - [r2:CHANNEL] -> (Game)
RETURN r1,r2;
MATCH (Game:player {channel_id:10003}),
(Admin:user {channel_id:10003})
CREATE (Game) - [r1:CHANNEL] -> (Admin)
CREATE (Admin) - [r2:CHANNEL] -> (Game)
RETURN r1,r2;
// 处理订单和玩家的关联
MATCH (Game:player {id: 1001, sid:1, server_id:1}),
(Order:recharge {player_id: 1001, sid:1, server_id:1})
CREATE (Game) - [r1:PAY] -> (Order)
CREATE (Order) - [r2:PAY] -> (Game)
RETURN r1,r2;
MATCH (Game:player {id: 1002, sid:1, server_id:1}),
(Order:recharge {player_id: 1002, sid:1, server_id:1})
CREATE (Game) - [r1:PAY] -> (Order)
CREATE (Order) - [r2:PAY] -> (Game)
RETURN r1,r2;
MATCH (Game:player {id: 1003, sid:1, server_id:1}),
(Order:recharge {player_id: 1003, sid:1, server_id:1})
CREATE (Game) - [r1:PAY] -> (Order)
CREATE (Order) - [r2:PAY] -> (Game)
RETURN r1,r2;
那么现在就是进行关联查询时刻, 假设目前后台管理员获取到 platform=1, 现在需要汇总统计该推广组数据充值:
// 查询后台渠道 = 1 所属推广账号各自充值数, 注意看关系的箭头指向
MATCH (Admin:user) - [:CHANNEL] -> (Game:player),(Game:player) <- [:PAY] - (Order:recharge)
WHERE Admin.platform_id=1
WITH Admin.admin_id as gid, Admin.channel_id as channel, SUM(Order.amount) as amount
ORDER BY amount DESC
RETURN gid,channel,amount;
可以利用脚本直接生成测试数据填充, 当数量级别过万的时候就能明显看到查询速度依旧保持 20 ms 左右,
对比起传统数据负载筛选过滤排序简直不是同个量级的差距.
架构方案
上面能够看到 MySQL 和 neo4j 查询对比, 那么是否完全采用 neo4j 而抛弃 MySQL 这种传统数据库?
这是完全错误想法, 对于简单的静态数据查询( 简单订单充值额度查询 )利用 MySQL 配合索引可以更快更高效直接查询出来.
而 neo4j 可以看到建立双向关系的时候, 双方数据关联开销成本比 MySQL 更加庞大, 并且 neo4j 事务处理比如 MySQL,
所以对于支付订单数据这种情况可能会出问题.
所以实践之中采用多种组件的聚合数据处理, 这里展示下最开始数据处理演变流程.
- 第一代结构: 依靠单纯
MySQL集群就能满足基本统计存底 - 第二代结构: 通过
MySQL + Redis集合, 附带有内存数据库可以加快数据查询效率 - 第三代结构:
MySQL负责数据保存, 之后推送数据库和对应表的标识id到Redis, 后台程序监听数据导入neo4j建立关系
第三代结构基本上分为三层, 支持游戏这种多服务器和多数据源结合的情况:
- 冷数据落地:
MySQL只负责数据格式保存, 不做直接查询 - 数据监听层: 当
增删改触发时候需要在Redis推送变动对应数据库来源|数据库表|数据库单行标识 - 数据关联层: 后台程序进程不断从
Redis导出数据来源信息从而去连接对应数据库导入neo4j建立关系
这里的管理后台的统计筛选不再直接接触 MySQL, 而是通过 neo4j 的关系查询导出数据报表等情况.
在游戏数据库当中可能有多级渠道( 比如抖音渠道内部旗下旗下其他产品的子渠道, 数据相互不互通 ), 可能会对应不同渠道数据库进行查询筛选
目前这套架构已经基本成型, 因为的缺点也就是产生的数据过多( 本地就是以 空间 来换取效率的情况 ), 后续可以靠自己需要扩展这套架构.