网站的速度,网站怎么添加导航栏,可玩儿小程序可以加盟么,旅游网站开发意义读写分离
什么时候需要读写分离
互联网大部分业务场景都是读多写少的#xff0c;读和写的请求对比可能差了不止一个数量级。为了不让数据库的读成为业务瓶颈#xff0c;同时也为了保证写库的成功率#xff0c;一般会采用读写分离的技术来保证。 读写分离的实现是把访问的压…读写分离
什么时候需要读写分离
互联网大部分业务场景都是读多写少的读和写的请求对比可能差了不止一个数量级。为了不让数据库的读成为业务瓶颈同时也为了保证写库的成功率一般会采用读写分离的技术来保证。 读写分离的实现是把访问的压力从主库转移到从库。分离读库和写库操作从 CRUD 的角度主数据库处理新增、修改、删除等事务性操作从数据库处理 SELECT 查询操作。实现上可以有一主一从一个主库配置一个从库也可以一主多从一个主库配置多个从库读操作通过多个从库进行支撑更高的读并发压力。
MySQL 主从复制技术
binlog 日志
MySQL InnoDB 引擎的主从复制是通过二进制日志 binlog 来实现。除了数据查询语句 select 以外binlog 日志记录了其他各类数据写入操作包括 DDL 和 DML 语句。
binlog 有三种格式Statement、Row 及 Mixed。 Statement 格式基于 SQL 语句的复制。binlog 会记录每一条修改数据的 SQL 操作从库拿到后在本地进行回放就可以了。 Row 格式基于行信息复制。以行为维度记录每一行数据修改的细节不记录执行 SQL 语句的上下文相关的信息仅记录行数据的修改。假设有一个批量更新操作会以行记录的形式来保存二进制文件这样可能会产生大量的日志内容。 Mixed 格式混合模式复制。是 Statement 与 Row 的结合在这种方式下不同的 SQL 操作会区别对待。比如一般的数据操作使用 row 格式保存有些表结构的变更语句使用 statement 来记录。
主从复制过程
主库将变更写入 binlog 日志从库连接到主库之后主库会创建一个log dump 线程用于发送 bin log 的内容。从库开启同步以后会创建一个 IO 线程用来连接主库请求主库中更新的 bin logI/O 线程接收到主库 binlog dump 进程发来的更新之后保存在本地 relay 日志中。接着从库中有一个 SQL 线程负责读取 relay log 中的内容同步到数据库存储中也就是在自己本地进行回放最终保证主从数据的一致性。
读写分离要注意的问题
主从复制下的延时问题
主库和从库是两个不同的数据源主从复制过程会存在一个延时。当主库有数据写入之后同时写入 binlog 日志文件中然后从库通过 binlog 文件同步数据额外执行日志同步和写入操作会有一定时间的延迟。特别是在高并发场景下刚写入主库的数据是不能马上在从库读取的要等待几十毫秒或者上百毫秒以后才可以。
在某些对一致性要求较高的业务场景中这种主从导致的延迟会引起一些业务问题比如订单支付付款已经完成主库数据更新了从库还没有这时候去从库读数据会出现订单未支付的情况在业务中是不能接受的。
解决方法
敏感业务强制读主库。业务需要写库后实时读数据的可以通过强制读主库来解决。关键业务不进行读写分离。对一致性不敏感的业务可以进行读写分离电商中的订单评论、个人信息等。对一致性要求比较高的业务不进行读写分离避免延迟导致的问题比如金融支付。
主从复制如何避免丢数据
假设在数据库主从同步时主库宕机并且数据还没有同步到从库就会出现数据丢失和不一致的情况。
MySQL 数据库主从复制有异步复制、半同步复制和全同步复制的方式。
异步复制。主库在接受并处理客户端的写入请求时直接返回执行结果不关心从库同步是否成功这样就会存在上面说的问题主库崩溃以后可能有部分操作没有同步到从库出现数据丢失问题。半同步复制。主库需要等待至少一个从库完成同步之后才完成写操作。主库在执行完客户端提交的事务后从库将日志写入自己本地的 relay log 之后会返回一个响应结果给主库主库确认从库已经同步完成才会结束本次写操作。相对于异步复制半同步复制提高了数据的安全性避免了主库崩溃出现的数据丢失但是同时也增加了主库写操作的耗时。全同步复制。是在多从库的情况下当主库执行完一个事务需要等待所有的从库都同步完成以后才完成本次写操作。全同步复制需要等待所有从库执行完对应的事务所以整体性能是最差的。
分库分表
背景
数据规模飞速增长传统的单库单表架构不足以支撑业务发展存在性能瓶颈。
读写的数据量限制
数据库的数据量增大会直接影响读写的性能比如一次查询操作扫描 5 万条数据和 500 万条数据查询速度肯定是不同的。
MySQL 单库和单表的数据量限制和不同的服务器配置以及不同结构的数据存储有关并没有一个确切的数字。这里参考阿里巴巴的《Java 开发手册》中数据库部分的建表规约 单表行数超过 500 万行或者单表容量超过 2GB才推荐进行分库分表。 避免过度设计。分库分表虽然可以提高性能但盲目地进行分库分表只会增加系统的复杂度。
数据库连接限制
数据库的连接是有限制的不能无限制创建比如 MySQL 中可以使用 max_connections 查看默认的最大连接数当访问连接数过多时就会导致连接失败。以电商为例假设存储没有进行分库用户、商品、订单和交易所有的业务请求都访问同一个数据库产生的连接数是非常可观的可能导致数据库无法支持业务请求。 如果不进行数据库拆分大量数据访问都集中在单台机器上对磁盘 IO、CPU 负载等都会产生很大的压力并且直接影响业务操作的性能。
分库分表原理
垂直切分
一般是按照业务和功能的维度进行拆分把数据分别放到不同的数据库中。 垂直分库 针对的是一个系统中对不同的业务进行拆分。 根据业务维度进行数据的分离剥离为多个数据库。比如电商网站早期商品数据、会员数据、订单数据都是集中在一个数据库中随着业务的发展单库处理能力已成为瓶颈这个时候就需要进行相关的优化进行业务维度的拆分分离出会员数据库、商品数据库和订单数据库等。
垂直分表 针对业务上的字段比较多的大表进行的。 一般是把业务宽表中比较独立的字段或者不常用的字段拆分到单独的数据表中。比如早期的商品表中可能包含了商品信息、价格、库存等可以拆分出来价格扩展表、库存扩展表等。
水平切分 把相同的表结构分散到不同的数据库和不同的数据表中避免访问集中的单个数据库或者单张数据表具体的分库和分表规则一般是通过业务主键进行哈希取模操作。
例如电商业务中的订单信息访问频繁可以将订单表分散到多个数据库中实现分库在每个数据库中继续进行拆分到多个数据表中实现分表。路由策略可以使用订单 ID 或者用户 ID进行取模运算路由到不同的数据库和数据表中。
分库分表后引入的问题
分布式事务问题
对业务进行分库之后同一个操作会分散到多个数据库中涉及跨库执行 SQL 语句也就出现了分布式事务问题。
比如数据库拆分后订单和库存在两个库中一个下单减库存的操作就涉及跨库事务。可以使用分布式事务中间件实现 TCC 等事务模型也可以使用基于本地消息表的分布式事务实现。
跨库关联查询问题
分库分表后跨库和跨表的查询操作实现起来会比较复杂性能也无法保证。在实际开发中针对这种需要跨库访问的业务场景一般会使用额外的存储比如维护一份文件索引。另一个方案是通过合理的数据库字段冗余避免出现跨库查询。
跨库跨表的合并和排序问题
分库分表以后数据分散存储到不同的数据库和表中如果查询指定数据列表或者需要对数据列表进行排序时就变得异常复杂则需要在内存中进行处理整体性能会比较差一般来说会限制这类型的操作。
分库分表中间件实现
业务中实现分库分表需要自己去实现路由规则实现跨库合并排序等操作具有一定的开发成本可以考虑使用开源的分库分表中间件。这里比较推荐 Apache ShardingSphere另外也可以参考淘宝的 TDDL 等。
ShardingSphere 的前身是当当开源的 Sharding-JDBC目前更名为 ShardingSphere并且已经加入 Apache 基金会。ShardingSphere 在 Sharding-JDBC 的基础上额外提供了 Sharding-Proxy以及正在规划中的 Sharding-Sidecar。其中 Sharding-JDBC 用来实现分库分表另外也添加了对分布式事务等的支持。 ShardingSphere 的具体应用《ShardingSphere 用户手册》。
TDDLTaobao Distributed Data Layer是淘宝团队开发的数据库中间件用于解决分库分表场景下的访问路由TDDL 在淘宝大规模应用 TDDL 项目
存储拆分后解决唯一主键问题
在单库单表时业务 ID 可以依赖数据库的自增主键实现存储拆分到了多处如何解决主键。
生成主键方案
使用单独的自增数据表。
存储拆分以后创建一张单点的数据表比如现在需要生成订单 ID我们创建下面一张数据表
CREATE TABLE IF NOT EXISTS order_sequence(order_id INT UNSIGNED AUTO_INCREMENT,PRIMARY KEY ( order_id ))ENGINEInnoDB DEFAULT CHARSETutf8;当每次需要生成唯一 ID 时就去对应的这张数据表里新增一条记录使用返回的自增主键 ID 作为业务 ID。
问题
性能无法保证在并发比较高的情况下如果通过这样的数据表来创建自增 ID生成主键很容易成为性能瓶颈。存在单点故障如果生成自增 ID 的数据库挂掉那么会直接影响创建功能。
使用 UUID 能否实现
生成一个 UUID
public String getUUID(){UUID uuidUUID.randomUUID();return uuid.toString();}问题
UUID 作为数据库主键太长了会导致比较大的存储开销。UUID 是无序的如果使用 UUID 作为主键会降低数据库的写入性能。
以 MySQL 为例MySQL 建议使用自增 ID 作为主键我们知道 MySQL InnoDB 引擎支持索引底层数据结构是 B 树如果主键为自增 ID 的话那么 MySQL 可以按照磁盘的顺序去写入如果主键是非自增 ID在写入时需要增加很多额外的数据移动将每次插入的数据放到合适的位置上导致出现页分裂降低数据写入的性能。
基于 Snowflake 算法
Snowflake 是 Twitter 开源的分布式 ID 生成算法由 64 位的二进制数字组成一共分为 4 部分.
其中
第 1 位默认不使用作为符号位总是 0保证数值是正数41 位时间戳表示毫秒数我们计算一下41 位数字可以表示 241 毫秒换算成年结果是 69 年多一点一般来说这个数字足够在业务中使用了10 位工作机器 ID支持 210 也就是 1024 个节点12 位序列号作为当前时间戳和机器下的流水号每个节点每毫秒内支持 212 的区间也就是 4096 个 ID换算成秒相当于可以允许 409 万的 QPS如果在这个区间内超出了 4096则等待至下一毫秒计算。
Twitter 给出了 Snowflake 算法的示例具体实现应用了大量的位运算可以点击具体的代码库查看。
Snowflake 算法可以作为一个单独的服务部署在多台机器上产生的 ID 是趋势递增的不需要依赖数据库等第三方系统并且性能非常高理论上 409 万的 QPS 是一个非常可观的数字可以满足大部分业务场景其中的机器 ID 部分可以根据业务特点来分配比较灵活。
不足
存在时钟回拨问题
服务器的本地时钟并不是绝对准确的在一些业务场景中比如在电商的整点抢购中为了防止不同用户访问的服务器时间不同则需要保持服务器时间的同步。为了确保时间准确会通过 NTP 的机制来进行校对NTPNetwork Time Protocol指的是网络时间协议用来同步网络中各个计算机的时间。
如果服务器在同步 NTP 时出现不一致出现时钟回拨那么 SnowFlake 在计算中可能出现重复 ID。除了 NTP 同步闰秒也会导致服务器出现时钟回拨不过时钟回拨是小概率事件在并发比较低的情况下一般可以忽略。时钟回拨问题可以进行延迟等待直到服务器时间追上来为止。
数据库维护区间分配
淘宝的 TDDL 等数据库中间件使用的主键生成策略。基于数据库维护自增ID区间结合内存分配的策略。
步骤
首先在数据库中创建 sequence 表其中的每一行用于记录某个业务主键当前已经被占用的 ID 区间的最大值。
sequence 表的主要字段是 name 和 value其中 name 是当前业务序列的名称value 存储已经分配出去的 ID 最大值。
CREATE TABLE sequence (id bigint(20) NOT NULL AUTO_INCREMENT COMMENT Id,name varchar(64) NOT NULL COMMENT sequence name,value bigint(32) NOT NULL COMMENT sequence current value,PRIMARY KEY (id),UNIQUE KEY unique_name (name)
) ENGINEInnoDB DEFAULT CHARSETutf8; 接下来插入一条行记录当需要获取主键时每台服务器主机从数据表中取对应的 ID 区间缓存在本地同时更新 sequence 表中的 value 最大值记录。
现在我们新建一条记录比如设置一条 order 更新的规则插入一行记录如下
INSERT INTO sequence (name,value) values(order_sequence,1000);i当服务器在获取主键增长区段时首先访问对应数据库的 sequence 表更新对应的记录占用一个对应的区间。比如我们这里设置步长为 200原先的 value 值为 1000更新后的 value 就变为了 1200。
取到对应的 ID 区间后在服务器内部进行分配涉及的并发问题可以依赖乐观锁等机制解决。
有了对应的 ID 增长区间在本地就可以使用 AtomicInteger 等方式进行 ID 分配。
不同的机器在相同时间内分配出去的 ID 可能不同这种方式生成的唯一 ID不保证严格的时间序递增但是可以保证整体的趋势递增在实际生产中有比较多的应用。
为了防止单点故障sequence 表所在的数据库通常会配置多个从库实现高可用。
除了上面的几种方案实际开发中还可以应用 Redis 作为解决方案即通过 Redis Incr 命令来实现。
分库分表以后扩容
业务场景
设计电商网站的订单数据库模块经过对业务增长的估算预估三年后数据规模可能达到 6000 万每日订单数会超过 10 万。
存储实现订单作为电商业务的核心数据应该尽量避免数据丢失并且对数据一致性有强要求肯定是选择支持事务的关系型数据库比如使用 MySQL 及 InnoDB 存储引擎。数据库的高可用订单数据是典型读多写少的数据不仅要面向消费者端的读请求内部也有很多上下游关联的业务模块在调用针对订单进行数据查询的调用量会非常大。基于这一点我们在业务中配置基于主从复制的读写分离并且设置多个从库提高数据安全。数据规模6000 万的数据量显然超出了单表的承受范围参考《阿里巴巴 Java 开发手册》中「单表行数超过 500 万行」进行分表的建议此时需要考虑进行分库分表那么如何设计路由规则和拆分方案呢接下来会对此展开讨论。
路由规则与扩容方案
3 种路由规则对主键进行哈希取模、基于数据范围进行路由、结合哈希和数据范围的分库分表规则。
1. 哈希取模的方式 根据不同的业务主键输入对数据库进行取模得到插入数据的位置。
6000 万的数据规模按照单表承载百万数量级来拆分拆分成 64 张表进一步可以把 64 张表拆分到两个数据库中每个库中配置 32 张表。 当新订单创建时首先生成订单 ID对数据库个数取模计算对应访问的数据库接下来对数据表取模计算路由到的数据表当处理查询操作时也通过同样的规则处理这样就实现了通过订单 ID 定位到具体数据表。
优点是数据拆分比较均匀。 缺点是不利于后面的扩容。
假设订单增长速度超出预估数据规模很快达到了几亿的数量级原先的数据表已经不满足性能要求数据库需要继续进行拆分。
数据库拆分以后订单库和表的数量都需要调整路由规则也需要调整为了适配新的分库分表规则保证数据的读写正常要进行数据迁移具体的操作可以分为停机迁移和不停机迁移两种方式。
停机迁移
停机迁移的方式比较简单比如我们在使用一些网站或者应用时经常会收到某段时间内暂停服务的通知一般是在这段时间内完成数据迁移将历史数据按照新的规则重新分配到新的存储中然后切换服务。
不停机迁移
动态扩容依赖业务上的双写操作实现需要同时处理存量和增量数据并且做好各种数据校验。
数据库扩容方式有基于原有存储增加节点和重新部署一套新的数据库两种策略。
重新部署新的数据库存储的步骤
创建一套新的订单数据库在某个时间点上将历史数据按照新的路由规则分配到新的数据库中在旧数据库的操作中开启双写同时写入到两个数据库用新的读写服务逐步替代旧服务同步进行数据不一致校验最后完成全面切流。
2. 基于数据范围进行拆分
根据特定的字段进行划分不同区间对订单表进行拆分中如果基于数据范围路由可以按照订单 ID 进行范围的划分。 同样是拆分成 64 张数据表可以把订单 ID 在 3000万 以下的数据划分到第一个订单库3000 万以上的数据划分到第二个订单库在每个数据库中继续按照每张表 100万 的范围进行划分。
基于数据范围进行路由的规则当进行扩容时可以直接增加新的存储将新生成的数据区间映射到新添加的存储节点中不需要进行节点之间的调整也不需要迁移历史数据。
缺点是数据访问不均匀。按照这种规则另外一个数据库在很长一段时间内都得不到应用导致数据节点负荷不均在极端情况下当前热点库可能出现性能瓶颈无法发挥分库分表带来的性能优势。
3. 结合数据范围和哈希取模
设计这样的一个路由规则首先对订单 ID 进行哈希取模然后对取模后的数据再次进行范围分区。 通过哈希取模结合数据区间的方式可以比较好地平衡两种路由方案的优缺点。当数据写入时首先通过一次取模计算出一个数据库然后使用订单 ID 的范围进行二次计算将数据分散到不同的数据表中。
避免了单纯基于数据范围可能出现的热点存储并且在后期扩展时可以直接增加对应的扩展表避免了复杂的数据迁移工作。
NoSQL 数据库
对比关系型数据库
关系型数据库通过关系模型来组织数据在关系型数据库当中一个表就是一个模型一个关系数据库可以包含多个表不同数据表之间的联系反映了关系约束。
关系型数据库对事务支持较好支持 SQL 规范中的各种复杂查询比如 join、union 等操作。正是由于对 SQL 规范的支持也使得关系型数据库对扩展不友好比较难进行分布式下的集群部署。
NoSQL 数据库特性
良好的扩展性容易通过集群部署
关系型数据库在进行扩展时要考虑到如何分库分表、扩容等各种实现方案都比较重对业务侵入较大。NoSQL 数据库去掉了关系型数据库的关系特性对集群友好这样就非常容易扩展。
读写性能高支持大数据量
关系型数据库对一致性的要求较高数据表的结构复杂读写的性能要低于非关系型数据库。另外一方面部分 NoSQL 数据库采用全内存实现更适合一些高并发的访问场景。
不限制表结构灵活的数据模型
应用关系型数据库需要通过 DML 语句创建表结构数据表创建以后增删字段需要重新修改表结构。如果使用 NoSQL一般不需要事先为数据建立存储结构和字段可以存储各种自定义的数据。
关系型数据库和非关系型数据库是相辅相成的。从性能的角度来讲NoSQL 数据库的性能优于关系型数据库从持久化角度关系型数据库优于 NoSQL 数据库。 NoSQL 数据库一般提供弱一致性的保证实现最终一致性CP 模型 NoSQL 关注的是 AP 模型同时应用 NoSQL 和关系型数据库可以满足高性能的基础上同时保证数据可靠性。
NoSQL 数据库应用
Key-Value 数据库
Key-Value 存储就是 Map 结构支持高性能的通过 Key 定位和存储。通常用来实现缓存等应用典型的有 Redis 和 Memcached。
从性能的角度为了提高读写效率Redis 在最开始的版本中一直使用单线程模型避免上下文切换和线程竞争资源并且采用了 IO 多路复用的技术提升了性能另外在最近的版本更新中Redis也开始支持多线程处理。
从存储结构的角度Redis 支持多种数据结构,有丰富的应用场景并且针对不同的数据规模等Redis 采取多种内存优化方式尽量减少内存占用。比如List 结构内部有压缩列表和双向链表两种实现在数据规模较小时采用 ZipList 实现特别是在新的版本更新中又添加了 QuickList 的实现减少内存的消耗。
从高可用的角度作为一个内存数据库Redis实现了AOF和RDB的数据持久化机制另外Redis支持了多种集群方式包括主从同步Sentinel和Redis Cluster等机制提高了整体的数据安全和高可用保障。
文档型数据库
文档型数据库可以存储结构化的文档比如 JSON 或者 XML从这个角度上看文档型数据库比较接近关系型数据库。但是对比关系型数据库文档性数据库中不需要预先定义表结构并且可以支持文档之间的嵌套典型的比如 MongoDB这一点和关系型数据库有很大的不同。
以 MongoDB 为例采用了基于 JSON 扩展的 BSON 存储结构可以进行自我描述这种灵活的文档类型特别适合应用在内容管理系统等业务中。MongoDB 还具备非常优秀的扩展能力对分片等集群部署的支持非常全面可以快速扩展集群规模。
列存储数据库
列式数据库被用来存储海量数据比如 Cassandra、HBase 等特点是大数据量下读写速度较快、可扩展性强更容易进行分布式部署。
以 HBase 为例HBase 支持海量数据的读写特别是写入操作可以支持 TB 级的数据量。列式数据库通常不支持事务和各种索引优化比如 HBase 使用 LSM 树组织数据对比 MySQL 的 B 树在高并发写入时有更好的性能。
图形数据库
在一些特定的应用场景可以应用特殊的数据库比如图形数据库。社交网络中的用户关系可以使用图来存储于是诞生了一些图形数据库可以方便地操作图结构的相关算法比如最短路径、关系查找等。
ElasticSearch 索引
ElasticSearch 简介
Lucene 是一个开源的全文检索引擎类库支持各种分词以及搜索相关的实现可以极大地简化搜索开发的成本但 Lucene 只是一个工具包在实际项目中进行二次开发。
ElasticSearch 是一个基于 Lucene 的分布式全文检索框架在 Lucene 类库的基础上实现可以避免直接基于 Lucene 开发这一点和 Java 中 Netty 对 IO/NIO 的封装有些类似。
ElasticSearch 开放了一系列的 RESTful API基于这些 API可以快捷地实现各种搜索功能。除了搜索相关的功能ElasticSearch 还对分布式场景下的应用有特别好的支持包括良好的扩展性可以扩展到上百台服务器的集群规模以及近似实时分析的索引实现。这些特点使得 ElasticSearch 在各类搜索场景、大数据分析等业务中广泛应用。
ElasticSearch 应用
ElasticSearch 对搜索的支持非常好但是和 NoSQL 数据库一样对事务、一致性等的支持较低。
常见的数据库-索引-缓存系统架构图 ElasticSearch 一般是作为持久性数据库的辅助存储是和 SQL NoSQL 数据库一起使用对外提供索引查询功能。关系型数据库保证数据更新的准确性在关系型数据库更新以后通过 binlog 同步结合消息队列分发的方式来更新文件索引提供一致性保证。
ELK stack
ElasticSearch 是由 Elastic 公司创建的除了 ElasticSearchElastic 公司还有另外两款产品分别是 Logstash 及 Kibana 开源项目这三个开源项目组合在一起称为 ELK stack。
在 ELK 技术栈中ElasticSearch 用于数据分析和检索Logstash 用于日志收集Kibana 用于界面的展示ELK 可以用于快速查询数据并可视化分析在日志处理、大数据等领域有非常广泛的应用。
索引是如何建立的
ElasticSearch 存储的单元是索引关系型数据库是按照关系表的形式组织数据大部分 NoSQL 数据库是 K-Value 的键值对方式。
ElasticSearch 索引的实现基于 Lucene使用倒排索引的结构倒排索引的引入使得 ElasticSearch 可以非常高效地实现各种文件索引。倒排索引不光是在 ElasticSearch 等组件中应用它还是百度等搜索引擎实现的底层技术之一。在搜索引擎中索引的建立需要经过网页爬取、信息采集、分词、索引创建的过程不过在 ElasticSearch 内部存储的实现中数据的写入可以对比搜索引擎对网页的抓取和信息采集的过程只需要关注分词和索引的创建。
分词和索引
分词是在索引建立中特别重要的一个环节分词的策略会直接影响索引结果。Lucene 提供了多种分词器分词器是一个可插拔的组件包括内置的标准分词器 也可以引入对中文支持较好的 IKAnalyze 中文分词器等。
假设我们在 ElasticSearch 中新增了两个文档每个文档包含如下内容
文档1Jerry and Tom are good friends.文档2Good friends should help each other.
英文是有单词的单词之间通过空格进行拆分所以对英文的分词相对容易比如上面的内容可以直接对字符串按照空格拆分得到分词后的数组。 Jerry / / and / / Tom / / are / / good / / friends / . Good / / friends / / should / / help / / each / / other / . 一般来说中文分词用得比较多的策略是基于字典的最长字符串匹配方式这种策略可以覆盖大多数场景不过还是有一小部分天然存在歧义的文档是无法处理的。比如「学生会组织各种活动」按照最长串匹配的方式可以切分成“学生会/组织各种活动”但实际要表达的可能是“学生/会/组织各种活动”。
建立索引
索引存储的结构是倒排索引倒排索引是相对于正排索引来说的倒排索引描述了一个映射关系包括文档中分词后的结果以及分别包含这些单词的文档列表。
索引描述的其实就是关键词和文档的关系正排索引就是“文档—关键词”的格式倒排索引则相反是“关键词—文档”的格式。可以看到当需要使用关键词进行检索时使用倒排索引才能实现快速检索的目的。
针对上面的分词示例我们简单起见统一为小写把分词之后的单词组成一个不重复的分词列表为了更好地进行查找可以按照字典序排序。 and,are,each,friends,good,help,jerry,other,should,tom 比如其中“friends”在文档 1 和文档 2 中都出现了“Tom”和“Jerry”只在文档 1 中出现了 1 次其他的单词也进行同样地处理于是我们可以构建下面的倒排索引
分词文档列表……friends文档 1文档 2good文档 1文档 2jerry文档 1tom文档 1…以下省略
具体到数据结构的实现可以通过实现一个字典树也就是 Trie 树对字典树进行扩展额外存储对应的数据块地址定位到具体的数据位置。
对比 B 树
严格地说这两类索引是不能在一起比较的B 树描述的是索引的数据结构而倒排索引是通过索引的组织形式来命名的。比如我们上面的例子中倒排指的是关键词和文档列表的结构关系。
对于数据库来说索引的作用是提高数据查询的性能考虑到磁盘寻址的特性选择了 B 树作为索引的实现结构可以更好地实现通过主键以及通过区间范围查找的要求。
对于倒排索引则是对应具体的应用场景在搜索中是通过一些关键词定位到具体的文档。所以倒排索引实现的是根据关键词也就是分词的结果去查找文档或者不同的网页。