文档路由

es 是一个分布式系统,当我们存储一个文档到 es 上之后,这个文档实际上是被存储到 master 节点中的某一个主分片上。

PUT blog/_doc/a
{
  "title":"a"
}


文档保存成功后,可以查看该文档被保存到哪个分片中去了:

GET _cat/shards/blog?v
查看结果如下:

index shard prirep state   docs store ip        node
blog  1     p      STARTED    0  208b 127.0.0.1 slave01
blog  0     p      STARTED    1 3.6kb 127.0.0.1 master

es 中的路由机制是通过哈希算法,将具有相同哈希值的文档放到一个主分片中,分片位置的计算方式如下:

shard=hash(routing) % number_of_primary_shards

routing 可以是一个任意字符串,es 默认是将文档的 id 作为 routing 值,通过哈希函数根据 routing 生成一个数字,然后将该数字和分片数取余,取余的结果就是分片的位置。

默认的这种路由模式,最大的优势在于负载均衡,这种方式可以保证数据平均分配在不同的分片上。

但是他有一个很大的劣势,就是查询时候无法确定文档的位置,此时它会将请求广播到所有的分片上去执行。

另一方面,使用默认的路由模式,后期修改分片数量不方便。

当然开发者也可以自定义 routing 的值,方式如下:

PUT blog/_doc/d?routing=javaboy
{
  "title":"d"
}


如果文档在添加时指定了 routing,则查询、删除、更新时也需要指定 routing。

GET blog/_doc/d?routing=javaboy

自定义 routing 有可能会导致负载不均衡,这个还是要结合实际情况选择。

典型场景:

对于用户数据,我们可以将 userid 作为 routing,这样就能保证同一个用户的数据保存在同一个分片中,检索时,同样使用 userid 作为 routing,这样就可以精准的从某一个分片中获取数据。

锁和版本控制

悲观锁

很悲观,每一次去读取数据的时候,都认为别人可能会修改数据,所以屏蔽一切可能破坏数据完整性的操作。关系型数据库中,悲观锁使用较多,例如行锁、表锁等等。

乐观锁

很乐观,每次读取数据时,都认为别人不会修改数据,因此也不锁定数据,只有在提交数据时,才会检查数据完整性。这种方式可以省去锁的开销,进而提高吞吐量。

在 es 中,实际上使用的就是乐观锁。

es6.7之前

在 es6.7 之前,使用 version+version_type 来进行乐观并发控制。根据前面的介绍,文档每被修改一个,version 就会自增一次,es 通过 version 字段来确保所有的操作都有序进行。

version 分为内部版本控制和外部版本控制。

---------内部版本
es 自己维护的就是内部版本,当创建一个文档时,es 会给文档的版本赋值为 1。

每当用户修改一次文档,版本号就回自增 1。

如果使用内部版本,es 要求 version 参数的值必须和 es 文档中 version 的值相当,才能操作成功。

---------外部版本
也可以维护外部版本。

在添加文档时,就指定版本号:

PUT blog/_doc/1?version=200&version_type=external
{
  "title":"2222"
}


以后更新的时候,版本要大于已有的版本号。

vertion_type=external 或者 vertion_type=external_gt 表示以后更新的时候,版本要大于已有的版本号。

vertion_type=external_gte 表示以后更新的时候,版本要大于等于已有的版本号。

Es6.7 之后

现在使用 if_seq_no 和 if_primary_term 两个参数来做并发控制。

seq_no 不属于某一个文档,它是属于整个索引的(version 则是属于某一个文档的,每个文档的 version 互不影响)。

现在更新文档时,使用 seq_no 来做并发。由于 seq_no 是属于整个 index 的,所以任何文档的修改或者新增,seq_no 都会自增。

现在就可以通过 seq_no 和 primary_term 来做乐观并发控制。

PUT blog/_doc/2?if_seq_no=5&if_primary_term=1
{
  "title":"6666"
}