开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

SpringBoot 整合ES 解放你的mysql 引言 安

发表于 2021-11-19

代码已经上传到码云:gitee.com/lezaiclub/s…欢迎白嫖

引言

平时我们存储数据用的最多的就是mysql,在前面的文章里我也分享过很多关于mysql的知识,今天我们来集成另外一种数据存储系统ES,它是一款NoSql型数据库,主要使用场景有商品搜索,文章搜索等,关键词就是搜索。 我们先简单介绍下ES。 ​

Elasticsearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。Elasticsearch是用Java语言开发的,并作为Apache许可条款下的开放源码发布,是一种流行的企业级搜索引擎。Elasticsearch用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便 ​

这是来自于百度百科的解释,其实我们就直接把它理解成搜索引擎就行了,接下来,我们就开始快速集成,然后上手使用吧! ​

安装ES

本篇介绍的是如何通过docker安装es,提前你得有docker环境 ​

拉取ES镜像

1
复制代码docker pull elasticsearch:7.6.2

创建挂载目录

1
2
3
bash复制代码mkdir -p /Users/lezai/docker/volumes/data/elasticsearch/config/
mkdir -p /Users/lezai/docker/volumes/data/elasticsearch/data
mkdir -p /Users/lezai/docker/volumes/data/elasticsearch/plugins

编辑配置文件

1
arduino复制代码vim /Users/lezai/docker/volumes/data/elasticsearch/config/elasticsearch.yml

elasticsearch.yml

1
makefile复制代码http.host: 0.0.0.0

运行执行脚本

1
2
3
4
5
6
7
bash复制代码docker run --name elasticsearch -p 9200:9200  -p 9300:9300 \
 -e "discovery.type=single-node" \
 -e ES_JAVA_OPTS="-Xms256m -Xmx256m" \
 -v /Users/lezai/docker/volumes/data/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
 -v /Users/lezai/docker/volumes/data/elasticsearch/data:/usr/share/elasticsearch/data \
 -v /Users/lezai/docker/volumes/data/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
 -d elasticsearch:7.6.2

安装成功后

在浏览器输入 http://localhost:9200,如果出现以下内容,则代表安装成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
json复制代码{
  "name" : "43e2638f84ac",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "hZKT7NQNRl-Dg2Xrb3isGg",
  "version" : {
    "number" : "7.6.2",
    "build_flavor" : "default",
    "build_type" : "docker",
    "build_hash" : "ef48eb35cf30adf4db14086e8aabd07ef6fb113f",
    "build_date" : "2020-03-26T06:34:37.794943Z",
    "build_snapshot" : false,
    "lucene_version" : "8.4.0",
    "minimum_wire_compatibility_version" : "6.8.0",
    "minimum_index_compatibility_version" : "6.0.0-beta1"
  },
  "tagline" : "You Know, for Search"
}

配置项目

添加必要依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
xml复制代码    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!--        lombok 不用写写get和set,不是本部分必备包-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.18</version>
        </dependency>
    </dependencies>

添加实体

其实这里的实体对应的就是ES索引 @Document(indexName = “sys_user”) 代表映射的是sys_user 索引 @Field(type = FieldType.Keyword) 代表字段应设在es中是keyword类型 ​

这里就不介绍过多的ES用法 ​

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
kotlin复制代码package com.aims.springbootes.entity;

import lombok.Builder;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

import java.io.Serializable;
import java.util.List;

@Document(indexName = "sys_user")   //文档
@Data
@Builder
public class SysUser implements Serializable {
    @Id //主键
    private String id;  //ES中id不能定义为Long
    private String username;
    private String password;
    private int level;
    @Field(type = FieldType.Keyword)
    private List<String> roles;
}

添加操作es的dao

ElasticsearchRepository 遵循Spring-data的规范,所以操作es,就相当于我们使用jpa去操作数据库一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kotlin复制代码package com.aims.springbootes.dao;

import com.aims.springbootes.entity.SysUser;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 * 第一种方式,类似于JPA,编写一个ElasticsearchRepository
 *  第一个泛型为Bean的类型
 *  第二个泛型为Bean的主键类型
 */
@Repository
public interface SysUserDao extends ElasticsearchRepository<SysUser,String> {

}

添加配置信息

1
2
3
4
less复制代码spring:
    elasticsearch:
      rest:
        uris: http://localhost:9200

编写test,测试添加数据到ES中

这里通过往ES中塞入1000条数据,我们运行下,但是我们现在不知道有没有插入进去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
csharp复制代码    @Test
    public void testInsert() {
        List<String> list = new ArrayList<>();
        list.add("teacher");
        list.add("student");
        list.add("admin");
        list.add("leader");
        for (int i = 0; i < 1000; i++) {
            int toIndex = new Random(1).nextInt(4);
            SysUser build = SysUser.builder()
                    .password("123456")
                    .username("AI码师")
                    .level(i)
                    .roles(list.subList(0, toIndex))
                    .build();
            sysUserDao.save(build);
        }
        System.out.printf("结束");
    }

在编写一个查询的,来验证是否插入成功

1
2
3
4
5
6
7
scss复制代码    @Test
    public void testFindAll(){
        Iterable<SysUser> all = sysUserDao.findAll();
        all.forEach((sysUser)->{
            System.out.printf(sysUser.getId());
        });
    }

到这已经集成了ES,更多Spring-Data语法可以参考JPA的写法,在IDEA中会有很多智能提示,帮助你写的。 ​

集成Spring-data-es后的思考

使用spring-data-es 提供的ElasticsearchRepository 只能进行简单的增删改查操作,如果碰到一些稍微复杂的聚合操作,他就很难应付了,所以这里有几个建议: ​

  • 如果只需要做简单增删改查操作,直接继承ElasticsearchRepository即可
  • 如果项目中有非常复杂的查询或聚合操纵,可以使用结合ElasticsearchRestTemplate做一些复杂的操作,这个包不需要再引用其他依赖,已经集成在spring-data-es里面了。

​

如何利用客户端快速编写ES 语句

这里我要介绍一款软件 kibana,它和es是老组合了,通过它能够直接连接es,直接在页面编写ES语句,值得一提的是它的语法智能提示简直不要太棒了

docker安装kibana

拉取镜像

这个一定要和es的版本保持一致,防止api不兼容

1
复制代码docker pull kibana:7.6.2

创建挂载目录

1
arduino复制代码mkdir -p /Users/lezai/docker/volumes/data/kibana/config/

编辑配置文件

`vim /Users/lezai/docker/volumes/data/kibana/config/kibana.yml

1
2
3
4
5
6
7
8
makefile复制代码#
# ** THIS IS AN AUTO-GENERATED FILE **
#
# Default Kibana configuration for docker target
server.name: kibana
server.host: "0"
elasticsearch.hosts: [ "http://elasticsearch:9200" ]
xpack.monitoring.ui.container.elasticsearch.enabled: true

运行启动脚本

当然要先保证es已经启动了,否则kibana无法启动,因为要依赖前者

1
2
3
4
5
6
7
lua复制代码 docker run -d \
  --name=kibana \
  --restart=always \
  --link elasticsearch:elasticsearch \
  -p 5601:5601 \
  -v /Users/lezai/docker/volumes/data/kibana/config/kibana.yml:/usr/share/kibana/config/kibana.yml \
  kibana:7.6.2

等待一两分钟,打开浏览器 http://localhost:5601 如果出现下面这个,就再等一会 在这里插入图片描述

点击小扳手,打开客户端

在这里插入图片描述

在这里插入图片描述

输入查询语句

1
2
3
bash复制代码POST sys_user/_search
{
}

点击执行按钮,可以看到我们插入的数据 在这里插入图片描述

到这里,已经介绍完了如何集成es,以及如何使用kibana 快速编写sql,进行查询,咱们下期再见

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MySQL事务的实现原理之Undo Log的分析

发表于 2021-11-19

「这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战」

Undo Log日志是何许人也

MySQL事务中,主要的功能特性,依赖于 Undo log ,因为四个特性

  • 原子性, 持久性—-> Redo log
  • 隔离性 —–>MVCC机制和锁
  • 一致性 —–>Undo log

因为 Undo log 在Mysql事务实现中,主要有两方面,

    1. 多版本的并发事务控制-MVCC版本
    1. 回滚事务

undo Log的产生过程

在MySQL事务开始之前,比如我有一个查询的数据的功能, 在识别到当前可能会查询到这个表的数据后, 然后快照其中的数据, 如果 数据库的事务 发生回滚操作, 或者是数据库发生异常导致宕机或者是故障,这时候Undo log,对数据库中未提交的事务进行回滚操作, 从而保证数据库中, 数据查询到的数据的一致性。

本质: undo log 是 为了回滚事务存在。

Undo log日志属于逻辑日志,相比于Redo log 的物理日志,(操作性的),这个日志记录的是数据快照和当前数据 反向事务, 举个例子,

如果我先需要insert 一条数据 ,到用户表中,场景是我注册了一个新的用户,但是在这个事务,也就是说Mysql接到这个添加的事务请求开始时候,先进行数据快照,然后在运行的过程中,会添加一个undo log 日志是删除这条产生的数据 delete语句, 也即是说会回滚 事务–。

同理可得, 如果说是delete事务, undo log 就会产生 一条 insert的数据, 因为这个日志就是为了回滚日志产生的。

undo log 的MVCC的版本控制

mvcc多路并发的版本控制, 简单理解就是,为了标记数据的更新的版本,
创建了两个隐藏列—>

  • 保存行的创建版本
  • 保存行的 删除版本(更新,删除都可以)

其中对于,自己 InnoDb引擎,产生 Undo log也需要进行持久化的操作, 所以Undo lgo 也会产生Redo log ,由于Undo log 的完整性和可靠性需要Redo log 来保证,因此数据库奔溃的时候,

需要先做物理的Redo log数据恢复,然后做Undo log回滚。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何为 Django 服务配置负载均衡

发表于 2021-11-19

k现在的 Web 服务有一个很重要的性能指标叫 QPS,QPS 的全称是 Queries Per Second 意思是“每秒查询率”,是一台服务器每秒能够相应的查询次数,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。 QPS 越高,说明并发度高,服务器每秒可以服务更多的用户。

Python 爱好者多数会选择 Django 来开发 Web 应用。但是 Python 多线程的性能并不是很高,如何提升 Django 服务的并发性和可用性呢? 一个很简单的办法就是负载均衡,本文分享如何为 Django 服务配置负载均衡。

所谓负载均衡不难理解,就是我们会有多个后端服务,一般分配在多个服务器上,前端的请求会被均衡的分配到后端的服务上,示意图如下所示:

负载均衡器接收来自客户端的请求,然后将请求转发到其中一台服务器。服务器然后将所需的 HTML 内容或资源返回给负载均衡器,然后负载均衡器将其转发回客户端。

有多种负载均衡器可供选择,例如 Nginx、Apache、Tomcat 和 HAProxy,你可以选择其中任何一个,但现在我将只展示如何在 Windows 上使用 Nginx。

下载 nginx

首先,到 nginx 的下载地址nginx.org/en/download…,它会显示如下页面:

然后点击任一版本的 Nginx,我推荐使用稳定版,但其实无所谓。下载后,将其解压缩到任何位置(在您的桌面或文档上即可)。解压后,进入刚才安装的 nginx 目录,在目录地址输入“cmd”,如下图。并且会出现命令提示符。

在命令提示符下,输入“start nginx”。完成后,您可以通过打开浏览器并在 url 栏中输入“localhost”来检查 nginx 是否正常工作。如果在输入“localhost”后出现下面的屏幕,Nginx 可以正常工作。

一旦 Nginx 完美运行,我们就可以配置 Nginx 以将负载分配到我们的服务器,我这里用 Django 的 runserver 作为服务进行演示。

配置 nginx

进入nginx’目录,打开“conf”文件夹,然后你应该看到“nginx.conf”文件。在您熟悉的任何文本编辑器上打开它,我现在将使用 Visual Studio Code,默认情况下 nginx.conf 的内容如下所示:

只需要注释掉 http 部分或者直接删除它,添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
nginx复制代码http {
upstream myproject {
server localhost:8000;
server localhost:8001;
server localhost:8002;
server localhost:8003;
}

server {
listen 80;
server_name localhost;
location / {
proxy_pass http://myproject;
}
}
}

这段配置的意思为 nginx 会监听本机的 80 端口,一旦有访问,就会随机分配到以下四个服务进行请求:

1
2
3
4
sh复制代码    server localhost:8000;
server localhost:8001;
server localhost:8002;
server localhost:8003;

请确保开启 4 个终端来模拟 4 个服务,每个终端都执行这个命令,后面跟上自己的端口号:

1
sh复制代码python manage.py runserver {port}

配置 Django

不过你可能很想知道我这个请求到底分配给了谁,那就在 views.py 中加入这个方法:

1
2
3
4
python复制代码from django.http import HttpResponse
def nginx_view(request):
html = "<h1>Using port " + request.META["SERVER_PORT"] + "<h1>"
return HttpResponse(html)

然后在对应的 urls.py 上配置好路径 / ,来映射到上面的视图函数:

1
2
3
4
5
6
python复制代码from django.contrib import admin
from django.urls import path
from .views import nginx_view
urlpatterns = [
path('test_load_banlance/', nginx_view),
]

验证

打开浏览器,地址栏输入 localhost/story10 回车,会发现如下页面:

然后关闭 8000 服务,再次刷新浏览器,会发现负载均衡器已经转发至 8001:

是不是很酷呢?

如果你持续刷新,它会在剩余的服务中进行随机循环请求,也就是说每当刷新一次,就会随机换一个服务器进行请求。当然了,你可以参考 nginx 文档对这种选择后端服务的方式进行修改。

最后的话

本文分享了 nginx 作为负载均衡器的最简单配置,以及如何在 Django 中检测访问的是哪一个服务,都比较简单,如果有收获还请点赞、在看、关注支持。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

总结Map集合的遍历方式及其效率情况

发表于 2021-11-19

「这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战」

前言

  大家好,大家在项目中经常使用到Map集合来封装数据,正好近期也在使用,当遍历Map中的数据的时候,他们的效率是怎么样的呢,近期就进行了一下介绍。

Map

  Map供了一个更通用的元素存储方法,Map接口存储一组键值对象,提供key(键)到value(值)的映射。Map 集合中的每一个元素都包含一个键对象和一个值对象。

初始化数据

  首先为了便于介绍Map的遍历和他们的执行效率,本次测试先初始化一部分数据,本次将基于四种方式针对Map集合进行遍历,并测试他们的效率。四种方式分别是:Map.keySet()、map.entrySet()、Iterator迭代器、map.values()这四种方式进行。介绍方式,再进行性能比较。构造的测试数据如下:

1
2
3
4
5
js复制代码
Map<String, String> map = new HashMap<String, String>();
map.put("1", "掘金");
map.put("2", "代码不止");
map.put("3", "掘金不停");

Map.keySet()

  第一种介绍基于Map.keySet遍历key和value的方式遍历Map中的元素。

1
2
3
js复制代码for (String key : map.keySet()) {
System.out.println("key= "+ key + " and value= " + map.get(key));
}

  Map.keySet()遍历Map集合元素输出结果如下:

图片.png

Iterator迭代器

  第二种方式采用Iterator迭代器使用iterator迭代器遍历key和value。测试代码如下:

1
2
3
4
5
6
js复制代码
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
System.out.println("通过Map.entrySet.key= " + entry.getKey() + " and value= " + entry.getValue());
}

  Iterator迭代器遍历Map集合元素输出结果如下:

图片.png

Map.entrySet()

第三种方式采用Map.entrySet遍历key和value的方式。测试代码如下:

1
2
3
4
js复制代码
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println("通过Map.entrySet遍历.key= " + entry.getKey() + " and value= " + entry.getValue());
}

  Map.entrySet()遍历Map集合元素输出结果如下:

图片.png

Map.values()

  第四种通过Map.values()的方式遍历Map元素,通过Map.values()遍历所有的value,但不能遍历key。测试代码如下:

1
2
3
js复制代码for (String v : map.values()) {
System.out.println("value= " + v);
}

  Map.values()遍历Map集合元素value值输出结果如下:

图片.png
通过以上四种方式遍历Map,第四种方式只能获取到Map的value值,前三种都能获取到key和value。大家熟悉了集中遍历方式,下面进行性能测试。

性能测试

以上介绍了4种Map的遍历方式,下面针对4种遍历的方式进行性能测试。看那种遍历性能比较好、效率高。
100条数据执行时间如下:

图片.png

1000条数据执行时间如下:

图片.png

10000条数据执行时间如下:

图片.png
100000条数据执行时间如下:

图片.png
1000000 条数据执行时间如下:
图片.png
10000000 条数据执行时间如下:
图片.png
数据表格统计如下:

执行方式 100条 1000条 10000条 100000条 1000000条 10000000条
Map.keySet() 2ms 10ms 54ms 414ms 5215ms 52741ms
Iterator 2ms 7ms 46ms 417ms 4735ms 48822ms
Map.entrySet() 2ms 11ms 51ms 439ms 4708ms 48186ms
Map.values() 0ms 9ms 41ms 385ms 4696ms 46896ms

  通过表格中的数量量和所使用的不同方式执行Map遍历所耗时时间的表格可以看出,在数量量小的时候,可以使用Iterator迭代器的方式效率比较高,在数据量多到一定程度之后,使用Map.entrySet()的方式能明显提升执行的效率。当然这些数据只是代码作者测试的时候产生的耗时,可能基于机器等外界条件,会有部分误差,仅供大家参考学习。

结语

  好了,以上就是总结的Map集合的遍历方式及其效率情况,感谢您的阅读,希望您喜欢,如对您有帮助,欢迎点赞收藏。如有不足之处,欢迎评论指正。下次见。

  作者介绍:【小阿杰】一个爱鼓捣的程序猿,JAVA开发者和爱好者。公众号【Java全栈架构师】维护者,欢迎关注阅读交流。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

使用AOP记录日志

发表于 2021-11-19

@Aspect注解将表示它是一个切面

@Component表示它是一个Spring的组件

切片Aspect,既然Spring那么支持AOP,就肯定都能拿。有人会问如何拿原始的HTTP请求和响应的信息,通过以下代码应该拿得到啊哈哈哈哈

ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

HttpServletRequest request = attributes.getRequest();

1
2
3
4
5
6
7
8
xml复制代码<aop:config>
<!-- order:指定切面bean的优先级,值越小,优先级越高 -->
<aop:aspect id="fourAdviceAspect" ref="fourAdviceBean" order="2">
<aop:around method="processTask" pointcut="execution(* com.wangjun.aop.xml.*.*(..))"/>
<aop:before method="authority" pointcut="execution(* com.wangjun.aop.xml.*.*(..))"/>
<aop:after-returning method="log" returning="rvt" pointcut="execution(* com.wangjun.aop.xml.*.*(..))"/>
</aop:aspect>
</aop:config>

其中aop:aspect/标签就是切面,此标签下面的aop:around/、aop:before/这些就是增强处理,那么在哪里进行增强处理呢?pointcut属性就定义了切入点,也就是在哪里进行增强处理。这里的表达式比如execution(* com.wangjun.aop.xml..(..))含义如下:

指定在com.wangjun.aop.xml包中任意类方法;

第一个表示返回值不限,第二个表示类名不限;

第三个*表示方法名不限,圆括号中的(..)表示任意个数、类型不限的形参。

使用场景

日志记录、审计、声明式事务、安全性和缓存等。

AspectJ和Spring AOP的区别

正好代表了实现AOP的两种方式:

AspectJ是静态实现AOP的,即在编译阶段对程序进行修改,需要特殊的编译器,具有较好的性能;

Spring AOP是动态实现AOP的,即在运行阶段动态生成AOP代理,纯java实现,因此无需特殊的编译器,但是通常性能较差。

案例:记录日志

业务日志表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
typescript复制代码package chin.common;

/**
* 操作类型枚举
*/
public enum OpearteType {
QUERY("检索","QUERY"),
CREATE("录入","CREATE"),
UPDATE("维护","UPDATE"),
DELETE("删除","DELETE"),
REVIEW("审核","REVIEW"),;

private String name;
private String code;

private OpearteType(String name, String code) {
this.name = name;
this.code = code;
}

public static String getName(String code) {
for (OpearteType c : OpearteType.values()) {
if (c.getCode().equals(code)) {
return c.name;
}
}
return null;
}

public static String getCode(String name) {
for (OpearteType c : OpearteType.values()) {
if (c.getName().equals(name)) {
return c.code;
}
}
return null;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getCode() {
return code;
}

public void setCode(String code) {
this.code = code;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码package chin.annotation;

import chin.common.BusinessModule;

import java.lang.annotation.*;

/**
* 业务模块注解
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value = ElementType.TYPE)
public @interface BusinessModuleAttributes {
BusinessModule businessModule();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码package chin.annotation;

import chin.common.OpearteType;

import java.lang.annotation.*;

/**
* 业务日志注解
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value = ElementType.METHOD)
public @interface BusinessLogAttributes {
OpearteType opearteType();
String opearteDescription();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
ini复制代码package chin.aspect;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.beyondbit.ias.core.base.BaseController;
import com.beyondbit.ias.core.util.IPUtil;
import chin.annotation.BusinessLogAttributes;
import chin.annotation.BusinessModuleAttributes;
import chin.entity.BusinessLog;
import chin.service.BusinessLogService;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.ui.Model;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Date;

/**
* 业务日志切面
*/
@Aspect
@Component
public class BusinessLogAspect extends BaseController {

@Autowired
private BusinessLogService businessLogService;

@Pointcut("@annotation(chin.annotation.BusinessLogAttributes)")
public void logPointCut() {

}

@Around("logPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

BusinessModuleAttributes moduleAttributes = point.getTarget().getClass().getAnnotation(BusinessModuleAttributes.class);

MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();

BusinessLogAttributes logAttributes = method.getAnnotation(BusinessLogAttributes.class);

//获取请求参数:
MethodSignature ms = (MethodSignature) point.getSignature();
//获取请求参数类型
String[] parameterNames = ms.getParameterNames();
//获取请求参数值
Object[] parameterValues = point.getArgs();
StringBuilder strParams = new StringBuilder();
//组合请求参数,进行日志打印
if (parameterNames != null && parameterNames.length > 0) {
for (int i = 0; i < parameterNames.length; i++) {
if (parameterNames[i].equals("bindingResult")) {
break;
}
if ((parameterValues[i] instanceof HttpServletRequest) || (parameterValues[i] instanceof HttpServletResponse)||(parameterValues[i] instanceof Model)||(parameterValues[i] instanceof MultipartFile)) {
strParams.
append("[").
append(parameterNames[i]).append("=").append(parameterValues[i])
.append("]");
} else {
try
{
strParams.
append("[").
append(parameterNames[i]).append("=")
.append(JSON.toJSONString(parameterValues[i], SerializerFeature.WriteDateUseDateFormat))
.append("]");
}
catch(Throwable throwable){
strParams.
append("[").
append(parameterNames[i]).append("=").append(parameterValues[i])
.append("]");
}

}
}
}

Object result = null;
BusinessLog log = new BusinessLog();
log.setUuid(java.util.UUID.randomUUID().toString());
log.setClientIP(IPUtil.getClientIp(request));
log.setBusinessModule(moduleAttributes.businessModule().getCode());
log.setOpearteType(logAttributes.opearteType().getCode());
log.setOpearteDescription(logAttributes.opearteDescription());
log.setOpeartor(super.getCurrentUser().getUserUid());
log.setOpeartorName(super.getCurrentUser().getName());
log.setOpearteDateTime(new Date());
log.setReqUrl(request.getRequestURL().toString());
log.setClazz(point.getTarget().getClass().getName());
log.setMethod(method.getName());
log.setParams(strParams.toString());
try {
// 执行方法
result = point.proceed();
log.setStatus("1");
} catch (Exception e) {

log.setStatus("0");
log.setException(e.getStackTrace().toString());
throw e;
} finally {
businessLogService.insertBusinessLog(log);
}
return result;
}

}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

马拉车算法解最长回文子串!Manacher

发表于 2021-11-19

这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战

一、写在前面

LeetCode 第一题两数之和传输门:听说你还在写双层for循环解两数之和?

LeetCode 第二题两数之和传输门:# 两个排序数组的中位数,“最”有技术含量的解法!

今天给大家分享的是LeetCode 数组与字符串 第三题:最长回文子串,为面试而生,期待你的加入。

二、今日题目

给定一个字符串 s,找到 s 中最长的回文子串。
你可以假设 s 的最大长度为1000。

示例:

1
2
3
4
5
6
7
8
9
10
python复制代码示例 1:

输入: "babad"
输出: "bab"
注意: "aba"也是一个有效答案。

示例 2:

输入: "cbbd"
输出: "bb"

三、 分析

这个题目呢,之前参加校IT精英赛时遇到过,当时用c写的,呃···可惜,没写出来,所以咋看第一眼,有点心凉的感觉,当然今日之我已非彼时,早已深知回文字符是个啥玩意,比如日期:2018102,就是个回文字符串。

我是这样想的,要找字符串中最长的回文字符串,肯定就要先找出这个字符串的子串中那些是回文串,然后再求他们中最长的,就可以找到答案了,理清思路,我就开始兴奋的敲代码了,然而…

四、解题

  • 方法一:
    根据上面的思路,一步步来,时间复杂度,嗯,好像有O(n^4)…
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
python复制代码class Solution:
def longestPalindrome(self, s):
"""
:type s: str
:rtype: str
"""
len_s = len(s)
if len_s == 1:
return s
substring = ' '
substring_set = []
for i in range(len_s):
for j in range(len_s):
if i < j :
substring = s[i:j+1]
if self.is_Palindrome(substring) == 1:
substring_set.append(substring)
longest_s = ' '
if substring_set:
longest_s = substring_set[0]
else:
return s[0]
for i in range(len(substring_set) - 1):
if len(longest_s) < len(substring_set[i + 1]):
longest_s = substring_set[i + 1]
return longest_s
# 判断是否为回文字符
def is_Palindrome(self,str_t):
len_t = len(str_t)
for i in range(len_t):
if not str_t[i] == str_t[len_t - 1 - i]:
return 0
return 1
s = 'assas'
s0 = Solution()
l_Palindrome = s0.longestPalindrome(s)
print(l_Palindrome)
  • 提交结果:

提交之后,老半天,给出结果,运行超时(hhh,结果是对的,就是时间上还有待优化)
运行超时

  • 方法二:
    对于方法一,无话可说,思前想后,没个结果,百度,嗯,百度是个好东西。
    从从中心向外扩散,时间复杂度:O(n^2)

从中心向外扩散思想

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
python复制代码'''
思想参考:https://blog.csdn.net/qq_32354501/article/details/80084325
原作者用java实现
'''
class Solution:
# 类变量,类全局可调用
longest_s = '' # 最长回文字符串
maxLen = 0 # 长度

def longestPalindrome(self, s):
"""
:type s: str
:rtype: str
"""
len_s = len(s)
if len_s == 1: # 单字符串
return s
for i in range(len_s):
# 单核(奇数向两边延伸)
self.find_longest_Palindrome(s, i, i)
# 双核(偶数向两边延伸)
self.find_longest_Palindrome(s, i, i + 1)
return self.longest_s

# 找出最长的回文字符串
def find_longest_Palindrome(self, s, low, high):
# 从中间向两端延伸,判断是否为回文字符串的同时寻找最长长度
while low >= 0 and high < len(s):
if s[low] == s[high]:
low -= 1 # 向左延伸
high += 1 # 向右延伸
else:
break
# high - low - 1 表示当前回文字符串长度
if high - low - 1 > self.maxLen:
self.maxLen = high - low - 1
self.longest_s = s[low + 1:high]
  • 提交结果

在这里插入图片描述

测试数据:103组
运行时间:1256ms
击败人百分比:61.95%

  • 方法三:
    Manacher算法

时间复杂度:O(n)

算法只有遇到还没匹配的位置时才进行匹配,已经匹配过的位置不再进行匹配,因此大大的减少了重复匹配的步骤,对于S_new中的每个字符只进行一次匹配。所以该算法的时间复杂度为O(2n+1)—>O(n)(n为原字符串的长度),所以其时间复杂度依旧是线性的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
python复制代码class Solution:
def longestPalindrome(self, s):
"""
:type s: str
:rtype: str
"""
t0 = '#'.join(s)
s_new = '#' + t0 + '#'
len_new = []
sub = '' # 最长回文字符串
sub_midd = 0 # 表示在i之前所得到的Len数组中的最大值所在位置
sub_side = 0 # 表示以sub_midd为中心的最长回文子串的最右端在S_new中的位置
for i in range(len(s_new)):
if i < sub_side :
# i < sub_side时,在Len[j]和sub_side - i中取最小值,省去了j的判断
j = 2 * sub_midd - i
if j >= 2 * sub_midd - sub_side and len_new[j] <= sub_side - i:
len_new.append(len_new[j])
else:
len_new.append(sub_side - i + 1)
else:
# i >= sub_side时,从头开始匹配
len_new.append(1)
while ((i - len_new[i] >= 0 and i + len_new[i] < len(s_new)) and (s_new[i - len_new[i]] == s_new[i + len_new[i]])):
# s_new[i]两端开始扩展匹配,直到匹配失败时停止
len_new[i] = len_new[i] + 1

if len_new[i] >= len_new[sub_midd]:
sub_side = len_new[i] + i - 1
sub_midd = i
a0 = int((2 * sub_midd - sub_side)/2)
b0 = int(sub_side / 2)
sub = s[a0 :b0 ] # 在s中找到最长回文子串的位置
return sub
  • 提交结果
    方法三提交结果

测试数据:103组
运行时间:232ms
击败人百分比:72.36%

五、结语

坚持 and 努力 : 终有所获。

思想很复杂,

实现很有趣,

只要不放弃,

终有成名日。

—《老表打油诗》

下期见,我是爱猫爱技术的老表,如果觉得本文对你学习有所帮助,欢迎点赞、评论、关注我!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

记录一次错误使用 yum 命令的尴尬

发表于 2021-11-19

「这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战」

目录

前言

正文

结尾

前言

本来想安装 iotop 工具查看存储服务器的 IO 运行状态,啥也没想,直接安装:

yum install iotop -y

结果遇到了如下报错信息:

mirrors.aliyun.com/centos/%24r…: [Errno 14] HTTP Error 404 - Not Found

Trying other mirror.

mirrors.aliyuncs.com/centos/%24r…: [Errno 12] Timeout on mirrors.aliyuncs.com/centos/$rel…: (28, ‘Connection timed out after 30001 milliseconds’)

Trying other mirror.

mirrors.cloud.aliyuncs.com/centos/%24r…: [Errno 14] HTTP Error 404 - Not Found

Trying other mirror.

Error: Cannot retrieve repository metadata (repomd.xml) for repository: base. Please verify its path and try again

报错截图:

)

正文

猛的一看是以为镜像仓库的问题,自然而然想到了更新工具仓库,于是运行命令:

yum update

又出现了类似的错误:

mirrors.aliyun.com/centos/%24r…: [Errno 14] HTTP Error 404 - Not Found

Trying other mirror.

mirrors.aliyuncs.com/centos/%24r…: [Errno 12] Timeout on mirrors.aliyuncs.com/centos/$rel…: (28, ‘Connection timed out after 30001 milliseconds’)

Trying other mirror.

mirrors.cloud.aliyuncs.com/centos/%24r…: [Errno 14] HTTP Error 404 - Not Found

Trying other mirror.

Error: Cannot retrieve repository metadata (repomd.xml) for repository: base. Please verify its path and try again

报错截图:

)​

我突然意识到问题不一般,使用如下命令查看系统版本:

uname -a

输出结果:

Linux al-bj-boom-hb-backstage-01 4.15.0-147-generic #151-Ubuntu SMP Fri Jun 18 19:21:19 UTC 2021 x86_64 GNU/Linux

原来是 Ubuntu 系统,默认情况下,Ubuntu 系统是不支持 yum 命令,应该使用 apt。

但是也可以通过其他方式让 Ubuntu 系统支持 yum 命令,但这是后话了,自己也没有试过。

使用 apt 命令安装,工具 iotop 安装成功,使用如下命令查看服务器的 IO 状态:

iotop

执行结果如下:

)​

结尾

今天这件事纯属是一次乌龙,但是也说明了一个问题,做事情不要想当然,这样很容易陷入误区。

作者简介:大家好,我是 liuzhen007,是一位音视频技术爱好者,同时也是CSDN博客专家、华为云社区云享专家、签约作者,欢迎关注我分享更多音视频相关内容!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

学习设计模式——门面模式

发表于 2021-11-19

今天我们继续来学习前面没有学完的结构型设计模式中的一种:门面模式。门面模式也是一种不太常用的设计模式。所以,我们今天依旧是了解为主,暂时不去深入的学习。

概述

门面模式:(Facade Design Pattern)门面模式也叫外观模式,门面模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。

门面模式原理和实现也比较简单,应用场景也比较明确,主要在接口设计方面使用。

何时使用:

  • 解决易用性问题。
  • 解决性能问题。
  • 解决分布式事务问题。

UML 类图:

image.png
角色组成:

  1. 门面角色:客户端调用这个角色的方法。此角色知晓相关的子系统的功能和责任。正常情况下,本角色会将所有从客户端发来的请求委派到相应的子系统中去。
  2. 子系统角色:可以同时有一个或者多个子系统。每个子系统都不是一个单独的类,而是一个类的集合。每一个子系统都可以被客户端直接调用,或者被门面角色直接调用。子系统并不知道门面的存在,罪域子系统而言,门面仅仅是另一个客户端而已。

代码实现

子系统角色中的类:ModuleA.java、ModuleB.java、ModuleC.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public class ModuleA {
//示例方法
public void testA(){
System.out.println("调用ModuleA中的testA方法");
}
}


public class ModuleB {
//示例方法
public void testB(){
System.out.println("调用ModuleB中的testB方法");
}
}


public class ModuleC {
//示例方法
public void testC(){
System.out.println("调用ModuleC中的testC方法");
}
}

门面角色类:Facade.java

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class Facade {
//示例方法,满足客户端需要的功能
public void test(){
ModuleA a = new ModuleA();
a.testA();
ModuleB b = new ModuleB();
b.testB();
ModuleC c = new ModuleC();
c.testC();
}
}

客户端角色类:Client.java

1
2
3
4
5
6
7
8
9
java复制代码public class Client {

public static void main(String[] args) {

Facade facade = new Facade();
facade.test();
}

}

Facade类其实相当于A、B、C模块的外观界面,有了这个Facade类,那么客户端就不需要亲自调用子系统中的A、B、C模块了,也不需要知道系统内部的实现细节,甚至都不需要知道A、B、C模块的存在,客户端只需要跟Facade类交互就好了,从而更好地实现了客户端和子系统中A、B、C模块的解耦,让客户端更容易地使用系统。

解决的问题

1. 解决易用性问题

这个很容易理解,门面模式可以用来封装系统的底层实现,隐藏系统的复杂性,提供一组更加简单易用、更高层的接口。比如,Linux 系统调用函数就可以看作一种“门面”。它是 Linux 操作系统暴露给开发者的一组“特殊”的编程接口,它封装了底层更基础的 Linux 内核调用。

2. 解决性能问题

我们知道,App 和服务器之间是通过移动网络通信的,网络通信耗时比较多,为了提高 App 的响应速度,我们要尽量减少 App 与服务器之间的网络通信次数。

假设,完成某个业务功能(比如显示某个页面信息)需要“依次”调用 a、b、d 三个接口。如果我们发现过多的网络通信次数造成客户端响应速度慢,此时就可以利用门面模式,让后端服务器提供一个包裹 a、b、d 三个接口调用的接口 x。App 客户端调用一次接口 x 就可以了,减少了网络通信次数。

3. 解决分布式事务问题
假设在一个金融系统中,用户注册需要创建用户和创建用户钱包。用户注册需要支持事务,也就是创建用户和钱包两个操作,要么都成功,要么都失败,不能一个成功,一个失败。
要想两个接口调用在一个事务中执行,是比较难以实现的。此时,我们就可以使用门面模式,将两个接口操作实现放在一个接口中,并使用 Spring 框架提供的事务来控制。

总结

与代理模式的区别

在学习门面模式时,会发现它不仅与代理模式很像,与装饰器模式也很类似;

相似点:

  • 都引入了中介类(对象)
  • 中介对象都引用并把功能委托给了原对象
  • 都起到了”代理”功能

区别

  • 代理侧重于对原对象的访问控制(当然也可以不是控制而是功能增强)
  • 代理与原类实现相同的抽象(相同接口或直接继承原业)
  • 代理只代理一个类
  • 门面侧重于功能整合(多个小系统或小对象整合成一个功能丰富的大对象)
  • 门面可以与子系统具有不同的抽象(具有不同的接口,可以对方法重新起名)
  • 门面代理的是一系列类

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Bean的生命周期

发表于 2021-11-19

这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战

Spring作为当前Java最流行、最强大的轻量级框架,受到了程序员的热烈欢迎。因此在面试中也常常问及该框架的相关问题。本文将讲解常见面试题【Spring Bean的生命周期】。我们通常使用ApplicationContext作为Spring容器。这里,我们讲的也是 ApplicationContext中Bean的生命周期。而实际上BeanFactory也是差不多的,只不过处理器需要手动注册。

image.png

image.png

  1. Bean 容器找到配置⽂件中 Spring Bean 的定义。
  2. Bean 容器利⽤ Java Reflection API 创建⼀个Bean的实例。
  3. 如果涉及到⼀些属性值 利⽤ set() ⽅法设置⼀些属性值。
  4. 如果 Bean 实现了 BeanNameAware 接⼝,调⽤setBeanName() ⽅法,传⼊Bean的名字。
  5. 如果 Bean 实现了 BeanClassLoaderAware 接⼝,调⽤ setBeanClassLoader() ⽅法,传⼊ClassLoader 对象的实例。
  6. 与上⾯的类似,如果实现了其他 *.Aware 接⼝,就调⽤相应的⽅法。
  7. 如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执⾏ postProcessBeforeInitialization() ⽅法
  8. 如果Bean实现了 InitializingBean 接口,执行 afterPropertiesSet() ⽅法。
  9. 如果 Bean 在配置⽂件中的定义包含 init-method 属性,执⾏指定的⽅法。
  10. 如果有和加载这个 Bean的 Spring 容器相关的 BeanPostProcessor 对象,执⾏ postProcessAfterInitialization() ⽅法
  11. 当要销毁 Bean 的时候,如果 Bean 实现了 DisposableBean 接⼝,执⾏ destroy() ⽅法。
  12. 当要销毁 Bean 的时候,如果 Bean 在配置⽂件中的定义包含 destroy-method 属性,执⾏指定的⽅法。

简单概括:

  1. 实例化:配置⽂件找定义,利用反射建实例。
  2. 属性赋值:有值则用set()设置,再看有无Awar,如有则调相关set()。
  3. 初始化:
    如果有以下配置,则将执行对应method。
    若载BeanPostProcessor(),执⾏ postProcessBeforeInitialization() 。
    如有实现InitializingBean ,行 afterPropertiesSet()
    配置⽂件有定义,相关 init-method 属性的,则将执⾏指定⽅法。
    如有加载BeanPostProcessor 对象,执⾏ postProcessAfterInitialization() ⽅法

————这里就可以使用这个bean了————

  1. 销毁:
    如果实现DisposableBean 接⼝,执⾏ destroy() ⽅法。
    配置⽂件有定义,相关 destroy-method 属性的,执⾏指定的⽅法。

图片来源: www.cnblogs.com/zrtqsk/p/37…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

学习 Elasticsearch 的第15天

发表于 2021-11-19

这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战

解决并发问题

当我们允许多个人 同时 重命名文件或目录时,问题就来了。 设想一下,你正在对一个包含了成百上千文件的目录 /clinton 进行重命名操作。 同时,另一个用户对这个目录下的单个文件 /clinton/projects/elasticsearch/README.txt 进行重命名操作。 这个用户的修改操作,尽管在你的操作后开始,但可能会更快的完成。

以下有两种情况可能出现:

  • 你决定使用 version (版本)号,在这种情况下,当与 README.txt 文件重命名的版本号产生冲突时,你的批量重命名操作将会失败。
  • 你没有使用版本控制,你的变更将覆盖其他用户的变更。

问题的原因是 Elasticsearch 不支持 ACID 事务。 对单个文件的变更是 ACIDic 的,但包含多个文档的变更不支持。

如果你的主要数据存储是关系数据库,并且 Elasticsearch 仅仅作为一个搜索引擎 或一种提升性能的方法,可以首先在数据库中执行变更动作,然后在完成后将这些变更复制到 Elasticsearch。 通过这种方式,你将受益于数据库 ACID 事务支持,并且在 Elasticsearch 中以正确的顺序产生变更。 并发在关系数据库中得到了处理。

如果你不使用关系型存储,这些并发问题就需要在 Elasticsearch 的事务水准进行处理。 以下是三个切实可行的使用 Elasticsearch 的解决方案,它们都涉及某种形式的锁:

  • 全局锁
  • 文档锁
  • 树锁

全局锁

通过在任何时间只允许一个进程来进行变更动作,我们可以完全避免并发问题。 大多数的变更只涉及少量文件,会很快完成。一个顶级目录的重命名操作会对其他变更造成较长时间的阻塞,但可能很少这样做。

因为在 Elasticsearch 文档级别的变更支持 ACIDic,我们可以使用一个文档是否存在的状态作为一个全局锁。 为了请求得到锁,我们尝试 create 全局锁文档:

1
2
csharp复制代码PUT /fs/lock/global/_create
{}

如果这个 create 请求因冲突异常而失败,说明另一个进程已被授予全局锁,我们将不得不稍后再试。 如果请求成功了,我们自豪的成为全局锁的主人,然后可以继续完成我们的变更。一旦完成,我们就必须通过删除全局锁文档来释放锁:

1
csharp复制代码DELETE /fs/lock/global

根据变更的频繁程度以及时间消耗,一个全局锁能对系统造成大幅度的性能限制。 我们可以通过让我们的锁更细粒度的方式来增加并行度。

文档锁

我们可以使用前面描述相同的方法技术来锁定个体文档,而不是锁定整个文件系统。 我们可以使用 scrolled search 检索所有的文档,这些文档会被变更影响因此每一个文档都创建了一个锁文件:

1
2
3
4
5
bash复制代码PUT /fs/lock/_bulk
{ "create": { "_id": 1}}
{ "process_id": 123 }
{ "create": { "_id": 2}}
{ "process_id": 123 }
** lock 文档的 ID 将与应被锁定的文件的 ID 相同。
** process_id 代表要执行变更进程的唯一 ID。

如果一些文件已被锁定,部分的 bulk 请求将失败,我们将不得不再次尝试。

当然,如果我们试图再次锁定 所有 的文件, 我们前面使用的 create 语句将会失败,因为所有文件都已被我们锁定! 我们需要一个 update 请求带 upsert 参数以及下面这个 script ,而不是一个简单的 create 语句:

1
2
3
4
ini复制代码if ( ctx._source.process_id != process_id ) { 
assert false;
}
ctx.op = 'noop';
** process_id 是传递到脚本的一个参数。
** assert false 将引发异常,导致更新失败。
** 将 op 从 update 更新到 noop 防止更新请求作出任何改变,但仍返回成功。

完整的 update 请求如下所示:

1
2
3
4
5
6
7
8
9
bash复制代码POST /fs/lock/1/_update
{
"upsert": { "process_id": 123 },
"script": "if ( ctx._source.process_id != process_id )
{ assert false }; ctx.op = 'noop';"
"params": {
"process_id": 123
}
}

如果文档并不存在, upsert 文档将会被插入—​和前面 create 请求相同。 但是,如果该文件 确实 存在,该脚本会查看存储在文档上的 process_id 。 如果 process_id 匹配,更新不会执行( noop )但脚本会返回成功。 如果两者并不匹配, assert false 抛出一个异常,你也知道了获取锁的尝试已经失败。

一旦所有锁已成功创建,你就可以继续进行你的变更。

之后,你必须释放所有的锁,通过检索所有的锁文档并进行批量删除,可以完成锁的释放:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bash复制代码POST /fs/_refresh 

GET /fs/lock/_search?scroll=1m
{
"sort" : ["_doc"],
"query": {
"match" : {
"process_id" : 123
}
}
}

PUT /fs/lock/_bulk
{ "delete": { "_id": 1}}
{ "delete": { "_id": 2}}
** refresh 调用确保所有 lock 文档对搜索请求可见。
** 当你需要在单次搜索请求返回大量的检索结果集时,你可以使用 scroll 查询。

文档级锁可以实现细粒度的访问控制,但是为数百万文档创建锁文件开销也很大。 在某些情况下,你可以用少得多的工作量实现细粒度的锁定,如以下目录树场景中所示。

树锁

在前面的例子中,我们可以锁定的目录树的一部分,而不是锁定每一个涉及的文档。 我们将需要独占访问我们要重命名的文件或目录,它可以通过 独占锁 文档来实现:

1
json复制代码{ "lock_type": "exclusive" }

同时我们需要共享锁定所有的父目录,通过 共享锁 文档:

1
2
3
4
json复制代码{
"lock_type": "shared",
"lock_count": 1
}
** lock_count 记录持有共享锁进程的数量。

对 /clinton/projects/elasticsearch/README.txt 进行重命名的进程需要在这个文件上有 独占锁 , 以及在 /clinton 、 /clinton/projects 和 /clinton/projects/elasticsearch 目录有 共享锁 。

一个简单的 create 请求将满足独占锁的要求,但共享锁需要脚本的更新来实现一些额外的逻辑:

1
2
3
4
ini复制代码if (ctx._source.lock_type == 'exclusive') {
assert false;
}
ctx._source.lock_count++
** 如果 lock_type 是 exclusive (独占)的,assert 语句将抛出一个异常,导致更新请求失败。
** 否则,我们对 lock_count 进行增量处理。

这个脚本处理了 lock 文档已经存在的情况,但我们还需要一个用来处理的文档还不存在情况的 upsert 文档。 完整的更新请求如下:

1
2
3
4
5
6
7
8
9
bash复制代码POST /fs/lock/%2Fclinton/_update 
{
"upsert": {
"lock_type": "shared",
"lock_count": 1
},
"script": "if (ctx._source.lock_type == 'exclusive')
{ assert false }; ctx._source.lock_count++"
}
** 文档的 ID 是 /clinton ,经过URL编码后成为 %2fclinton 。
** upsert 文档如果不存在,则会被插入。

一旦我们成功地在所有的父目录中获得一个共享锁,我们尝试在文件本身 create 一个独占锁:

1
2
bash复制代码PUT /fs/lock/%2Fclinton%2fprojects%2felasticsearch%2fREADME.txt/_create
{ "lock_type": "exclusive" }

现在,如果有其他人想要重新命名 /clinton 目录,他们将不得不在这条路径上获得一个独占锁:

1
2
bash复制代码PUT /fs/lock/%2Fclinton/_create
{ "lock_type": "exclusive" }

这个请求将失败,因为一个具有相同 ID 的 lock 文档已经存在。 另一个用户将不得不等待我们的操作完成以及释放我们的锁。独占锁只能这样被删除:

1
bash复制代码DELETE /fs/lock/%2Fclinton%2fprojects%2felasticsearch%2fREADME.txt

共享锁需要另一个脚本对 lock_count 递减,如果计数下降到零,删除 lock 文档:

1
2
3
ini复制代码if (--ctx._source.lock_count == 0) {
ctx.op = 'delete'
}
** 一旦 lock_count 达到0, ctx.op 会从 update 被修改成 delete 。

此更新请求将为每级父目录由下至上的执行,从最长路径到最短路径:

此更新请求将为每级父目录由下至上的执行,从最长路径到最短路径:

1
2
3
4
bash复制代码POST /fs/lock/%2Fclinton%2fprojects%2felasticsearch/_update
{
"script": "if (--ctx._source.lock_count == 0) { ctx.op = 'delete' } "
}

树锁用最小的代价提供了细粒度的并发控制。当然,它不适用于所有的情况—​数据模型必须有类似于目录树的顺序访问路径才能使用。

这三个方案—​全局、文档或树锁—​都没有处理锁最棘手的问题:如果持有锁的进程死了怎么办?

一个进程的意外死亡给我们留下了2个问题:

  • 我们如何知道我们可以释放的死亡进程中所持有的锁?
  • 我们如何清理死去的进程没有完成的变更?

这些主题超出了本书的范围,但是如果你决定使用锁,你需要给对他们进行一些思考。

当非规范化成为很多项目的一个很好的选择,采用锁方案的需求会带来复杂的实现逻辑。 作为替代方案,Elasticsearch 提供两个模型帮助我们处理相关联的实体: 嵌套的对象 和 父子关系 。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…274275276…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%