啤酒就辣条

啤酒就辣条 查看完整档案

石家庄编辑  |  填写毕业院校  |  填写所在公司/组织 pjjlt.cn 编辑
编辑

我的公众号:啤酒就辣条

个人动态

啤酒就辣条 发布了文章 · 11月25日

Elasticsearch操作实践手册|建议收藏篇

我是啤酒就辣条。

但行好事,莫问前程。

官方为我们提供了多种语言操作Elasticsearch的API,可以很方便的在项目中操作。学习利用原生请求操作Elasticsearch,方便维护数据库,还能加快学习使用不同语言的API。

本博客使用kibana发起请求,使用kibana可以查看快捷键。

索引操作

创建索引

创建索引使用PUT请求,后面跟上索引名称就好了,由于7.x默认type为_doc,所以后面不必跟上type了。在PUT简单请求同时,可以加上JSON请求体,进行复杂创建。

PUT /user
{
  "settings": {
    "index": {
      "number_of_shards": 3,  
      "number_of_replicas": 2 
    }
  },
  "mappings": {
    "properties": {
      "name": { "type": "text" },
      "age": {"type": "short"},
      "city":{"type": "keyword"}
    }
  }
}

创建索引user,可以通过参数setting设置分片和副本数,通过number_of_shards设置一共有3个分片,通过number_of_replicas给每个分片设置2个副本,默认不指定的话,这两个参数都是1。通过mappings规定文档各个Filed插入类型。此外,还可以设置aliases字段,给索引创建别名,这样不仅可以通过别名访问该索引,还可以定义查询条件、指定查询分片等等,详情请参考

删除索引

删除索引,使用DELETE请求。

DELETE /user

查看索引

查看索引,使用GET请求,可以查看索引设置的参数。

删除之后,我又重新添加索引,没有设置settings,所以分片和副本都是1。

你也可以通过在索引后面加参数来查看某一具体参数,如:

GET /user/_settings
GET /user/_mappings
GET /user/_aliases

修改索引部分设置

可以通过PUT请求修改部分索引的settings,例如,分片副本数量可以修改,但是分片数量不可以通过这种方式修改。

PUT /user/_settings
{
  "number_of_replicas": 3 
}

可以通过以上请求,修改分片副本。

文档操作

创建好索引,来看下文档的增删改查,这是日常业务用的最多的地方。

插入文档

新增文档使用PUTPOST请求。

PUT /<target>/_doc/<_id>

POST /<target>/_doc/

PUT /<target>/_create/<_id>

POST /<target>/_create/<_id>

target为索引名称,_doc为默认type。通过前两种请求可看出,id可以自行指定,也可以由ES自动生成。_create可以保证只有当文档不存在的时候进行插入,而_doc还可以用于文档更新。

POST /user/_doc/1
{
  "name":"pjjlt",
  "age":26,
  "city":"sjz"
}

更新文档

更新文档可以使用PUT或者POST请求关键字,全量更新。

POST /user/_doc/1
{
  "name":"pjjlt",
  "age":3,
  "city":"sjz"
}

还可以通过_update命令局部更新,所谓局部更新,是讲请求体内不需要加入全部字段,只加入需要修改的字段就好,其他字段不变。全量更新会替换整个文档。

POST /user/_update/1
{
  "doc":{
    "age": 26
  }
}

此外还可以通过脚本修改,例如将所有存在age字段的文档,其值改成5岁。

POST /user/_update_by_query
{
  "script": {
    "source": "ctx._source['age']=5" 
  },
  "query": {
    "bool": {
      "must": [
        {
          "exists": {
            "field": "age"
          }
        }
      ]
    }
  }
}

此外update_update_by_query字段还可以修改Filed,例如将name修改成name1,这块内容使用较少,如感兴趣,请参考官方文档。

删除文档

删除文档可以使用DELEETE请求,删除指定id的文档,也可以使用_delete_by_query,删除指定条件下的文档。

DELETE /user/_doc/1
POST /user/_delete_by_query
{
  "query": {
    "match": {
      "name": "pjjlt"
    }
  }
}

查找文档

查找文档是ES操作最精彩的部分,这里只介绍简单的查询,复杂查询、聚合查询后面会介绍。

根据id查询某文档。

GET /user/_doc/1

查找某索引下的全部文档

GET /user/_search?pretty

pretty参数在浏览器中才会发挥作用,格式化返回json的。以上这条命令默认返回10条数据,想返回更多数据可以添加size字段。

GET /user/_search?pretty&size=20

可以看出,数据部分在hits里面,Spring提供的elasticsearch客户端会有对应的实体类,在项目中很方便的使用。下面看下这几部分的含义。

元素含义
took运行查询需要多长时间
time_out请求是否超时
_shards搜索了多少分片,多少成功
多少跳过,多少失败
hits.total总共有多少数据
hits._max_score查询到的文档中,关联度最大文档分数
hits.hits._source查询到的数据
hits.hits.id某文档的主键

批量操作

批量操作是指,一批命令同时执行(减少IO),这些命令不一定是同种类型。

使用_bulk命令可以进行文档的批量增删改。

POST _bulk
{ "update" : { "_index" : "user", "_id" : "1" } }
{ "doc" : {"age" : 18} }
{ "create" : { "_index" : "user", "_id" : "2" } }
{ "name" : "小明","age":32,"city":"beijing" }
{ "create" : { "_index" : "user", "_id" : "3" } }
{ "name" : "小红","age":21,"city":"sjz" }
{ "create" : { "_index" : "user", "_id" : "4" } }
{ "name" : "mark","age":22,"city":"tianjin" }
{ "delete" : { "_index" : "user", "_id" : "4" } }

以上命令更新了id为1文档的年龄,新增id为2、3、4的文档,再删除id为4的文档。

上边的命令堆在一起不方便看,下面单独写看,方便读者查看。

批量新增

POST _bulk
{ "create" : { "_index" : "user", "_id" : "2" } }
{ "name" : "小明","age":32,"city":"beijing" }
{ "create" : { "_index" : "user", "_id" : "3" } }
{ "name" : "小红","age":21,"city":"sjz" }

{ "index" : { "_index" : "user", "_id" : "4" } }
{ "name" : "mark","age":22,"city":"tianjin" }

create命令是只有文档不存在,才会插入,index会判断如果存在就更新,不存在就插入。

批量更新

POST _bulk
{ "update" : { "_index" : "user", "_id" : "1" } }
{ "doc" : {"age" : 18} }
{ "update" : { "_index" : "user", "_id" : "2" } }
{ "doc" : {"age" : 20} }

和新增一样,update命令下一行需要紧跟这data数据。

批量删除

POST _bulk
{ "delete" : { "_index" : "user", "_id" : "3" } }
{ "delete" : { "_index" : "user", "_id" : "4" } }

批量查找

批量查找,使用_mget关键字,批量查找如果不跨越索引,也具有简写形式。

GET /_mget
{
  "docs": [
    {
      "_index": "user",
      "_id": "1"
    },
    {
      "_index": "user",
      "_id": "2"
    }
  ]
}

# 还可以简写形式
POST /user/_mget
{
  "ids": [
    "1",
    "2",
    "3"
  ]
}

DSL查询

Elasticsearch提供了一个完整的基于JSON的查询DSL(领域特定语言)来定义查询。可以将查询DSL看作查询的AST(抽象语法树),它由两种类型的子句组成:Leaf query clauses(叶查询子句)和Compound query clauses(复合查询子句)

以上摘自官网,简单来说,DSL就是将查询条件放到JSON中,进行查询。

Leaf query clauses在特定字段上查找特定的值,例如matchtermrange查询等等。

Compound query clauses将叶查询子句和其他符合查询子句结合起来,例如bool查询等等。

match

match是一个标准查询,当查询一个文本的时候,会先将文本分词。当查询确切值的时候,会搜索给定的值,例如数字、日期、布尔或者被not_analyzed的字符串。
GET /user/_search
{
  "query": {
    "match": {
      "name":"小明"
    }
  }
}

上面的操作会先将“小明”分词为“小”、“明”(当然具体还要看你的分词器),然后再去所有文档中查找与之相匹配的文档,并根据关联度排序返回。

match_phrase

match_phrase会保留空格,match会把空格忽略。
GET /user/_search
{
  "query": {
    "match_phrase": {
      "name":"小 明"
    }
  }
}

注意,分词是空格会给前一个元素,比如上面的字符串分子之后是,“小 ”,“明”。

multi_match

多字段查询,一个查询条件,看所有多个字段是否有与之匹配的字段。后面我们也可以使用should更加灵活。
GET /user/_search
{
  "query": {
    "multi_match": {
      "query":    "哈哈", 
      "fields": [ "name","city" ] 
    }
  }
}

match_all

匹配所有,并可设置这些文档的_score,默认_score为1,辣条君认为这里没有计算_score,所以速度会快很多。
GET /user/_search
{
  "query": {
    "match_all": { "boost" : 1.2 }
  }
}

boost参数可以省略,默认是1。

term

term是一种完全匹配,主要用于精确查找,例如数字、ID、邮件地址等。
GET /user/_search
{
  "query": {
    "term": {
      "age": 18
    }
  }
}

terms

terms是term多条件查询,参数可以传递多个,以数组的形式表示。
GET /user/_search
{
  "query": {
    "terms": {
      "age":[18,21]
    }
  }
}

wildcard

通配符,看示例容易理解,通配符可以解决分词匹配不到的问题,例如'haha' 可以通过'*a'匹配。
GET /user/_search
{
  "query": {
    "wildcard": {
      "name":"*a"
    }
  }
}

exists

查看某文档是否有某属性,返回包含这个Filed的文档。
GET /user/_search
{
  "query": {
    "exists": {
      "field": "name"
    }
  }
}

fuzzy

返回与查询条件相同或者相似的匹配内容。
GET /user/_search
{
  "query": {
    "fuzzy": {
      "name":"mjjlt"
    }
  }
}

搜索条件是mjjlt,可以搜出来name为pjjlt的文档。这个操作是不是在百度的时候经常见到呢?

ids

多id查询,这个id是主键id,即你规定或者自动生成那个。
GET /user/_search
{
  "query": {
    "ids": {
      "values":[1,2,3]
    }
  }
}

prefix

前缀匹配
GET /user/_search
{
  "query": {
    "prefix": {
      "name":"pj"
    }
  }
}

range

范围匹配。参数可以是 gt(大于)、gte(大于等于)、lt(小于)、lte(小于等于)
GET /user/_search
{
  "query": {
    "range": {
      "age":{
          "gt":1,
          "lt":30
      }
    }
  }
}

regexp

正则匹配。value是正则表达式,flags是匹配格式,默认是ALL,开启所有。更多格式请戳
GET /user/_search
{
  "query": {
    "regexp": {
      "name":{
          "value": "p.*t",
          "flags": "ALL"
      }
    }
  }
}

bool

bool 可以用来组合其他子查询。其中常包含的子查询包含:must、filter、should、must_not

must

must内部的条件必须包含,内部条件是and的关系。如查看所有name中包含“小”并且age是32的用户文档。

GET /user/_search
{
  "query": {
    "bool" : {
      "must": [
        {"term" : { "name" : "小" }},
        {"term" : { "age" : 32 }}
      ]
    }
  }
}

filter

filter是文档通过一些条件过滤下,这是四个关键词中唯一和关联度无关的,不会计算_score,经常使用的过滤器会产生缓存。

GET /user/_search
{
  "query": {
    "bool" : {
      "filter": {
        "term" : { "name" : "小" }
      }
    }
  }
}

对比两张图可以看出,filter并没有计算_score,搜索速度较快。

must_not

这个和must相反,文档某字段中一定不能包含某个值,相当于“非”。

should

should可以看做or的关系,例如下面查询name包含"小"或者年龄是18岁的用户。

GET /user/_search
{
  "query": {
    "bool" : {
      "should": [
        {"term" : { "name" : "小" }},
        {"term" : { "age" : 18 }}
      ]
    }
  }
}

聚合查询

Elasticsearch除全文检索功能外提供的针对Elasticsearch数据做统计分析的功能。可以查询某组数据的最大最小值,分组查询某些数据。

  • Metric(指标): 指标分析类型,如计算最大值、最小值、平均值等等 (对桶内的文档进行聚合分析的操作)
  • Bucket(桶): 分桶类型,类似SQL中的GROUP BY语法 (满足特定条件的文档的集合)
  • Pipeline(管道): 管道分析类型,基于上一级的聚合分析结果进行在分析

Metric(指标)数据

常用数学操作

这里常用的数学操作有min(最小)、max(最大)、sum(和)、avg(平均数)。注意这些操作只能输出一个分析结果。使用方式大同小异。
GET /user/_search
{
    "aggs" : {
        "avg_user_age" : 
        { 
          "avg" : { "field" : "age" } 
        }
    }
}

以上示例查询所有用户的平均年龄,返回所有文档和聚合查询结果。aggs是聚合查询关键词,avg_user_age是查询名称,用户可以自行定义。

cardinality

计算某字段去重后的数量
GET /user/_search
{
    "aggs" : {
        "avg_user" : 
        { 
          "cardinality" : { "field" : "age" } 
        }
    }
}

可以计算,所有文档中年龄不相同的文档个数。

percentiles

对指定字段的值按从小到大累计每个值对应的文档数的占比,返回指定占比比例对应的值。

默认统计百分比为[ 1, 5, 25, 50, 75, 95, 99 ]

GET /user/_search
{
    "aggs" : {
        "avg_user" : 
        { 
          "percentiles": { "field" : "age" } 
        }
    }
}

# 返回值(省略文档部分,只分析结果部分)
  "aggregations" : {
    "avg_user" : {
      "values" : {
        "1.0" : 12.0,
        "5.0" : 12.0,
        "25.0" : 20.25,
        "50.0" : 29.0,
        "75.0" : 57.75,
        "95.0" : 123.0,
        "99.0" : 123.0
      }
    }
  }

可以看出,前1%的用户小于12岁,5%的用户小于12岁,25%的用户小于20.25岁,50%的用户小于29岁。。。

percentile_ranks

percentiles是通过百分比求出文档某字段,percentile_ranks是给定文档中的某字段求百分比。
GET /user/_search
{
    "aggs" : {
        "avg_user" : 
        { 
          "percentile_ranks": 
            { 
              "field" : "age",
              "values" : [18, 30]
            } 
        }
    }
}

# 返回值(省略文档部分,只分析结果部分)
  "aggregations" : {
    "avg_user" : {
      "values" : {
        "18.0" : 18.51851851851852,
        "30.0" : 54.44444444444445
      }
    }
  }

可以看出,小于等于18岁的用户有18.52%,小于等于30岁的用户有54.4%。

top_hits

top_hits可以得到某条件下top n的文档。
GET /user/_search
{
  "aggs": {
    "avg_user" : {
      "top_hits": {
          "sort": [
          {
            "age": {
              "order": "asc"
            }
          }
        ],
        "size": 1
      }
    }
  },
  "size": 0
}

取年龄最小的那一个。

Bucket(桶)

类似于分组的概念。

terms

根据给定的filed分组,返回每组多少文档。
GET /user/_search
{
    "aggs" : {
        "avg_user" : 
        { 
          "terms": {
                  "field": "city"
                }
        }
    }
}

以上根据城市分组,看每个城市有多少用户。

ranges

根据区间分组
GET /user/_search
{
  "aggs": {
    "price_ranges": {
      "range": {
        "field": "age",
        "ranges": [
          { "to": 20 },
          { "from": 20, "to": 30 },
          { "from": 30 }
        ]
      }
    }
  }
}

可以查看每个年龄层的用户数量。

还有些很有趣的指令,例如[IP range]可以根据ip段区间分组,以后用到再说吧,希望文本可以不段更新的说...

查看原文

赞 1 收藏 1 评论 0

啤酒就辣条 发布了文章 · 11月25日

Elasticsearch基本概念和索引原理

我是啤酒就辣条。

但行好事,莫问前程。

Elasticsearch是什么?

Elasticsearch是一个基于文档的NoSQL数据库,是一个分布式RESTful风格的搜索和数据分析引擎,同时也是Elastic Stack的核心,集中存储数据。Elasticsearch、Logstash、Kibana经常被用作日志分析系统,俗称ELK。

说白了,就是一个数据库,搜索贼快(但是插入更新较慢,要不然其他数据库别玩了)。速度快,还可以进行分词,非常适合做搜索,例如商城的商品搜索。为什么快,后面讲原理的时候会说,不单单是缓存的问题,原理非常精彩。而且它是nosql的,数据格式可以随便造。Elasticsearch还为我们提供了丰富的RESTful风格的API,写代码的成本极低。最后它支持分布式,高性能(搜索快),高可用(某些节点宕机可以接着用),可伸缩(可以方便的增加节点,解决物理内存上线问题),适合分布式系统开发。

Elasticsearch基本概念

为了快速了解Elasticsearch(后面可能会简称为ES),可以与mysql几个概念做个对比。

ElasticsearchMysql
字段(Filed)属性(列)
文档(Document)记录(行)
类型(Type)
索引(Index)数据库

是不是清楚多了?我们说Elasticsearch是基于文档的,就是因为记录元素(被搜索的最小单位)是文档。例如下面就是一个文档,

{
    "email":      "john@smith.com",
    "first_name": "John",
    "last_name":  "Smith",
    "info": {
        "bio":         "Eco-warrior and defender of the weak",
        "age":         25,
        "interests": [ "dolphins", "whales" ]
    },
    "join_date": "2014/05/01"
}

文档格式看起来很像Json吧。emailfirst_name等等就是Filed。由于结构是Json,所以value值就很方便放任意类型,这就是nosql的好处。

文档(Document)

ES中的一个对象将来会和Java代码中的一个对象对应。文档的每一个Filed可以是任意类型,但是一旦某索引(Index)(我们描述的时候,略过Type,但是Type依然存在)中插入了一个文档,某Filed被第一次使用,ES就会设置好此Filed的类型。例如你插入user的name是字符串类型,以后再插入文档,name字段必须是字符串类型。所以,建议在插入文档之前,先设置好每个Filed的类型。

如果插入文档的时候,不指定id,ES会帮助我们自动生成一个id,建议id是数字类型,这样搜索会快速很多。商城系统中的商品id建议使用雪花算法生成,这样既避免了自增id的安全性问题,又解决了字符串id检索慢的问题。

类型(Type)

关于Type,类型概念,在6.x版本中,一个索引(Index)可以拥有多个Type。在7.x版本(目前最新版本),一个索引只能拥有一个Type,默认的type就是_doc,在7.x版本中,已经建议删除了。在未来的8.x版本会彻底删除。但是在7.x版本中,一个文档还必须归属于一个类型。

索引(Index)

都说ES中的索引类似于mysql中的数据库,我觉得未来索引有成为mysql中概念的潜质。我们把相同特征(Filed数量和类型基本相同)的文档放到同一个索引(index)里面。这样方便提前通过mapping来规定各个Filed的类型。另外,索引名称必须全部小写,所以不建议写成驼峰式。

节点(Node)与分片(Shard)

在这里插入图片描述

由于生产环境下ES基本都是集群部署的,所以一定少不了节点的概念,一个节点就是一个ES实例,就是一个Java进程,这些Java进程部署在不同的服务器上,增加ES可用性。

ES节点根据功能可以分为三种:

  1. 主节点:职责是和集群操作相关的内容,如创建或删除索引,跟踪哪些节点是群集的一部分,并决定哪些分片分配给相关的节点。每个节点都可访问集群的状态,但是只有主节点可以修改集群的状态。
  2. 数据节点:数据节点主要是储存数据的节点,对文档进行增删改查,聚合操作等等,数据节点对cpu,内存,io要求较高,当资源不够的时候,可以增加新的节点,很方便的进行数据拓展。
  3. 客户端节点:本节点主要处理路由请求,分发索引的操作。实际上主节点和数据节点也有路由转发的功能,但是为了提高效率,还是建议生产环境单独创建客户端节点。

分片类似于mysql中的分表,在一个索引拆分成几个小索引,分布在不同的节点(不同服务器)上,每个小索引都具有完备的功能,当客户端发来请求的时候,客户端节点找到合适的分片上的小索引,进行数据查询,这一过程对于用户来说都是透明的,用户表面上看只是在操作一个索引。利用分片,可以避免单个节点的物理限制,还可以增加吞吐量。建议最开始一个索引要用多少分片设计好,因为修改分片数量是个相当麻烦的过程。

作为分布式的数据库,ES必须为咱们提供数据冗余功能,这就是分片副本,就是将某个分片copy一份放到其他节点上。注意,这里分片和分片副本必须在不同的节点上!分片副本也可以提高吞吐量。分片副本不同于分片,可以很方便的进行修改。

说完了所有概念,再去看本节最开始那张图,有一个索引,分了3分片在三个节点上,并且每个分片在不同的节点上有分片副本。

Elasticsearch索引原理

看完上面的内容,你对Elasticsearch有了基本的认识,再去看基本操作(我后面要写一篇基操博客),就可以在项目中使用Elasticsearch了。此刻你可以喘口气,以放松的心态看后面的内容。下面我们就讲讲索引为什么快?

首先,我们知道mysql底层数据结构使用的是B+Tree,这种BTree,将搜索时间复杂度变成了logN,已经很快了,我们Elasticsearch要比它还快。Elasticsearch是怎么做的呢?首先储存结构要优化,然后再提高下和磁盘的交互效率。

先说Elasticsearch索引结构,叫做倒排索引,啥是倒排索引呢?它的大概逻辑如下:

在这里插入图片描述

为了讲清楚这个概念,我们先看个例子,如下为我们user的数据:

IDNameAge
1Kate24
2John24
3Bill29
4Kate26
5Brand29

Elasticsearch会为以上数据建立两个索引树:

TermPosting List
Kate1,4
Brand5
John2
Bill3
TermPosting List
241,2
264
293,5

以上的索引树就叫做倒排索引,每个Filed字段对应着一组Term,每个Term后面跟着的id(还记着吗,这个主键用户不指定就会自动生成,所以一定存在)就是Posting List,它是一组id,有了id再去磁盘中对应的文档就so fast了。

你有没有发现,Term如果按序找会快点,将Term按序排,在进行二分查找,是不是速度就跟BTree一样了,时间复杂度为LogN。这个有序的Term组就是Term Dictionary

那么问题又来了,比如说数据库中有name前缀为A的同学1000万个,前缀为Z的同学有3个,我要查前缀为Z的同学,那二分查找不也很多次吗,所以,Elasticsearch把每个开头的地方标记一下,拿出来,再放到一颗树里,速度不是就快了嘛。这棵树就是Term IndexTerm Index前缀不一定是第一个字符,比如A、Ab、Abz,这种都可以在Term Index树里。并且Term Dictionary可能会太大,会被放到磁盘中,避免内存占用太多。

再看下面这张结构图是不是清楚多了。

在这里插入图片描述

由于Term Index被放到内存中,所以最好压缩一下,减少内存使用,压缩使用的是FST,这个东西讲起来比较复杂,反正就是能压缩,内存变小就好了。

Term压缩完了,那么Posting List是不是也可以压缩一下,省省空间啊?既然都是id,使用过redis的同学瞬间会想到bitMap,就是有个巨大的数组,储存着0或1,有就是1,没有就是0。例如上面的3、5放在BitMap中就是 1,0,1,0,0,0。虽说空间已经明显小多了,但是如果一个Posting List只储存着1,10000001这两个id,最后产生的数字是不是过大呢。于是乎,Roaring bitmaps就出来了,进行了一次指数降级,简单点说就是取商和余数储存,被除数是65535。例如 1000,62101,131385,196658, 这几个id,首先分组,分组规则就是商一样,例如上面id可分组为[(0,1000),(0,62101)],[],[(2,6915)],[(3,53)]。注意,没有商为1的值,我用空数组表示。此时,将某个组中的数字放到一个bitmap中。

查看原文

赞 0 收藏 0 评论 0

啤酒就辣条 提出了问题 · 11月25日

Elasticsearch基本概念和索引原理

我是啤酒就辣条。

但行好事,莫问前程。

Elasticsearch是什么?

Elasticsearch是一个基于文档的NoSQL数据库,是一个分布式RESTful风格的搜索和数据分析引擎,同时也是Elastic Stack的核心,集中存储数据。Elasticsearch、Logstash、Kibana经常被用作日志分析系统,俗称ELK。

说白了,就是一个数据库,搜索贼快(但是插入更新较慢,要不然其他数据库别玩了)。速度快,还可以进行分词,非常适合做搜索,例如商城的商品搜索。为什么快,后面讲原理的时候会说,不单单是缓存的问题,原理非常精彩。而且它是nosql的,数据格式可以随便造。Elasticsearch还为我们提供了丰富的RESTful风格的API,写代码的成本极低。最后它支持分布式,高性能(搜索快),高可用(某些节点宕机可以接着用),可伸缩(可以方便的增加节点,解决物理内存上线问题),适合分布式系统开发。

Elasticsearch基本概念

为了快速了解Elasticsearch(后面可能会简称为ES),可以与mysql几个概念做个对比。

ElasticsearchMysql
字段(Filed)属性(列)
文档(Document)记录(行)
类型(Type)
索引(Index)数据库

是不是清楚多了?我们说Elasticsearch是基于文档的,就是因为记录元素(被搜索的最小单位)是文档。例如下面就是一个文档,

{
    "email":      "john@smith.com",
    "first_name": "John",
    "last_name":  "Smith",
    "info": {
        "bio":         "Eco-warrior and defender of the weak",
        "age":         25,
        "interests": [ "dolphins", "whales" ]
    },
    "join_date": "2014/05/01"
}

文档格式看起来很像Json吧。emailfirst_name等等就是Filed。由于结构是Json,所以value值就很方便放任意类型,这就是nosql的好处。

文档(Document)

ES中的一个对象将来会和Java代码中的一个对象对应。文档的每一个Filed可以是任意类型,但是一旦某索引(Index)(我们描述的时候,略过Type,但是Type依然存在)中插入了一个文档,某Filed被第一次使用,ES就会设置好此Filed的类型。例如你插入user的name是字符串类型,以后再插入文档,name字段必须是字符串类型。所以,建议在插入文档之前,先设置好每个Filed的类型。

如果插入文档的时候,不指定id,ES会帮助我们自动生成一个id,建议id是数字类型,这样搜索会快速很多。商城系统中的商品id建议使用雪花算法生成,这样既避免了自增id的安全性问题,又解决了字符串id检索慢的问题。

类型(Type)

关于Type,类型概念,在6.x版本中,一个索引(Index)可以拥有多个Type。在7.x版本(目前最新版本),一个索引只能拥有一个Type,默认的type就是_doc,在7.x版本中,已经建议删除了。在未来的8.x版本会彻底删除。但是在7.x版本中,一个文档还必须归属于一个类型。

索引(Index)

都说ES中的索引类似于mysql中的数据库,我觉得未来索引有成为mysql中概念的潜质。我们把相同特征(Filed数量和类型基本相同)的文档放到同一个索引(index)里面。这样方便提前通过mapping来规定各个Filed的类型。另外,索引名称必须全部小写,所以不建议写成驼峰式。

节点(Node)与分片(Shard)

在这里插入图片描述

由于生产环境下ES基本都是集群部署的,所以一定少不了节点的概念,一个节点就是一个ES实例,就是一个Java进程,这些Java进程部署在不同的服务器上,增加ES可用性。

ES节点根据功能可以分为三种:

  1. 主节点:职责是和集群操作相关的内容,如创建或删除索引,跟踪哪些节点是群集的一部分,并决定哪些分片分配给相关的节点。每个节点都可访问集群的状态,但是只有主节点可以修改集群的状态。
  2. 数据节点:数据节点主要是储存数据的节点,对文档进行增删改查,聚合操作等等,数据节点对cpu,内存,io要求较高,当资源不够的时候,可以增加新的节点,很方便的进行数据拓展。
  3. 客户端节点:本节点主要处理路由请求,分发索引的操作。实际上主节点和数据节点也有路由转发的功能,但是为了提高效率,还是建议生产环境单独创建客户端节点。

分片类似于mysql中的分表,在一个索引拆分成几个小索引,分布在不同的节点(不同服务器)上,每个小索引都具有完备的功能,当客户端发来请求的时候,客户端节点找到合适的分片上的小索引,进行数据查询,这一过程对于用户来说都是透明的,用户表面上看只是在操作一个索引。利用分片,可以避免单个节点的物理限制,还可以增加吞吐量。建议最开始一个索引要用多少分片设计好,因为修改分片数量是个相当麻烦的过程。

作为分布式的数据库,ES必须为咱们提供数据冗余功能,这就是分片副本,就是将某个分片copy一份放到其他节点上。注意,这里分片和分片副本必须在不同的节点上!分片副本也可以提高吞吐量。分片副本不同于分片,可以很方便的进行修改。

说完了所有概念,再去看本节最开始那张图,有一个索引,分了3分片在三个节点上,并且每个分片在不同的节点上有分片副本。

Elasticsearch索引原理

看完上面的内容,你对Elasticsearch有了基本的认识,再去看基本操作(我后面要写一篇基操博客),就可以在项目中使用Elasticsearch了。此刻你可以喘口气,以放松的心态看后面的内容。下面我们就讲讲索引为什么快?

首先,我们知道mysql底层数据结构使用的是B+Tree,这种BTree,将搜索时间复杂度变成了logN,已经很快了,我们Elasticsearch要比它还快。Elasticsearch是怎么做的呢?首先储存结构要优化,然后再提高下和磁盘的交互效率。

先说Elasticsearch索引结构,叫做倒排索引,啥是倒排索引呢?它的大概逻辑如下:

在这里插入图片描述

为了讲清楚这个概念,我们先看个例子,如下为我们user的数据:

IDNameAge
1Kate24
2John24
3Bill29
4Kate26
5Brand29

Elasticsearch会为以上数据建立两个索引树:

TermPosting List
Kate1,4
Brand5
John2
Bill3
TermPosting List
241,2
264
293,5

以上的索引树就叫做倒排索引,每个Filed字段对应着一组Term,每个Term后面跟着的id(还记着吗,这个主键用户不指定就会自动生成,所以一定存在)就是Posting List,它是一组id,有了id再去磁盘中对应的文档就so fast了。

你有没有发现,Term如果按序找会快点,将Term按序排,在进行二分查找,是不是速度就跟BTree一样了,时间复杂度为LogN。这个有序的Term组就是Term Dictionary

那么问题又来了,比如说数据库中有name前缀为A的同学1000万个,前缀为Z的同学有3个,我要查前缀为Z的同学,那二分查找不也很多次吗,所以,Elasticsearch把每个开头的地方标记一下,拿出来,再放到一颗树里,速度不是就快了嘛。这棵树就是Term IndexTerm Index前缀不一定是第一个字符,比如A、Ab、Abz,这种都可以在Term Index树里。并且Term Dictionary可能会太大,会被放到磁盘中,避免内存占用太多。

再看下面这张结构图是不是清楚多了。

在这里插入图片描述

由于Term Index被放到内存中,所以最好压缩一下,减少内存使用,压缩使用的是FST,这个东西讲起来比较复杂,反正就是能压缩,内存变小就好了。

Term压缩完了,那么Posting List是不是也可以压缩一下,省省空间啊?既然都是id,使用过redis的同学瞬间会想到bitMap,就是有个巨大的数组,储存着0或1,有就是1,没有就是0。例如上面的3、5放在BitMap中就是 1,0,1,0,0,0。虽说空间已经明显小多了,但是如果一个Posting List只储存着1,10000001这两个id,最后产生的数字是不是过大呢。于是乎,Roaring bitmaps就出来了,进行了一次指数降级,简单点说就是取商和余数储存,被除数是65535。例如 1000,62101,131385,196658, 这几个id,首先分组,分组规则就是商一样,例如上面id可分组为[(0,1000),(0,62101)],[],[(2,6915)],[(3,53)]。注意,没有商为1的值,我用空数组表示。此时,将某个组中的数字放到一个bitmap中。

关注 1 回答 0

啤酒就辣条 发布了文章 · 11月22日

【那些年我们用过的Redis】还记得大明湖畔那些Redis数据吗?

redis五种常用的数据结构为string (字符串)、list (列表)、set (集合)、hash (哈希) 和 zset (有序集合)。小白易读,建议收藏。

万丈高楼平地起

reids是键值对结构的NoSql数据库,key都是字符串,常说的数据类型不同,说的都是value
图片来自:https://searchdatabase.techtarget.com.cn/7-20218/
redis所有的数据都会有一个dicEntry,众多dicEntry组成一个链表。上方那个sds就是key,可以看出是一个字符串。下方那个绿色的redisObject就是value。可以看出图中给的例子就是string类型。redisObject会指向真实的数据(比如图中的字符串“world”)。后面我们说的数据类型特指value部分。

string (字符串)

Redis 的字符串是动态字符串,是可以修改的字符串。当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。一个字符串最大可以承受512M。

常用指令

设置获取值

127.0.0.1:6379> set name pjjlt
OK
127.0.0.1:6379> get name
"pjjlt"
127.0.0.1:6379> exists name
(integer) 1

设置使用set,获取使用get,查看某key是否存在用exists

设置过期时间

127.0.0.1:6379> setex company 10  gongsi
OK
127.0.0.1:6379> get company
"gongsi"
127.0.0.1:6379> get company
(nil)

可以在设置值的时候直接指定,keycompany可以存活10秒。此外,也可以将设置值设置过期时间分开,使用expire

127.0.0.1:6379> set company gongsi
OK
127.0.0.1:6379> expire company 10
(integer) 1
127.0.0.1:6379> get company
"gongsi"
127.0.0.1:6379> get company
(nil)

保证不覆盖value

redis还提供了命令,在设置值的时候,如果发现key已存在,此次设置失败,保证原始value不被覆盖。使用setnx命令。

127.0.0.1:6379> setnx company gongsi
(integer) 1
# 可以看到第二次设置失败,返回值为 0.
127.0.0.1:6379> setnx company haha
(integer) 0
127.0.0.1:6379> get company
"gongsi"

批量设置获取值

127.0.0.1:6379> mset name pjjlt age 26 company gongsi
OK
127.0.0.1:6379> mget name age company
1) "pjjlt"
2) "26"
3) "gongsi"

批量设置使用mset,批量获取使用mget。批量设置获取,减少IO,提高性能,你值得拥有。

计数

redis还可以通过自增的方式计数。

127.0.0.1:6379> set key 10
OK
127.0.0.1:6379> incr key
(integer) 11
127.0.0.1:6379> incr key
(integer) 12
# 字符串报错
127.0.0.1:6379> set key2 haha
OK
127.0.0.1:6379> incr key2
(error) ERR value is not an integer or out of range
# 超出long的范围
127.0.0.1:6379> set key3 9223372036854775807
OK
127.0.0.1:6379> incr key3
(error) ERR increment or decrement would overflow
# key4不存在
127.0.0.1:6379> incr key4
(integer) 1
127.0.0.1:6379> incr key4
(integer) 2

可以通过incr关键字自增,可以看出key自增了两次。不能给字符串自增,那样会报错,例如key2。不能超过long的范围,那样也会报错,例如key3。如果初始key不存在,则增从0开始,例如key4。

追加值

127.0.0.1:6379> set name pj
OK
127.0.0.1:6379> append name jlt
(integer) 5
127.0.0.1:6379> get name
"pjjlt"

字符串长度

127.0.0.1:6379> get name
"pjjlt"
127.0.0.1:6379> strlen name
(integer) 5

设置并返回原先值

127.0.0.1:6379> get name
"pjjlt"
127.0.0.1:6379> getset name mj
"pjjlt"
127.0.0.1:6379> get name
"mj"

设置指定位置的字符

127.0.0.1:6379> get name
"mj"
127.0.0.1:6379> setrange name 0 p
(integer) 2
127.0.0.1:6379> get name
"pj"

获取部分字符串

127.0.0.1:6379> set name pjjlt
OK
127.0.0.1:6379> getrange name 0 2
"pjj"

总结

命令解释
set设置值
get获取值
setex设置值并添加过期时间
setnx保证不覆盖value
mset批量设置值
mget批量获取值
incr计数
append追加值
strlen字符串长度
getset设置并返回原先值
setrange设置指定位置的字符
getrange获取部分字符串

内部编码

虽然某种数据类型的value名称是一致的,比如都是string,但是根据数据量的大小,会采用不同的内部编码,这样可以更高效的利用空间嘛。内部编码类型也储存在redisObject中。利用object encoding key可查看内部编码类型。

int:长整型,超越长整型或者是字符串会升级。

embstr:小于等于44个字节的字符串。笔者用的是redis5.0.9,有人说这个字节范围是39,亲测是44。查了一下,源码确实改了,现在是44.

raw:大于44个字节的字符串。

127.0.0.1:6379> set name 1234567890
OK
127.0.0.1:6379> object encoding name
"int"
# 这里设置44个字符
127.0.0.1:6379> set name qwertyuiopqwertyuiopqwertyuiopqwertyuiopqwer
OK
127.0.0.1:6379> object encoding name
"embstr"
# 这里设置45个字符
127.0.0.1:6379> set name qwertyuiopqwertyuiopqwertyuiopqwertyuiopqwert
OK
127.0.0.1:6379> object encoding name
"raw"

使用场景

可以用于计数,比如网站访问量。
可以共享Session,比如分布式系统,多个实例验证用户是否登录。
可以限速,比如控制一个ip或者一个用户一定时间内访问次数。

list (列表)

Redis 的列表相当于 Java 语言里面的 LinkedList,注意它是链表而不是数组。这意味着list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为O(n)。list的两端都可以弹入弹出数据,所以可以做队列

栈与队列

栈如同一个死胡同,只有一个出口,后进来的先出,先进来的后出。

127.0.0.1:6379> rpush books python java golong
(integer) 3
127.0.0.1:6379> rpop books
"golong"
127.0.0.1:6379> rpop books
"java"
127.0.0.1:6379> rpop books
"python"
127.0.0.1:6379> rpop books
(nil)

数据从右边进(rpush),右边出(rpop),先进去的最后出来。

队列

队列如同排队打饭的同学们,先进先出。

127.0.0.1:6379> rpush books python java golong 
(integer) 3
127.0.0.1:6379> lpop books
"python"
127.0.0.1:6379> lpop books
"java"
127.0.0.1:6379> lpop books
"golong"
127.0.0.1:6379> lpop books
(nil)

数据从右边进(rpush),左边出(lpop),先进先出。

常用命令

向队列任意位置加入元素

刚才演示的rpushlpush都是从两头加入元素,这两个命令不再演示。还可以使用linsert在某指定元素前或后插入新的元素。

127.0.0.1:6379> rpush books python java golong
(integer) 3
# 在java前面插入 ruby
127.0.0.1:6379> linsert books before java ruby
(integer) 4
# 在java后面插入 c#
127.0.0.1:6379> linsert books after java c#
(integer) 5
# 查看所有元素
127.0.0.1:6379> lrange books 0 -1
1) "python"
2) "ruby"
3) "java"
4) "c#"
5) "golong"

根据上面在java前后插入了rubyc#

查找元素

127.0.0.1:6379> lrange books 0 -1
1) "python"
2) "ruby"
3) "java"
4) "c#"
5) "golong"
127.0.0.1:6379> lindex books 2
"java"
127.0.0.1:6379> llen books
(integer) 5

指令简单,索性写一块吧。
lrange可以遍历列表,参数为startend。这里0 -1,是指从第一个到最后一个,即遍历列表。
lindex查找指定位置的元素,参数是下标值。这个命令是慢查询,需要遍历链表。
llen可以查看列表元素个数。

删除数据

刚才演示的rpoplpop可以弹出一个元素,不再演示。

可以使用lrem 删除多个同一元素
count > 0:从左到右,删除最多 count 个元素。
count < 0:从右到左,删除最多 count 绝对值 个元素。
count = 0,删除所有。

# 从左删除a元素,删除了3个
127.0.0.1:6379> rpush char a a b b a a c 
(integer) 7
127.0.0.1:6379> lrem chae 3 a
(integer) 0
127.0.0.1:6379> lrem char 3 a
(integer) 3
127.0.0.1:6379> lrange char 0 -1
1) "b"
2) "b"
3) "a"
4) "c"
# 从右删除 3 个a元素
127.0.0.1:6379> rpush char1 a a b b a a c 
(integer) 7
127.0.0.1:6379> lrem char1 -3 a
(integer) 3
127.0.0.1:6379> lrange char1 0 -1
1) "a"
2) "b"
3) "b"
4) "c"
# 删除所有 a 元素
127.0.0.1:6379> rpush char2 a a b b a a c
(integer) 7
127.0.0.1:6379> lrem char2 0 a
(integer) 4
127.0.0.1:6379> lrange char2 0 -1
1) "b"
2) "b"
3) "c"

还可以使用ltrim截取一部分数据,删除其余数据

127.0.0.1:6379> rpush char3 a b c d e f g
(integer) 7
127.0.0.1:6379> ltrim char3 1 3
OK
127.0.0.1:6379> lrange char3 0 -1
1) "b"
2) "c"
3) "d"

修改

127.0.0.1:6379> lrange books 0 -1
1) "python"
2) "ruby"
3) "java"
4) "c#"
5) "golong"
127.0.0.1:6379> lset books 2 javaScript
OK
127.0.0.1:6379> lrange books 0 -1
1) "python"
2) "ruby"
3) "javaScript"
4) "c#"
5) "golong"

可以用lset更改某个位置上的元素,这也是个慢查询,时间复杂度为O(n)。

阻塞操作

blpopbrpoplpoprpop基础上增加了阻塞时间,如果直接获取,发现列表中没有数据,那么会阻塞等待一段时间,如果该段时间内还是无法得到数据,就返回等待时长。若设置的时间是0的话,即为无限等待。这里需要两个终端做配合。

# 终端1 
127.0.0.1:6379> lpop books
(nil)
127.0.0.1:6379> blpop books 5
(nil)
(5.06s)
# 这里需要在终端1 执行blpop后插入一条数据
127.0.0.1:6379> blpop books 20
1) "books"
2) "java"
(4.61s)
# 这里需要在终端1 执行blpop后插入一条数据
127.0.0.1:6379> blpop books 0
1) "books"
2) "python"
(9.66s)
# 除此之外,还可以同时阻塞多个队列,先有数据的那个弹出
127.0.0.1:6379> blpop books schools 0
1) "schools"
2) "hzsy"
(26.75s)

总结

命令解释
rpush lpush弹入数据
rpop lpop弹出数据
brpop blpop阻塞弹出数据
linsert向队列任意位置加入元素
lrange遍历列表
lindex查找指定位置上元素
llen列表长度
lrem删除多个同一元素
ltrim截取指定列表
lset修改列表指定位置元素

内部编码

ziplist:当列表的元素个数小于list-max-ziplist-entries配置(默认 512 个),同时列表中每个元素的值都小于list-max-ziplist-value 配置时(默认 64 字节),Redis 会选用ziplist 来作为 列表 的内部实现来减少内存的使用。
linkedlist:当列表类型无法满足ziplist的条件时,Redis会使用linkedlist作为列表的内部实现。

使用场景

可以做或者队列
还可以利用阻塞功能做消息队列

hash (哈希)

Redis 的字典相当于Java语言里面的HashMap,它是无序字典。内部实现结构上同Java的HashMap也是一致的,同样的数组 + 链表二维结构。扩容rehash的时候,采用渐进式。在rehash时,保留两个新旧hash结构,查询的时候都查,再根据定时任务,一点点将旧hash上的数据迁移到新的hash上,迁移完毕,旧hash被删除,并收回内存。我们默认key为hashKey,filed为小key

常用命令

设置值

127.0.0.1:6379> hset user name pjjlt
(integer) 1
127.0.0.1:6379> hset user age 26
(integer) 1
127.0.0.1:6379> hset user company gongsi
(integer) 1

获取值

127.0.0.1:6379> hget user name
"pjjlt"

删除field

127.0.0.1:6379> hdel user company
(integer) 1

计算field个数

127.0.0.1:6379> hlen user
(integer) 2

批量设置获取值

127.0.0.1:6379> hmset user name pjjlt age 26 city shijiazhuang
OK
127.0.0.1:6379> hmget user name age
1) "pjjlt"
2) "26"

判断filed是否存在

127.0.0.1:6379> hexists user name
(integer) 1

获取所有filed或者value

127.0.0.1:6379> hkeys user
1) "name"
2) "age"
3) "city"
127.0.0.1:6379> hvals user
1) "pjjlt"
2) "26"

获取所有filed和value

127.0.0.1:6379> hgetall user
1) "name"
2) "pjjlt"
3) "age"
4) "26"
5) "city"
6) "shijiazhuang"

自增

127.0.0.1:6379> hincrby user age -8
(integer) 18
127.0.0.1:6379> hset user scroe 99.6
(integer) 1
127.0.0.1:6379> hincrbyfloat user scroe 0.4
"100"

hincrbyhincrbyfloat分别增加或者减少整型浮点型

计算值的长度

127.0.0.1:6379> hget user name
"pjjlt"
127.0.0.1:6379> hstrlen user name
(integer) 5

总结

命令解释
hset设置值
hget获取值
hdel删除值
hlen计算field个数
hmset批量设置值
hmget批量获取值
hexists判断field是否存在
hkeys获取所有field
hvals获取所有value
hgetall获取所有filed和value
hincrby增加整型数值
hincrbyfloat增加浮点型数值
hstrlen计算值的长度

内部编码

ziplist:当列表的元素个数小于list-max-ziplist-entries配置(默认 512 个),同时列表中每个元素的值都小于list-max-ziplist-value 配置时(默认 64 字节),Redis 会选用ziplist 来作为 列表 的内部实现来减少内存的使用。

hashtable:当哈希类型无法满足ziplist的条件时,Redis会使用hashtable作为哈希的内部实现,因为此时ziplist的读写效率会下降,而hashtable的读写时间复杂度为O(1)。

使用场景

hash很适合缓存对象,比如商城系统可以存放商品,hashkey为商品id,field为各种属性,value为数据。当然string也可以存放商品,只不过它的value,时json串,还需要解析,从代码角度和网络代价来讲都不如hash

set (集合)

set相当于Java语言里面的 HashSet,它内部的键值对是无序的唯一的。它的内部实现相当于一个特殊的字典,字典中所有的value都是一个值NULL。

常用命令

增加元素

127.0.0.1:6379> sadd books java python python ruby java
(integer) 3

sadd可以添加一个或者多个元素,并且去重。

删除元素

127.0.0.1:6379> srem books python ruby
(integer) 2

srem可以删除一个或者多个元素。

计算元素个数

127.0.0.1:6379> sadd books python ruby c#
(integer) 3
127.0.0.1:6379> scard books
(integer) 4

判断元素是否在集合中

127.0.0.1:6379> sismember books java
(integer) 1
127.0.0.1:6379> sismember books c
(integer) 0

随机返回一定数量的元素

127.0.0.1:6379> srandmember books 2
1) "java"
2) "ruby"
127.0.0.1:6379> srandmember books 2
1) "c#"
2) "ruby"

随机弹出一个元素

127.0.0.1:6379> spop books
"ruby"
127.0.0.1:6379> scard books
(integer) 3

获取所有元素

127.0.0.1:6379> smembers books
1) "c#"
2) "java"
3) "python"

计算并查集

127.0.0.1:6379> sadd set1 a b c d e
(integer) 5
127.0.0.1:6379> sadd set2 d e f g
(integer) 4
# 计算两个集合交集
127.0.0.1:6379> sinter set1 set2
1) "e"
2) "d"
# 计算两个集合并集
127.0.0.1:6379> sunion set1 set2
1) "g"
2) "a"
3) "d"
4) "e"
5) "c"
6) "f"
7) "b"
# 计算两个集合差集
127.0.0.1:6379> sdiff set1 set2
1) "c"
2) "b"
3) "a"

总结

命令解释
sadd增加元素
srem删除元素
scard计算元素个数
sismember判断元素是否在集合中
srandmember随机返回一定数量的元素
spop随机弹出一个元素
smembers获取所有元素
sinter计算两个集合交集
sunion计算两个集合并集
sdiff计算两个集合差集

内部编码

intset:当集合中的元素都是整数且元素个数小于set-max-intset-entries配置(默认 512 个)时,Redis会选用intset来作为集合的内部实现,从而减少内存的使用。

hashtable:当集合类型无法满足intset的条件时,Redis会使用hashtable作为集合的内部实现。

使用场景

利用并查集可以用于查找用户共同爱好。

利用不可重复性,可以用于抽奖,保证每个用户只能中一次奖。

zset(有序集合)

zset可能是Redis提供的最为特色的数据结构。它类似于Java的SortedSet和HashMap的结合体,一方面它是一个set,保证了内部value的唯一性,另一方面它可以给每个value赋予一个score,代表这个value的排序权重。

常用命令

# 设置值
127.0.0.1:6379> zadd books 9 java
(integer) 1
127.0.0.1:6379> zadd books 8 python
(integer) 1
127.0.0.1:6379> zadd books 7 golang
(integer) 1
# 查看一定范围内的值
127.0.0.1:6379> zrange books 0 -1
1) "golang"
2) "python"
3) "java"
# 删除某个值
127.0.0.1:6379> zrem books golang
(integer) 1
# 根据score 正序排
127.0.0.1:6379> zrange books 0 -1
1) "python"
2) "java"
# 根据score 倒叙排
127.0.0.1:6379> zrevrange books 0 -1
1) "java"
2) "python"
# 查看元素个数
127.0.0.1:6379> zcard books
(integer) 2
# 查看某元素分值
127.0.0.1:6379> zscore books java
"9"
# 正序排名,从0开始
127.0.0.1:6379> zrank books  python
(integer) 0
127.0.0.1:6379> zrank books java
(integer) 1
# 一定范围内scor内的元素
127.0.0.1:6379> zrangebyscore books 0 8.8
1) "python"

总结

命令解释
zadd设置值
zrange查看一定范围内的值
zrem删除某个值
zrange根据score正序排
zrevrange根据score倒叙排
zcard查看元素个数
zscore查看某元素分值
zrank正序排名,从0开始
zrangebyscore一定范围内scor内的元素

内部编码

zset内部的排序功能是通过「跳跃列表」数据结构来实现的,它的结构非常特殊,也比较复杂。举个例子吧,就好像一个公司,有9个员工,分为3各小组,每个小组算一个小组长(注意小组长还具备员工角色,只不过多了小组长角色)小组长再选出一个技术总监(技术总监同时具备员工、小组长、技术总监角色)

使用场景

适合排名性质的场景,比如微博热搜,某技术网站热门博客等等。

总结不易,小伙伴给个赞再走吧。
在这里插入图片描述

查看原文

赞 0 收藏 0 评论 0

啤酒就辣条 发布了文章 · 11月22日

【那些年我们用过的Redis】Redis集群搭建原来这么简单

Reids服务集群模式可分为主从、哨兵、Cluster模式。本博客主要介绍主从和Cluster模式

三种集群模式简介

主从模式:主从模式是三种模式中最简单的,一般有一个master服务和多个(至少一个)slave服务组成。主要实现读写分离,减轻单Redis服务压力。master负责写数据,然后将数据copy给slaveslave服务负责读数据。

哨兵模式:主从模式有个弊端,就是一旦master服务挂掉,Redis服务就无法再写入数据,slave并不会升级为master。于是需要另外一种类型的服务Sentinel(哨兵)。Sentinel主要负责监控通知自动故障转移配置提供者。简单来说就是,Sentinel可以监督数据节点(masterslave)健康状态,并通知DBA(数据库管理人员),如果master挂了,从众多slave中选举一个新的master,并且告诉客户端(cli)数据节点的信息。

Cluster:哨兵模式虽然相较于主从模式来说,友善了很多,但是master不好做负载,并且储存受单机限制,还引入了另外一种类型的服务,有些繁琐。于是乎,Cluster就来了,通常具有 高可用、可扩展性、分布式、容错 等特性。当然,Cluster哨兵模式没有明确的谁好谁坏,还是要看自己的需求的。一般分布式,使用Cluster好一些。

主从模式

在搭建主从模式Cluster之前,我默认你会安装单机的redis,并且你的环境是Linux(CentOS7),你的Redis版本是5.0.9。如果不会单机redis安装,请戳

首先将原生的redis.conf复制两份到/usr/local路径下:

cp redis.conf /usr/local/redis_master.conf
cp redis.conf /usr/local/redis_replic.conf

然后利用vim修改两个配置文件,修改主要参数如下:
redis_master.conf

# 主机地址,默认是127.0.0.1,修改成0.0.0.0
bind 0.0.0.0
# 端口
port 16379
# 是否开启为守护线程,默认是no,改成yes
daemonize yes
# 指定redis进程的PID文件存放位置 给文件重命名
pidfile /var/run/redis_16379.pid
# 日志存放位置 给文件重命名
logfile /var/log/redis_16379.log
# rdb模式持久化快照
dbfilename dump-16379.rdb
# 持久化文件存放位置
dir ./redis-workir
# redis连接密码
requirepass 123456
# 连接主服务器的密码,这里可以省略
masterauth 123456

redis_replic.conf

# 主机地址,默认是127.0.0.1,修改成0.0.0.0
bind 0.0.0.0
# 端口
port 26379
# 是否开启为守护线程,默认是no,改成yes
daemonize yes
# 指定redis进程的PID文件存放位置 给文件重命名
pidfile /var/run/redis_26379.pid
# 日志存放位置 给文件重命名
logfile /var/log/redis_26379.log
# rdb模式持久化快照
dbfilename dump-26379.rdb
# 持久化文件存放位置
dir ./redis-workir
# redis连接密码
requirepass 123456
# 连接主服务器的密码
masterauth 123456
# 本复制节点的主节点地址
replicaof 你服务器外网ip 16379

上面注意,在之前的版本replicaof叫做slaveof 。这里,我们模仿有两台主机,所以主机ip不写127.0.0.1了,写你服务器外网的ip,后面也方便说下网关的问题。然后开启两个服务。

redis-server /usr/local/redis_master.conf
redis-server /usr/local/redis_replic.conf

此时查看两个服务日志,都成长启动了。但是此时master,写数据,slave
无法复制数据,查看日志报错

Error condition on socket for SYNC: No route to host

这边需要打开服务器的防火墙,用到iptables,安装请移步我上篇博客(请戳)。

iptables -I INPUT -p tcp -m tcp --dport 16379 -j ACCEPT
iptables -I INPUT -p tcp -m tcp --dport 26379 -j ACCEPT
service iptables save
# 查看一下
iptables -L -n -v
# 的确暴露出去这两个端口了
   66  3528 ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:26379
 196K   17M ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:16379

然后使用一下

[root@VM_0_10_centos ~]# redis-cli -h 127.0.0.1 -p 16379 -a 123456
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:16379> set today 1106
OK
127.0.0.1:16379> 
[root@VM_0_10_centos ~]# redis-cli -h 127.0.0.1 -p 26379 -a 123456
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:26379> get today
"1106"

Cluster模式

首先我们创建一个文件夹/usr/local/redis-cluster。并在它下面创建logdataconf,并在data文件中创建不同服务的文件夹。基本就是cd到位置,然后使用mkdir,比较简单,就看下目录结构吧

[root@VM_0_10_centos redis-cluster]# tree -d
.
|-- conf
|-- data
|   |-- redis-6378
|   |-- redis-6380
|   |-- redis-6381
|   |-- redis-6389
|   |-- redis-6390
|   |-- redis-6391
`-- log

然后在conf文件下创建这几个服务的配置文件,这几个文件,一摸一样,只需要关键位置替换端口号就好了,你懂的,下面以6378为例。

cd /usr/local/redis-cluster/conf
# copy一份原始文件过来
cp /usr/local/redis.conf /usr/local/redis-cluster/conf/redis-6378.conf
vim redis-6378.conf

修改关键配置如下,其他默认不变
redis-6378.conf

# 主机地址,默认是127.0.0.1,修改成0.0.0.0
bind 0.0.0.0
# 端口
port 6378
# 是否开启为守护线程,默认是no,改成yes
daemonize yes
# 指定redis进程的PID文件存放位置 给文件重命名
pidfile /var/run/redis-cluster/redis-6378.pid
# 日志存放位置 给文件重命名
logfile /usr/local/redis-cluster/log/redis-6378.log
# rdb模式持久化快照
dbfilename dump-6378.rdb
# 持久化文件存放位置
dir /usr/local/redis-cluster/data/redis-6378
# redis连接密码
requirepass 123456
# 连接主服务器的密码
masterauth 123456
# 开启AOF持久化模式
appendonly yes
# 开启cluster集群模式
cluster-enabled yes
# 集群的配置,配置文件首次启动自动生成
cluster-config-file /usr/local/redis-cluster/conf/node-6378.conf
# 请求超时,默认的就好
cluster-node-timeout 15000

cluster-enabled开启cluster集群模式,appendonly开启AOF持久化模式,一种实时性比RDB模式更好的持久化模式。生成的aof格式文件也在dir路径下,并且被读进内存的优先级比dump-6378.rdb文件要高。cluster-config-file是存放集群配置文件的地方,自动生成。同样的配置,将端口号换成其他五个,生成redis-6380.conf redis-6381.conf redis-6389.conf redis-6390.conf redis-6391.conf 。然后开启这些服务。

redis-server redis-6378.conf 
redis-server redis-6380.conf 
redis-server redis-6381.conf 
redis-server redis-6389.conf 
redis-server redis-6390.conf 
redis-server redis-6391.conf 
# 查看下是否开启
ps -ef | grep redis
# 进程已存在
root      3009     1  0 Nov05 ?        00:01:49 redis-server 0.0.0.0:6378 [cluster]
root      3047     1  0 Nov05 ?        00:01:47 redis-server 0.0.0.0:6381 [cluster]
root      3056     1  0 Nov05 ?        00:01:50 redis-server 0.0.0.0:6389 [cluster]
root      3073     1  0 Nov05 ?        00:01:49 redis-server 0.0.0.0:6390 [cluster]
root      3083     1  0 Nov05 ?        00:01:50 redis-server 0.0.0.0:6391 [cluster]
root     26443     1  0 Nov05 ?        00:01:39 redis-server 0.0.0.0:6380 [cluster]

注意,它们后面都显示它们是以cluster集群形式开启的。下面开启集群,如果是redis5.0以前的版本,需要下载ruby,通过ruby开启,redis5.0以后可直接开启,命令如下:

# 开启集群
redis-cli -a 123456--cluster create 127.0.0.1:6378 127.0.0.1:6380 127.0.0.1:6381 127.0.0.1:6389 127.0.0.1:6390 127.0.0.1:6391 --cluster-replicas 1
# 会多一步询问,输入yes即可
Can I set the above configuration? (type 'yes' to accept): yes
#开启代理防火墙
iptables -I INPUT -p tcp -m tcp --dport 6378 -j ACCEPT
iptables -I INPUT -p tcp -m tcp --dport 6380 -j ACCEPT
iptables -I INPUT -p tcp -m tcp --dport 6381 -j ACCEPT
iptables -I INPUT -p tcp -m tcp --dport 6389 -j ACCEPT
iptables -I INPUT -p tcp -m tcp --dport 6390 -j ACCEPT
iptables -I INPUT -p tcp -m tcp --dport 6391 -j ACCEPT
service iptables save

开启集群的命令,会自动生成主从关系,分配solt,终端会询问这种分配方式ok吗?如果操作者觉得可以的话,需要输入yes。用cli方式随便连接一个redis服务。

redis-cli -c -h 127.0.0.1 -p 6378 -a 123456
127.0.0.1:6378> CLUSTER INFO
# 此处省略一些信息
127.0.0.1:6378> CLUSTER NODES
114d082bb43c67042c806e0060f5fb8157736b98 127.0.0.1:6390@16390 slave ca998854f1d96e2b64b7304aea5e5b4c96a9d85a 0 1604672507350 5 connected
1efc0139a5422c68f1c43428d1d6f566ce361fed 127.0.0.1:6378@16378 myself,master - 0 1604672504000 1 connected 0-5460
f10fc263b1b887fe175307b2fb7600370d39c0e1 127.0.0.1:6389@16389 master - 0 1604672505000 8 connected 5461-10922
e1eb98ead97d9b22fe4538601cbcd193e998baad 127.0.0.1:6380@16380 slave f10fc263b1b887fe175307b2fb7600370d39c0e1 0 1604672505347 8 connected
ca998854f1d96e2b64b7304aea5e5b4c96a9d85a 127.0.0.1:6381@16381 master - 0 1604672506000 3 connected 10923-16383
0f75c7f994a43551671e955a9f72c28a9c6edb56 127.0.0.1:6391@16391 slave 1efc0139a5422c68f1c43428d1d6f566ce361fed 0 1604672504344 6 connected

可以看出637863896381master节点,分别管理卡槽0-54605461-1092210923-16383。并且可以根据id值,判断出三个slave节点的主节点是谁,例如6390主节点id值是ca998854f1d96e2b64b7304aea5e5b4c96a9d85a,所以就是6381了。

这里需要说明一下,当我们存值的时候,key会根据hash函数计算出来,落在那个卡槽上。

127.0.0.1:6378> set haha 123
OK
127.0.0.1:6378> set name pjjlt
-> Redirected to slot [5798] located at 127.0.0.1:6389
OK
127.0.0.1:6389> 

可以看出来,haha经过hash运算,落在的卡槽在0-5460范围内,name经过hash运算,落在的卡槽在5461-10922范围内,所以服务终端也自动重定向到6389服务上。get也会导致服务终端重定向。

127.0.0.1:6389> get name
"pjjlt"
127.0.0.1:6389> get haha
-> Redirected to slot [3662] located at 127.0.0.1:6378
"123"
127.0.0.1:6378> 

增加和移除节点

cluster集群可以友好的水平拓展。下面增加两个节点,同上面一样,增加配置文件redis-6395.confredis-6396.conf,并开启这两个服务

redis-server redis-6395.conf 
redis-server redis-6396.conf 

加入集群

# 随便连接一个服务
redis-cli -c -h 127.0.0.1 -p 6378 -a 123456
# 加入集群
CLUSTER MEET 127.0.0.1 6395
CLUSTER MEET 127.0.0.1 6395
# 查看状态
CLUSTER NODES
114d082bb43c67042c806e0060f5fb8157736b98 127.0.0.1:6390@16390 slave ca998854f1d96e2b64b7304aea5e5b4c96a9d85a 0 1604673986000 5 connected
1efc0139a5422c68f1c43428d1d6f566ce361fed 127.0.0.1:6378@16378 myself,master - 0 1604673985000 1 connected 0-5460
2eefbdc56047282fc80baf3cc7bc08ff86dbdf9e 127.0.0.1:6395@16395 master - 0 1604673985000 7 connected
f10fc263b1b887fe175307b2fb7600370d39c0e1 127.0.0.1:6389@16389 master - 0 1604673988720 8 connected 5461-10922
e1eb98ead97d9b22fe4538601cbcd193e998baad 127.0.0.1:6380@16380 slave f10fc263b1b887fe175307b2fb7600370d39c0e1 0 1604673987717 8 connected
f8c884f07375cb524236341c4fcb963f58f82622 127.0.0.1:6396@16396 master - 0 1604673987517 7 connected
ca998854f1d96e2b64b7304aea5e5b4c96a9d85a 127.0.0.1:6381@16381 master - 0 1604673986000 3 connected 10923-16383
0f75c7f994a43551671e955a9f72c28a9c6edb56 127.0.0.1:6391@16391 slave 1efc0139a5422c68f1c43428d1d6f566ce361fed 0 1604673986716 6 connected

可以看出,此时加入的两个节点全都是master服务,现在将6396设置成6395的slave节点。

redis-cli -c -h 127.0.0.1 -p 6396 -a 123456 cluster replicate 2eefbdc56047282fc80baf3cc7bc08ff86dbdf9e
# 随便连接一个服务
redis-cli -c -h 127.0.0.1 -p 6378 -a 123456
CLUSTER NODES
114d082bb43c67042c806e0060f5fb8157736b98 127.0.0.1:6390@16390 slave ca998854f1d96e2b64b7304aea5e5b4c96a9d85a 0 1604673986000 5 connected
1efc0139a5422c68f1c43428d1d6f566ce361fed 127.0.0.1:6378@16378 myself,master - 0 1604673985000 1 connected 0-5460
2eefbdc56047282fc80baf3cc7bc08ff86dbdf9e 127.0.0.1:6395@16395 master - 0 1604673985000 7 connected
f10fc263b1b887fe175307b2fb7600370d39c0e1 127.0.0.1:6389@16389 master - 0 1604673988720 8 connected 5461-10922
e1eb98ead97d9b22fe4538601cbcd193e998baad 127.0.0.1:6380@16380 slave f10fc263b1b887fe175307b2fb7600370d39c0e1 0 1604673987717 8 connected
f8c884f07375cb524236341c4fcb963f58f82622 127.0.0.1:6396@16396 slave 2eefbdc56047282fc80baf3cc7bc08ff86dbdf9e 0 1604673987517 7 connected
ca998854f1d96e2b64b7304aea5e5b4c96a9d85a 127.0.0.1:6381@16381 master - 0 1604673986000 3 connected 10923-16383
0f75c7f994a43551671e955a9f72c28a9c6edb56 127.0.0.1:6391@16391 slave 1efc0139a5422c68f1c43428d1d6f566ce361fed 0 1604673986716 6 connected

可以看出,此时6396节点已经变成了6395的slave节点了。

移除集群

规则:不能移除登录节点,不能移除自己的master节点。可以删除其他的master或者slave节点

# 根据上面可以看出 1efc0139a5422c68f1c43428d1d6f566ce361fed 就是6378节点
127.0.0.1:6378> CLUSTER FORGET 1efc0139a5422c68f1c43428d1d6f566ce361fed
(error) ERR I tried hard but I can't forget myself...
# 登录6378的slave节点6391,删除6378,失败。
[root@VM_0_10_centos conf]# redis-cli -c -h 127.0.0.1 -p 6391 -a pjjlt1126
127.0.0.1:6391> CLUSTER FORGET 1efc0139a5422c68f1c43428d1d6f566ce361fed
(error) ERR Can't forget my master!
# 将刚加入的节点 6396
127.0.0.1:6378> CLUSTER FORGET f8c884f07375cb524236341c4fcb963f58f82622
OK
127.0.0.1:6378> CLUSTER NODES
114d082bb43c67042c806e0060f5fb8157736b98 127.0.0.1:6390@16390 slave ca998854f1d96e2b64b7304aea5e5b4c96a9d85a 0 1604675461284 5 connected
1efc0139a5422c68f1c43428d1d6f566ce361fed 127.0.0.1:6378@16378 myself,master - 0 1604675460000 1 connected 0-5460
2eefbdc56047282fc80baf3cc7bc08ff86dbdf9e 127.0.0.1:6395@16395 master - 0 1604675460283 7 connected
f10fc263b1b887fe175307b2fb7600370d39c0e1 127.0.0.1:6389@16389 master - 0 1604675462284 8 connected 5461-10922
e1eb98ead97d9b22fe4538601cbcd193e998baad 127.0.0.1:6380@16380 slave f10fc263b1b887fe175307b2fb7600370d39c0e1 0 1604675462000 8 connected
ca998854f1d96e2b64b7304aea5e5b4c96a9d85a 127.0.0.1:6381@16381 master - 0 1604675460000 3 connected 10923-16383
0f75c7f994a43551671e955a9f72c28a9c6edb56 127.0.0.1:6391@16391 slave 1efc0139a5422c68f1c43428d1d6f566ce361fed 0 1604675463287 6 connected

发现移除了,由于node.conf文件还存在(cluster-config-file配置的那个文件),还可以恢复移除

# 恢复移除
127.0.0.1:6378> CLUSTER SAVECONFIG 
OK
# 发现6396它又回来了
127.0.0.1:6378> CLUSTER NODES
114d082bb43c67042c806e0060f5fb8157736b98 127.0.0.1:6390@16390 slave ca998854f1d96e2b64b7304aea5e5b4c96a9d85a 0 1604675573000 5 connected
1efc0139a5422c68f1c43428d1d6f566ce361fed 127.0.0.1:6378@16378 myself,master - 0 1604675569000 1 connected 0-5460
2eefbdc56047282fc80baf3cc7bc08ff86dbdf9e 127.0.0.1:6395@16395 master - 0 1604675572000 7 connected
f10fc263b1b887fe175307b2fb7600370d39c0e1 127.0.0.1:6389@16389 master - 0 1604675572000 8 connected 5461-10922
e1eb98ead97d9b22fe4538601cbcd193e998baad 127.0.0.1:6380@16380 slave f10fc263b1b887fe175307b2fb7600370d39c0e1 0 1604675571000 8 connected
f8c884f07375cb524236341c4fcb963f58f82622 127.0.0.1:6396@16396 slave 2eefbdc56047282fc80baf3cc7bc08ff86dbdf9e 0 1604675572509 7 connected
ca998854f1d96e2b64b7304aea5e5b4c96a9d85a 127.0.0.1:6381@16381 master - 0 1604675574514 3 connected 10923-16383
0f75c7f994a43551671e955a9f72c28a9c6edb56 127.0.0.1:6391@16391 slave 1efc0139a5422c68f1c43428d1d6f566ce361fed 0 1604675573511 6 connected

模拟宕机

首先说一个坑:在cluster集群模式下,不能直接kill -9 某个节点,只能用kill(当然模拟宕机咱们用的是CLUSTER FORGET命令)。kill -9 ,再重启集群的时候会报错,需要删除pidfile、data、log等等一些东西,才能重启集群成功,所以尽量不要kill -9。

直接kill 6378 试试

[root@VM_0_10_centos conf]# redis-cli -c -h 127.0.0.1 -p 6391 -a 123456
127.0.0.1:6391> CLUSTER NODES
0f75c7f994a43551671e955a9f72c28a9c6edb56 127.0.0.1:6391@16391 myself,master - 0 1604675774000 9 connected 0-5460
ca998854f1d96e2b64b7304aea5e5b4c96a9d85a 127.0.0.1:6381@16381 master - 0 1604675779737 3 connected 10923-16383
f8c884f07375cb524236341c4fcb963f58f82622 127.0.0.1:6396@16396 slave 2eefbdc56047282fc80baf3cc7bc08ff86dbdf9e 0 1604675778000 7 connected
f10fc263b1b887fe175307b2fb7600370d39c0e1 127.0.0.1:6389@16389 master - 0 1604675777000 8 connected 5461-10922
2eefbdc56047282fc80baf3cc7bc08ff86dbdf9e 127.0.0.1:6395@16395 master - 0 1604675776000 7 connected
114d082bb43c67042c806e0060f5fb8157736b98 127.0.0.1:6390@16390 slave ca998854f1d96e2b64b7304aea5e5b4c96a9d85a 0 1604675779000 5 connected
1efc0139a5422c68f1c43428d1d6f566ce361fed 127.0.0.1:6378@16378 master,fail - 1604675757260 1604675756000 1 disconnected
e1eb98ead97d9b22fe4538601cbcd193e998baad 127.0.0.1:6380@16380 slave f10fc263b1b887fe175307b2fb7600370d39c0e1 0 1604675778000 8 connected

发现此时6378为master,fail,而它原来的slave节点为master。再重启6378节点看看。

[root@VM_0_10_centos conf]# redis-server redis-6378.conf 
[root@VM_0_10_centos conf]# redis-cli -c -h 127.0.0.1 -p 6391 -a 123456
127.0.0.1:6391> CLUSTER NODES
0f75c7f994a43551671e955a9f72c28a9c6edb56 127.0.0.1:6391@16391 myself,master - 0 1604675996000 9 connected 0-5460
ca998854f1d96e2b64b7304aea5e5b4c96a9d85a 127.0.0.1:6381@16381 master - 0 1604675993000 3 connected 10923-16383
f8c884f07375cb524236341c4fcb963f58f82622 127.0.0.1:6396@16396 slave 2eefbdc56047282fc80baf3cc7bc08ff86dbdf9e 0 1604675992368 7 connected
f10fc263b1b887fe175307b2fb7600370d39c0e1 127.0.0.1:6389@16389 master - 0 1604675997000 8 connected 5461-10922
2eefbdc56047282fc80baf3cc7bc08ff86dbdf9e 127.0.0.1:6395@16395 master - 0 1604675996376 7 connected
114d082bb43c67042c806e0060f5fb8157736b98 127.0.0.1:6390@16390 slave ca998854f1d96e2b64b7304aea5e5b4c96a9d85a 0 1604675997376 5 connected
1efc0139a5422c68f1c43428d1d6f566ce361fed 127.0.0.1:6378@16378 slave 0f75c7f994a43551671e955a9f72c28a9c6edb56 0 1604675995000 9 connected
e1eb98ead97d9b22fe4538601cbcd193e998baad 127.0.0.1:6380@16380 slave f10fc263b1b887fe175307b2fb7600370d39c0e1 0 1604675998378 8 connected

发现此时原来的master节点6378变成了6391的slave节点了。

但是,只有当master节点有slot卡槽的时候,才会出现主从节点转化,当master节点没有卡槽时,不会进行转化。例如,删除6395,再重启,发现6395还是master,6396还是slave

另外,上面提到的node.conf,不仅可以帮助移除恢复(CLUSTER SAVECONFIG),还可以记录刚刚的CLUSTER 指令日志。

以上,就是cluster模式的搭建介绍,谢谢观看。

查看原文

赞 0 收藏 0 评论 0

啤酒就辣条 发布了文章 · 11月22日

【那些年我们用过的Redis】Linux下Redis安装与使用

一款由C编写的开源、基于内存、单线程、可持久化的NoSql数据库。

环境与版本

linux环境为腾讯云单核`CentOS7.2.1511`。
redis版本为5.0.9Redis官方下载地址.

为什么不安装redis6版本?

CentOS7,原装的gcc版本为4.8.5。安装redis6需要升级gcc版本,目前最新的gcc版本为gcc10,辣条君尝试安装gcc9版本,发现编译2个多小时都没有结束。网上说,编译时可以使用make -j4,可以开启多进程编译,but辣条君的服务器是单核的,遂放弃升级gcc,乖乖的安装redis5。redis5诞生于2018年底,基本功能蛮强大了。

安装

可以使用wget命令下载redis安装包,或者从官网上下载安装包copy到你的环境中。

# 下载
cd /usr/local/
wget http://download.redis.io/releases/redis-5.0.9.tar.gz 
# 解压
tar -zxvf redis-5.0.9.tar.gz 

编译

cd /redis-5.0.9
make
make install PREFIX=/usr/local/redis

此时,查看/usr/local/redis/bin路径会发现redis可执行文件。将常用的可执行文件放到环境变量中。

cd ..
cd redis/bin/
# 一般都具有超级权限,sudo可以去掉
sudo cp redis-cli redis-server redis-sentinel /usr/local/bin

此时,你的redis就可以启动使用了,为了便于管理文件,咱们把配置文件放到合适的位置,并修改下配置文件。

cd /usr/local/redis-5.0.9/
# 咱们使用的配置文件在 /usr/local/redis.conf
cp redis.conf /usr/local/
cd..
# 使用vim 查看并修改redis.conf
vim redis.conf

修改配置文件

咱们使用vim修改部分参数,其他默认就好。

# 主机地址,默认是127.0.0.1,修改成0.0.0.0
bind 0.0.0.0
# 端口 默认就好
port 6379
# 是否开启为守护线程,默认是no,改成yes
daemonize yes
# 指定redis进程的PID文件存放位置 给文件重命名
pidfile /var/run/redis_6379.pid
# 日志存放位置 给文件重命名
logfile /var/log/redis_6379.log
# rdb模式持久化快照,默认就好
dbfilename dump.rdb
# 持久化文件存放位置 默认就好
dir ./
# redis连接密码
requirepass 123456

以上,可以根据注释很好的理解并修改部分配置文件。具体解释下几个参数:

bind设置成0.0.0.0,是为了保证本服务器以外的客户端可以访问本redis服务。

daemonize设置成yes,将redis服务变成守护进程。在该模式下,redis会在后台运行,并将进程pid号写入至redis.conf选项pidfile设置的文件中,此时redis将一直运行,除非手动kill该进程。daemonize选项设置成no时,当前界面将进入redis的命令行界面,exit强制退出或者关闭连接工具(putty,xshell等)都会导致redis进程退出。

requirepass :如果你是云服务,这个密码一定要设置!否则,服务器很容易被某些人拿去当矿机,就是所有服务都给你停掉,只开起挖矿程序,那你真成了打工人了。别问辣条君怎么知道的。。

启动与使用

启动so easy,就一行。

redis-server /usr/local/redis.conf
# 查看一下是否启动
ps -ef | grep redis
# 发现进程说明OK了
root     11890     1  0 Nov04 ?        00:02:39 redis-server 0.0.0.0:6379

咱们的使用,分为3种情况:本服务器内使用、其他机器(你的个人电脑)可视化客户端使用、其他机器(你的个人电脑)cli客户端使用。

  1. 首先看本服务器内使用
redis-cli -h 127.0.0.1 -p 6379 -a 123456
# 然后终端前缀会变成
127.0.0.1:6379>
# 使用一下
127.0.0.1:6379> set name pjjlt
OK
127.0.0.1:6379> get name
"pjjlt"
  1. 其他机器(你的个人电脑)可视化客户端使用

直接说吧,使用Redis Desktop Manager可以连接并维护数据。

  1. 其他机器(你的个人电脑)cli客户端使用

在你个人电脑,打开cmd,利用redis-cli连接redis服务,发现gg。这时,需要在我们Linux服务器上打开防火墙,利用iptables

iptables其实不是真正的防火墙,我们可以把它理解成一个客户端代理,用户通过iptables这个代理,将用户的安全设定执行到对应的"安全框架"中,这个"安全框架"才是真正的防火墙,这个框架的名字叫netfilter。

安装并使用iptables

# 关闭防火墙
systemctl stop firewalld 
# 安装
yum install iptables-services
# 启动
systemctl enable iptables
# 打开
systemctl start iptables
# 暴露端口配置
iptables -I INPUT -p tcp -m tcp --dport 6379 -j ACCEPT
# 保存配置
service iptables save
# 重启 iptables 服务
service iptables restart
# 查看下 iptable配置
iptables -L -n -v
# 发现 6379端口暴露出去了
21169 1657K ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:6379

ok,至此,其他机器上的cli也可以访问我们的Redis服务了。

查看原文

赞 0 收藏 0 评论 0

啤酒就辣条 发布了文章 · 11月22日

辣条君写爬虫5【恋爱综艺《心动的信号》视频下载】

最近辣条君看到一部综艺《心动的信号》,打算看一看,学一学年轻人该怎样谈恋爱。可是,某讯视频要会员,从小艰苦朴素的辣条君当然选择白嫖了。于是乎,发现有在线资源哎~

Tip:本文仅供学习与交流,切勿用于非法用途!!!请大家支持正版!!!

分析与思路

今天爬取的页面是http://www.ikanmv.com/k/39050.html。底下一共有10集。

在这里插入图片描述

我们随便打开一集,点击network,发现视频是m3u8格式的。有两个m3u8格式文件,第一个是一些限制条件,真正的m3u8列表。后面就是列表中逐条下载的ts文件。
在这里插入图片描述
我们的目标是,把一集的所有ts文件下载下来,通过ffmpeg软件进行拼接、转化,最终生成一个.mp4文件。ffmpeg从官网下载较慢,如有需要请戳下面连接(windows10版本)。

链接: https://pan.baidu.com/s/1_1htbaRQP_6y_G2xHOmWBw
提取码: wivw

其实直接用ffmpeg也可以根据m3u8列表连接直接下载,但是速度较慢,咱们代码中使用多进程,加速下载。

不过还有个问题,经过测试,ffmpeg合并ts文件,一次性最多合并4、5百个(每集ts文件大概在1500~2000个之间),再多就会报错,所以我们先以300个ts文件为1组,将他们合并一下,得到几个大个的ts文件,再将这几个大个的ts文件合并成一集一个整个的ts文件,再转化mp4。

所以我们的代码流程为:==下载某集所有ts文件->合并成几个大个的ts文件->合并成一个ts文件->转化成mp4->删除没用的ts文件==

代码实现

首先创建一个文件夹D:\ts用于存放ts文件。

根据上节的代码流程,我们可以写出主函数,几个方法逐步实现:

if __name__ == '__main__':
    urls = [
       'https://zy.kubozy-youku-163-aiqi.com/20190710/13975_9273f090/1000k/hls/index.m3u8',
       'https://zy.kubozy-youku-163-aiqi.com/20190717/14563_0eca69d7/1000k/hls/index.m3u8',
       'https://zy.kubozy-youku-163-aiqi.com/20190724/15218_7487f9e5/1000k/hls/index.m3u8'
    ]
    # 开始的集数
    index = 4
    for url in urls:
        # 获取某集需要下载的ts列表
        get_list = get_m3u8(url)
        # 下载ts文件到本地
        download('D:\\ts', get_list, url[0:-10])
        # 合并
        merge_ts('D:\\ts', str(index)+'.mp4')
        # 删除没用的 ts
        delete_ts('D:\\ts')
        # 下一集
        index = index + 1

我们模拟下载4~6集(由于没找到规律,需要手动获取几集的第二个m3u8请求连接)

随便点击两个ts连接就会发现,某一集的ts文件请求前面都是一样的,最后拼接上xxxxx00x.ts,然后get请求就可以得到某个ts视频资源了。首先获取某集所有ts文件后缀,就是==xxxxx00x.ts部分==,实现一下get_m3u8方法

# 获取某集需要下载的ts列表
def get_m3u8(url):
    result = requests.get(url)
    lines = result.text.split('\n')
    all_ts = []
    for line in lines:
        if line.endswith('.ts'):
            all_ts.append(line)
    return all_ts

然后下载到本地

# 下载一个ts文件
def download_ts(ts, root, url):
    res = requests.get(url+ts)
    with open(root+"\\" + ts, 'ab') as f:
        f.write(res.content)
        f.flush()
        print(ts+'下载完毕!')

# 下载ts文件到本地
def download(root, get_list, url):
    # 已经下载过的文件
    had_list = []
    # 还有没下载完的就继续
    while len(list(set(get_list).difference(set(had_list)))) > 0:
        had_list = os.listdir(root)
        # 还需要下载的
        needs = list(set(get_list).difference(set(had_list)))
        print('还需要下载个数为:'+str(len(needs)))
        pool = Pool(10)
        group = ([ts for ts in needs])
        pool.map(partial(download_ts, root=root, url=url), group)
        pool.close()
        pool.join()

download方法,即主函数中使用的方法。第一个参数root是ts视频下载的地址,第二个参数get_list是某集所有ts文件后缀(==xxxxx00x.ts部分==),第三个参数url是某个ts请求前缀。首先这里用到了多进程,往期博客介绍过,这里照抄就好。

值得一提的时,这边有个while循环。因为多进程下载某集所有ts文件过程中,我发现可能会造成某些ts文件的丢失未下载,所以每次下载完成,我们获取已下载完成的文件名称(文件名称就是ts后缀),和某集所有的ts文件后缀,做差集。继续下载这些丢失的,知道差集为空,就证明全部下载完毕。

接下来需要合并。

# 合并ts,由于一次无法合并那么多,合并两次
def merge_ts(root, name):
    os.chdir(root)
    all_ts = os.listdir(root)
    # 计数器
    counter = 0
    big_list = []
    per_list = []
    # 将多个ts文件名,300为一组拼接在一起
    for ts in all_ts:
        if counter > 300:
            counter = 0
            big_list.append('+'.join(per_list))
            per_list = []
        else:
            counter = counter+1
            per_list.append(ts)

    batch = 0
    batch_ts = []
    for big in big_list:
        batch = batch + 1
        batch_ts.append(str(batch)+'.ts')
        os.system('copy /b ' + big + f' batch{batch}.ts')
    shell_str = '+'.join(batch_ts)
    # 第二次拼接
    os.system('copy /b '+shell_str+' '+name)

这里需要两次合并,第二次合并和转mp4格式是一个脚本操作。这里使用os.system调用系统方法,由于我们的ffmpeg已经放入到环境变量中,所以这里就是直接操作ffmpeg软件了。

生成某集的mp4文件之后,我们需要删除根目录下所有的.ts文件,一来节约硬盘空间,二来为下一集下载.ts文件清除干扰。

def delete_ts(root):
    os.chdir(root)
    all_file = os.listdir(root)
    for file in all_file:
        if file.endswith('.ts'):
            os.remove(root+'\\'+file)

以上我们就可以下载到喜欢看的视频了。喜欢的小伙伴快去试试吧~

查看原文

赞 0 收藏 0 评论 0

啤酒就辣条 发布了文章 · 11月22日

辣条君写爬虫4【帮小姐姐删垃圾邮件】

我司系统报错或者重要监控数据会发邮件到个人邮箱,这就导致邮箱一段时间就好几百页了。小姐姐说,辣条君和某昊然谁先帮她删完邮件,就和谁去看电影。于是乎,打算写个小脚本,先拿自己的账号实验一下吧~

Tip:本文仅供学习与交流,切勿用于非法用途!!!

1.需求分析

这次,我们需要删除的QQ邮箱内的部分邮件。

只需要将我司日志那些邮件,全部勾选,然后点击最上面的彻底删除就好了嘛。so easy。首先我们可以抓包,找接口。由于涉及登录,我打算用笨方法(还不是菜),让页面自己跑。用到了selenium。用这个库,你首先需要将你浏览器对应的驱动放到环境变量中。然后模拟认为操作,一步一步写代码就好了。

2.撸起袖子写代码

首先,我们需要登录,我们使用账号和密码登录(这样就知道小姐姐qq密码了)。打开qq邮箱,qq推荐的是快捷登录,我们需要模拟点击转换按钮,选择账号密码登录方式。

from selenium import webdriver
driver = webdriver.Chrome()
# 获取页面
driver.get("https://mail.qq.com/")

def login(qq, password):
    driver.switch_to.frame("login_frame")
    # 可能会出现QQ快捷登录页面
    if driver.find_element_by_xpath('//*[@id="switcher_plogin"]'):
        driver.find_element_by_xpath('//*[@id="switcher_plogin"]').click()
    driver.find_element_by_xpath('//*[@id="u"]').send_keys(qq)
    driver.find_element_by_xpath('//*[@id="p"]').send_keys(password)
    driver.find_element_by_xpath('//*[@id="login_button"]').click()

我使用的是谷歌浏览器,所以使用Chrome()方法,利用xpath选取控件,xpath路径可以直接从浏览器处copy。

登录之后,点击左边收件箱就可以进入到邮件列表页面。这里需要注意一下,因为这个页面内部包含frame,所以需要进入到某标签的frame才可以顺利获取某页面。一页有25个邮件,我们for循环注意判断如果是我司日志,就可以在多选框把它勾上了。然后点击最上面的彻底删除,点击确定,之后会弹出来一个删除成功的Dialog,再点击取消就可以了。

    # 回到主页面
    driver.switch_to.default_content()
    # 点击左边 收件箱 按钮
    driver.find_element_by_xpath('//*[@id="folder_1"]').click()
    # 进入列表的mainFrame
    driver.switch_to.frame("mainFrame")
    # for循环每一页的25个列表,判断打勾
    for i in range(1,26):
        try:
            span = f'//*[@id="div_showbefore"]/table[{i}]/tbody/tr/td[3]/table/tbody/tr/td[1]/nobr/span'
            input = f'//*[@id="div_showbefore"]/table[{i}]/tbody/tr/td[1]/input'
            if driver.find_element_by_xpath(span).text == del_text:
                driver.find_element_by_xpath(input).click()
        except Exception as e:
            print(e.args)
    driver.find_element_by_xpath('//*[@id="quick_completelydel"]').click()
    # 切换到弹出框
    driver.switch_to.parent_frame()
    # 点击确定删除
    driver.find_element_by_xpath('//*[@id="QMconfirm_QMDialog_confirm"]').click()
    # 显示等待,等待取消按钮出现
    WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.ID, 'QMconfirm_QMDialog_cancel')))
    driver.find_element_by_xpath('//*[@id="QMconfirm_QMDialog_cancel"]').click()

del_text就是我司日志四个字。首先利用switch_to.default_content()进入主页面,然后点击收件箱按钮,在跳转到邮件列表所在framemainFrame。接着for循环,看看那个适合打上勾。接着点击确定和删除成功提示按钮即可,这一过程需要切换frame。如果一切顺利的话,页面会自动刷新,然后我们进行第二次筛选、删除即可。

这里有个问题,如果当前页(或者说第一页),没有我司日志,你不停的彻底删除,当前页的数据一直不变,那岂不是后面的数据一直得不到删除啊。所以我们需要记录下当前需要跳转的页数,然后逐页跳转到可能拥有我司日志的页码上就好了。

# 需要跳转的页
need_turn_page = 1

def delete(del_text):
    global need_turn_page
    # 从第一页开始
    current_page = 1
    # 记录一下,本页需要删除的有邮件
    count = 0
    # 仿佛有一些代码
    # 跳转到当前页
    if need_turn_page > current_page:
        for page in range(1, need_turn_page):
            print("我跳转了")
            current_page = current_page + 1
            driver.find_element_by_xpath('//*[@id="nextpage1"]').click()
    # for循环每一页的25个列表,判断打勾
    for i in range(1,26):
        try:
            # 仿佛有一些代码
            if driver.find_element_by_xpath(span).text == del_text:
                count = count+1
                driver.find_element_by_xpath(input).click()
        except Exception as e:
            print(e.args)
    if count > 0:
        # 仿佛有一些代码
    # 否则翻页
    else:
        need_turn_page = need_turn_page + 1

3.完整代码

'''
QQ 邮箱删除垃圾邮件
'''
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time
driver = webdriver.Chrome()

# 需要跳转的页
need_turn_page = 1
# 获取页面
driver.get("https://mail.qq.com/")

# 登录
def login(qq, password):
    driver.switch_to.frame("login_frame")
    # 可能会出现QQ快捷登录页面
    if driver.find_element_by_xpath('//*[@id="switcher_plogin"]'):
        driver.find_element_by_xpath('//*[@id="switcher_plogin"]').click()
    driver.find_element_by_xpath('//*[@id="u"]').send_keys(qq)
    driver.find_element_by_xpath('//*[@id="p"]').send_keys(password)
    driver.find_element_by_xpath('//*[@id="login_button"]').click()

def delete(del_text):
    global need_turn_page
    # 从第一页开始
    current_page = 1
    # 防止太快,腾讯找我麻烦
    time.sleep(1)
    # 记录一下,本页需要删除的有邮件
    count = 0
    # 回到主页面
    driver.switch_to.default_content()
    # 点击左边 收件箱 按钮
    driver.find_element_by_xpath('//*[@id="folder_1"]').click()
    # 进入列表的mainFrame
    driver.switch_to.frame("mainFrame")
    # 跳转到当前页
    if need_turn_page > current_page:
        for page in range(1, need_turn_page):
            print("我跳转了")
            current_page = current_page + 1
            driver.find_element_by_xpath('//*[@id="nextpage1"]').click()
    # for循环每一页的25个列表,判断打勾
    for i in range(1,26):
        try:
            span = f'//*[@id="div_showbefore"]/table[{i}]/tbody/tr/td[3]/table/tbody/tr/td[1]/nobr/span'
            input = f'//*[@id="div_showbefore"]/table[{i}]/tbody/tr/td[1]/input'
            if driver.find_element_by_xpath(span).text == del_text:
                count = count+1
                driver.find_element_by_xpath(input).click()
        except Exception as e:
            print(e.args)
    if count > 0:
        driver.find_element_by_xpath('//*[@id="quick_completelydel"]').click()
        # 切换到弹出框
        driver.switch_to.parent_frame()
        # 点击确定删除
        driver.find_element_by_xpath('//*[@id="QMconfirm_QMDialog_confirm"]').click()
        # 显示等待,等待取消按钮出现
        WebDriverWait(driver, 20).until(EC.presence_of_element_located((By.ID, 'QMconfirm_QMDialog_cancel')))
        driver.find_element_by_xpath('//*[@id="QMconfirm_QMDialog_cancel"]').click()
    # 否则翻页
    else:
        need_turn_page = need_turn_page + 1





if __name__ == '__main__':
    login('your qq','your password')
    for page in range(1, 126):
        delete('我司日志')

结束了,好了,现在要去帮小姐姐删邮件去了。右手拍座椅,转180度,“I want you”。咦,他们人呢???

4.代码优化

既然他们走了,那我还是默默留下优化下代码吧。

'''
QQ 邮箱删除垃圾邮件
'''
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time
driver = webdriver.Chrome()

# 获取页面
driver.get("https://mail.qq.com/")

# 登录
def login(qq, password):
    driver.switch_to.frame("login_frame")
    # 可能会出现QQ快捷登录页面
    if driver.find_element_by_xpath('//*[@id="switcher_plogin"]'):
        driver.find_element_by_xpath('//*[@id="switcher_plogin"]').click()
    driver.find_element_by_xpath('//*[@id="u"]').send_keys(qq)
    driver.find_element_by_xpath('//*[@id="p"]').send_keys(password)
    driver.find_element_by_xpath('//*[@id="login_button"]').click()

def delete(del_text):
    # 防止太快,腾讯找我麻烦
    time.sleep(3)
    # 记录一下,本页需要删除的有邮件
    count = 0
    # for循环每一页的25个列表,判断打勾
    for i in range(1,26):
        try:
            span = f'//*[@id="div_showbefore"]/table[{i}]/tbody/tr/td[3]/table/tbody/tr/td[1]/nobr/span'
            input = f'//*[@id="div_showbefore"]/table[{i}]/tbody/tr/td[1]/input'
            if driver.find_element_by_xpath(span).text == del_text:
                count = count+1
                driver.find_element_by_xpath(input).click()
        except Exception as e:
            print(e.args)
    print("count:"+str(count))
    if count > 0:
        driver.find_element_by_xpath('//*[@id="quick_completelydel"]').click()
        # 切换到弹出框
        driver.switch_to.parent_frame()
        # 点击确定删除
        driver.find_element_by_xpath('//*[@id="QMconfirm_QMDialog_confirm"]').click()
        # 显示等待,等待取消按钮出现
        try:
            WebDriverWait(driver, 5).until(EC.presence_of_element_located((By.ID, 'QMconfirm_QMDialog_cancel')))
            driver.find_element_by_xpath('//*[@id="QMconfirm_QMDialog_cancel"]').click()
        except Exception as e:
            pass
    # 否则翻页
    else:
        # 回到主页面
        driver.switch_to.default_content()
        # 进入列表的mainFrame
        driver.switch_to.frame("mainFrame")
        WebDriverWait(driver, 20).until(EC.presence_of_element_located((By.ID, 'nextpage1')))
        driver.find_element_by_xpath('//*[@id="nextpage1"]').click()

if __name__ == '__main__':
    login('your qq','your password')
    WebDriverWait(driver, 20).until(EC.presence_of_element_located((By.ID, 'folder_1')))
    # 回到主页面
    driver.switch_to.default_content()
    # 点击左边 收件箱 按钮
    driver.find_element_by_xpath('//*[@id="folder_1"]').click()
    # 进入列表的mainFrame
    driver.switch_to.frame("mainFrame")
    for page in range(1, 126):
        delete('我司日志')
查看原文

赞 1 收藏 1 评论 0

啤酒就辣条 发布了文章 · 11月4日

搞明白synchronized和ReetrantLock

上一篇文章,我们熟悉了Java锁的分类。今天,来学习下Java中常用的悲观锁synchronized和ReetrantLock吧。学习使我快乐,哦耶!

synchronized

synchronized是什么?

synchronized关键字可以保证,一段时间内共享资源只能被一个线程所使用,或者说一段代码一段时间内只能被一个线程执行,并且共享资源对其他线程是可见的。

实际上,synchronized就是,某个线程拿到一个锁,锁住共享资源,当使用完,放开锁,让其他线程申请锁并使用共享资源。

synchronized锁的级别

synchronized作用在普通方法或者代码片段上时,锁为对象本身。作用在static方法或者代码片段上时,锁为类本身

synchronized的基本使用

我们设想一个卖票场景,有A、B两个售票窗口卖票,票池(共享资源)只有一个。
实验1

public class SellTicketRunnable implements Runnable {
    // 剩余票数
    static int ticket = 1000;
    @Override
    public void run() {
        for (int i=0;i<550;i++){
            sell();
        }
    }
    // 买票操作
    private synchronized void sell() {
        System.out.println(Thread.currentThread().getName()+"开始卖票");
        try {
            // 模拟卖票
            if (ticket <= 0){
                System.out.println(Thread.currentThread().getName()+"窗口通知,票卖完了~");
            }else {
                Thread.sleep(5);
                ticket--;
                System.out.println(Thread.currentThread().getName()+"出票成功,现在还有"+ticket+"张票");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"结束卖票");
    }
}

public static void main(String[] args) throws InterruptedException {
        // 代码示例 1 基本使用
        SellTicketRunnable sellTicketRunnable = new SellTicketRunnable();
        Thread thread_1 = new Thread(sellTicketRunnable,"A窗口");
        Thread thread_2 = new Thread(sellTicketRunnable,"B窗口");
        thread_1.start();
        thread_2.start();
        thread_1.join();
        thread_2.join();
        System.out.println("运行结束,剩下"+SellTicketRunnable.ticket+"张票");
}

首先创建售票类SellTicketRunnable,定义公告资源ticket为1000张票,我们每个窗口模拟卖票550张,如果发现票卖完了,就系统提示,否则票数减1。main方法开启两个线程,发现完美运行。发现票数最终为0,并且每个线程访问共享资源的时间内都是独享的。

A窗口开始卖票
A窗口窗口通知,票卖完了~
A窗口结束卖票
B窗口开始卖票
B窗口窗口通知,票卖完了~
B窗口结束卖票
运行结束,剩下0张票

synchronized对象级别的锁

刚才只生成了一个SellTicketRunnable,只有一把锁。那我们生成两个SellTicketRunnable对象,会不会有两把锁呢?
实验2

   SellTicketRunnable sellTicketRunnable = new SellTicketRunnable();
   SellTicketRunnable sellTicketRunnable_backups = new SellTicketRunnable();
   Thread thread_1 = new Thread(sellTicketRunnable,"A窗口");
   Thread thread_2 = new Thread(sellTicketRunnable_backups,"B窗口");
   thread_1.start();
   thread_2.start();
   thread_1.join();
   thread_2.join();
   System.out.println("运行结束,剩下"+SellTicketRunnable.ticket+"张票");
// 运行结果如下:
A窗口开始卖票
B窗口开始卖票
A窗口出票成功,现在还有999张票
A窗口结束卖票
A窗口开始卖票
B窗口出票成功,现在还有998张票
B窗口结束卖票
B窗口开始卖票
A窗口出票成功,现在还有997张票
A窗口结束卖票
...
运行结束,剩下-1张票

main方法改成上边所示。首先,访问共享资源的时间不再独享。A窗口还没访问完数据库呢,B窗口就去访问了。这最终导致票可能超卖。(就剩1张票了,A、B窗口同时卖出,同时更新共享资源)。当然这段代码你多运行几次才会出现剩余-1的情况,有时候可能为0,毕竟那么巧的事,不是每次都遇到哈。说明,此时锁是对象级别的

实际上,如果synchronized作用在对象级别上。内存中,对象的对象头会记录当前获取锁的线程,利用的是Monitor机制。

synchronized类级别的锁

实验3

public class SellTicketRunnablePlus implements Runnable {
    // 剩余票数
    static int ticket = 1000;
    @Override
    public void run() {
        for (int i=0;i<550;i++){
            sell();
        }
    }
    public void sell() {
        synchronized(SellTicketRunnablePlus.class){
            System.out.println(Thread.currentThread().getName()+"开始卖票");
            try {
                // 模拟卖票
                if (ticket <= 0){
                    System.out.println(Thread.currentThread().getName()+"窗口通知,票卖完了~");
                }else {
                    Thread.sleep(5);
                    ticket--;
                    System.out.println(Thread.currentThread().getName()+"出票成功,现在还有"+ticket+"张票");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"结束卖票");
        }
    }
}
// 运行结果
A窗口开始卖票
A窗口窗口通知,票卖完了~
A窗口结束卖票
B窗口开始卖票
B窗口窗口通知,票卖完了~
B窗口结束卖票
运行结束,剩下0张票

实验2主函数中的SellTicketRunnable类换成SellTicketRunnablePlusSellTicketRunnablePlus只是给sell方法内部,synchronized锁的是SellTicketRunnablePlus类。此时又是一把锁了,所以两个窗口又可以某段时间内独享共享资源了。

synchronized是重入锁

synchronized可以保证不同线程同一时间只能有有一个独享共享资源,比如说线程1持有了锁,线程2去申请锁的时候,发现线程1持有锁呢,所以线程2需要等会(线程阻塞)。那么线程1在持有锁的情况下,可以再申请一把同样的锁吗?
实验4

public class ReentryTest {
    public synchronized void outMethod(){
        innerMethod();
        System.out.println("这是外部方法,执行了");
    }
    private synchronized void innerMethod(){
        System.out.println("这是内部方法,执行了");
    }
}
// main方法
 ReentryTest reentryTest = new ReentryTest();
 reentryTest.outMethod();
//运行结果
这是内部方法,执行了
这是外部方法,执行了

当线程1执行outMethod方法时,获得了锁。outMethod调用innerMethod方法时,线程1又去申请了同一把锁,发现申请成功了。可重入锁是指同一个线程可以多次加同一把锁。

自JDK1.6开始,当只有两个线程竞争锁时,synchronized是轻量级锁,超过两个线程竞争的时候是重量级锁。关于锁的分类,请戳链接: Java锁分类原来是这个样子

在这里插入图片描述

ReetrantLock

synchronized是关键字,很多操作都是隐式的,比如说释放锁自旋次数等,都是虚拟机帮你搞定的。为了显示操作,并且拥有更强大的功能,ReetrantLock来了。

ReetrantLock基本使用

实验5

        ReentrantLock lock = new ReentrantLock();
        lock.lock();
        try{
            // 业务逻辑
        }catch (Exception e){
        }finally {
            lock.unlock();
        }

ReetrantLock需要手动申请锁和释放锁,分别为方法lockunlock

ReetrantLock重入性

synchronized一样,ReetrantLock也具备重入性。
实验6

        ReentrantLock lock = new ReentrantLock();
        int count = 0;
        for (int i = 1; i <= 3; i++) {
            lock.lock();
            System.out.println("说明获取锁"+ ++count +"次");
        }
        for (int i = 1; i <= 3; i++) {
            lock.unlock();
        }
//
说明获取锁1次
说明获取锁2次
说明获取锁3次

公平锁和非公平锁

ReetrantLock可以申请公平锁或者非公平锁(了解锁的分类:Java锁分类原来是这个样子)。

首先我们补充一个知识点,ReetrantLock是实现AQS机制的,就是说所有申请锁的线程,会被按需放到一个队列中,然后依次获取锁。公平锁保证了,获取锁的顺序性。

实验7

//主函数
        ReentrantLock lock = new ReentrantLock(true);
        for (int i=1;i<=5;i++){
            new Thread(new FairLockThread(lock),"第"+i+"个").start();
        }
// FairLockThread类
public class FairLockThread implements Runnable {
    ReentrantLock lock;
    public FairLockThread(ReentrantLock lock) {
        this.lock = lock;
    }
    @Override
    public void run() {
        for (int i = 1; i <= 2; i++) {
            lock.lock();
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "开始执行了");
            System.out.println(Thread.currentThread().getName() + ":" + lock.getQueueLength());
            lock.unlock();
        }
    }
}
// 结果
第1个开始执行了
第1个:4
第2个开始执行了
第2个:4
第3个开始执行了
第3个:4
第4个开始执行了
第4个:4
第5个开始执行了
第5个:4
第1个开始执行了
第1个:4
第2个开始执行了
第2个:3
第3个开始执行了
第3个:2
第4个开始执行了
第4个:1
第5个开始执行了
第5个:0

ReentrantLocknew的时候传入true,就是申请了一把公平锁。FairLockThread方法里面让一个线程执行两次申请锁、释放锁操作,并且模拟使用锁0.5秒。getQueueLength方法就是查看,当前队列中阻塞的线程数。可以看出,锁的两遍申请是按照顺序的,从1~5。从线程是也可以看出,没有哪个线程可以偷偷的自己两边都执行完。

还是实验7

ReentrantLock lock = new ReentrantLock(false);
// 结果
第2个开始执行了
第2个:4
第2个开始执行了
第2个:4
第1个开始执行了
第1个:3
第1个开始执行了
第1个:3
第3个开始执行了
第3个:2
第4个开始执行了
第4个:2
第4个开始执行了
第4个:2
第5个开始执行了
第5个:1
第5个开始执行了
第5个:1
第3个开始执行了
第3个:0

我们只需要将主函数,newReentrantLock的时候设置成false,此时申请的就是非公平锁了。再看运行结果,某个线程执行完第一遍,很大概率上就会执行第二遍。没有按照顺序执行,这是不公平的。

执行完一遍,然后紧接着执行第二遍,不用切换上下文,某线程一致使用CPU,这样效率更快的,所以非公平锁效率更高

ReetrantLock可中断,预防死锁问题

试想一下,如果线程1已经持有锁1,现在想拿锁2,然后就可以开心的结束了。线程2已经持有锁2,现在想拿锁1,然后就可以开心的结束了。这俩线程还愉快的碰面了,结果谁都不放手,谁都不能愉快的结束,于是乎,死锁就产生了。

实验8

public class InterruptThread implements Runnable{
    ReentrantLock firstLock;
    ReentrantLock secondLock;
    public InterruptThread(ReentrantLock firstLock, ReentrantLock secondLock) {
        this.firstLock = firstLock;
        this.secondLock = secondLock;
    }
    @Override
    public void run() {
        try {
            firstLock.lock();
            Thread.sleep(1000);
            secondLock.lock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            firstLock.unlock();
            secondLock.unlock();
            System.out.println(Thread.currentThread().getName()+"正常结束!");
        }
    }
}
// 主函数
 ReentrantLock lock = new ReentrantLock();
 ReentrantLock lock2 = new ReentrantLock();
 Thread a = new Thread(new InterruptThread(lock, lock2), "A");
 Thread b = new Thread(new InterruptThread(lock2, lock), "B");
 a.start();
 b.start();
//结果
没有结果...

以上,运行到电脑死机也不会结束了。如果我们在主函数最后一行后面加上一行

a.interrupt();

运行结果,放个图吧。
在这里插入图片描述
可以看出,虽然A牺牲掉了,但是由于A的中断(放弃持有锁1)。B顺利完成了!为小A默哀一分钟。。。

相同与不同

相同

  1. synchronizedReetrantLock都是悲观锁、可重入锁。

不同

  1. synchronized是隐士申请、释放锁,虚拟机层面维护。ReetrantLock是显示操作,代码维护。
  2. 在JDK1.6之前, synchronized性能极差,1.6之后,它俩性能差不多。
  3. ReetrantLock可中断,避免死锁产生。
  4. ReetrantLock可以申请公平锁或者非公平锁,可根据需求定制。

呜呼,从探索到验证,辣条君用了一天,小伙伴们点个赞再走吧。

在这里插入图片描述

查看原文

赞 0 收藏 0 评论 0

啤酒就辣条 发布了文章 · 11月4日

Java锁分类原来是这个样子

学了几天python,辣条君始终不忘自己其实是个Javaer。来,跟着辣条君一起看看Java锁是如何分类的吧。

Java锁是为了保证并发过程中,数据维护的准确性。

乐观锁与悲观锁

乐观锁:认为当前读取数据的时候,不会有线程去修改数据,所以不需要加锁。当更新数据的时候,首先查看数据和自己曾经记录的数据是否一致,如果一致,则更新之;如果不一致,采取一些手段,比如报错或者自旋(自旋后面会讲)。

举个例子,一个线程A读取账户余额时,不会加锁,读到20元,线程A账户记录更新为20元。然后线程A为账户余额增加5元,现在想把账户余额更新为25元,先去查看账户现在的数值为20元,和账户记录一致,就将账户余额直接更新为25元,同时将自己的账户记录更新为25元。过了一会儿,线程A又想给账户余额增加5元,于是拿着30元去更新账户余额。此时,发现账户余额为100元,和自己的账户记录(25元)不一致了,就报错(或者自旋),此次更新失败。

CAS(比较交换)就是一种常见的乐观锁实现方案,java.util.concurrent.atomic中的那些原子类就是通过CAS算法实现的。为了保证更新的原子性,原子类最终实质上是通过JNL调用了CPU的原子操作。CAS先天有两点不足:1、ABA问题。2、长时间的自旋会消耗过多的资源。

所以乐观锁多用于读数据多的场景,效率较高。

悲观锁:认为无论自己进行什么操作,那一瞬间都会有其他线程来污染数据,所以一定要加锁。

悲观锁很好理解,不需要加什么例子了,synchronizedLock都属于悲观锁。关于这两个类的使用,我想另写一篇博客,毕竟日常使用比较多嘛。悲观锁多用于写数据多的场景

在这里插入图片描述

自旋锁

上一节反复提到自旋,自旋究竟是个什么东东呢?

首先我们要知道,一个Java线程被阻塞,会放弃CPU使用权;被唤醒,会重新获得CPU使用权。这两个切换上下文的过程,是极其消耗资源的。如果,一个同步操作(线程占用锁)的时间极短,那需要用锁的线程可以先等一会儿,待会不用进行上下文切换,拿到锁直接执行,那岂不是极好的。这个等待操作就叫做自旋。

自旋操作一般会规定自旋次数,如果一定次数还是没有得到锁,那就放弃自旋,进行阻塞。为了更加提升效率,自适应自旋锁出现了,它不拘泥于固定的次数,而是根据以往经验,如果以前自旋一段时间可以得到锁,那么超过最大自旋数的时候,允许多自旋几次;如果以往经验总是失败,那么不一定非得到达最大自旋数,就直接进入自旋状态。

无锁、偏向锁、轻量级锁、重量级锁

根据切换资源消耗成本,可以将锁分为无锁、偏向锁、轻量级锁、重量级锁。

无锁:就是不对资源加锁,例如上面讲到的CAS算法,只是在更新的时候进行一下比较判断就好。

偏向锁:有一种理想的状态,一段时间内只有一个线程访问同步代码块,这样是不是连更新时比较的步骤都可以省略了。这种情况下可以挂上偏向锁,这样该线程在访问同步代码块的时候就不需要CAS操作了。当,有其他线程来访问共享资源的时候,偏向锁自动升级为轻量级锁。如果没有线程来打扰,只有当虚拟机运行到全局安全点的时候才能撤销偏向锁。

轻量级锁:当一个线程拥有轻量级锁,另一个线程想拥有这把锁,不会进入阻塞状态,而是先自旋,等待获得锁的机会。但是,当多个线程(至少两个)来获取这把锁时,这把锁会直接升级为重量级锁

重量级锁:当一个线程拥有重量级锁时,其他线程想要获取该锁,都会直接进入阻塞状态。在JDK1.6之前synchronized机制使用的时重量级锁,1.6版本之后开始使用轻量级锁偏向锁

在这里插入图片描述

公平锁和非公平锁

公平锁:当多个线程请求获取锁时,根据请求的先后顺序放到一个队列里,然后按顺序获取锁。此时,线程从阻塞到唤醒是需要上下文切换的。保证公平性,但是效率可能较低。

非公平锁:非公平锁,尝试被线程获取的时候,不一定从线程队列中获取,先看看此时有没有新的线程来获取本锁,如果有,直接把锁给该线程,不需要进行上下文切换。失去公平性,但是可能会提高效率。

可重入锁

可重入锁是指同一个线程可以多次加同一把锁。ReentrantLocksynchronized都属于可重入锁

public class MyTest {
    // 方法嵌套
    public synchronized void outThing() {
        // do someting
        innerThing();
    }
    public synchronized void innerThing() {
        // do something
    }
}

看上面这种情况,方法嵌套通过synchronized机制2次获取了对象的锁(Monitor)。如果是非可重入锁,一定会发生死锁。

共享锁和独享锁

共享锁:一个线程给共享资源加上共享锁后,其他线程还可以给这个共享资源加上其他的共享锁。比如常见的读锁。

独享锁:一个资源被加上独享锁后,就不能添加其他锁了。比如常见的写锁。

共享锁和独享锁在mysql层面也是通用概念。

小结

有了这些Java锁的概念,再去看代码就方便多了。接下来会好好研究下synchronizedReentrantLock

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 18 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2019-05-21
个人主页被 761 人浏览