首页
统计
留言
Search
1
PHP中使用反射
996 阅读
2
phpstorm配置SFTP
940 阅读
3
Go语言——结构体
792 阅读
4
PhpStorm 使用 AI 代码生成工具 Codeium
787 阅读
5
关于PHP的垃圾回收机制
763 阅读
后端
PHP
Go
数据库
其他
前端
其他技术
生活杂谈
登录
Search
标签搜索
Laravel
Mysql
RPC
Redis
Liunx
PHP
CSS
ES
算法
开发工具
断点续传
反射
phpstorm
工具
防盗链
CURL
设计模式
面试
Nginx
搜索引擎
quhe.net
首页
栏目
后端
PHP
Go
数据库
其他
前端
其他技术
生活杂谈
页面
统计
留言
搜索到
57
篇与
quhe.net
的结果
2020-08-13
[精选] 这篇MySQL锁写的很有深度,请深入
首先对mysql锁进行划分:按照锁的粒度划分:行锁、表锁、页锁按照锁的使用方式划分:共享锁、排它锁(悲观锁的一种实现)还有两种思想上的锁:悲观锁、乐观锁。InnoDB中有几种行级锁类型:Record Lock、Gap Lock、Next-key LockRecord Lock:在索引记录上加锁Gap Lock:间隙锁Next-key Lock:Record Lock+Gap Lock1.行锁 行级锁是Mysql中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。有可能会出现死锁的情况。 行级锁按照使用方式分为共享锁和排他锁。共享锁用法(S锁 读锁): 若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。select ... lock in share mode; 共享锁就是允许多个线程同时获取一个锁,一个锁可以同时被多个线程拥有。排它锁用法(X 锁 写锁): 若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。这保证了其他事务在T释放A上的锁之前不能再读取和修改A。select ... for update 排它锁,也称作独占锁,一个锁在某一时刻只能被一个线程占有,其它线程必须等待锁被释放之后才可能获取到锁。2.表锁 表级锁是mysql锁中粒度最大的一种锁,表示当前的操作对整张表加锁,资源开销比行锁少,不会出现死锁的情况,但是发生锁冲突的概率很大。被大部分的mysql引擎支持,MyISAM和InnoDB都支持表级锁,但是InnoDB默认的是行级锁。共享锁用法:LOCK TABLE table_name [ AS alias_name ] READ 排它锁用法:LOCK TABLE table_name [AS alias_name][ LOW_PRIORITY ] WRITE 解锁用法:unlock tables; 3.页锁 页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。BDB支持页级锁4.乐观锁和悲观锁 在数据库的锁机制中介绍过,数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。 乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。 无论是悲观锁还是乐观锁,都是人们定义出来的概念,可以认为是一种思想。其实不仅仅是关系型数据库系统中有乐观锁和悲观锁的概念,像memcache、hibernate、tair等都有类似的概念。 针对于不同的业务场景,应该选用不同的并发控制方式。所以,不要把乐观并发控制和悲观并发控制狭义的理解为DBMS中的概念,更不要把他们和数据中提供的锁机制(行锁、表锁、排他锁、共享锁)混为一谈。其实,在DBMS中,悲观锁正是利用数据库本身提供的锁机制来实现的。4.1悲观锁 在关系数据库管理系统里,悲观并发控制(又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”)是一种并发控制的方法。它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作对某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。悲观并发控制主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中。 悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度(悲观),因此,在整个数据处理过程中,将数据处于锁定状态。 悲观锁的实现,往往依靠数据库提供的锁机制 (也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)悲观锁的具体流程:在对任意记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。 具体响应方式由开发者根据实际需要决定。如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。其间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常。在mysql/InnoDB中使用悲观锁: 首先我们得关闭mysql中的autocommit属性,因为mysql默认使用自动提交模式,也就是说当我们进行一个sql操作的时候,mysql会将这个操作当做一个事务并且自动提交这个操作。1.开始事务 begin;/begin work;/start transaction; (三者选一就可以) 2.查询出商品信息 select ... for update; 4.提交事务 commit;/commit work; 通过下面的例子来说明:1.当你手动加上排它锁,但是并没有关闭mysql中的autocommit。SESSION1: mysql> select * from user for update; +----+------+--------+ | id | name | psword | +----+------+--------+ | 1 | a | 1 | | 2 | b | 2 | | 3 | c | 3 | +----+------+--------+ 3 rows in set 这里他会一直提示Unknown mysql> update user set name=aa where id=1; 1054 - Unknown column 'aa' in 'field list' mysql> insert into user values(4,d,4); 1054 - Unknown column 'd' in 'field list' 2.正常流程窗口1: mysql> set autocommit=0; Query OK, 0 rows affected 我这里锁的是表 mysql> select * from user for update; +----+-------+ | id | price | +----+-------+ | 1 | 500 | | 2 | 800 | +----+-------+ 2 rows in set 窗口2: mysql> update user set price=price-100 where id=1; 执行上面操作的时候,会显示等待状态,一直到窗口1执行commit提交事务才会出现下面的显示结果 Database changed Rows matched: 1 Changed: 1 Warnings: 0 窗口1: mysql> commit; Query OK, 0 rows affected mysql> select * from user; +----+-------+ | id | price | +----+-------+ | 1 | 400 | | 2 | 800 | +----+-------+ 2 rows in set 上面的例子展示了排它锁的原理:一个锁在某一时刻只能被一个线程占有,其它线程必须等待锁被释放之后才可能获取到锁或者进行数据的操作。悲观锁的优点和不足: 悲观锁实际上是采取了“先取锁在访问”的策略,为数据的处理安全提供了保证,但是在效率方面,由于额外的加锁机制产生了额外的开销,并且增加了死锁的机会。并且降低了并发性;当一个事物所以一行数据的时候,其他事物必须等待该事务提交之后,才能操作这行数据。4.2乐观锁在关系数据库管理系统里,乐观并发控制(又名“乐观锁”,Optimistic Concurrency Control,缩写“OCC”)是一种并发控制的方法。它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会进行回滚。 乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。 相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本。数据版本,为数据增加的一个版本标识。当读取数据时,将版本标识的值一同读出,数据每更新一次,同时对版本标识进行更新。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的版本标识进行比对,如果数据库表当前版本号与第一次取出来的版本标识值相等,则予以更新,否则认为是过期数据。乐观锁的优点和不足: 乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。但如果直接简单这么做,还是有可能会遇到不可预期的结果,例如两个事务都读取了数据库的某一行,经过修改以后写回数据库,这时就遇到了问题。5.1InnoDB锁的特性在不通过索引条件查询的时候,InnoDB使用的确实是表锁!由于 MySQL 的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行 的记录,但是如果是使用相同的索引键,是会出现锁冲突的。当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论 是使用主键索引、唯一索引或普通索引,InnoDB 都会使用行锁来对数据加锁。即便在条件中使用了索引字段,但是否使用索引来检索数据是由 MySQL 通过判断不同 执行计划的代价来决定的,如果 MySQL 认为全表扫 效率更高,比如对一些很小的表,它 就不会使用索引,这种情况下 InnoDB 将使用表锁,而不是行锁。因此,在分析锁冲突时, 别忘了检查 SQL 的执行计划(explain查看),以确认是否真正使用了索引。有关执行计划的解释可以看着这篇文章:https://www.jianshu.com/p/b5c01bd4a3061.通过非索引项检索数据,加表锁!price属性并没有加索引,因此这时候添加的锁为表级锁! 窗口1: mysql> select * from product where price=88 for update; +----+------+-------+-----+ | id | name | price | num | +----+------+-------+-----+ | 2 | 蒙牛 | 88 | 1 | +----+------+-------+-----+ 窗口2: mysql> update product set price=price-100 where id=6; 这里会等待,直到窗口1 commit后显示下面结果! Query OK, 1 row affected Rows matched: 1 Changed: 1 Warnings: 0 2.使用相同索引值但是不同行引发的冲突这里的num属性 加上了普通索引,price属性并没有索引 窗口1: mysql> set autocommit=0; Query OK, 0 rows affected mysql> select * from product where num=1 and price=68 for update; +----+------+-------+-----+ | id | name | price | num | +----+------+-------+-----+ | 1 | 伊利 | 68 | 1 | +----+------+-------+-----+ 窗口2: mysql> update product set price=price+100 where num=1 and price=88; 这里会发生等待,直到窗口1 commit 显示下面结果 Query OK, 1 row affected Rows matched: 1 Changed: 1 Warnings: 0 mysql> select * from product; +----+----------+-------+-----+ | id | name | price | num | +----+----------+-------+-----+ | 1 | 伊利 | 68 | 1 | | 2 | 蒙牛 | 188 | 1 | +----+----------+-------+-----+ 3.当使用索引检索数据时不同事务可以操作不同行数据锁一行数据,DML操作其他行并没有影响 窗口1: mysql> select * from user where id=1 for update; +----+-------+ | id | price | +----+-------+ | 1 | 400 | +----+-------+ 窗口2: mysql> update user set price=price+100 where id=2; 无需等待窗口1 commit Database changed Rows matched: 1 Changed: 1 Warnings: 0 6.Record Lock、Gap Lock、Next-key Lock锁6.1.Record Lock 单条索引上加锁,record lock 永远锁的是索引,而非数据本身,如果innodb表中没有索引,那么会自动创建一个隐藏的聚集索引,锁住的就是这个聚集索引。所以说当一条sql没有走任何索引时,那么将会在每一条聚集索引后面加X锁,这个类似于表锁,但原理上和表锁应该是完全不同的。6.2.Gap Lock 间隙锁,是在索引的间隙之间加上锁,这是为什么Repeatable Read隔离级别下能防止幻读的主要原因。有关幻读的详细解释:https://blog.csdn.net/qq_38238296/article/details/883630176.2.1 什么叫间隙锁 直接通过例子来说明:mysql> select * from product_copy; +----+--------+-------+-----+ | id | name | price | num | +----+--------+-------+-----+ | 1 | 伊利 | 68 | 1 | | 2 | 蒙牛 | 88 | 1 | | 6 | tom | 2788 | 3 | | 10 | 优衣库 | 488 | 4 | +----+--------+-------+-----+ 其中id为主键 num为普通索引 窗口A: mysql> select * from product_copy where num=3 for update; +----+------+-------+-----+ | id | name | price | num | +----+------+-------+-----+ | 6 | tom | 2788 | 3 | +----+------+-------+-----+ 1 row in set 窗口B: mysql> insert into product_copy values(5,'kris',1888,2); 这里会等待 直到窗口A commit才会显示下面结果 Query OK, 1 row affected 但是下面是不需要等待的 mysql> update product_copy set price=price+100 where num=1; Query OK, 2 rows affected Rows matched: 2 Changed: 2 Warnings: 0 mysql> insert into product_copy values(5,'kris',1888,5); Query OK, 1 row affected 通过上面的例子可以看出Gap 锁的作用是在1,3的间隙之间加上了锁。而且并不是锁住了表,我更新num=1,5的数据是可以的.可以看出锁住的范围是(1,3]U[3,4)。6.2.2 为什么说gap锁是RR隔离级别下防止幻读的主要原因。首先得理解什么是幻读:https://blog.csdn.net/qq_38238296/article/details/88363017解决幻读的方式很简单,就是需要当事务进行当前读的时候,保证其他事务不可以在满足当前读条件的范围内进行数据操作。根据索引的有序性,我们可以从上面的例子推断出满足where条件的数据,只能插入在num=(1,3]U[3,4)两个区间里面,只要我们将这两个区间锁住,那么就不会发生幻读。6.2.3. 主键索引/唯一索引+当前读会加上Gap锁吗?直接通过例子来说明窗口A: mysql> select * from product_copy where id=6 for update; +----+------+-------+-----+ | id | name | price | num | +----+------+-------+-----+ | 6 | tom | 2788 | 3 | +----+------+-------+-----+ 窗口B:并不会发生等待 mysql> insert into product_copy values(5,'kris',1888,3); Query OK, 1 row affected 例子说明的其实就是行锁的原因,我只将id=6的行数据锁住了,用Gap锁的原理来解释的话:因为主键索引和唯一索引的值只有一个,所以满足检索条件的只有一行,故并不会出现幻读,所以并不会加上Gap锁。6.2.4通过范围查询是否会加上Gap锁 前面的例子都是通过等值查询,下面测试一下范围查询。窗口A: mysql> select * from product_copy where num>3 for update; +----+--------+-------+-----+ | id | name | price | num | +----+--------+-------+-----+ | 10 | 优衣库 | 488 | 4 | +----+--------+-------+-----+ 窗口B:会等待 mysql> insert into product_copy values(11,'kris',1888,5); Query OK, 1 row affected 不会等待 mysql> insert into product_copy values(3,'kris',1888,2); Query OK, 1 row affected 其实原因都是一样,只要满足检索条件的都会加上Gap锁6.2.5 检索条件并不存在的当前读会加上Gap吗?1.等值查询窗口A: mysql> select * from product_copy where num=5 for update; Empty set 窗口B:6 和 4都会等待 mysql> insert into product_copy values(11,'kris',1888,6); Query OK, 1 row affected mysql> insert into product_copy values(11,'kris',1888,4); Query OK, 1 row affected 原因一样会锁住(4,5]U[5,n)的区间2.范围查询这里就会有点不一样窗口A: mysql> select * from product_copy where num>6 for update; Empty set 窗口B:8 和 4 都会锁住 mysql> insert into product_copy values(11,'kris',1888,4); Query OK, 1 row affected mysql> insert into product_copy values(11,'kris',1888,8); Query OK, 1 row affected 上面的2例子看出当你查询并不存在的数据的时候,mysql会将有可能出现区间全部锁住。6.3.Next-Key Lock这个锁机制其实就是前面两个锁相结合的机制,既锁住记录本身还锁住索引之间的间隙。7.死锁的原理及分析7.1. MVCC MySQL InnoDB存储引擎,实现的是基于多版本并发控制协议—MVCC(Multi-Version Concurrency Control) MVCC最大的好处,相信也是耳熟能详:读不加锁,读写不冲突。在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能,这也是为什么现阶段,几乎所有的RDBMS,都支持了MVCC。7.2. 2PL:Two-Phase Locking 传统RDBMS(关系数据库管理系统)加锁的一个原则,就是2PL (二阶段锁):Two-Phase Locking。相对而言,2PL比较容易理解,说的是锁操作分为两个阶段:加锁阶段与解锁阶段,并且保证加锁阶段与解锁阶段不相交。下面,仍旧以MySQL为例,来简单看看2PL在MySQL中的实现。transactionmysqlbegin;加锁阶段insert into加insert对应的锁update table加update对应的锁delete from加delete对应的锁commit解锁阶段 将insert、update、delete的锁全部解开 上面的例子可以看出2PL就是将加锁、解锁分为两个阶段,并且互相不干扰。加锁阶段只加锁,解锁阶段只解锁。7.3 为什么会发生死锁 MyISAM中是不会产生死锁的,因为MyISAM总是一次性获得所需的全部锁,要么全部满足,要么全部等待。而在InnoDB中,锁是逐步获得的,就造成了死锁的可能。(不过现在一般都是InnoDB引擎,关于MyISAM不做考虑) 在InnoDB中,行级锁并不是直接锁记录,而是锁索引。索引分为主键索引和非主键索引两种,如果一条sql语句操作了主键索引,MySQL就会锁定这条主键索引;如果一条语句操作了非主键索引,MySQL会先锁定该非主键索引,再锁定相关的主键索引。 当两个事务同时执行,一个锁住了主键索引,在等待其他相关索引。另一个锁定了非主键索引,在等待主键索引。这样就会发生死锁。通过两个SQL死锁的例子来说明1.两个session的两条语句 这种情况很好理解,首先session1获得 id=1的锁 session2获得id=5的锁,然后session想要获取id=5的锁 等待,session2想要获取id=1的锁 ,也等待!2.两个session的一条语句 这种情况需要我们了解数据的索引的检索顺序原理简单说下:普通索引上面保存了主键索引,当我们使用普通索引检索数据时,如果所需的信息不够,那么会继续遍历主键索引。 假设默认情况是RR隔离级别,针对session 1 从name索引出发,检索到的是(hdc,1)(hdc,6)不仅会加name索引上的记录X锁,而且会加聚簇索引上的记录X锁,加锁顺序为先[1,hdc,100],后[6,hdc,10] 这个顺序是因为B+树结构的有序性。而Session 2,从pubtime索引出发,[10,6],[100,1]均满足过滤条件,同样也会加聚簇索引上的记录X锁,加锁顺序为[6,hdc,10],后[1,hdc,100]。发现没有,跟Session 1的加锁顺序正好相反,如果两个Session恰好都持有了第一把锁,请求加第二把锁,死锁就发生了。避免死锁,这里只介绍常见的三种如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率;
2020年08月13日
166 阅读
0 评论
8 点赞
2020-07-09
laravel5.5以下的版本安装包的时候为什么需要添加包的服务提供者,而5.5以上就不需要
在 Laravel 5.5 之前的版本,安装新的包需要手动添加包的服务提供者(Service Provider)到应用程序的配置文件中,以便让 Laravel 能够识别并加载这些包的功能。这是因为 Laravel 5.5 之前的版本中,服务提供者必须手动注册才能生效。 而从 Laravel 5.5 开始,引入了自动包发现(Package Auto-Discovery)功能,这个功能可以自动扫描安装的包,并注册这些包的服务提供者,从而不再需要手动添加服务提供者到配置文件中。 这个自动包发现功能的实现方式是通过扫描所有安装的 Composer 包中的特定文件 composer.json 中的 extra 字段中的 laravel 节点来实现的。在这个 laravel 节点中可以指定包的服务提供者和别名,Laravel 会自动将其注册到应用程序中。如图 因此,如果你使用的是 Laravel 5.5 及以上的版本,可以直接安装包而不需要手动添加服务提供者到配置文件中。但如果你使用的是 Laravel 5.5 之前的版本,则需要手动添加服务提供者。
2020年07月09日
168 阅读
0 评论
4 点赞
2020-06-08
Elasticsearch 如何做到快速检索
本文大致包括以下内容:关于搜索传统关系型数据库和 ES 的差别搜索引擎原理细究倒排索引倒排索引具体是个什么样子的(posting list -> term dic -> term index)关于 postings list 的一些巧技 (FOR、Roaring Bitmaps)如何快速做联合查询?二、关于搜索先设想一个关于搜索的场景,假设我们要搜索一首诗句内容中带“前”字的古诗,用 传统关系型数据库和 ES 实现会有什么差别?如果用像 MySQL 这样的 RDBMS 来存储古诗的话,我们应该会去使用这样的 SQL 去查询select name from poems where content like "%前%";这种我们称为顺序扫描法,需要遍历所有的记录进行匹配。不但效率低,而且不符合我们搜索时的期望,比如我们在搜索“ABCD"这样的关键词时,通常还希望看到"A","AB","CD",“ABC”的搜索结果。于是乎就有了专业的搜索引擎,比如我们今天的主角 -- ES。搜索引擎原理搜索引擎的搜索原理简单概括的话可以分为这么几步,内容爬取,停顿词过滤比如一些无用的像"的",“了”之类的语气词/连接词内容分词,提取关键词根据关键词建立倒排索引用户输入关键词进行搜索这里我们就引出了一个概念,也是我们今天的要剖析的重点 - 倒排索引。也是 ES 的核心知识点。如果你了解 ES 应该知道,ES 可以说是对 Lucene 的一个封装,里面关于倒排索引的实现就是通过 lucene 这个 jar 包提供的 API 实现的,所以下面讲的关于倒排索引的内容实际上都是 lucene 里面的内容。三、倒排索引首先我们还不能忘了我们之前提的搜索需求,先看下建立倒排索引之后,我们上述的查询需求会变成什么样子,这样我们一输入“前”,借助倒排索引就可以直接定位到符合查询条件的古诗。当然这只是一个很大白话的形式来描述倒排索引的简要工作原理。在 ES 中,这个倒排索引是具体是个什么样的,怎么存储的等等,这些才是倒排索引的精华内容。1. 几个概念在进入下文之前,先描述几个前置概念。term关键词这个东西是我自己的讲法,在 ES 中,关键词被称为 term。postings list还是用上面的例子,{静夜思, 望庐山瀑布}是 "前" 这个 term 所对应列表。在 ES 中,这些被描述为所有包含特定 term 文档的 id 的集合。由于整型数字 integer 可以被高效压缩的特质,integer 是最适合放在 postings list 作为文档的唯一标识的,ES 会对这些存入的文档进行处理,转化成一个唯一的整型 id。再说下这个 id 的范围,在存储数据的时候,在每一个 shard 里面,ES 会将数据存入不同的 segment,这是一个比 shard 更小的分片单位,这些 segment 会定期合并。在每一个 segment 里面都会保存最多 2^31 个文档,每个文档被分配一个唯一的 id,从0到(2^31)-1。相关的名词都是 ES 官方文档给的描述,后面参考材料中都可以找到出处。2. 索引内部结构上面所描述的倒排索引,仅仅是一个很粗糙的模型。真的要在实际生产中使用,当然还差的很远。在实际生产场景中,比如 ES 最常用的日志分析,日志内容进行分词之后,可以得到多少的 term?那么如何快速的在海量 term 中查询到对应的 term 呢?遍历一遍显然是不现实的。term dictionary于是乎就有了 term dictionary,ES 为了能快速查找到 term,将所有的 term 排了一个序,二分法查找。是不是感觉有点眼熟,这不就是 MySQL 的索引方式的,直接用 B+树建立索引词典指向被索引的数据。term index但是问题又来了,你觉得 Term Dictionary 应该放在哪里?肯定是放在内存里面吧?磁盘 io 那么慢。就像 MySQL 索引就是存在内存里面了。但是如果把整个 term dictionary 放在内存里面会有什么后果呢?内存爆了...别忘了,ES 默认可是会对全部 text 字段进行索引,必然会消耗巨大的内存,为此 ES 针对索引进行了深度的优化。在保证执行效率的同时,尽量缩减内存空间的占用。于是乎就有了 term index。Term index 从数据结构上分类算是一个“Trie 树”,也就是我们常说的字典树。这是一种专门处理字符串匹配的数据结构,用来解决在一组字符串集合中快速查找某个字符串的问题。这棵树不会包含所有的 term,它包含的是 term 的一些前缀(这也是字典树的使用场景,公共前缀)。通过 term index 可以快速地定位到 term dictionary 的某个 offset,然后从这个位置再往后顺序查找。就想右边这个图所表示的。(怎么样,像不像我们查英文字典,我们定位 S 开头的第一个单词,或者定位到 Sh 开头的第一个单词,然后再往后顺序查询)lucene 在这里还做了两点优化,一是 term dictionary 在磁盘上面是分 block 保存的,一个 block 内部利用公共前缀压缩,比如都是 Ab 开头的单词就可以把 Ab 省去。二是 term index 在内存中是以 FST(finite state transducers)的数据结构保存的。FST 有两个优点:空间占用小。通过对词典中单词前缀和后缀的重复利用,压缩了存储空间查询速度快。O(len(str)) 的查询时间复杂度。FST 的理论比较复杂,本文不细讲延伸阅读:https://www.shenyanchao.cn/blog/2018/12/04/lucene-fst/OK,现在我们能得到 lucene 倒排索引大致是个什么样子的了。四、关于 postings list 的一些巧技在实际使用中,postings list 还需要解决几个痛点,postings list 如果不进行压缩,会非常占用磁盘空间,联合查询下,如何快速求交并集(intersections and unions)对于如何压缩,可能会有人觉得没有必要,”posting list 不是已经只存储文档 id 了吗?还需要压缩?”,但是如果在 posting list 有百万个 doc id 的情况,压缩就显得很有必要了。(比如按照朝代查询古诗?),至于为啥需要求交并集,ES 是专门用来搜索的,肯定会有很多联合查询的需求吧 (AND、OR)。按照上面的思路,我们先将如何压缩。1. 压缩Frame of Reference在 lucene 中,要求 postings lists 都要是有序的整形数组。这样就带来了一个很好的好处,可以通过 增量编码(delta-encode)这种方式进行压缩。比如现在有 id 列表 [73, 300, 302, 332, 343, 372],转化成每一个 id 相对于前一个 id 的增量值(第一个 id 的前一个 id 默认是 0,增量就是它自己)列表是[73, 227, 2, 30, 11, 29]。在这个新的列表里面,所有的 id 都是小于 255 的,所以每个 id 只需要一个字节存储。实际上 ES 会做的更加精细,它会把所有的文档分成很多个 block,每个 block 正好包含 256 个文档,然后单独对每个文档进行增量编码,计算出存储这个 block 里面所有文档最多需要多少位来保存每个 id,并且把这个位数作为头信息(header)放在每个 block 的前面。这个技术叫 Frame of Reference。上图也是来自于 ES 官方博客中的一个示例(假设每个 block 只有 3 个文件而不是 256)。FOR 的步骤可以总结为:进过最后的位压缩之后,整型数组的类型从固定大小 (8,16,32,64 位)4 种类型,扩展到了[1-64] 位共 64 种类型。通过以上的方式可以极大的节省 posting list 的空间消耗,提高查询性能。不过 ES 为了提高 filter 过滤器查询的性能,还做了更多的工作,那就是缓存。Roaring Bitmaps (for filter cache)在 ES 中,可以使用 filters 来优化查询,filter 查询只处理文档是否匹配与否,不涉及文档评分操作,查询的结果可以被缓存。对于 filter 查询,es 提供了 filter cache 这种特殊的缓存,filter cache 用来存储 filters 得到的结果集。缓存 filters 不需要太多的内存,它只保留一种信息,即哪些文档与 filter 相匹配。同时它可以由其它的查询复用,极大地提升了查询的性能。我们上面提到的 Frame Of Reference 压缩算法对于 postings list 来说效果很好,但对于需要存储在内存中的 filter cache 等不太合适。filter cache 会存储那些经常使用的数据,针对 filter 的缓存就是为了加速处理效率,对压缩算法要求更高。对于这类 postings list,ES 采用不一样的压缩方式。那么让我们一步步来。首先我们知道 postings list 是 Integer 数组,具有压缩空间。假设有这么一个数组,我们第一个压缩的思路是什么?用位的方式来表示,每个文档对应其中的一位,也就是我们常说的位图,bitmap。它经常被作为索引用在数据库、查询引擎和搜索引擎中,并且位操作(如 and 求交集、or 求并集)之间可以并行,效率更好。但是,位图有个很明显的缺点,不管业务中实际的元素基数有多少,它占用的内存空间都恒定不变。也就是说不适用于稀疏存储。业内对于稀疏位图也有很多成熟的压缩方案,lucene 采用的就是roaring bitmaps。我这里用简单的方式描述一下这个压缩过程是怎么样,将 doc id 拆成高 16 位,低 16 位。对高位进行聚合 (以高位做 key,value 为有相同高位的所有低位数组),根据低位的数据量 (不同高位聚合出的低位数组长度不相同),使用不同的 container(数据结构) 存储。len<4096 ArrayContainer 直接存值len>=4096 BitmapContainer 使用 bitmap 存储分界线的来源:value 的最大总数是为2^16=65536. 假设以 bitmap 方式存储需要 65536bit=8kb,而直接存值的方式,一个值 2 byte,4K 个总共需要2byte*4K=8kb。所以当 value 总量 <4k 时,使用直接存值的方式更节省空间。空间压缩主要体现在:高位聚合 (假设数据中有 100w 个高位相同的值,原先需要 100w2byte,现在只要 12byte)低位压缩缺点就在于位操作的速度相对于原生的 bitmap 会有影响。这就是 trade-off 呀。平衡的艺术。2. 联合查询讲完了压缩,我们再来讲讲联合查询。先讲简单的,如果查询有 filter cache,那就是直接拿 filter cache 来做计算,也就是说位图来做 AND 或者 OR 的计算。如果查询的 filter 没有缓存,那么就用 skip list 的方式去遍历磁盘上的 postings list。以上是三个 posting list。我们现在需要把它们用 AND 的关系合并,得出 posting list 的交集。首先选择最短的 posting list,逐个在另外两个 posting list 中查找看是否存在,最后得到交集的结果。遍历的过程可以跳过一些元素,比如我们遍历到绿色的 13 的时候,就可以跳过蓝色的 3 了,因为 3 比 13 要小。用 skip list 还会带来一个好处,还记得前面说的吗,postings list 在磁盘里面是采用 FOR 的编码方式存储的会把所有的文档分成很多个 block,每个 block 正好包含 256 个文档,然后单独对每个文档进行增量编码,计算出存储这个 block 里面所有文档最多需要多少位来保存每个 id,并且把这个位数作为头信息(header)放在每个 block 的前面。因为这个 FOR 的编码是有解压缩成本的。利用 skip list,除了跳过了遍历的成本,也跳过了解压缩这些压缩过的 block 的过程,从而节省了 cpu。五、总结下面我们来做一个技术总结(感觉有点王刚老师的味道😂)为了能够快速定位到目标文档,ES 使用倒排索引技术来优化搜索速度,虽然空间消耗比较大,但是搜索性能提高十分显著。为了能够在数量巨大的 terms 中快速定位到某一个 term,同时节约对内存的使用和减少磁盘 io 的读取,lucene 使用 "term index -> term dictionary -> postings list" 的倒排索引结构,通过 FST 压缩放入内存,进一步提高搜索效率。为了减少 postings list 的磁盘消耗,lucene 使用了 FOR(Frame of Reference)技术压缩,带来的压缩效果十分明显。ES 的 filter 语句采用了 Roaring Bitmap 技术来缓存搜索结果,保证高频 filter 查询速度的同时降低存储空间消耗。在联合查询时,在有 filter cache 的情况下,会直接利用位图的原生特性快速求交并集得到联合查询结果,否则使用 skip list 对多个 postings list 求交并集,跳过遍历成本并且节省部分数据的解压缩 cpu 成本Elasticsearch 的索引思路将磁盘里的东西尽量搬进内存,减少磁盘随机读取次数 (同时也利用磁盘顺序读特性),结合各种压缩算法,用及其苛刻的态度使用内存。所以,对于使用 Elasticsearch 进行索引时需要注意:不需要索引的字段,一定要明确定义出来,因为默认是自动建索引的同样的道理,对于 String 类型的字段,不需要 analysis 的也需要明确定义出来,因为默认也是会 analysis 的选择有规律的 ID 很重要,随机性太大的 ID(比如 Java 的 UUID) 不利于查询最后说一下,技术选型永远伴随着业务场景的考量,每种数据库都有自己要解决的问题(或者说擅长的领域),对应的就有自己的数据结构,而不同的使用场景和数据结构,需要用不同的索引,才能起到最大化加快查询的目的。这篇文章讲的虽是 Lucene 如何实现倒排索引,如何精打细算每一块内存、磁盘空间、如何用诡谲的位运算加快处理速度,但往高处思考,再类比一下 MySQL,你就会发现,虽然都是索引,但是实现起来,截然不同。笼统的来说,b-tree 索引是为写入优化的索引结构。当我们不需要支持快速的更新的时候,可以用预先排序等方式换取更小的存储空间,更快的检索速度等好处,其代价就是更新慢,就像 ES。参考文档https://www.elastic.co/cn/blog/frame-of-reference-and-roaring-bitmapshttps://www.elastic.co/cn/blog/found-elasticsearch-from-the-bottom-uphttp://blog.mikemccandless.com/2014/05/choosing-fast-unique-identifier-uuid.htmlhttps://www.infoq.cn/article/database-timestamp-02https://zhuanlan.zhihu.com/p/137574234- END -
2020年06月08日
445 阅读
0 评论
28 点赞
2020-06-05
PHP如何使用 ElasticSearch 做搜索
ElasticSearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。Elasticsearch是用Java开发的,并作为Apache许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。在做搜索的时候想到了 ElasticSearch ,而且其也支持 PHP,所以就做了一个简单的例子做测试,感觉还不错,做下记录。环境php 8.0elasticsearch 8.2elasticsearch-php 8.2安装 elasticsearch下载源文件,解压,重新建一个用户,将目录的所属组修改为此用户,因为 elasticsearch 无法用 root 用户启动。wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.2.3.tar.gz tar zxvf elasticsearch-8.2.3.tar.gz useradd elasticsearch password elasticsearch chown elasticsearch:elasticsearch elasticsearch-8.2.3 cd elasticsearch-8.2.3 ./bin/elasticsearch // 启动安装 PHP 扩展我这里使用的是 composer 安装 elasticsearch-php。在 composer.json 文件中加入 "elasticsearch/elasticsearch": "~8.2.3",执行 composer update。{ "require": { // ... "elasticsearch/elasticsearch": "~8.2.3" // ... } }测试例子创建表和测试数据我这里准备了一张文章表来进行测试,首先是建表,其次写入测试数据,准备工作完毕之后,就开始编辑测试用例。{ #创建articles表 create table articles( id int not null primary key auto_increment, title varchar(200) not null comment '标题', content text comment '内容' ); #插入文章内容 insert into articles(title, content) values ('Laravel 测试1', 'Laravel 测试文章内容1'),('Laravel 测试2', 'Laravel 测试文章内容2'),('Laravel 测试3', 'Laravel 测试文章内容3'); 从 Mysql 读取数据try { $db = new PDO('mysql:host=127.0.0.1;dbname=test', 'root',''); $sql = 'select * from articles'; $query = $db->prepare($sql); $query->execute(); $lists = $query->fetchAll(); print_r($lists); } catch (Exception $e) { echo $e->getMessage(); }实例化require './vendor/autoload.php'; use Elasticsearch\ClientBuilder; $client = ClientBuilder::create()->build();名词解释:索引相当于 MySQL 中的表,文档相当于 MySQL 中的行记录elasticsearch 的动态性质,在添加第一个文档的时候自动创建了索引和一些默认设置。将文档加入索引foreach ($lists as $row) { $params = [ 'body' => [ 'id' => $row['id'], 'title' => $row['title'], 'content' => $row['content'] ], 'id' => 'article_' . $row['id'], 'index' => 'articles_index', 'type' => 'articles_type' ]; $client->index($params); }从索引中获取文档$params = [ 'index' => 'articles_index', 'type' => 'articles_type', 'id' => 'articles_1' ]; $res = $client->get($params); print_r($res);从索引中删除文档$params = [ 'index' => 'articles_index', 'type' => 'articles_type', 'id' => 'articles_1' ]; $res = $client->delete($params); print_r($res);删除索引$params = [ 'index' => 'articles_index' ]; $res = $client->indices()->delete($params); print_r($res);创建索引$params['index'] = 'articles_index'; $params['body']['settings']['number_of_shards'] = 2; $params['body']['settings']['number_of_replicas'] = 0; $client->indices()->create($params);搜索$params = [ 'index' => 'articles_index', 'type' => 'articles_type', ]; $params['body']['query']['match']['content'] = 'Laravel'; $res = $client->search($params); print_r($res);
2020年06月05日
183 阅读
0 评论
6 点赞
2020-05-13
Go语言——结构体
结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正地分配内存,因此必须在定义结构体并实例化后才能使用结构体的字段。实例化就是根据结构体定义的格式创建一份与格式一致的内存区域,结构体实例与实例间的内存是完全独立的。Go语言可以通过多种方式实例化结构体,根据实际需要可以选用不同的写法。基本的实例化形式结构体本身是一种类型,可以像整型、字符串等类型一样,以 var 的方式声明结构体即可完成实例化。基本实例化格式如下:var ins T其中,T 为结构体类型,ins 为结构体的实例。用结构体表示的点结构(Point)的实例化过程请参见下面的代码: type Point struct { X int Y int } var p Point p.X = 10 p.Y = 20 type Point struct { X int Y int } var p Point p.X = 10 p.Y = 20在例子中,使用.来访问结构体的成员变量,如 p.X 和 p.Y 等,结构体成员变量的赋值方法与普通变量一致。创建指针类型的结构体Go语言中,还可以使用 new 关键字对类型(包括结构体、整型、浮点数、字符串等)进行实例化,结构体在实例化后会形成指针类型的结构体。使用 new 的格式如下:ins := new(T)其中:T 为类型,可以是结构体、整型、字符串等。ins:T 类型被实例化后保存到 ins 变量中,ins 的类型为 *T,属于指针。Go语言让我们可以像访问普通结构体一样使用.来访问结构体指针的成员。下面的例子定义了一个玩家(Player)的结构,玩家拥有名字、生命值和魔法值,实例化玩家(Player)结构体后,可对成员进行赋值,代码如下: type Player struct{ Name string HealthPoint int MagicPoint int } tank := new(Player) tank.Name = "Canon" tank.HealthPoint = 300 type Player struct{ Name string HealthPoint int MagicPoint int } tank := new(Player) tank.Name = "Canon" tank.HealthPoint = 300经过 new 实例化的结构体实例在成员赋值上与基本实例化的写法一致。Go语言和 C/C++在 C/C++ 语言中,使用 new 实例化类型后,访问其成员变量时必须使用->操作符。在Go语言中,访问结构体指针的成员变量时可以继续使用.,这是因为Go语言为了方便开发者访问结构体指针的成员变量,使用了语法糖(Syntactic sugar)技术,将 ins.Name 形式转换为 (*ins).Name。取结构体的地址实例化在Go语言中,对结构体进行&取地址操作时,视为对该类型进行一次 new 的实例化操作,取地址格式如下:ins := &T{}其中:T 表示结构体类型。ins 为结构体的实例,类型为 *T,是指针类型。下面使用结构体定义一个命令行指令(Command),指令中包含名称、变量关联和注释等,对 Command 进行指针地址的实例化,并完成赋值过程,代码如下: type Command struct { Name string // 指令名称 Var *int // 指令绑定的变量 Comment string // 指令的注释 } var version int = 1 cmd := &Command{} cmd.Name = "version" cmd.Var = &version cmd.Comment = "show version" type Command struct { Name string // 指令名称 Var *int // 指令绑定的变量 Comment string // 指令的注释 } var version int = 1 cmd := &Command{} cmd.Name = "version" cmd.Var = &version cmd.Comment = "show version"代码说明如下:第 1 行,定义 Command 结构体,表示命令行指令第 3 行,命令绑定的变量,使用整型指针绑定一个指针,指令的值可以与绑定的值随时保持同步。第 7 行,命令绑定的目标整型变量:版本号。第 9 行,对结构体取地址实例化。第 10~12 行,初始化成员字段。取地址实例化是最广泛的一种结构体实例化方式,可以使用函数封装上面的初始化过程,代码如下: func newCommand(name string, varref *int, comment string) *Command { return &Command{ Name: name, Var: varref, Comment: comment, } } cmd = newCommand( "version", &version, "show version", )
2020年05月13日
792 阅读
0 评论
31 点赞
1
...
7
8
9
...
12