
YashanDB这几年在国内数据库圈子里讨论度确实高主打Oracle兼容和国产化替代但大多数人聊的都是“能不能平滑迁移”“TPCC能跑多少分”。我这次想换个角度聊把它放到一个具体业务场景里——社交网络数据。说实话社交业务的数据量和复杂度比一般企业管理系统高出不止一个量级亿级用户、十亿级关注关系、每天上亿条动态还要抗住突发热点带来的流量尖峰。YashanDB在这种场景下到底能不能站稳脚跟、怎么用才能不拖后腿是我今天想讲清楚的事。下面这些内容不是我凭空想的是我最近带团队做的一套社交App数据层优化方案。用的就是YashanDB作为核心OLTP库跑了三个月也踩了不少坑。这篇东西不写官方宣传文案只讲实操适合后端开发、DBA和准备做国产数据库选型的朋友参考。1. 整体设计思路社交网络数据为什么需要一款靠谱的关系型数据库1.1 YashanDB选型时的三个关键理由先说结论我们最终选了YashanDB不是因为它“国产”这个标签而是因为在真实评测里它确实扛住了我们的核心负载。这里有个大背景要交代——我们的业务形态是偏微博和朋友圈结合的社交产品核心数据包括用户资料、关注关系、帖子内容、互动记录事务要求高数据一致性不能妥协。第一兼容Oracle语法和生态。我们团队有一半是从Oracle迁移过来的DBA和开发选YashanDB意味着大家的SQL经验和PL/SQL技能可以直接复用学习成本被压到了很低。第二高并发读写下的事务能力。社交场景里发帖、评论、点赞、关注都是高频小事务数据库必须把行锁冲突控制在合理范围YashanDB的锁机制和MVCC实现实测在并发写压下没有出现严重的锁等待。第三支持灵活的部署形态。可以是单机高性能版也能扩展成共享集群甚至分布式架构这样我们从一主两从起步后面流量上来再平滑扩容不用中途换存储引擎。但选型不是听厂商讲PPT就够了。我们内部做了两周的压测模拟的是真实社交行为分布90%读、10%写、读写并发跑满32核。YashanDB的表现是事务延迟P99能稳定在50ms以内脏读、幻读都没有出现。应对热点行的高并发更新时比如百万粉丝账号的粉丝数YashanDB也能通过合理的索引和事务控制保证不把数据库拖死。这也是我敢把它写进来的底气。1.2 社交数据建模的底层逻辑关系型建模为主、JSON为辅很多做社交产品的团队一上来就迷信NoSQL觉得关系型数据库扛不住直接用MongoDB存帖子、用Neo4j存关系。这个思路不能说错但会让架构变得很重——你至少得维护三套存储。我在设计时坚持一个原则能用关系模型表达的结构就规规矩矩建表只有那些结构不可控的扩展字段才交给JSON字段处理。为什么社交场景的核心关系是“用户-关注-内容-互动”这就是一张天然的星型模型。用户表和关注关系表用主外键加索引查询效率非常高帖子表和互动表借助分区和复合索引一样能跑出很好的性能。相反如果你把关系和内容全部揉进文档里查询“我关注的人最近发了什么”这种高频需求反而要在文档里做嵌套遍历性能很容易失控。YashanDB支持JSON数据类型这正好补上了关系模型的灵活性短板。我的做法是用户表里放“个性化设置”JSON字段帖子表里放“扩展属性”JSON字段用于存那些上线后随时可能变化的小功能。但注意JSON字段绝不参与JOIN和频繁的条件过滤它只做展示和低频读避免影响查询性能。1.3 一套可落地的分库分表思路在YashanDB里用分区代替拆库做社交数据库几乎所有人第一反应就是“要不要分库分表”。以我们现在的用户量级如果照搬MySQL时代的方案按user_id分1024个表接一个sharding中间件开发和运维复杂度会非常高。YashanDB里的分区表其实给了我一个更优雅的选择。具体思路是这样的能按分区解决的绝不拆物理库拆物理库只留给真正跨区的数据隔离需求。对于用户关系数据和帖子数据用主键上的UID哈希分区和创建时间的范围分区底层还是按分区独立存储、独立索引。业务代码只需要操作一张逻辑表YashanDB会自动路由到对应分区。这样做的收益很明显查询裁剪直接命中少量分区写入不再争抢同一组热页历史数据也能按分区快速归档。说句大实话分区表方案也不是万能的。如果单分区数据量超过几个TB或者单条查询并发极高该水平拆分还是得拆。我的建议是刚开始上线按分区走同时保留拆分的扩展性设计比如主键里已经带上UID前缀将来真要到分库那一步改造成本也在可控范围内。2. 核心细节解析与实操要点表结构、索引和分区策略2.1 用户、关注关系、内容、互动四类核心表的设计逻辑社交业务再复杂落到存储层核心就是四类表用户表、关注关系表、内容表、互动表。我把这四类表的建模思路挨个拆开讲你从自己的业务里找对应关系就行。用户表保存基础资料和统计冗余。注意粉丝数、关注数这种高频读的指标我是直接冗余在用户表里的而不是每次实时count。这跟数据库设计的范式理论是相悖的但社交场景里为了读性能必须做冗余。关注的写操作去维护冗余数值用事务保证一致性分摊到每次关注/取关操作里开销极小。关注关系表这是社交数据库里最大的表。我有两个核心设计一是把方向拆分两个用户之间的关注动作存一行还是两行要看业务是否有互关概念。我们的产品更接近单向关注所以干脆拆成follower_id和followee_id两列存一行。二是必须带创建时间因为关注顺序要用来做推荐的候选集。这张表我建议用HASH分区分区键选follower_id因为最频繁的查询是“某人关注了谁”和“某人被谁关注”两边都能均匀散开。内容表和互动表是热点行竞争最激烈的地方。内容表存帖子正文、图片链接、标签、创建时间。主键用全局ID生成器产生分区键用创建时间的月度范围分区。互动表统一存点赞、收藏、评论计数等行为用“互动类型对象ID”做联合主键方便实时更新和查询。互动表压力最大必须及时清理过期数据我习惯按月分区保留最近12个自然月的数据。2.2 索引不是越多越好组合索引与函数索引的取舍社交场景的索引设计特别容易走两种极端。一种是什么都不建全靠主键查询结果一跑统计报表就全表扫描另一种是每个字段都想建索引结果写入越来越慢索引膨胀比实际数据还大。我的经验是为每一个真实业务查询建立组合索引但绝不单独为一个字段建索引。拿信息流来说最核心的查询是“拉取我关注的作者的帖子按时间倒序”。对内容表来说建(author_id, create_time DESC)的组合索引是最佳方案。因为YashanDB的B树索引按前缀匹配加后缀时间列后一次索引扫描就能取到正确排序的结果不需要额外的排序操作。函数索引是另一个利器。我们有“查最近7天粉丝增长排行”的需求按用户ID分组count关注关系表。其实这种统计实时算的压力太大我更推荐用汇总表加函数索引同步更新。例如在用户表上建一个基于粉丝量区间的函数索引让统计查询快速命中目标行。虽然YashanDB的优化器对函数索引支持很好但函数索引会牺牲插入性能必须控制数量。我定了条规矩单表函数索引不超过2个且必须同时用于线上最核心的SQL。2.3 分区表按时间归档、按UID散列热点和冷数据分离分区设计的好坏直接决定一张千万级表能不能维持分钟级响应。我上文提到过内容表按时间分区关注关系表按UID散列。我来解释“为什么是这个组合”。内容数据有天然的时间冷热差异最近一周的生产内容占查询量的70%以上。按创建时间做RANGE分区每个月一个区查询时如果带上时间条件优化器会自动裁剪掉冷分区。更实用的一个功能是分区归档每月底把三个月前的分区做DETACH转成单独的归档表挪到低成本存储上。这样主表数据量被控制在合理范围大查询和备份时长都能得到明显改善。关注关系表则是另一类特征没有时间属性所有行被访问的频率几乎相等。这时候按时间分区分不到点上反而是按UID做HASH分区最合适。我用的是YashanDB的HASH分区分成64个分区每个分区数据量均匀。查询关注列表的时候条件里有follower_idYashanDB能秒级定位分区。如果是双向查询我还在SQL里带上两个条件让优化器去分区间并行实测性能也不错。3. 实操过程与核心环节实现从DDL到慢SQL优化3.1 一份可直接参考的核心表DDL理论上把你讲得多玄都不如放一份真实能跑的DDL。下面是我们线上裁剪过后的核心表结构你直接抄是没问题的只需要把字段长度和注释换成自己的业务语义。-- 用户表 CREATE TABLE social_user ( uid NUMBER(20) NOT NULL, nickname VARCHAR2(64) NOT NULL, avatar_url VARCHAR2(256), profile CLOB, fans_count NUMBER(10) DEFAULT 0 NOT NULL, follow_count NUMBER(10) DEFAULT 0 NOT NULL, create_time DATE DEFAULT SYSDATE NOT NULL, setting_json VARCHAR2(2000), CONSTRAINT pk_user PRIMARY KEY (uid) ) PARTITION BY HASH(uid) PARTITIONS 32; CREATE INDEX idx_user_create_time ON social_user(create_time);用户表本身数据量不大哈希分区更多是留扩容余地复合主键直接落uid。profile用CLOB存长篇个人介绍setting_json用字符串模拟JSON存储是我们对特别灵活字段的折中办法。-- 关注关系表 CREATE TABLE social_follow ( follower_id NUMBER(20) NOT NULL, followee_id NUMBER(20) NOT NULL, create_time DATE DEFAULT SYSDATE NOT NULL, CONSTRAINT pk_follow PRIMARY KEY (follower_id, followee_id) ) PARTITION BY HASH(follower_id) PARTITIONS 128; CREATE INDEX idx_follow_followee ON social_follow(followee_id, create_time DESC);这表字段很少但体量最大。主键按follower_id分区INCLUDE了followee_id天然支持“我关注了谁”。反向查询“谁关注了我”走idx_follow_followee索引这两类高频查询都不需要回表。-- 动态内容表 CREATE TABLE social_post ( post_id NUMBER(20) NOT NULL, author_id NUMBER(20) NOT NULL, content CLOB, image_urls CLOB, topic_ids VARCHAR2(500), like_count NUMBER(10) DEFAULT 0 NOT NULL, comment_count NUMBER(10) DEFAULT 0 NOT NULL, create_time DATE DEFAULT SYSDATE NOT NULL, del_flag NUMBER(1) DEFAULT 0 NOT NULL, CONSTRAINT pk_post PRIMARY KEY (post_id, create_time) ) PARTITION BY RANGE(create_time) INTERVAL (NUMTODSINTERVAL(30,day)) ( PARTITION p_first VALUES LESS THAN (TO_DATE(2024-01-01,YYYY-MM-DD)) ); CREATE INDEX idx_post_author_time ON social_post(author_id, create_time DESC) LOCAL; CREATE INDEX idx_post_topic ON social_post(topic_ids) GLOBAL;内容表的分区是自动间隔分区每个月建一个。主键带上create_time是确保走分区裁剪时必须的条件。LOACAL索引配合分区让每个分区内的作者时间索引独立维护写入并行度会更好。-- 互动记录表 CREATE TABLE social_interact ( target_type NUMBER(2) NOT NULL, target_id NUMBER(20) NOT NULL, user_id NUMBER(20) NOT NULL, interact_type NUMBER(2) NOT NULL, create_time DATE DEFAULT SYSDATE NOT NULL, CONSTRAINT pk_interact PRIMARY KEY (target_type, target_id, user_id) ) PARTITION BY RANGE(create_time) INTERVAL (NUMTODSINTERVAL(30,day)) ( PARTITION p_first VALUES LESS THAN (TO_DATE(2024-01-01,YYYY-MM-DD)) );互动表按时间分区最适合定期清理历史记录。点赞和收藏都复用这张表靠target_type区分这个设计比拆成多张表更省空间写起来也简单。3.2 翻页拉取信息流ROWNUM/分页SQL的写法与优化信息流分页是社交场景里最容易写烂的SQL之一。很多开发同学从MySQL带过来的习惯一上来就是LIMIT offset, size。在YashanDB的Oracle兼容模式下这种大偏移量翻页会让数据库扫描到offsetsize行然后再丢弃offset行页数越深越慢这是它最常被吐槽的坑。正确的姿势是用键集分页也叫seek method。我们把上一次拉到的最后一条帖子的create_time和post_id记在客户端下一次查询直接把它作为条件。举个例子一个用户关注了1000个作者拉信息流时SQL写成下面这样SELECT p.post_id, p.author_id, p.content, p.create_time FROM social_post p WHERE p.author_id IN ( SELECT followee_id FROM social_follow WHERE follower_id 10086 ) AND (p.create_time, p.post_id) (:last_time, :last_id) ORDER BY p.create_time DESC, p.post_id DESC FETCH FIRST 20 ROWS ONLY;这条SQL的优势在于它天然只扫描符合条件的头部数据排序和ROWNUM裁切都非常高效。author_id IN (子查询)的写法在关注数不大时完全够用但如果某个用户关注了几千个作者IN子查询的效率会下降。这时候可以改成连表写法SELECT p.post_id, p.author_id, p.content, p.create_time FROM social_post p JOIN social_follow f ON f.follower_id 10086 AND f.followee_id p.author_id WHERE (p.create_time, p.post_id) (:last_time, :last_id) ORDER BY p.create_time DESC, p.post_id DESC FETCH FIRST 20 ROWS ONLY;JOIN配合索引idx_follow_followee和idx_post_author_time会比IN子查询更能利用基数估算优化器也能选出更合适的驱动表。这里有个细节(p.create_time, p.post_id) (:last_time, :last_id)这一行元组比较语法在Oracle兼容模式下可能不直接支持。如果遇到语法错误退化成两个独立条件p.create_time :last_time AND (p.create_time :last_time OR p.post_id :last_id)也是可以的。3.3 关系查询与统计场景用SQL输出粉丝增长、好友推荐候选集存储层要能支撑运营的很多临时统计需求不能每出一个报表就拉数据到Spark算一遍那样时效性跟不上。我举两个利用YashanDB SQL能力直接搞定的常用场景代码量不大但很体现对数据库的理解。第一个是粉丝增长曲线。运营想看某大V最近30天每天涨了多少粉直接对关注关系表按天统计就行。不过全量扫social_follow肯定不现实我们用汇总表做预聚合每天晚上定时任务跑一次把增量数据merge进去。CREATE TABLE follow_daily_stat ( stat_date DATE NOT NULL, followee_id NUMBER(20) NOT NULL, inc_count NUMBER(10) NOT NULL, CONSTRAINT pk_follow_daily PRIMARY KEY (stat_date, followee_id) ); -- 每天定时增量统计前一天新增关注 INSERT INTO follow_daily_stat(stat_date, followee_id, inc_count) SELECT TRUNC(f.create_time), f.followee_id, COUNT(*) FROM social_follow f WHERE f.create_time TRUNC(SYSDATE - 1) AND f.create_time TRUNC(SYSDATE) GROUP BY TRUNC(f.create_time), f.followee_id;这其实就是用空间换时间。等运营要曲线数据时一条简单SQL就出来了没必要天天全扫上亿行的大表。你可能会问既然已经有YashanDB为什么还要拿一张汇总表因为group by一个亿级表的费用太高哪怕是分区裁剪也不适合频繁执行。第二个是好友推荐候选集。社交产品最基础的黑洞推荐是“你可能感兴趣的人”核心SQL就是找二度关系。假设用户A关注了BB又关注了C那C就是A的潜在推荐。SQL可以这样写SELECT sfo.followee_id AS candidate, COUNT(*) AS common_cnt FROM social_follow f1 JOIN social_follow f2 ON f2.follower_id f1.followee_id JOIN social_follow sfo ON sfo.follower_id f2.followee_id WHERE f1.follower_id 10086 AND sfo.followee_id NOT IN ( SELECT followee_id FROM social_follow WHERE follower_id 10086 ) GROUP BY sfo.followee_id ORDER BY common_cnt DESC FETCH FIRST 30 ROWS ONLY;这个嵌套JOIN的关键点在于每个JOIN都走了主键索引。第一层JOIN用(follower_id, followee_id)第二层用idx_follow_followee不会出现大表全扫描。实测在百万关注关系量级下这个查询大概是几十毫秒级别压到给用户端实时展示也能扛住。真实推荐系统还会有过滤逻辑比如排除已经拉黑的用户、排除性别年龄不匹配的分层但这套SQL骨架是通用的。4. 常见问题与排查技巧实录高并发下的那些坑4.1 慢SQL排查执行计划怎么读、统计信息为何重要上线第一个月监控系统报了三次慢SQL全都和没有收集统计信息有关。YashanDB的优化器跟Oracle类似基于代价估算来选执行计划。如果统计信息过旧或者刚导完大量数据没做收集优化器就会选错索引。要么走了全表扫描要么没用上分区裁剪SQL慢了十倍不止。排查步骤我总结成了一套标准化流程把慢SQL捞出来看是不是同一个模板如果同一模板出现多次优先处理。查看执行计划用EXPLAIN PLAN FOR加上SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY)重点看TABLE ACCESS的访问路径是FULL还是INDEX RANGE SCAN。检查执行计划里的估算行数和实际行数差多少。如果估算只有100行实际扫了100万行基本就是统计信息不准。立即收集统计信息YashanDB的命令和Oracle的DBMS_STATS.GATHER_TABLE_STATS基本一致跑一次再观察。如果统计信息新鲜还慢就要怀疑SQL写法本身去优化SQL和索引设计。这里多说一句自动收集统计信息的任务一定要开起来但收集窗口要避开业务高峰。社交产品的凌晨两三点流量才低这时候收集正好。4.2 行锁竞争与事务控制点赞、阅读数这类热点怎么处理点赞数、阅读数是社交产品最典型的“热点行”更新。全世界用户同时点一个热门帖子数据库里如果都去UPDATE social_post SET like_count like_count 1 WHERE post_id ?哪怕YashanDB行锁粒度再细大量会话也会堵在这一行上等待事件飙升。我处理的方式是异步化延迟合并。前端把点赞请求发给后端后端只往Redis的SortedSet里记录一个点赞行为每分钟把增量批量刷到数据库。数据库端用的SQL是MERGE INTO social_post p USING ( SELECT 5 AS cnt FROM dual ) s ON (p.post_id :post_id) WHEN MATCHED THEN UPDATE SET p.like_count p.like_count s.cnt;这个操作把5000个单独update合并成一条MERGE行锁从5000次降到1次效果立竿见影。当然这不是YashanDB特有的技巧任何关系型数据库都适用。但YashanDB的MERGE INTO执行得很稳定尤其是高频执行时不会出现锁升级问题这让我在实施时少操很多心。另外提醒一点微博上的未读数、粉丝数也最好是这种合并写策略千万不能让客户端每拉一次信息流就实时去count一次。4.3 连接数被打满连接池参数与YashanDB并发参数配合运行期间我们还遇到一次连接数告警。当时应用侧使用的是HikariCP最大连接数配置了50而YashanDB实例的processes参数默认只有150一个集群有十几个微服务实例瞬间就把数据库连接占满了。这个问题的坑在于每个服务觉得自己才占50但十几个服务加起来已经四五百远超数据库上限。排查和解决的办法是双向调整。数据库侧把processes和sessions参数调大同时确认内存够用。应用侧连接池配置必须遵循总量收敛原则最大连接数 数据库允许总连接数 / 应用实例数还得留10%到20%的余量。另外微服务里要配置连接空闲回收时间HikariCP里idleTimeout不要设成0否则空闲连接一直占着不放。还有一个很重要但容易被忽略的操作给核心接口设置超时时间。如果某个第三方接口卡住连接池里的连接被长期占用数据库侧会看到越来越多idle in transaction最终拖垮整个服务。在YashanDB监控视图里看到大量ACTIVE连接长时间不释放优先去查应用代码大概率是事务没提交或没回滚。5. 架构配合与长期运维不只有数据库5.1 与Redis、消息队列的配合一致性主库加速缓存削峰MQ搞社交的很少有人敢让数据库完全裸奔扛所有流量。我们业务的分层是这样的YashanDB当一致性主库存所有最终态数据Redis当加速层缓存用户信息、信息流列表、热点帖子内容Kafka当削峰层承接点赞、评论、发帖这些流量异步消费后批处理入库。这套架构下数据库的定位不是“能扛多少并发”而是“保证数据不丢、不重、最终一致”。比如用户主页上的粉丝数Redis里存一份实时计数定时刷到YashanDB。用户看到的粉丝数可能滞后几秒但后台所有对账、结算、统计都以YashanDB的数字为准。这个分寸要把握好——数据库永远不能成为唯一的数据入口但它必须是数据真相的唯一来源。我甚至建议把发帖主流程做成先写Kafka返回“发布中”给用户消费者从Kafka里拿消息写YashanDB写成功后发一条通知到Redis用户端轮询到状态才显示成功。很多产品觉得这样太重但遇到热点事件时削峰能救你一命。YashanDB的批量提交能力在这种模式下能撑住比直接并发写大一个数量级的吞吐。5.2 备份恢复与容灾演练RPO、RTO怎么定社交数据一旦丢失产品可以直接宣告死亡。所以YashanDB的备份恢复策略我提的指标是RPO≤5分钟RTO≤30分钟。这个指标比较务实让运维在还原时既有压力也能在多数故障场景下做到。实际操作层面我配置了每天全备加每小时的增量备份备份文件直接传异地对象存储。YashanDB支持物理备份和逻辑备份我用物理备份做容灾恢复逻辑备份做误删数据找回。这里容易踩的坑是只做逻辑备份备份单太大、恢复时间长容灾根本来不及。必须每天至少一次物理全备。容灾演练一定要定期做别等出事了才第一次测恢复。我们有一次演练就发现恢复出来的库lag了三个小时原因是从备份集恢复后归档日志没有正确续传。后来把归档日志的备份策略改成随物理备份一起拉取才真正达到RPO可控。5.3 监控指标与容量规划数据库里最该盯着的指标YashanDB的监控面板我们主要盯五个指标只有它们出问题才需要紧张指标健康阈值参考出问题时的典型表现活跃会话数不超过连接池上限的80%新连接等待、超时行锁等待事件平均等待小于5ms热门帖子点赞卡顿缓冲区命中率大于99%大量物理读响应变慢慢SQL数量单日超过10条就要查页面接口超时磁盘空间使用率低于70%备份失败、写入阻塞容量规划方面社交数据的增长几乎线性但动态表体积增长不是匀速的。我的经验是每季度做一次数据量评估根据日增量和备份策略算出未来12个月的存储空间提前扩容。尤其要注意的是分区表的归档目录我们有一段时间DETACH出来的分区没及时清空归档表导致磁盘空间被占满只能临时砍备份保留天数救急。这一整套跑下来我对YashanDB的最大感受是它不是那种“装完就完事”的数据库需要你像对待Oracle一样认真做索引规划、分区设计、执行计划调优。但只要把该做的功课做了它在社交网络这种重读写混合场景里表现是相当稳定的。最后再分享一个我在实操里的体会不要试图把所有东西都塞进数据库里。社交业务的数据分层设计比单点调优重要得多。把YashanDB放在它该在的位置让它负责事务一致性、关系复杂查询、高可用容灾把纯高并发的读流量交给缓存把非核心的异步流量挡在消息队列后面。数据库不再是瓶颈你的架构自然就撑得住用户增长。