首页
统计
留言
Search
1
PHP中使用反射
995 阅读
2
phpstorm配置SFTP
938 阅读
3
Go语言——结构体
792 阅读
4
PhpStorm 使用 AI 代码生成工具 Codeium
778 阅读
5
关于PHP的垃圾回收机制
762 阅读
后端
PHP
Go
数据库
其他
前端
其他技术
生活杂谈
登录
Search
标签搜索
Laravel
Mysql
RPC
Redis
Liunx
PHP
CSS
ES
算法
开发工具
断点续传
反射
phpstorm
工具
防盗链
CURL
设计模式
面试
Nginx
搜索引擎
quhe.net
首页
栏目
后端
PHP
Go
数据库
其他
前端
其他技术
生活杂谈
页面
统计
留言
搜索到
10
篇与
其他
的结果
2024-08-02
更换宝塔面板www目录的操作方法
一:如果是纯净系统还没安装宝塔面板:/mnt 更换成你的另一块磁盘分区名称第一步:进入mnt目录(mnt是数据盘名称,有很多是home,你的数据盘名称是什么就用那么名称替换data就好)cd /mnt 第二步:创建宝塔面板安装需要用的www目录mkdir www 第三步:建立/mnt/www的软连接到/www(也就是给系统根目录建立一个www的“快捷方式”指向/mnt/www)ln -s /mnt/www /www 第四步:正常安装宝塔面板即可二:如果已经安装了宝塔面板和WEB环境:/mnt 更换成你的另一块磁盘分区名称第一步:移动系统根目录下的www到mntmv /www /mnt/www 第二步:建立/data/www的软连接到/wwwln -s /mnt/www /www 第三步:重启服务器reboot 第四步:重启宝塔面板服务service bt restart 第五步:打开宝塔面板,CTRL+F5刷新浏览器缓存PS:Linux下的软链接类似于windows下的快捷方式 ,如上面的示例,当我们执行命令 cd /www/的时候 实际上是进入了 /mnt/ ,操作前切记备份数据,防止因误操作引起数据丢失!!!
2024年08月02日
40 阅读
0 评论
0 点赞
2022-01-05
什么是RPC
什么是RPCRPC的中文是“远程过程调用”,对应的英文全称是:Remote Procedure Call,可以简单理解为一个节点请求另一个节点提供的服务。请先自行思考一下什么是“本地过程调用”,可以更好的理解“远程过程调用”。知识点:RPC主要依赖于客户端与服务端建立socket链接;而HTTP REST实现通讯的代价比较高,这是RPC的一个优势体现。为什么用RPC就是因为无法在同一个进程内,或者无法在同一个服务器上通过本地调用的方式实现我们的需求。HTTP能满足需求但是不够高效,所以我们需要使用RPC。 @知乎大神的回答RPC的优势RPC能够跨多种开发工具和平台RPC能够跨语言调用RPC能够提高系统的可扩展性,解耦,提高复用RPC相较于HTTP,传输效率更高,性能消耗更小,自带负载均衡策略,自动实现服务治理RPC和HTTP对比RPC主要用于公司内部的服务调用,性能消耗低,传输效率高,服务治理方便。HTTP主要用于对外的异构环境,浏览器接口调用,APP接口调用,第三方接口调用等。@RPC和HTTP的详细对别 可以看这篇文章,不作为本篇的重点RPC的使用边界通过和HTTP的对比,我们可以倒推出RPC的边界:对外的异构环境,浏览器接口调用,APP接口调用,第三方接口调用。上述这些都不适合RPC,不知道RPC不适合做什么,比知道RPC能做什么更重要。RPC入门1:net/rpc基本构成RPC的基本构成:服务端,客户端服务端基本构成:结构体,请求结构体,响应结构体客户端基本构成:请求结构体,响应结构体代码示例rpc_service.gopackage main import ( "errors" "fmt" "log" "net" "net/http" "net/rpc" "os" ) type Arith struct { } //请求结构体 type ArithRequest struct { A int B int } //响应结构体 type ArithResponse struct { Pro int //乘积 Quo int //商 Rem int //余数 } //乘积方法 func (this *Arith) Multiply(req ArithRequest,res *ArithResponse) error{ res.Pro = req.A * req.B return nil } //除法运算方法 func (this *Arith) Divide(req ArithRequest,res *ArithResponse) error{ if req.B ==0 { return errors.New("divide by zero") } res.Quo = req.A / req.B res.Rem = req.A % req.B return nil } func main() { //注册rpc服务 rpc.Register(new(Arith)) //采用http协议作为rpc载体 rpc.HandleHTTP() lis,err := net.Listen("tcp","127.0.0.1:8095") if err!=nil { log.Fatalln("fatal error:",err) } fmt.Fprintf(os.Stdout,"%s","start connection\n") //常规启动http服务 http.Serve(lis,nil) }rpc\_client.gopackage main import ( "fmt" "log" "net/rpc" ) //算数运算请求结构体 type ArithRequest struct { A int B int } //响应结构体 type ArithResponse struct { Pro int //乘 Quo int //商 Rem int //余数 } func main() { conn,err := rpc.DialHTTP("tcp","127.0.0.1:8095") if err!=nil { log.Fatalln("dialing error:",err) } req := ArithRequest{10,20} var res ArithResponse err = conn.Call("Arith.Multiply",req,&res) //乘法运算 if err!=nil { log.Fatalln("arith error:",err) } fmt.Printf("%d * %d = %d\n",req.A,req.B,res.Pro) //除法运算 err = conn.Call("Arith.Divide",req,&res) if err!=nil { log.Fatalln("arith error:",err) } fmt.Printf("%d / %d = %d 余数是:%d",req.A,req.B,res.Quo,res.Rem) }运行结果先启动服务端,再启动客户端连接服务端//服务端console start connection //客户端console 10 * 20 = 200 10 / 20 = 0 余数是:10 RPC入门2:net/rpc/jsonrpc实现跨语言调用jsonrpc_server.gopackage main import ( "errors" "fmt" "log" "net" "net/rpc" "net/rpc/jsonrpc" "os" ) type Arith struct { } //请求结构体 type ArithRequest struct { A int B int } //响应结构体 type ArithResponse struct { Pro int //乘积 Quo int //商 Rem int //余数 } //乘积方法 func (this *Arith) Multiply(req ArithRequest,res *ArithResponse) error{ res.Pro = req.A * req.B return nil } //除法运算方法 func (this *Arith) Divide(req ArithRequest,res *ArithResponse) error{ if req.B ==0 { return errors.New("divide by zero") } res.Quo = req.A / req.B res.Rem = req.A % req.B return nil } func main() { //注册rpc服务 rpc.Register(new(Arith)) //采用http协议作为rpc载体 rpc.HandleHTTP() lis,err := net.Listen("tcp","127.0.0.1:8096") if err!=nil { log.Fatalln("fatal error:",err) } fmt.Fprintf(os.Stdout,"%s","start connection\n") //接收客户端请求 并发处理 jsonrpc for { conn,err :=lis.Accept() //接收客户端连接请求 if err!=nil { continue } //并发处理客户端请求 go func(conn net.Conn) { fmt.Fprintf(os.Stdout,"%s","new client in coming\n") jsonrpc.ServeConn(conn) }(conn) } //常规启动http服务 //http.Serve(lis,nil) }jsonrpc_client.gopackage main import ( "fmt" "log" "net/rpc/jsonrpc" ) //算数运算请求结构体 type ArithRequest struct { A int B int } //响应结构体 type ArithResponse struct { Pro int //乘 Quo int //商 Rem int //余数 } func main() { // 只有这里不一样 conn,err := jsonrpc.Dial("tcp","127.0.0.1:8096") if err!=nil { log.Fatalln("dialing error:",err) } req := ArithRequest{9,2} var res ArithResponse err = conn.Call("Arith.Multiply",req,&res) //乘法运算 if err!=nil { log.Fatalln("arith error:",err) } fmt.Printf("%d * %d = %d\n",req.A,req.B,res.Pro) //除法运算 err = conn.Call("Arith.Divide",req,&res) if err!=nil { log.Fatalln("arith error:",err) } fmt.Printf("%d / %d = %d 余数是:%d",req.A,req.B,res.Quo,res.Rem) }运行结果先启动服务端,再启动客户端连接服务端//服务端console start connection //客户端console 9 * 2 = 18 9 / 2 = 4 余数是:1 //服务端console new client in comingRPC入门3:go php跨语言调用Go作为服务端,PHP作为客户端jsonrpc_server.go:和入门2服务端的代码一样下面是PHP代码jsonrpc_client.php<?php class JsonRPC { private $conn; function __construct($host, $port) { $this->conn = fsockopen($host, $port, $errno, $errstr, 3); if (!$this->conn) { return false; } } public function Call($method, $params) { if (!$this->conn) { return false; } $err = fwrite($this->conn, json_encode(array( 'method' => $method, 'params' => array($params), 'id' => 0, )) . "\n"); if ($err === false) { return false; } stream_set_timeout($this->conn, 0, 3000); $line = fgets($this->conn); if ($line === false) { return NULL; } return json_decode($line, true); } } $client = new JsonRPC("127.0.0.1", 8096); $args = array('A' => 9, 'B' => 2); $r = $client->Call("Arith.Multiply", $args); printf("%d * %d = %d\n", $args['A'], $args['B'], $r['result']['Pro']); $r = $client->Call("Arith.Divide", array('A' => 9, 'B' => 2)); printf("%d / %d, Quo is %d, Rem is %d\n", $args['A'], $args['B'], $r['result']['Quo'], $r['result']['Rem']);@如何在本地启动PHP 不作为本文重点,可以看这篇文章。运行结果本地启动PHP服务:http://127.0.0.1/jsonrpc_client.php运行结果如下:9 * 2 = 18 9 / 2, Quo is 4, Rem is 1
2022年01月05日
65 阅读
0 评论
0 点赞
2021-06-18
PHP实现无限级分类生成树结构
方法封装/** * @param $current_key string 当前编号 * @param $parent_key string 父级编号 * @param $array array 数据 * @return array */ public function getTreeList($current_key,$parent_key,$array){ //第一步 构造数据 $items = array(); foreach($array as $value){ $items[$value[$current_key]] = $value; } //第二部 遍历数据 生成树状结构 $tree = array(); foreach($items as $key => $value){ if(isset($items[$value[$parent_key]])){ $items[$value[$parent_key]]['children'][] = &$items[$key]; }else{ $tree[] = &$items[$key]; } } return $tree; }使用例子$arrayData=array( array('name'=>'小明','wbs_no'=>1,'p_wbs_no'=>0), array('name'=>'小王','wbs_no'=>2,'p_wbs_no'=>1), array('name'=>'小李','wbs_no'=>3,'p_wbs_no'=>2), ); $result=$this->getTreeList('wbs_no','p_wbs_no',$arrayData); 输出
2021年06月18日
50 阅读
0 评论
2 点赞
2020-09-05
用PHP与Redis实现单据锁,防止并发重复写入数据
一、何为单据锁:在整个供应链系统中,会有很多种单据(采购单、入库单、到货单、运单等等),在涉及写单据数据的接口时(增删改操作),即使前端做了相关限制,还是有可能因为网络或异常操作产生并发重复调用的情况,导致对相同单据做相同的处理;为了防止这种情况对系统造成异常影响,我们通过Redis实现了一个简单的单据锁,每个请求需先获取锁才能执行业务逻辑,执行结束后才会释放锁;保证了同一单据的并发重复操作请求只有一个请求可以获取到锁(依赖Redis的单线程),是一种悲观锁的设计;注:Redis锁在我们的系统中一般只用于解决并发重复请求的情况,对于非并发的的重复请求一般会去数据库或日志校验数据的状态,两种机制结合起来才能保证整个链路的可靠。二、加锁机制:主要依赖Redis setnx指令实现:但使用setnx有一个问题,即setnx指令不支持设置过期时间,需要使用expire指令另行为key设置超时时间。这样整个加锁操作就不是一个原子性操作,有可能存在setnx加锁成功,但因程序异常退出导致未成功设置超时时间,在不及时解锁的情况下,有可能会导致死锁(即使业务场景中不会出现死锁,无用的key一直常驻内存也不是很好的设计);这种情况可以使用Redis事务解决,把setnx与expire两条指令作为一个原子性操作执行,但这样做相对而言会比较麻烦,好在Redis 2.6.12之后版本,Redis set指令支持了nx、ex模式,并支持原子化地设置过期时间:三、加锁实现(完整测试代码会贴在最后):/** * 加单据锁 * @param int $intOrderId 单据ID * @param int $intExpireTime 锁过期时间(秒) * @return bool|int 加锁成功返回唯一锁ID,加锁失败返回false */ public static function addLock($intOrderId, $intExpireTime = self::REDIS_LOCK_DEFAULT_EXPIRE_TIME) { //参数校验 if (empty($intOrderId) || $intExpireTime <= 0) { return false; } //获取Redis连接 $objRedisConn = self::getRedisConn(); //生成唯一锁ID,解锁需持有此ID $intUniqueLockId = self::generateUniqueLockId(); //根据模板,结合单据ID,生成唯一Redis key(一般来说,单据ID在业务中系统中唯一的) $strKey = sprintf(self::REDIS_LOCK_KEY_TEMPLATE, $intOrderId); //加锁(通过Redis setnx指令实现,从Redis 2.6.12开始,通过set指令可选参数也可以实现setnx,同时可原子化地设置超时时间) $bolRes = $objRedisConn->set($strKey, $intUniqueLockId, ['nx', 'ex'=>$intExpireTime]); //加锁成功返回锁ID,加锁失败返回false return $bolRes ? $intUniqueLockId : $bolRes; }四、解锁机制:解锁即比对加锁时的唯一lock id,如果比对成功,则删除key;需要注意的是,解锁整个过程中同样需要保证原子性,这里依赖redis的watch与事务实现;WATCH命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行。监控一直持续到EXEC命令(事务中的命令是在EXEC之后才执行的,所以在MULTI命令后可以修改WATCH监控的键值)五、解锁实现(完整测试代码会贴在最后):/** * 解单据锁 * @param int $intOrderId 单据ID * @param int $intLockId 锁唯一ID * @return bool */ public static function releaseLock($intOrderId, $intLockId) { //参数校验 if (empty($intOrderId) || empty($intLockId)) { return false; } //获取Redis连接 $objRedisConn = self::getRedisConn(); //生成Redis key $strKey = sprintf(self::REDIS_LOCK_KEY_TEMPLATE, $intOrderId); //监听Redis key防止在【比对lock id】与【解锁事务执行过程中】被修改或删除,提交事务后会自动取消监控,其他情况需手动解除监控 $objRedisConn->watch($strKey); if ($intLockId == $objRedisConn->get($strKey)) { $objRedisConn->multi()->del($strKey)->exec(); return true; } $objRedisConn->unwatch(); return false; }六、附整体测试代码(此代码仅为简易版本)<?php /** * Class Lock_Service 单据锁服务 */ class Lock_Service { /** * 单据锁redis key模板 */ const REDIS_LOCK_KEY_TEMPLATE = 'order_lock_%s'; /** * 单据锁默认超时时间(秒) */ const REDIS_LOCK_DEFAULT_EXPIRE_TIME = 86400; /** * 加单据锁 * @param int $intOrderId 单据ID * @param int $intExpireTime 锁过期时间(秒) * @return bool|int 加锁成功返回唯一锁ID,加锁失败返回false */ public static function addLock($intOrderId, $intExpireTime = self::REDIS_LOCK_DEFAULT_EXPIRE_TIME) { //参数校验 if (empty($intOrderId) || $intExpireTime <= 0) { return false; } //获取Redis连接 $objRedisConn = self::getRedisConn(); //生成唯一锁ID,解锁需持有此ID $intUniqueLockId = self::generateUniqueLockId(); //根据模板,结合单据ID,生成唯一Redis key(一般来说,单据ID在业务中系统中唯一的) $strKey = sprintf(self::REDIS_LOCK_KEY_TEMPLATE, $intOrderId); //加锁(通过Redis setnx指令实现,从Redis 2.6.12开始,通过set指令可选参数也可以实现setnx,同时可原子化地设置超时时间) $bolRes = $objRedisConn->set($strKey, $intUniqueLockId, ['nx', 'ex'=>$intExpireTime]); //加锁成功返回锁ID,加锁失败返回false return $bolRes ? $intUniqueLockId : $bolRes; } /** * 解单据锁 * @param int $intOrderId 单据ID * @param int $intLockId 锁唯一ID * @return bool */ public static function releaseLock($intOrderId, $intLockId) { //参数校验 if (empty($intOrderId) || empty($intLockId)) { return false; } //获取Redis连接 $objRedisConn = self::getRedisConn(); //生成Redis key $strKey = sprintf(self::REDIS_LOCK_KEY_TEMPLATE, $intOrderId); //监听Redis key防止在【比对lock id】与【解锁事务执行过程中】被修改或删除,提交事务后会自动取消监控,其他情况需手动解除监控 $objRedisConn->watch($strKey); if ($intLockId == $objRedisConn->get($strKey)) { $objRedisConn->multi()->del($strKey)->exec(); return true; } $objRedisConn->unwatch(); return false; } /** * Redis配置:IP */ const REDIS_CONFIG_HOST = '127.0.0.1'; /** * Redis配置:端口 */ const REDIS_CONFIG_PORT = 6379; /** * 获取Redis连接(简易版本,可用单例实现) * @param string $strIp IP * @param int $intPort 端口 * @return object Redis连接 */ public static function getRedisConn($strIp = self::REDIS_CONFIG_HOST, $intPort = self::REDIS_CONFIG_PORT) { $objRedis = new Redis(); $objRedis->connect($strIp, $intPort); return $objRedis; } /** * 用于生成唯一的锁ID的redis key */ const REDIS_LOCK_UNIQUE_ID_KEY = 'lock_unique_id'; /** * 生成锁唯一ID(通过Redis incr指令实现简易版本,可结合日期、时间戳、取余、字符串填充、随机数等函数,生成指定位数唯一ID) * @return mixed */ public static function generateUniqueLockId() { return self::getRedisConn()->incr(self::REDIS_LOCK_UNIQUE_ID_KEY); } } //test $res1 = Lock_Service::addLock('666666'); var_dump($res1);//返回lock id,加锁成功 $res2 = Lock_Service::addLock('666666'); var_dump($res2);//false,加锁失败 $res3 = Lock_Service::releaseLock('666666', $res1); var_dump($res3);//true,解锁成功 $res4 = Lock_Service::releaseLock('666666', $res1); var_dump($res4);//false,解锁失败
2020年09月05日
212 阅读
0 评论
13 点赞
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日
444 阅读
0 评论
28 点赞
1
2