暂时未有相关云产品技术能力~
3、Centos7通过systemctl enble配置服务自启动在Centos7后,更推荐通过systemctl来控制服务。任务旧指令新指令使某服务自动启动Chkconfig --level 3 httpd onsystemctl enable httpd.service使某服务不自动启动chkconfig --level 3 httpd offsystemctl disable httpd.service检查服务状态service httpd statussystemctl status httpd.service (服务详细信息)systemctl is-active httpd.service(仅显示是否Active)显示所有已启动的服务chkconfig --listsystemctl list-unit-filessystemctl list-units --type=service启动某服务service httpd startsystemctl start httpd.service停止某服务service httpd stopsystemctl stop httpd.service重启某服务service httpd restartsystemctl restart httpd.service1、systemctl服务的目录介绍知道服务的管理是通过 systemd,而 systemd 的配置文件大部分放置于 /usr/lib/systemd/目录内。但是 Red Hat 官方文件指出, 该目录的文件主要是原本软件所提供的设置,建议不要修改!而要修改的位置应该放置于 /etc/systemd/system/目录内。详情查看:https://wizardforcel.gitbooks.io/vbird-linux-basic-4e/content/150.htmlCentos 系统服务脚本目录:/usr/lib/systemd/有系统(system)和用户(user)之分,如需要开机没有登陆情况下就能运行的程序,存在系统服务(system)里,即:/usr/lib/systemd/system/反之,用户登录后才能运行的程序,存在用户(user)里,服务以.service结尾。/usr/lib/systemd/user/2、建立kibana开机服务1)、建立kibana服务文件cd /etc/systemd/system/ vim kibana.service脚本内容:[Unit] Description=nginx After=network.target [Service] Type=forking User=nginx Group=nginx ExecStart=/etc/init.d/nginx start ExecReload=/etc/init.d/nginx restart ExecStop=/etc/init.d/nginx stop PrivateTmp=true [Install] WantedBy=multi-user.target注意⚠️:这里ExecStart、ExecReload、ExecStop的命令还是借助了上文在/etc/init.d目录下配置kibana脚本来实现。[Service]的启动、重启、停止命令全部要求使用绝对路径[Install]服务安装的相关设置,可设置为多用户参数说明:Description:描述服务After:描述服务类别[Service]服务运行参数的设置Type=forking是后台运行的形式User 服务启动用户Group 服务启动用户组ExecStart 为服务的具体运行命令ExecReload 为重启命令ExecStop 为停止命令PrivateTmp=True表示给服务分配独立的临时空间2)、赋予执行权限chmod 754 kibana.service依照上面的表格,权限组合就是对应权限值求和,如下:7 = 4 + 2 + 1 读写运行权限5 = 4 + 1 读和运行权限4 = 4 只读权限.这句命令的意思是将filename文件的读写运行权限赋予文件所有者,把读和运行的权限赋予群组用户,把读的权限赋予其他用户。3)、服务的启动、停止、开机启动查看服务状态//重新加载某个服务的配置文件,如果新安装了一个服务,归属于 systemctl 管理,要是新服务的服务程序配置文件生效,需重新加载 systemctl daemon-reload //查看服务状态 systemctl status kibana.service启动服务其中,disabled说明服务还没有开启开机自启动。开启服务开机启动systemctl enable kibana.service服务状态说明:systemctl status 服务名称状态说明loaded系统服务已经初始化完成,加载过配置active(running)正有一个或多个程序正在系统中执行, vsftpd就是这种模式atcive(exited)僅執行一次就正常結束的服務, 目前並沒有任何程序在系統中執行atcive(waiting)正在執行當中,不過還再等待其他的事件才能继续处理inactive服务关闭enbaled服务开机启动disabled服务开机不自启static服务开机启动项不可被管理failed服务开机启动项不可被管理常用服务文件1.nginx.service[Unit] Description=nginx - high performance web server After=network.target remote-fs.target nss-lookup.target [Service] Type=forking ExecStart=/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf ExecReload=/usr/local/nginx/sbin/nginx -s reload ExecStop=/usr/local/nginx/sbin/nginx -s stop [Install] WantedBy=multi-user.target2.mysql.service[Unit] Description=mysql After=network.target remote-fs.target nss-lookup.target [Service] Type=forking ExecStart=/usr/local/mysql/support-files/mysql.server start #ExecReload=/usr/local/mysql/support-files/mysql.server restart #ExecStop=/usr/local/mysql/support-files/mysql.server stop #PrivateTmp=true [Install] WantedBy=multi-user.target4.redis.service[Unit] Description=Redis After=network.target remote-fs.target nss-lookup.target [Service] Type=forking ExecStart=/usr/local/bin/redis-server /etc/redis.conf ExecStop=kill -INT `cat /tmp/redis.pid` User=www Group=www [Install] WantedBy=multi-user.target5.supervisord.service[Unit] Description=Process Monitoring and Control Daemon After=rc-local.service [Service] Type=forking ExecStart=/usr/bin/supervisord -c /etc/supervisord.conf SysVStartPriority=99 [Install] WantedBy=multi-user.target总结本文主要总结了Centos上配置开机自启动的3种方式方式一:直接在/etc/rc.d/rc.local中添加服务启动命令方式二:通过chkconfig配置服务自启动方式三:Centos7通过systemctl enble配置服务开机自启动鸟哥的Linux私房菜最详细的CentOS7设置自定义开机启动服务教程
前言在服务器上安装的各种中间件,一般都需要配置成开机自启动。但是有些中间件的安装过程中并没有提供相关配置开机自启动的说明文档。今天总结一下Centos下配置服务开机自启动的3种方式。一、Centos上配置开机自启动的几种方式方式一:直接在/etc/rc.d/rc.local中添加服务启动命令方式二:通过chkconfig配置服务自启动方式三:Centos7通过systemctl enble配置服务自启动二、实践演示1、在/etc/rc.d/rc.local中添加服务启动命令/etc/rc.d/rc.local脚本会在Centos系统启动时被自动执行,所以可以把需要开机后执行的命令直接放在这里。示例:配置开机启动apollovi /etc/rc.d/rc.local想简单点可以像上面这样直接将服务的启动命令添加到/etc/rc.d/rc.local中。也可以自己编写服务启动的脚本。由于重启时是以root用户重启,需要保证root用户有脚本执行权限。1)、编写服务启动的脚本vi /opt/script/autostart.sh#!/bin/bash /root/Downloads/docker-quick-start/docker-compose up -d2)、赋予脚本可执行权限(/opt/script/autostart.sh是你的脚本路径)chmod +x /opt/script/autostart.sh3)、打开/etc/rc.d/rc.local文件,在末尾增加如下内容/opt/script/autostart.sh3)、在centos7中,/etc/rc.d/rc.local的权限被降低了,所以需要执行如下命令赋予其可执行权限chmod +x /etc/rc.d/rc.local2、通过chkconfig配置在CentOS7之前,可以通过chkconfig来配置开机自启动服务。chkconfig相关命令:chkconfig –-add xxx //把服务添加到chkconfig列表 chkconfig --del xxx //把服务从chkconfig列表中删除 chkconfig xxx on //开启开机自动启动 chkconfig xxx off //关闭开机自动启动 chkconfig --list //查看所有chklist中服务 chkconfig --list xxx 查看指定服务chkconfig运行级别level和启动顺序的概念:chkconfig --list这里的0到6其实指的就是服务的level。–level<等级代号> 指定系统服务要在哪一个执行等级中开启或关毕。等级0表示:表示关机等级1表示:单用户模式等级2表示:无网络连接的多用户命令行模式等级3表示:有网络连接的多用户命令行模式等级4表示:不可用等级5表示:带图形界面的多用户模式等级6表示:重新启动比如如下命令://设定mysqld在等级3和5为开机运行服务 chkconfig --level 35 mysqld on //设置network服务开机自启动,会把2~5的等级都设置为on chkconfig network on表示开机启动配置成功。服务的启动顺序又指的什么呢?服务的启动顺序是指在服务器启动后服务启动脚本执行的顺序。以系统默认服务network说明:cat /etc/init.d/network其中 # chkconfig: 2345 10 90用来指定服务在各个level下的启动顺序。该配置的含义是network服务在2、3、4、5的level下的启动顺序是10,在1和6的level等级下的启动顺序是90。chkconfig配置的服务启动顺序最后都会在/etc/rc.d/目录下体现出来:cd /etc/rc.d/文件中脚本命名规则,首字母K表示关闭脚本,首字母S表示启用脚本,数字表示启动的顺序.chkconfig配置实例通常kibana的官方配置是没有介绍如何配置开机自启动的。这里我配置kibana开机自启动来说明。1、在/etc/init.d目录下,新建脚本kibanacd /etc/init.d vi kibana脚本内容如下:#!/bin/bash # chkconfig: 2345 98 02 # description: kibana KIBANA_HOME=/usr/local/kibana-6.2.4-linux-x86_64 case $1 in start) $KIBANA_HOME/bin/kibana & echo "kibana start" ;; stop) kibana_pid_str=`netstat -tlnp |grep 5601 | awk '{print $7}'` kibana_pid=`echo ${kibana_pid_str%%/*}` kill -9 $kibana_pid echo "kibana stopped" ;; restart) kibana_pid_str=`netstat -tlnp |grep 5601 | awk '{print $7}'` kibana_pid=${kibana_pid_str%%/*} kibana_pid=`echo ${kibana_pid_str%%/*}` kill -9 $kibana_pid echo "kibana stopped" $KIBANA_HOME/bin/kibana & echo "kibana start" ;; status) kibana_pid_str=`netstat -tlnp |grep 5601 | awk '{print $7}'` if test -z $kibana_pid_str; then echo "kibana is stopped" else pid=`echo ${kibana_pid_str%%/*}` echo "kibana is started,pid:"${pid} fi ;; *) echo "start|stop|restart|status" ;; esac注意⚠️:每个被chkconfig管理的服务需要在对应的init.d下的脚本加上两行或者更多行的注释。第一行告诉chkconfig缺省启动的运行级以及启动和停止的优先级。如果某服务缺省不在任何运行级启动,那么使用 - 代替运行级。第二行对服务进行描述,可以用\ 跨行注释。#!/bin/bash #chkconfig:2345 98 02 #description:kibana解释说明:配置kibana服务在2、3、4、5的level等级下脚本执行顺序是98,1、6的level等级下脚本执行顺序是01。2、增加脚本的可执行权限chmod +x kibana3、查看chkconfig listchkconfig --list4、把服务添加到chkconfig列表chkconfig --add kibana5、设置kibana服务自启动chkconfig kibana on //开启开机自动启动6、查看kibana服务自启动状态chkconfig --list kibana如果2~5都是on,就表明会自动启动了7、服务的启动、停止、重启和状态查看//查看服务状态 service kibana status //服务启动 service kibana start //服务停止 service kibana stop //服务重启 service kibana restart
前言在实际项目开发中,我们经常将Mysql作为业务数据库,ES作为查询数据库,用来实现读写分离,缓解Mysql数据库的查询压力,应对海量数据的复杂查询。这其中有一个很重要的问题,就是如何实现Mysql数据库和ES的数据同步,今天和大家聊聊Mysql和ES数据同步的各种方案。一、Mysql和ES各自的特点为什么选用MysqlMySQL 在关系型数据库历史上并没有特别优势的位置,Oracle/DB2/PostgreSQL(Ingres) 三老比 MySQL 开发早了 20 来年, 但是乘着 2000 年的互联网东风, LAMP 架构得到迅速的使用,特别在中国,大部分新兴企业的 IT 系统主数据沉淀于 MySQL 中。核心特点:开源免费、高并发、稳定、支持事务、支持SQL查询高并发能力:MySQL 内核特征特别适合高并发简单 SQL 操作 ,链接轻量化(线程模式),优化器、执行器、事务引擎相对简单粗暴,存储引擎做得比较细致稳定性好:主数据库最大的要求就是稳定、不丢数据,MySQL 内核特征反倒让其特点鲜明,从而达到很好的稳定性,主备系统也很早就 ready ,应对崩溃情况下的快速切换,innodb 存储引擎也保障了 MySQL 下盘稳定操作便捷:良好、便捷的用户体验(相比 PostgreSQL) , 让应用开发者非常容易上手 ,学习成本较低开源生态:MySQL 是一款开源产品,让上下游厂商围绕其构建工具相对简单,HA proxy、分库分表中间件让其实用性大大加强,同时开源的特质让其有大量的用户为什么选用 ESES 几个显著的特点,能够有效补足 MySQL 在企业级数据操作场景的缺陷,而这也是我们将其选择作为下游数据源重要原因核心特点:支持分词检索,多维筛选性能好,支持海量数据查询文本搜索能力:ES 是基于倒排索引实现的搜索系统,配合多样的分词器,在文本模糊匹配搜索上表现得比较好,业务场景广泛多维筛选性能好:亿级规模数据使用宽表预构建(消除 join),配合全字段索引,使 ES 在多维筛选能力上具备压倒性优势,而这个能力是诸如 CRM, BOSS, MIS 等企业运营系统核心诉求,加上文本搜索能力,独此一家开源和商业并行:ES 开源生态非常活跃,具备大量的用户群体,同时其背后也有独立的商业公司支撑,而这让用户根据自身特点有了更加多样、渐进的选择二、数据同步方案1.同步双写这是一种最为简单的方式,在将数据写到mysql时,同时将数据写到ES。伪代码:/** * 新增商品 */ @Transactional(rollbackFor = Exception.class) public void addGoods(GoodsDto goodsDto) { //1、保存Mysql Goods goods = new Goods(); BeanUtils.copyProperties(goodsDto,goods); GoodsMapper.insert(); //2、保存ES IndexRequest indexRequest = new IndexRequest("goods_index","_doc"); indexRequest.source(JSON.toJSONString(goods), XContentType.JSON); indexRequest.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); highLevelClient.index(indexRequest); }优点:1、业务逻辑简单2、实时性高缺点:1、 硬编码,有需要写入mysql的地方都需要添加写入ES的代码;2、 业务强耦合;3、 存在双写失败丢数据风险;4、 性能较差:本来mysql的性能不是很高,再加一个ES,系统的性能必然会下降。附:上面说的双写失败风险,包括以下几种:1) ES系统不可用;2) 程序和ES之间的网络故障;3) 程序重启,导致系统来不及写入ES等。针对这种情况,有数据强一致性要求的,就必须双写放到事务中来处理,而一旦用上事物,则性能下降更加明显。2.异步双写(MQ方式)针对多数据源写入的场景,可以借助MQ实现异步的多源写入,这种情况下各个源的写入逻辑互不干扰,不会由于单个数据源写入异常或缓慢影响其他数据源的写入,虽然整体写入的吞吐量增大了,但是由于MQ消费是异步消费,所以不适合实时业务场景。优点:1、性能高2、不易出现数据丢失问题,主要基于MQ消息的消费保障机制,比如ES宕机或者写入失败,还能重新消费MQ消息。3、多源写入之间相互隔离,便于扩展更多的数据源写入缺点:1、硬编码问题,接入新的数据源需要实现新的消费者代码3、系统复杂度增加:引入了消息中间件4、可能出现延时问题:MQ是异步消费模型,用户写入的数据不一定可以马上看到,造成延时。3.基于Mysql表定时扫描同步上面两种方案中都存在硬编码问题,也就是有任何对mysq进行增删改查的地方要么植入ES代码,要么替换为MQ代码,代码的侵入性太强。如果对实时性要求不高的情况下,可以考虑用定时器来处理,具体步骤如下:1、数据库的相关表中增加一个字段为timestamp的字段,任何crud操作都会导致该字段的时间发生变化;2、原来程序中的CURD操作不做任何变化;3、增加一个定时器程序,让该程序按一定的时间周期扫描指定的表,把该时间段内发生变化的数据提取出来;4、逐条写入到ES中。如下图所示:该方案的典型实现是借助logstash实现数据同步,其底层实现原理就是根据配置定期使用sql查询新增的数据写入ES中,实现数据的增量同步。具体实现可以参考:通过Logstash实现mysql数据定时增量同步到ES优点:1、不改变原来代码,没有侵入性、没有硬编码;2、没有业务强耦合,不改变原来程序的性能;3、Worker代码编写简单不需要考虑增删改查;缺点:1、时效性较差,由于是采用定时器根据固定频率查询表来同步数据,尽管将同步周期设置到秒级,也还是会存在一定时间的延迟。2、对数据库有一定的轮询压力,一种改进方法是将轮询放到压力不大的从库上。4.基于Binlog实时同步上面三种方案要么有代码侵入,要么有硬编码,要么有延迟,那么有没有一种方案既能保证数据同步的实时性又没有代入侵入呢?当然有,可以利用mysql的binlog来进行同步。其实现原理如下:具体步骤如下:1) 读取mysql的binlog日志,获取指定表的日志信息;2) 将读取的信息转为MQ;3) 编写一个MQ消费程序;4) 不断消费MQ,每消费完一条消息,将消息写入到ES中。优点:1、没有代码侵入、没有硬编码;2、原有系统不需要任何变化,没有感知;3、性能高;4、业务解耦,不需要关注原来系统的业务逻辑。缺点:1、构建Binlog系统复杂;2、如果采用MQ消费解析的binlog信息,也会像方案二一样存在MQ延时的风险。业界目前较为流行的方案:使用canal监听binlog同步数据到escanal ,译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费。说白了就是,根据Mysql的binlog日志进行增量同步数据。要理解canal的原理,就要先了解mysql的主从复制原理:1、所有的create update delete操作都会进入MySQLmaster节点2、master节点会生成binlog文件,每次操作mysql数据库就会记录到binlog文件中3、slave节点会订阅master节点的binlog文件,以增量备份的形式同步数据到slave数据canal原理就是伪装成mysql的从节点,从而订阅master节点的binlog日志,主要流程为:1、canal服务端向mysql的master节点传输dump协议2、mysql的master节点接收到dump请求后推送binlog日志给canal服务端,解析binlog对象(原始为byte流)转成Json格式3、canal客户端通过TCP协议或MQ形式监听canal服务端,同步数据到ES三、数据迁移同步工具选型数据迁移同步工具的选择比较多样,下表仅从 MySQL 同步 ES 这个场景下,对一些笔者深度使用研究过的数据同步工具进行对比,用户可以根据自己的实际需要选取适合自己的产品。特性\产品CanalDTSCloudCanal是否支持自建ES是否是ES对端版本支持丰富度中支持ES6和ES7高支持ES5,ES6和ES7中支持ES6和ES7嵌套类型支持join/nested/objectobjectnested/objectjoin支持方式基于join父子文档&反查无基于宽表预构建&反查是否支持结构迁移否是是是否支持全量迁移是是是是否支持增量迁移是是是数据过滤能力中-仅全量可添加where条件高-全增量阶段where条件高-全增量阶段where条件是否支持时区转换否是是同步限流能力无有有任务编辑能力无有无数据源支持丰富度中高中架构模式订阅消费模式需先写入消息队列直连模式直连模式监控指标丰富度中性能指标监控中性能指标监控高性能指标、资源指标监控报警能力无针对延迟、异常的电话报警针对延迟、异常的钉钉、短信、邮件报警任务可视化创建&配置&管理能力无有有是否开源是否否是否免费是否 是社区版、SAAS版免费是否支持独立输出是否依赖云平台整体输出是是否支持SAAS化使用否是是总结本文主要对Mysql和ES进行数据同步的常见方案进行了汇总说明。1.同步双写是最简单的同步方式,能最大程度保证数据同步写入的实时性,最大的问题是代码侵入性太强。2.异步双写引入了消息中间件,由于MQ都是异步消费模型,所以可能出现数据同步延迟的问题。好处是在大规模消息同步时吞吐量更、高性能更好,便于接入更多的数据源,且各个数据源数据消费写入相互隔离互不影响。3.基于Mysql表定时扫描同步 ,原理是通过定时器定时扫描表中的增量数据进行数据同步,不会产生代码侵入,但由于是定时扫描同步,所以也会存在数据同步延迟问题,典型实现是采用 Logstash 实现增量同步。4.基于Binlog实时同步 ,原理是通过监听Mysql的binlog日志进行增量同步数据。不会产生代码侵入,数据同步的实时也能得到保障,弊端是Binlog系统都较为复杂。典型实现是采用 canal 实现数据同步。参考:MySQL 数据实时同步到 Elasticsearch 的技术方案选型和思考
前言中秋放假期间,线上mysql数据库突然提示出现死锁异常怎么办?是不是内心突然慌的一批,假期再也不能愉快的玩耍了。莫慌莫慌,今天老万教你遇到了mysql死锁应该怎么办。一、什么是死锁所谓死锁:是指多个事务在并发执行过程中由于相互持有对方需要的锁,都在等待资源变的可用而不会主动释放自身持有的锁,从而导致循环等待的情况。通常表级锁不会产生死锁,所以解决死锁主要还是针对于最常用的InnoDB。官方文档:Innodb死锁:https://dev.mysql.com/doc/refman/8.0/en/innodb-deadlocks.html二、死锁的产生条件发生死锁的必要条件有4个, 分别为互斥条件、不可剥夺条件、请求与保持条件和循环等待条件。从这几点来看,mysql中的死锁产生条件和java程序中死锁产生条件是一致的。但是java程序中的死锁往往会产生更严重的后果,而mysql中的死锁由于数据库内部的死锁处理机制,一般不会产生很严重的影响。三、死锁示例表和数据准备:DROP TABLE if EXISTS user; CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `account` varchar(30) DEFAULT NULL COMMENT '账号', `name` varchar(30) DEFAULT NULL COMMENT '姓名', `age` int(11) DEFAULT NULL COMMENT '年龄', `email` varchar(50) DEFAULT NULL COMMENT '邮箱', PRIMARY KEY (`id`), UNIQUE KEY `uk_account` (`account`), KEY `ik_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 英文名,主要是更容易验证间隙锁 INSERT INTO `user` (`id`,`account`,`name`, `age`, `email`) VALUES (3, '000003','Andi', 12, '10003@qq.com'); INSERT INTO `user` (`id`, `account`,`name`, `age`, `email`) VALUES (10,'000010', 'Jack', 20, '100010@qq.com'); INSERT INTO `user` (`id`, `account`,`name`, `age`, `email`) VALUES (20, '000020','Tom', 30, '100020@qq.com'); INSERT INTO `user` (`id`, `account`,`name`, `age`, `email`) VALUES (30, '000030','Tom', 60, '100030@qq.com');事务A:mysql> START TRANSACTION; Query OK, 0 rows affected (0.00 sec) mysql> SELECT * FROM `user` WHERE id = 3 FOR UPDATE; +----+---------+------+-----+--------------+ | id | account | name | age | email | +----+---------+------+-----+--------------+ | 3 | 000003 | Andi | 12 | 10003@qq.com | +----+---------+------+-----+--------------+ 1 row in set (0.01 sec) mysql> SELECT * FROM `user` WHERE id = 10 FOR UPDATE; +----+---------+------+-----+---------------+ | id | account | name | age | email | +----+---------+------+-----+---------------+ | 10 | 000010 | Jack | 20 | 100010@qq.com | +----+---------+------+-----+---------------+ 1 row in set (2.57 sec)事务B:mysql> START TRANSACTION; Query OK, 0 rows affected (0.00 sec) mysql> SELECT * FROM `user` WHERE id = 10 FOR UPDATE; +----+---------+------+-----+---------------+ | id | account | name | age | email | +----+---------+------+-----+---------------+ | 10 | 000010 | Jack | 20 | 100010@qq.com | +----+---------+------+-----+---------------+ 1 row in set (0.00 sec) mysql> SELECT * FROM `user` WHERE id = 3 FOR UPDATE; 1213 - Deadlock found when trying to get lock; try restarting transaction循环等待示意图:四、死锁的分析和查看1.查看最近1个死锁信息show engine innodb status;其中和死锁相关的信息:2.查看正在运行中的事务信息select * from information_schema.innodb_trx;说明:trx_state中的LOCK WAIT表示出现锁等待trx_query中可以查看导致锁等待的sql语句3.查看加锁信息-- 查看加锁信息(MySQL5.X) select * from information_schema.innodb_locks; -- 查看锁等待(MySQL5.X) select * from information_schema.innodb_lock_waits; --查看加锁信息(MySQL8.0) SELECT * FROM performance_schema.data_locks; --查看锁等待(MySQL8.0) SELECT * FROM performance_schema.data_lock_waits;五、死锁的内部处理方案mysql内部采用2种机制解决死锁问题:死锁探测机制 innodb_deadlock_detect 默认开启锁等待超时机制 innodb_lock_wait_timeout1.死锁探测机制当启用死锁检测(默认情况下)时,InnoDB 会自动检测事务死锁并回滚一个或多个事务以打破死锁。InnoDB 尝试选择要回滚的小事务,其中事务的大小由插入、更新或删除的行数决定。如果使用 innodb_deadlock_detect 变量禁用死锁检测,则 InnoDB 依赖于 innodb_lock_wait_timeout 设置,在发生死锁的情况下回滚事务。当检测到死锁后,就会出现下面这个提示:mysql中的死锁探测机制有3种判定方式:1.等待图(wait-for graph)回路检测2.等待的事务列表超过200个认为是死锁3.等待的事务持有锁的总数超过1,000,000官网说明:If the LATEST DETECTED DEADLOCK section of InnoDB Monitor output includes a message stating TOO DEEP OR LONG SEARCH IN THE LOCK TABLE WAITS-FOR GRAPH, WE WILL ROLL BACK FOLLOWING TRANSACTION, this indicates that the number of transactions on the wait-for list has reached a limit of 200. A wait-for list that exceeds 200 transactions is treated as a deadlock and the transaction attempting to check the wait-for list is rolled back. The same error may also occur if the locking thread must look at more than 1,000,000 locks owned by transactions on the wait-for list.除了超时机制,当前数据库都普遍采用等待图(wait-for graph)的方式来进行死锁检测。wait-for graph要求数据库保存以下两种信息:锁的信息链表事务等待链表通过上述链表可以构造出一张图,而在这个图中若存在回路,就代表存在死锁,因此资源间相互发生等待。在 wait-for graph中,事务为图中的节点。而在图中,事务T1指向T2边的定义为:事务T1等待事务T2所占用的资源事务T1最终等待T2所占用的资源,也就是事务之间在等待相同的资源,而事务T1发生在事务T2的后面来看一个例子:通过 Transaction Wait Lists中可以看到共有4个事务t1、t2、t3、t4。通过Lock List列表,可以看到加锁的等待顺序。在row1上,t1:s等待t2:x释放独占锁,才能添加共享锁。在row2上,t1:s和t4:s持有相同的共享锁。t2:x需要等待t1:s和t4:s释放共享锁后,才能添加独占锁。t3:x需要等待t1:s和t4:s释放共享锁,并且t2:x释放独占锁后,才能添加独占锁。故在wait-for graph中应有4个节点。根据等待关系画出等待图:通过上图可以发现存在回路(t1,t2),因此存在死锁。可以发现wait-for graph是一种较为主动的死锁检测机制,在每个事务请求锁并发生等待时都会判断是否存在回路,若存在则有死锁,通常来说InnoDB存储引擎选择回滚undo量最小的事务。关闭死锁探测:对于高并发的系统,当大量线程等待同一个锁时,死锁检测可能会导致性能的下降。此时,如果禁用死锁检测,而改为依靠参数 innodb_lock_wait_timeout 执行发生死锁时的事务回滚可能会更加高效。在 MySQL 8.0 中,增加了一个新的动态变量:innodb_deadlock_detect,可以用于控制 InnoDB 是否执行死锁检测。该参数的默认值为 ON,即打开死锁检测。注意⚠️:innodb_deadlock_detect是一个全局变量,在进行变量设置的时候需要加上global。查看是否开启死锁探测:mysql> show global variables like 'innodb_deadlock_detect'; +------------------------+-------+ | Variable_name | Value | +------------------------+-------+ | innodb_deadlock_detect | ON | +------------------------+-------+ 1 row in set (0.02 sec)关闭死锁检测:mysql> set global innodb_deadlock_detect=off; Query OK, 0 rows affected (0.01 sec)检测是否成功关闭:mysql> show global variables like 'innodb_deadlock_detect'; +------------------------+-------+ | Variable_name | Value | +------------------------+-------+ | innodb_deadlock_detect | OFF | +------------------------+-------+ 1 row in set (0.01 sec)2.锁等待超时机制通常来说,应该启用死锁检测,并且在应用程序中尽量避免产生死锁,同时对死锁进行相应的处理,例如重新开始事务。只有在确认死锁检测影响了系统的性能,并且禁用死锁检测不会带来负面影响时,可以尝试关闭 innodb_deadlock_detect 选项。另外,如果禁用了 InnoDB 死锁检测,需要调整参数 innodb_lock_wait_timeout 的值,以满足实际的需求。默认的锁等待超时时间是50s,当发生超时后,就出现下面这个提示:查看变量 innodb_lock_wait_timeout ://查看全局变量 mysql> show global variables like 'innodb_lock_wait_timeout'; +--------------------------+-------+ | Variable_name | Value | +--------------------------+-------+ | innodb_lock_wait_timeout | 50 | +--------------------------+-------+ 1 row in set (0.02 sec) //查看session级别变量 mysql> show variables like 'innodb_lock_wait_timeout'; +--------------------------+-------+ | Variable_name | Value | +--------------------------+-------+ | innodb_lock_wait_timeout | 50 | +--------------------------+-------+ 1 row in set (0.02 sec)修改锁超时等待时长://修改全局变量 mysql> set global innodb_lock_wait_timeout=30; Query OK, 0 rows affected (0.00 sec) //修改session级别变量 mysql> set innodb_lock_wait_timeout=30; Query OK, 0 rows affected (0.00 sec)再次查看变量,发现超时时间都变为30s。注意⚠️:innodb_lock_wait_timeout 参数分为session级别和global级别,如果发现锁等待超时时间一直没有设置成功,检测参数级别是否正确。六、手动释放锁手动解除正在死锁的状态有两种方法:1.表级锁手动释放1.查询是否锁表show OPEN TABLES where In_use > 0;2.查询进程(如果您有SUPER权限,您可以看到所有线程。否则,您只能看到您自己的线程)show processlist3.杀死进程id(就是上面命令的id列)kill id2.行级锁手动释放1.查看下正在等待锁的事务SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;其中trx_state中的LOCK WAIT表示出现锁等待。2.杀死进程id(就是上面命令的trx_mysql_thread_id列)kill 线程ID七、死锁的优化策略Mysql中的死锁并不可怕,因为Mysql会通过内部的死锁探测机制和锁等待超时机制自动回滚事务释放锁。除非它们非常频繁,以至于您根本无法运行某些事务。最简单的死锁异常处理方式:重试,可以通过捕捉死锁异常,进行指定次数的重试操作。死锁的优化:空间维度和时间维度空间维度:减少锁的范围,保持加锁顺序采用乐观锁,避免加锁,类似java中的cas机制尽量通过索引来检索,缩小锁的范围统一事务中数据操作的顺序,避免出现循环等待不要对不存在的记录执行update、delete操作,避免出现无意义的间隙锁时间维度:减少加锁时间控制事务的大小,避免大事务长时间持有锁涉及事务加锁操作,尽量放在事务的最后执行尽可能使用低级别的事务隔离机制总结本文主要是对mysql的死锁相关问题进行了介绍。1、死锁产生的原因2、为什么mysql中的死锁一般不会产生非常严重的影响3、mysql内部对死锁的两种处理机制:死锁探测机制innodb_deadlock_detect和锁等待超时机制innodb_lock_wait_timeout4、怎么查看锁的相关信息,怎么分析死锁5、怎么手动释放锁6、通过哪些手段可以减少死锁的产生
前言前面关于mybatis-plus的文章中提到过内置的批量插入方法saveBatch并不是真正的批量写入,而是通过executeBatch分批提交。所以我们通过sql注入器注入InsertBatchSomeColumn方法实现了insert的多值插入,提升了批量插入的性能。但其实还有更简单的优化方式,只通过添加一个参数,就能让采用executeBatch批量插入数据的性能实现逆袭。这就是今天给大家介绍的rewriteBatchedStatements参数。一、实战演示项目工程依然采用之前mybatis-plus系列文章中的工厂。这里我们通过在不添加rewriteBatchedStatements参数的前后采用executeBatch批量执行插入1万数据,并与InsertBatchSomeColumn方法进行对比。1、单元测试 @Test public void testBatchInsert() { System.out.println("----- batch insert method test ------"); long startTime = System.currentTimeMillis(); List<User> list = new ArrayList<>(); for (int i = 0; i < 10000; i++) { User user = new User(); user.setName("test"); user.setAge(13); user.setEmail("101@qq.com"); list.add(user); } userService.saveBatch(list); System.out.println("耗时:" + (System.currentTimeMillis() - startTime)); }saveBatch方法默认情况下,每次提交1000条sql。saveBatch方法的底层实现是通过executeBatch批量执行sql。default boolean saveBatch(Collection<T> entityList) { return this.saveBatch(entityList, 1000); } public boolean saveBatch(Collection<T> entityList, int batchSize) { String sqlStatement = this.getSqlStatement(SqlMethod.INSERT_ONE); return this.executeBatch(entityList, batchSize, (sqlSession, entity) -> { sqlSession.insert(sqlStatement, entity); }); }2、不添加rewriteBatchedStatements参数属性配置:spring.datasource.url = jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true spring.datasource.username = root spring.datasource.password = 123456测试结果:说明是一条insert语句插入一条记录。插入10000条数据,耗时49646ms3、添加rewriteBatchedStatements参数在mysql的数据库连接参数中添加rewriteBatchedStatements=truespring.datasource.url = jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&rewriteBatchedStatements=true spring.datasource.username = root spring.datasource.password = 123456执行结果:添加rewriteBatchedStatements=true后,executeBatch批量提交到mysql的sql语句还是一条insert语句插入一条记录。插入10000条数据耗时1289ms,批量插入的效率得到大幅提升。4、采用InsertBatchSomeColumn方法这里我们只需要将UserServiceImpl中采用InsertBatchSomeColumn重写的saveBatch方法的注释放开即可。@Service @Slf4j public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { @Resource private UserMapper userMapper; /** * * @param entityList * @param batchSize * @return */ @Override @Transactional(rollbackFor = {Exception.class}) public boolean saveBatch(Collection<User> entityList, int batchSize) { try { int size = entityList.size(); int idxLimit = Math.min(batchSize, size); int i = 1; //保存单批提交的数据集合 List<User> oneBatchList = new ArrayList<>(); for(Iterator<User> var7 = entityList.iterator(); var7.hasNext(); ++i) { User element = var7.next(); oneBatchList.add(element); if (i == idxLimit) { userMapper.insertBatchSomeColumn(oneBatchList); //每次提交后需要清空集合数据 oneBatchList.clear(); idxLimit = Math.min(idxLimit + batchSize, size); } } }catch (Exception e){ log.error("saveBatch fail",e); return false; } return true; } }执行单元测试:可以看到,采用insertBatchSomeColumn方法进行的批量插入是采用了insert的多值插入,一条insert语句插入多条记录。这里每批插入1000条记录。最终,插入10000条记录只话费了663msinsertBatchSomeColumn方法由于底层并不是走的executeBatch批量提交sql,所以性能并不会受rewriteBatchedStatements参数的影响。二、官方文档Mysql官方文档:rewriteBatchedStatements核心:prepared statements for INSERT into multi-value inserts when executeBatch() is calledrewriteBatchedStatements选项默认是关闭的,3.1.13以后的mysql连接驱动都支持该配置。如果开启该配置rewriteBatchedStatements=true,在调用 executeBatch() 批量执行 INSERT语句时,mysql内部会自动将批量提交的sql重写为insert多值插入再执行。总结本文主要介绍在采用executeBatch进行mysql批量数据插入时,通过在mysql连接信息中添加rewriteBatchedStatements=true使得执行效率大幅提升。1、批量sql重写开关参数rewriteBatchedStatements默认是关闭的,mysql连接驱动器版本3.1.13以后支持该配置。2、其底层原理是:将通过executeBatch方法批量提交到mysql服务端的sql重写为insert多值插入再执行。3、通过测试发现,开启rewriteBatchedStatements后,采用executeBatch方法批量插入的性能已经接近InsertBatchSomeColumn真实insert多值批量写入。
前言例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。一、官方文档Mybatis-Plus分页插件:https://baomidou.com/pages/97710a/PageHelper分页插件:https://pagehelper.github.io/Tip⚠️:官网链接,第一手资料。二、内置的分页方法1、内置方法在Mybatis-Plus的BaseMapper中,已经内置了2个支持分页的方法:public interface BaseMapper<T> extends Mapper<T> { <P extends IPage<T>> P selectPage(P page, @Param("ew") Wrapper<T> queryWrapper); <P extends IPage<Map<String, Object>>> P selectMapsPage(P page, @Param("ew") Wrapper<T> queryWrapper); …… }2、selectPage单元测试使用selectPage方法分页查询年纪age = 13的用户。 @Test public void testPage() { System.out.println("----- selectPage method test ------"); //分页参数 Page<User> page = Page.of(1,10); //queryWrapper组装查询where条件 LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(User::getAge,13); userMapper.selectPage(page,queryWrapper); page.getRecords().forEach(System.out::println); }执行结果:查询出了表中满足条件的所有记录,说明默认情况下,selectPage方法并不能实现分页查询。3、PaginationInnerInterceptor分页插件配置mybatis-plus中的分页查询功能,需要PaginationInnerInterceptor分页插件的支持,否则分页查询功能不能生效。@Configuration public class MybatisPlusConfig { /** * 新增分页拦截器,并设置数据库类型为mysql */ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }再次执行单元测试:先执行count查询查询满足条件的记录总数,然后执行limit分页查询,查询分页记录,说明分页查询生效。三、分页原理分析查看PaginationInnerInterceptor拦截器中的核心实现://select查询请求的前置方法 public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { //根据请求参数来判断是否采用分页查询,参数中含有IPage类型的参数,则执行分页 IPage<?> page = (IPage)ParameterUtils.findPage(parameter).orElse((Object)null); if (null != page) { boolean addOrdered = false; String buildSql = boundSql.getSql(); List<OrderItem> orders = page.orders(); if (CollectionUtils.isNotEmpty(orders)) { addOrdered = true; buildSql = this.concatOrderBy(buildSql, orders); } //根据page参数,组装分页查询sql Long _limit = page.maxLimit() != null ? page.maxLimit() : this.maxLimit; if (page.getSize() < 0L && null == _limit) { if (addOrdered) { PluginUtils.mpBoundSql(boundSql).sql(buildSql); } } else { this.handlerLimit(page, _limit); IDialect dialect = this.findIDialect(executor); Configuration configuration = ms.getConfiguration(); DialectModel model = dialect.buildPaginationSql(buildSql, page.offset(), page.getSize()); MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql); List<ParameterMapping> mappings = mpBoundSql.parameterMappings(); Map<String, Object> additionalParameter = mpBoundSql.additionalParameters(); model.consumers(mappings, configuration, additionalParameter); mpBoundSql.sql(model.getDialectSql()); mpBoundSql.parameterMappings(mappings); } } }再来看看ParameterUtils.findPage()方法的实现://发现参数中的IPage对象 public static Optional<IPage> findPage(Object parameterObject) { if (parameterObject != null) { //如果是多个参数,会转为map对象;只要任意一个value中包含IPage类型的对象,返回IPage对象 if (parameterObject instanceof Map) { Map<?, ?> parameterMap = (Map)parameterObject; Iterator var2 = parameterMap.entrySet().iterator(); while(var2.hasNext()) { Entry entry = (Entry)var2.next(); if (entry.getValue() != null && entry.getValue() instanceof IPage) { return Optional.of((IPage)entry.getValue()); } } //如果只有单个参数,且类型为IPage,则返回IPage对象 } else if (parameterObject instanceof IPage) { return Optional.of((IPage)parameterObject); } } return Optional.empty(); }小结:mybatis-plus分页查询的实现原理:1、由分页拦截器PaginationInnerInterceptor拦截所有查询请求,在执行查询前判断参数中是否包含IPage类型的参数。2、如果包含IPage类型的参数,则根据分页信息,重新组装成分页查询的SQL。四、自定义分页方法搞清楚mybatis-plus中分页查询的原理,我们来自定义分页查询方法。这里我使用的是mybatis-plus 3.5.2的版本。<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.2</version> </dependency>在UserMapper中新增selectPageByDto方法。public interface UserMapper extends CommonMapper<User> { /** * 不分页dto条件查询 * @param userDto * @return */ List<User> selectByDto(@Param("userDto") UserDto userDto); /** * 支持分页的dto条件查询 * @param page * @param userDto * @return */ IPage<User> selectPageByDto(IPage<User> page,@Param("userDto") UserDto userDto); }说明:1、mybatis-plus中分页接口需要包含一个IPage类型的参数。2、多个实体参数,需要添加@Param参数注解,方便在xml中配置sql时获取参数值。UserMapper.xml中的分页sql配置:这里由于selectByDto和selectPageByDto两个方法都是根据dto进行查询,sql语句完全一样,所以将相同的sql抽取了出来,然后用include标签去引用。<sql id="selectByDtoSql"> select * from user t <where> <if test="userDto.name != null and userDto.name != '' "> AND t.name like CONCAT('%',#{userDto.name},'%') </if> <if test="userDto.age != null"> AND t.age = #{userDto.age} </if> </where> </sql> <select id="selectByDto" resultType="com.laowan.mybatis_plus.model.User"> <include refid="selectByDtoSql"/> </select> <select id="selectPageByDto" resultType="com.laowan.mybatis_plus.model.User"> <include refid="selectByDtoSql"/> </select>1、2种分页写法方式一:Page对象既作为参数,也作为查询结果接受体@Test public void testSelectPageByDto() { System.out.println("----- SelectPageByDto method test ------"); //分页参数Page,也作为查询结果接受体 Page<User> page = Page.of(1,10); //查询参数 UserDto userDto = new UserDto(); userDto.setName("test"); userMapper.selectPageByDto(page,userDto); page.getRecords().forEach(System.out::println); }方式二:Page作为参数,用一个新的IPage对象接受查询结果。 @Test public void testSelectPageByDto() { System.out.println("----- SelectPageByDto method test ------"); //查询参数 UserDto userDto = new UserDto(); userDto.setName("test"); //PageDTO.of(1,10)对象只作为查询参数, IPage<User> page = userMapper.selectPageByDto(PageDTO.of(1,10),userDto); page.getRecords().forEach(System.out::println); }下面是官网的一些说明:这是官网针对自定义分页的说明。个人建议:如果定义的方法名中包含Page说明是用来分页查询的,返回结果尽量用IPage,而不要用List。防止出现不必要的错误,也更符合见名之一和单一指责原则。2、利用page.convert方法实现Do到Vo的转换public IPage<UserVO> list(PageRequest request) { IPage<UserDO> page = new Page(request.getPageNum(), request.pageSize()); LambdaQueryWrapper<UserDO> qw = Wrappers.lambdaQuery(); page = userMapper.selectPage(page, qw); return page.convert(u->{ UserVO v = new UserVO(); BeanUtils.copyProperties(u, v); return v; }); }五、分页插件 PageHelper很多人已经习惯了在mybatis框架下使用PageHelper进行分页查询,在mybatis-plus框架下依然也可以使用,和mybatis-plus框架自带的分页插件没有明显的高下之分。个人认为mybatis-plus的分页实现可以从方法命名、方法传参方面更好的规整代码。而PageHelper的实现对代码的侵入性更强,不符合单一指责原则。推荐在同一个项目中,只选用一种分页方式,统一代码风格。PageHelper的使用:1.引入maven依赖<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>最新版本</version> </dependency>2.PageHelper分页查询代码如下(示例)://获取第1页,10条内容,默认查询总数count PageHelper.startPage(1, 10); List<Country> list = countryMapper.selectAll(); //用PageInfo对结果进行包装 PageInfo page = new PageInfo(list);总结本文主要对mybatis-plus分页查询的原理和使用进行了详细介绍。1、要开启mybatis-plus分页查询功能首先需要配置PaginationInnerInterceptor分页查询插件。2、PaginationInnerInterceptor分页查询插件的实现原理是:拦截所有查询请求,分析查询参数中是否包含IPage类型的参数。如果有则根据分页信息和数据库类型重组sql。3、提供了2种分页查询的写法。4、和经典的PageHelper分页插件进行了对比。两者的使用都非常简单,在单一项目中任选一种,统一代码风格即可。
问题描述在Mybatis-Plus中调用updateById方法进行数据更新默认情况下是不能更新空值字段的。而在实际开发过程中,往往会遇到需要将字段值更新为空值的情况。那么如果让Mybatis-Plus中的updateById方法支持空值更新呢?演示:实体User:@TableName(value ="user") @Data public class User implements Serializable { @TableId(value = "id",type = IdType.ASSIGN_ID) private Long id; private String name; private Integer age; private String email; }updateById方法单元测试:@Test public void testUpdateById() { System.out.println("----- updateById method test ------"); User user = new User(); user.setId(1543920054188400641L); user.setName("test"); user.setAge(13); //user.setEmail(); userMapper.updateById(user); System.out.println(user.toString()); }执行结果:可以看到由于email字段的值为null,所以执行updateById方法时没有对email字段进行更新。原因分析:Mybatis-Plus中字段的更新策略是通过FieldStrategy属性控制的。在实体字段上,如果不通过@TableField注解指定字段的更新策略,字段默认的更新策略是FieldStrategy.DEFAULT,即跟随全局策略。而Mybatis-Plus的全局配置中,字段的默认更新策略是FieldStrategy.NOT_NULL,即进行空值判断,不对NULL值数据进行处理。public DbConfig() { this.idType = IdType.ASSIGN_ID; this.tableUnderline = true; this.capitalMode = false; this.logicDeleteValue = "1"; this.logicNotDeleteValue = "0"; this.insertStrategy = FieldStrategy.NOT_NULL; this.updateStrategy = FieldStrategy.NOT_NULL; this.whereStrategy = FieldStrategy.NOT_NULL; }相关文档:Mybatis-Plus中FieldStrategy说明Mybatis-Plus字段策略FieldStrategy详解Tip⚠️:官网链接,自力更生。解决方案:1、设置字段级别的更新策略IGNORED如果只需要实体中的几个字段支持空值更新,则通过@TableField注解指定字段的更新策略为FieldStrategy.IGNORED,忽略空值判断,直接更新即可。该方式的控制级别是字段级别的控制。实体User:@TableName(value ="user") @Data public class User implements Serializable { @TableId(value = "id",type = IdType.ASSIGN_ID) private Long id; private String name; private Integer age; @TableField(updateStrategy = FieldStrategy.IGNORED) private String email; }再次执行上面的单元测试:email字段虽然是空值,但仍然进行了更新操作,说明此时email字段已经支持空值更新。2、设置全局更新策略IGNORED如果需要全局所有实体的更新操作都需要支持空值更新,可以修改Mybatis-Plus的全局更新策略。该方式的控制级别是项目级别的控制。在spring boot中修改如下属性即可:mybatis-plus.global-config.db-config.update-strategy=ignored测试:实体User:@TableName(value ="user") @Data public class User implements Serializable { @TableId(value = "id",type = IdType.ASSIGN_ID) private Long id; private String name; private Integer age; private String email; }单元测试:@Test public void testUpdateById() { System.out.println("----- updateById method test ------"); User user = new User(); user.setId(1543920054188400641L); user.setName("test"); //user.setAge(13); //user.setEmail(); userMapper.updateById(user); System.out.println(user.toString()); }执行结果:age和email字段都支持空值更新,说明全局更新策略ignored生效。3、采用alwaysUpdateSomeColumnById方法进行全字段更新Mybatis-Plus中自带的扩展方法alwaysUpdateSomeColumnById会忽略字段的更新策略,直接对实体中的每一个字段都执行更新操作。如果你不想修改全局的字段更新策略,又需要项目中某个实体的所有字段都支持空值更新,推荐采用该方法。该方式的控制级别是实体级别的控制。实现步骤:1、继承DefaultSqlInjector扩展sql注入器,注入AlwaysUpdateSomeColumnById方法public class MySqlInjector extends DefaultSqlInjector { @Override public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) { List<AbstractMethod> methodList = super.getMethodList(mapperClass,tableInfo); //自动填充策略为更新填充策略时,不用插入值 methodList.add(new InsertBatchSomeColumn(i -> i.getFieldFill() != FieldFill.UPDATE)); //自动填充策略为插入时自动填充时,字段不用更新 methodList.add(new AlwaysUpdateSomeColumnById(i -> i.getFieldFill() != FieldFill.INSERT)); return methodList; } }2、将扩展的sql注入器配置到spring容器中@Configuration public class MybatisPlusConfig { @Bean public MySqlInjector sqlInjector() { return new MySqlInjector(); } }3、扩展自己的通用Mapper接口CommonMapperpublic interface CommonMapper<T> extends BaseMapper<T> { /** * 全量插入,等价于insert * @param entityList * @return */ int insertBatchSomeColumn(List<T> entityList); /** * 全字段更新,不会忽略null值 * @param entity * @return */ int alwaysUpdateSomeColumnById(@Param("et") T entity); }4、UserMapper继承自定义的CommonMapperpublic interface UserMapper extends CommonMapper<User> { }5、实体User@TableName(value ="user") @Data public class User implements Serializable { @TableId(value = "id",type = IdType.ASSIGN_ID) private Long id; private String name; private Integer age; private String email; }6、单元测试@Test public void testUpdateById() { System.out.println("----- updateById method test ------"); User user = new User(); user.setId(1543920054188400641L); user.setName("test"); user.setAge(13); //user.setEmail(); userMapper.alwaysUpdateSomeColumnById(user); System.out.println(user.toString()); }执行结果:虽然没有修改Mybatis-Plus全局的更新策略,也没有在实体字段上使用@TableField注解修改字段的更新策略,但是alwaysUpdateSomeColumnById方法仍然可以对空值字段进行更新。小结:本文主要是对Mybatis-Plus中updateById方法不能更新空值问题进行了分析说明,并提供了3种解决方案。1.字段级别解决方案采用@TableField注解修改字段默认的更新策略为FieldStrategy.IGNORED。@TableField(updateStrategy = FieldStrategy.IGNORED) private String email;2.实体级别解决方案调用Mybatis-Plus中的扩展方法alwaysUpdateSomeColumnById,忽略字段更新策略,直接对实体中所有字端进行更新。3.全局级别解决方案修改Mybatis-Plus的全局更新策略为ignoredmybatis-plus.global-config.db-config.update-strategy=ignored
问题描述我们在使用mybatis或mybatis-plus作为持久化框架的时候,通过dao层接口调用xml中配置好的sql时,常常会遇到org.apache.ibatis.binding.BindingException Invalid bound statement的问题。异常简单来说:就是无效的sql绑定,即通过dao层接口的方法名称没有找到对应的sql语句。在百度上查询的文章中发现大家对该问题的解决和总结都非常片面,不够全面。所以分享下自己对该问题的解决思路。项目环境spring boot + mybatis-plus解决方案:1、检测mapper-locations配置项是否正确mapper-locations配置项是用来告诉Mapper所对应的XML文件的位置。如果该文件位置配置错误,那么其他内容配置再怎么正确,依然会报错。这也是该异常最重要又最容易被忽略的一个原因。另外,由于Mapper所对应的XML文件属于静态文件资源,所以一定要存放在resoureces目录。这里说下我项目中遇到的问题:已经在配置文件中添加了如下配置,指定Mapper所对应的XML文件的位置:mybatis.mapper-locations=classpath:mybatis/mapper/*.xml并且也检查了配置的路径是正确的,没有问题。但在执行dao层自定义的方法时,还是一直出现异常。原因:在官网查看了mybatis-plus的配置文档后,终于发现在mybatis-plus框架下,需要通过mybatis-plus.mapper-locations来指定XML文件的位置。修改为如下配置后,问题修复。#默认classpath*:/mapper/**/*.xml,推荐明确配置,出现问题更好排查 mybatis-plus.mapper-locations=classpath*:/mapper/*.xml小结⚠️:注意在mybatis框架和mybatis-plus框架下配置项的区别:mybatis框架下指定XML路径maven依赖:<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.4</version> </dependency> </dependency>对应的xml路径的配置属性mybatis.mapper-locations:mybatis.mapper-locations=classpath:mybatis/mapper/*.xmlmybatis-plus框架下指定XML路径maven依赖:<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.2</version> </dependency>对应的xml路径的配置属性mybatis-plus.mapper-locations:mybatis-plus.mapper-locations=classpath:mybatis/mapper/*.xml注意⚠️:很多人可能会奇怪自己项目中明明没有明确配置mapper-locations的属性,但是执行mapper中自定义的方法确并没有报错。这是由于无论是mybatis框架还是mybatis-plus框架下的start自动配置包都对mapper-locations属性有默认的路径配置classpath*:/mapper/**/*.xml,这也符合spring boot中约定大于配置的规范。大家在使用IDEA开发中,也可以注意配置项下面的黄线,会提示不能解析对应的配置项。一般引入了框架对应的start依赖后,配置框架相关的配置项是不会出现黄线下划线提醒的。只有用户自定义的一些配置项,会出现黄线下划线提醒。所以,框架的配置项如果出现黄线下划线提示,大家一定要高度重视。2、检测dao层接口是否注入spring容器这部分在之前的文章中已经提到,@MapperScan和@Mapper一定要任选一种进行配置,如果采用@MapperScan则一定要正确配置中Mapper接口所在的目录,确保在项目启动时,Mapper接口都注入到Spring容器中。启动类Application:@SpringBootApplication @Slf4j @MapperScan("com.laowan.mybatis_plus.mapper") public class MybatisPlusApplication { public static void main(String[] args) { SpringApplication.run(MybatisPlusApplication.class, args); log.info("mybatis_plus_demo 启动成功"); } }@Mapper配置方法:@Mapper public interface UserMapper extends BaseMapper<User> { }两者任意选择一种方式配置即可,如果都不配置,那么在执行dao层方法进行数据操作时,会出现在spring容器中找不到对应的bean的异常。@Mapper和@MapperScan都不配置调用mapper方法时出现的异常:Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.laowan.mybatis_plus.mapper.UserMapper' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}3、检测xml文件中namespace是否配置正确针对这部分的检测,强烈推荐大家在idea上安装mybatisx插件,借助插件快速检查是否匹配。官方文档:https://baomidou.com/pages/ba5b24安装好插件后,在mapper接口和xml文件中就会出现这样的小企鹅。点击小企鹅就可以在mapper接口和xml文件间相互跳转。如果配置错误,将不能进行跳转或调整到错误的文件中。4、检测dao层接口的方法名和sql的id是否匹配也可以借助mybatisx插件快速检测。同样,如果名称不匹配,不会出现对应的红蓝小企鹅。Mapper接口方法:xml中对于的sql:5、检测resultType和resultMap的配置总结本文主要是针对spring boot + mybatis-plus框架下的常见异常:org.apache.ibatis.binding.BindingException Invalid bound statement的一些解决手段进行了说明。1、在mybatis框架下,主要需要保证2点:Mapper所对应的XML文件的位置要通过mapper-locations属性配置正确Mapper接口需要通过@MapperScan或@Mapper注入到Spring容器中2、注意mybatis框架和mybatis-plus配置项的区别mybatis框架下mapper-locations配置项为:mybatis.mapper-locationsmybatis-plus框架下mapper-locations配置项为:mybatis-plus.mapper-locations3、利用mybatisx插件快速检测命名空间、方法名和sql的id之间的映射关系。
前言很多人在使用Mybatis-Plus的时候可能会疑惑,自己明明没有配置主键的生成策略,但是执行新增操作时却自动生成了主键,而且还特别长。这是由于Mybatis-Plus默认就会采用雪花算法填充主键字段。今天就和大家详解聊聊Mybatis-Plus中主键生成的相关策略。一、官网Mybatis-Plus主键策略:https://baomidou.com/pages/e131bd/Mybatis-Plus自定义ID生成器:https://baomidou.com/pages/568eb2/TIP⚠️:推荐学习框架的时候,多研究下官网,获取第一手资料。二、主键注解@TableId说明1、源码@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) public @interface TableId { String value() default ""; IdType type() default IdType.NONE; }2、作用标识主键字段使用@TableId可以标识实体对象中和数据库表中主键对应的字段。如果不添加@TableId注解,会默认匹配id字段为主键。变量名称和主键字段名称的匹配如果表中的主键字段名称和实体中的主键字段名称不相同,这时候就要通过@TableId中的value属性明确指出对应的数据库主键字段的名称。指定主键的生成方式可以通过@TableId注解中的type属性指定主键的生成策略,具体支持哪些策略可以在IdType枚举中查看。3、使用@TableName(value ="user") @Data public class User implements Serializable { /** * 主键ID */ @TableId(value = "id",type = IdType.ASSIGN_ID) private Long userId; private String name; private Integer age; private String email;三、主键生成策略-IdType枚举说明通过查看IdType枚举类的源码,可以发现Mybatis-Plus中默认支持5种主键生成方式。1、源码public enum IdType { AUTO(0), NONE(1), INPUT(2), ASSIGN_ID(3), ASSIGN_UUID(4); private final int key; private IdType(int key) { this.key = key; } public int getKey() { return this.key; } }2、说明值描述AUTO数据库 ID自增,这种情况下将表中主键设置为自增,否则,没有设置主动设置id值进行插入时会报错NONE无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里默认 ASSIGN_ID),注意这里官网文档有误INPUTinsert 前自行 set 主键值,在采用IKeyGenerator类型的ID生成器时必须为INPUTASSIGN_ID分配 ID(主键类型为 Number(Long 和 Integer)或 String)(since 3.3.0),使用接口IdentifierGenerator的方法nextId(默认实现类为DefaultIdentifierGenerator雪花算法)ASSIGN_UUID分配 UUID,主键类型为 String(since 3.3.0),使用接口IdentifierGenerator的方法nextUUID(默认 default 方法)3、全局设置IdType默认的全局设置为IdType.ASSIGN_ID,即由mybatis-plus主动分配主键,默认情况下由默认主键生成器实现类DefaultIdentifierGenerator采用雪花算法填充主键。public DbConfig() { this.idType = IdType.ASSIGN_ID; this.tableUnderline = true; this.capitalMode = false; this.logicDeleteValue = "1"; this.logicNotDeleteValue = "0"; this.insertStrategy = FieldStrategy.NOT_NULL; this.updateStrategy = FieldStrategy.NOT_NULL; this.whereStrategy = FieldStrategy.NOT_NULL; }在spring boot中,可以通过如下配置更改全局配置。mybatis-plus.global-config.db-config.id-type=assign_id三、ID生成器介绍Mybatis-Plus中的ID生成器主要分为2类,一类是IdentifierGenerator,另一类是IKeyGenerator。1、IdentifierGenerator源码如下:public interface IdentifierGenerator { //根据id是否为null判断是否需要主动分配Id default boolean assignId(Object idValue) { return StringUtils.checkValNull(idValue); } //生成数值型Id Number nextId(Object entity); //生成字符型uuid default String nextUUID(Object entity) { return IdWorker.get32UUID(); } }说明:IdentifierGenerator生成器中主要提供了3个方法。其使用场景是:不依赖数据库生成ID,而是由mybatis-plus自己提供一套id生成算法。 对应的主键生成方式为IdType.ASSIGN_ID、ASSIGN_UUID。assignId 是否需要分配idnextId 获取下一个数值型IdnextUUID 获取下一个uuid典型的实现是默认的id生成器DefaultIdentifierGenerator,基于雪花算法生成id。`public class DefaultIdentifierGenerator implements IdentifierGenerator { private final Sequence sequence; public DefaultIdentifierGenerator() { this.sequence = new Sequence((InetAddress)null); } public DefaultIdentifierGenerator(InetAddress inetAddress) { this.sequence = new Sequence(inetAddress); } public DefaultIdentifierGenerator(long workerId, long dataCenterId) { this.sequence = new Sequence(workerId, dataCenterId); } public DefaultIdentifierGenerator(Sequence sequence) { this.sequence = sequence; } public Long nextId(Object entity) { return this.sequence.nextId(); } }具体使用:1、声明由mybatis-plus分配主键值@TableName(value ="user") @Data public class User implements Serializable { /** * 主键ID */ @TableId(value = "id",type = IdType.ASSIGN_ID) private Long userId; private String name; private Integer age; private String email;2、指定idGenerator的实现类如果是默认的DefaultIdentifierGenerator,则不需要用户重新指定。@Configuration public class IdAutoConfig { @Value("${mybatis-plus.zookeeper.serverLists}") private String zkServerLists; @Bean public IdentifierGenerator idGenerator() { return new ImadcnIdentifierGenerator(zkServerLists); } }2、IKeyGenerator源码如下:public interface IKeyGenerator { //执行sql生成id String executeSql(String incrementerName); //获取数据库类型 DbType dbType(); }说明:IKeyGenerator 生成器主要是根据不同的数据库类型,执行sql语句生成对应的主键。典型的数据库如Oracle,Postgre,需要根据序列器生成表主键。相关实现类:OracleKeyGenerator中的实现:可以发现,是通过执行sql调用序列器生成的id。public class OracleKeyGenerator implements IKeyGenerator { public OracleKeyGenerator() { } public String executeSql(String incrementerName) { return "SELECT " + incrementerName + ".NEXTVAL FROM DUAL"; } public DbType dbType() { return DbType.ORACLE; } }具体使用:1、在实体中通过@KeySequence指定序列器名称,并通过@TableId指定主键生成策略为IdType.INPUT@KeySequence(value = "SEQ_ORACLE_STRING_KEY", clazz = String.class) public class YourEntity { @TableId(value = "ID_STR", type = IdType.INPUT) private String idStr; }2、spring boot配置列中配置keyGenerator具体实现类@Bean public IKeyGenerator keyGenerator() { return new OracleKeyGenerator(); }也可以通过配置项指定:mybatis-plus.global-config.db-config.key-generators=com.baomidou.mybatisplus.extension.incrementer.OracleKeyGenerator四、自定义主键生成器自定义主键生成器也有2种方式。如果需要通过执行sql语句来生成id的,可以通过实现IKeyGenerator接口来自定义。 如果不想依赖数据库,完全自定义一套主键生成策略,那么可以通过实现IdentifierGenerator接口来扩展。下面演示如何通过实现IdentifierGenerator接口,自定义主键生成器。1、自定义id生成器@Component public class CustomIdGenerator implements IdentifierGenerator { @Override public Long nextId(Object entity) { //可以将当前传入的class全类名来作为bizKey,或者提取参数来生成bizKey进行分布式Id调用生成. String bizKey = entity.getClass().getName(); //根据bizKey调用分布式ID生成 long id = ....; //返回生成的id值即可. return id; } }2、配置类中指定id生成器@Bean public IdentifierGenerator idGenerator() { return new CustomIdGenerator(); }3、实体类中指定主键分配策略IdType.ASSIGN_ID@TableName(value ="user") @Data public class User implements Serializable { /** * 主键ID */ @TableId(value = "id",type = IdType.ASSIGN_ID) private Long userId; private String name; private Integer age; private String email;总结本文主要是介绍了Mybatis-Plus主键生成策略及其相关的扩展方法。1、详细介绍了@TableId注解的属性和作用,推荐项目中在实体的主键字段上明确添加@TableId注解,标识id字段以及id生成策略IdType。2、目前mybatis-plus中有5种Id生成策略IdType,搞清楚各种的用法和使用场景。AUTO 数据库 ID自增NONE 未设置主键类型,也就是跟随全局策略,全局策略默认为ASSIGN_IDINPUT insert 前自行 set 主键值ASSIGN_ID 分配 IDASSIGN_UUID 分配 UUID3、Mybatis-Plus中的ID生成器主要分为2类,一类是IdentifierGenerator,另一类是IKeyGenerator,搞清楚他们的区别和各自的使用场景。IdentifierGenerator 适用于不依赖数据库,用户自定义的主键生成场景。IKeyGenerator 依赖数据库,通过执行sql语句生成主键的场景。
前言最近都是Mybatis-Plus系列的小白文,算是对工作中最常使用的框架的细节扫盲。有在学习Mybatis-Plus使用的,可以关注一波。今天主要是对Mybatis-Plus字段策略FieldStrategy进行介绍。一、官方文档Mybatis-Plus中FieldStrategy说明:https://baomidou.com/pages/223848/#tableidTip⚠️:官网链接,自力更生。二、字段策略介绍1、FieldStrategy作用Mybatis-Plus字段策略FieldStrategy的作用主要是在进行新增、更新时,根据配置的策略判断是否对实体对象的值进行空值判断,如果策略为字段不能为空,则不会对为空的字段进行赋值或更新。同样,在进行where条件查询时,根据whereStrategy策略判断是否对字段进行空值判断,如果策略为字段不能为空,则为空的字段不会作为查询条件组装到where条件中。三个配置,对应三种使用场景insertStrategy 在insert操作时的字段策略,是否进行空值判断,插入空值updateStrategy 在update操作时的字段策略,是否进行空值判断,插入空值whereStrategy 在where条件组装时,是否进行控制判断,将空值作为查询条件2、FieldStrategy类型FieldStrategy的源码中,一共有5种策略类型。public enum FieldStrategy { IGNORED, NOT_NULL, NOT_EMPTY, DEFAULT, NEVER; private FieldStrategy() { } }每种策略的作用:值描述IGNORED忽略空值判断,实体对象的字段是什么值就用什么值更新,支持null值更新操作NOT_NULL进行非NULL判断,也是默认策略,相当于age!=nullNOT_EMPTY进行非空判断,主要是针对字符串类型,相当于name != null and name != ‘’NEVER从不更新,不管字段是否有值,都不进行更新DEFAULT追随全局配置3、FieldStrategy配置全局策略配置在全局配置中,三者的默认值都是FieldStrategy.NOT_NULL,即进行空值判断,不对NULL值数据进行处理。public DbConfig() { this.idType = IdType.ASSIGN_ID; this.tableUnderline = true; this.capitalMode = false; this.logicDeleteValue = "1"; this.logicNotDeleteValue = "0"; this.insertStrategy = FieldStrategy.NOT_NULL; this.updateStrategy = FieldStrategy.NOT_NULL; this.whereStrategy = FieldStrategy.NOT_NULL; }在spring boot中可以通过配置属性修改全局字段策略:mybatis-plus.global-config.db-config.update-strategy=not_null mybatis-plus.global-config.db-config.insert-strategy=not_null mybatis-plus.global-config.db-config.where-strategy=not_null单字段策略配置在实体对象中,通过@TableField注解可以针对单个字段指定字段策略。示例:@TableName(value ="user") @Data public class User implements Serializable { @TableId private Long id; private String name; private Integer age; //配置字段更新策略:不能为空 @TableField(updateStrategy = FieldStrategy.NOT_EMPTY) private String email; }@TableField注解的源码:@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) public @interface TableField { String value() default ""; boolean exist() default true; String condition() default ""; String update() default ""; //插入策略 FieldStrategy insertStrategy() default FieldStrategy.DEFAULT; //更新策略 FieldStrategy updateStrategy() default FieldStrategy.DEFAULT; //where条件策略 FieldStrategy whereStrategy() default FieldStrategy.DEFAULT; FieldFill fill() default FieldFill.DEFAULT; boolean select() default true; boolean keepGlobalFormat() default false; String property() default ""; JdbcType jdbcType() default JdbcType.UNDEFINED; Class<? extends TypeHandler> typeHandler() default UnknownTypeHandler.class; boolean javaType() default false; String numericScale() default ""; }其中,insertStrategy、updateStrategy和whereStrategy的默认策略都是FieldStrategy.DEFAULT,表示跟随全局配置。三、实战说明以更新操作updateById为例,演示各种策略的作用。1.默认策略 - NOT_NULL默认策略为FieldStrategy.NOT_NULL,表示需要进行非NULL判断,只有不为NULL的字段才会参与数据处理。相当于mybatis的xml文件中的if判定条件判断:age!=null<if test="age != null"> AND t.age = #{age} </if>代码如下(示例):@TableName(value ="user") @Data public class User implements Serializable { @TableId private Long id; private String name; private Integer age; //NOT_NULL为默认的全局策略 //@TableField(updateStrategy = FieldStrategy.NOT_NULL) private String email; }单元测试:@Test public void testUpdateById() { System.out.println("----- updateById method test ------"); User user = new User(); user.setId(1543920054188400641L); user.setName("test"); user.setAge(13); //email字段为null //user.setEmail(""); userMapper.updateById(user); System.out.println(user.toString()); }执行结果:为null的字段email没有参与更新操作。updateStrategy的默认策略是FieldStrategy.DEFAULT,表示跟随全局配置。而全局的默认策略是FieldStrategy.NOT_NULL,即进行NULL值判断,如果为NULL,则不更新对应的字段。2.忽略判断-IGNORED@TableName(value ="user") @Data public class User implements Serializable { @TableId private Long id; private String name; private Integer age; @TableField(updateStrategy = FieldStrategy.IGNORED) private String email; }再次执行上面的单元测试:可以看到,尽管email字段的值为null,但还是进行了更新操作。说明策略FieldStrategy.IGNORED会忽略字段值的空值判断,无论实体对象的字段值是否为空,都会进行更新操作。3.从不处理-NEVER@TableName(value ="user") @Data public class User implements Serializable { @TableId private Long id; private String name; private Integer age; @TableField(updateStrategy = FieldStrategy.NEVER) private String email; }指定email字段不为空,进行单元测试:@Test public void testUpdateById() { System.out.println("----- updateById method test ------"); User user = new User(); user.setId(1543920054188400641L); user.setName("test"); user.setAge(13); //email字段不为空 user.setEmail("101@qq.com"); userMapper.updateById(user); System.out.println(user.toString()); }执行结果:尽管email字段有值,但还是没有进行了更新操作。说明策略FieldStrategy.NEVER不但会忽略字段值的空值判断,而且不管标识的字段是否有值,都不会进行更新操作。4.字符不为空-NOT_EMPTY策略FieldStrategy.NOT_EMPTY表示需要对字符串进行空值判断,只有非空字符串的字段才会参与数据处理。相当于mybatis的xml文件中的if判定条件判断:name != null and name != ''<if test="name != null and name != '' "> AND t.name like CONCAT('%',#{name},'%') </if>@TableName(value ="user") @Data public class User implements Serializable { @TableId private Long id; private String name; private Integer age; @TableField(updateStrategy = FieldStrategy.NOT_EMPTY) private String email; }指定email字段不为空,进行单元测试:@Test public void testUpdateById() { System.out.println("----- updateById method test ------"); User user = new User(); user.setId(1543920054188400641L); user.setName("test"); user.setAge(13); //email字段不为空 user.setEmail("101@qq.com"); userMapper.updateById(user); System.out.println(user.toString()); }执行结果:email字段有值的时候,正常更新。指定email字段为空字符串,进行单元测试:@Test public void testUpdateById() { System.out.println("----- updateById method test ------"); User user = new User(); user.setId(1543920054188400641L); user.setName("test"); user.setAge(13); //email字段为空字符串 user.setEmail(""); userMapper.updateById(user); System.out.println(user.toString()); }执行结果:email字段为空字符串时,不会参与更新操作。5.跟随全局-DEFAULT策略FieldStrategy.DEFAULT表示追随全局配置的字段策略,这也是字段级别的默认策略。而全局的字段策略,默认是FieldStrategy.NOT_NULL。这里就不做继续演示。总结本文主要是详细介绍了Mybatis-Plus字段策略FieldStrategy的作用和使用方法。1、字段策略的3个使用场景:insertStrategy insert操作时的字段策略,是否进行空值判断,插入空值updateStrategy update操作时的字段策略,是否进行空值判断,插入空值whereStrategy where条件组装时的字段策略,是否进行控制判断,将空值作为查询条件2、字段策略的5种类型:IGNORED 忽略空值判断,实体对象的字段是什么值就用什么值更新,支持null值更新操作NOT_NULL 进行非NULL判断,相当于age!=null,也是默认的策略NOT_EMPTY 进行非空判断,主要是针对字符串类型的字段,相当于name != null and name != ''NEVER 从不更新,不管字段是否有值,都不进行更新DEFAULT 追随全局配置
前言批量插入是实际工作中常见的一个功能,mysql支持一条sql语句插入多条数据。但是Mybatis-Plus中默认提供的saveBatch方法并不是真正的批量插入,而是遍历实体集合每执行一次insert语句插入一条记录。相比批量插入,性能上显然会差很多。今天谈一下,在Mybatis-Plus中如何通过SQL注入器实现真正的批量插入。一、mysql批量插入的支持insert批量插入的语法支持:INSERT INTO user (id, name, age, email) VALUES (1, 'Jone', 18, 'test1@baomidou.com'), (2, 'Jack', 20, 'test2@baomidou.com'), (3, 'Tom', 28, 'test3@baomidou.com'), (4, 'Sandy', 21, 'test4@baomidou.com'), (5, 'Billie', 24, 'test5@baomidou.com');二、Mybatis-Plus默认saveBatch方法解析1、测试工程建立测试的数据表:CREATE TABLE `user` ( `id` bigint(20) NOT NULL COMMENT '主键ID', `name` varchar(30) DEFAULT NULL COMMENT '姓名', `age` int(11) DEFAULT NULL COMMENT '年龄', `email` varchar(50) DEFAULT NULL COMMENT '邮箱', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;在IDEA中配置好数据库连接,并安装好MybatisX-Generator插件,生成对应表的model、mapper、service、xml文件。生成的文件推荐保存在工程目录下,generator目录下。先生成文件,用户根据自己的需要,再将文件移动到指定目录,这样避免出现文件覆盖。生成实体的配置选项,这里我勾选了Lombok和Mybatis-Plus3,生成的类更加优雅。移动生成的文件到对应目录:由于都是生成的代码,这里就不补充代码了。2、默认批量插入saveBatch方法测试@Test public void testBatchInsert() { System.out.println("----- batch insert method test ------"); List<User> list = new ArrayList<>(); for (int i = 0; i < 10; i++) { User user = new User(); user.setName("test"); user.setAge(13); user.setEmail("101@qq.com"); list.add(user); } userService.saveBatch(list); }执行日志:显然,这里每次执行insert操作,都只插入了一条数据。3、saveBatch方法实现分析//批量保存的方法,做了分批请求处理,默认一次处理1000条数据 default boolean saveBatch(Collection<T> entityList) { return this.saveBatch(entityList, 1000); } //用户也可以自己指定每批处理的请求数量 boolean saveBatch(Collection<T> entityList, int batchSize);public static <E> boolean executeBatch(Class<?> entityClass, Log log, Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) { Assert.isFalse(batchSize < 1, "batchSize must not be less than one", new Object[0]); return !CollectionUtils.isEmpty(list) && executeBatch(entityClass, log, (sqlSession) -> { int size = list.size(); int idxLimit = Math.min(batchSize, size); int i = 1; for(Iterator var7 = list.iterator(); var7.hasNext(); ++i) { E element = var7.next(); consumer.accept(sqlSession, element); //每次达到批次数,sqlSession就刷新一次,进行数据库请求,生成Id if (i == idxLimit) { sqlSession.flushStatements(); idxLimit = Math.min(idxLimit + batchSize, size); } } }); }我们将批次数设置为3,用来测试executeBatch的处理机制。@Test public void testBatchInsert() { System.out.println("----- batch insert method test ------"); List<User> list = new ArrayList<>(); for (int i = 0; i < 10; i++) { User user = new User(); user.setName("test"); user.setAge(13); user.setEmail("101@qq.com"); list.add(user); } //批次数设为3,用来测试 userService.saveBatch(list,3); }执行结果,首批提交的请求,已经生成了id,还没有提交的id为null。(这里的提交是sql请求,而不是说的事物提交)小结:Mybatis-Plus中默认的批量保存方法saveBatch,底层是通过sqlSession.flushStatements()将一个个单条插入的insert语句分批次进行提交。相比遍历集合去调用userMapper.insert(entity),执行一次提交一次,saveBatch批量保存有一定的性能提升,但从sql层面上来说,并不算是真正的批量插入。补充:遍历集合单次提交的批量插入。@Test public void forEachInsert() { System.out.println("forEachInsert 插入开始========"); long start = System.currentTimeMillis(); for (int i = 0; i < list.size(); i++) { userMapper.insert(list.get(i)); } System.out.println("foreach 插入耗时:"+(System.currentTimeMillis()-start)); }三、Mybatis-plus中SQL注入器介绍SQL注入器官方文档:https://baomidou.com/pages/42ea4a/1.sqlInjector介绍SQL注入器sqlInjector 用于注入 ISqlInjector 接口的子类,实现自定义方法注入。参考默认注入器 DefaultSqlInjector。Mybatis-plus默认可以注入的方法如下,大家也可以参考其实现自己扩展:默认注入器DefaultSqlInjector的内容:public class DefaultSqlInjector extends AbstractSqlInjector { public DefaultSqlInjector() { } public List<AbstractMethod> getMethodList(Class<?> mapperClass) { //注入通用的dao层接口的操作方法 return (List)Stream.of(new Insert(), new Delete(), new DeleteByMap(), new DeleteById(), new DeleteBatchByIds(), new Update(), new UpdateById(), new SelectById(), new SelectBatchByIds(), new SelectByMap(), new SelectOne(), new SelectCount(), new SelectMaps(), new SelectMapsPage(), new SelectObjs(), new SelectList(), new SelectPage()).collect(Collectors.toList()); } }2.扩展中提供的4个可注入方法实现目前在mybatis-plus的扩展插件中com.baomidou.mybatisplus.extension,给我们额外提供了4个注入方法。1.AlwaysUpdateSomeColumnById 根据Id更新每一个字段,全量更新不忽略null字段,解决mybatis-plus中updateById默认会自动忽略实体中null值字段不去更新的问题。2.InsertBatchSomeColumn 真实批量插入,通过单SQL的insert语句实现批量插入3.DeleteByIdWithFill 带自动填充的逻辑删除,比如自动填充更新时间、操作人4.Upsert 更新or插入,根据唯一约束判断是执行更新还是删除,相当于提供insert on duplicate key update支持insert into t_name (uid, app_id,createTime,modifyTime) values(111, 1000000,'2017-03-07 10:19:12','2017-03-07 10:19:12') on duplicate key update uid=111, app_id=1000000, createTime='2017-03-07 10:19:12',modifyTime='2017-05-07 10:19:12'mysql在存在主键冲突或者唯一键冲突的情况下,根据插入策略不同,一般有以下三种避免方法。insert ignore replace into insert on duplicate key update这里不展开介绍,大家可以自行查看:https://blog.csdn.net/weixin_42506706/article/details/113301248四、通过SQL注入器实现真正的批量插入通过SQL注入器sqlInjector 增加批量插入方法InsertBatchSomeColumn的过程如下:1.继承DefaultSqlInjector扩展自定义的SQL注入器代码如下:/** * 自定义Sql注入 */ public class MySqlInjector extends DefaultSqlInjector { @Override public List<AbstractMethod> getMethodList(Class<?> mapperClass) { List<AbstractMethod> methodList = super.getMethodList(mapperClass); //更新时自动填充的字段,不用插入值 methodList.add(new InsertBatchSomeColumn(i -> i.getFieldFill() != FieldFill.UPDATE)); return methodList; } }2.将自定义的SQL注入器注入到Mybatis容器中代码如下:@Configuration public class MybatisPlusConfig { @Bean public MySqlInjector sqlInjector() { return new MySqlInjector(); } }3.继承 BaseMapper 添加自定义方法public interface CommonMapper<T> extends BaseMapper<T> { /** * 全量插入,等价于insert * @param entityList * @return */ int insertBatchSomeColumn(List<T> entityList); }4.Mapper层接口继承新的CommonMapperpublic interface UserMapper extends CommonMapper<User> { }5.单元测试@Test public void testBatchInsert() { System.out.println("----- batch insert method test ------"); List<User> list = new ArrayList<>(); for (int i = 0; i < 10; i++) { User user = new User(); user.setName("test"); user.setAge(13); user.setEmail("101@qq.com"); list.add(user); } userMapper.insertBatchSomeColumn(list); }执行结果:可以看到已经实现单条insert语句支持数据的批量插入。注意⚠️:默认的insertBatchSomeColumn实现中,并没有类似saveBatch中的分配提交处理,这就存在一个问题,如果出现一个非常大的集合,就会导致最后组装提交的insert语句的长度超过mysql的限制。6.insertBatchSomeColumn添加分批处理机制@Service @Slf4j public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { @Resource private UserMapper userMapper; /** * 采用insertBatchSomeColumn重写saveBatch方法,保留分批处理机制 * @param entityList * @param batchSize * @return */ @Override @Transactional(rollbackFor = {Exception.class}) public boolean saveBatch(Collection<User> entityList, int batchSize) { try { int size = entityList.size(); int idxLimit = Math.min(batchSize, size); int i = 1; //保存单批提交的数据集合 List<User> oneBatchList = new ArrayList<>(); for(Iterator<User> var7 = entityList.iterator(); var7.hasNext(); ++i) { User element = var7.next(); oneBatchList.add(element); if (i == idxLimit) { userMapper.insertBatchSomeColumn(oneBatchList); //每次提交后需要清空集合数据 oneBatchList.clear(); idxLimit = Math.min(idxLimit + batchSize, size); } } }catch (Exception e){ log.error("saveBatch fail",e); return false; } return true; }更好的实现是继承ServiceImpl实现类,自己扩展通用的服务实现类,在其中重写通用的saveBatch方法,这样就不用在每一个服务类中都重写一遍saveBatch方法。单元测试:@Test public void testBatchInsert() { System.out.println("----- batch insert method test ------"); List<User> list = new ArrayList<>(); for (int i = 0; i < 10; i++) { User user = new User(); user.setName("test"); user.setAge(13); user.setEmail("101@qq.com"); list.add(user); } //批次数设为3,用来测试 userService.saveBatch(list,3); }执行结果:分4次采用insert批量新增,符合我们的结果预期。总结本文主要介绍了Mybatis-Plus中如何通过SQL注入器实现真正的批量插入。主要掌握如下内容:1、了解Mybatis-Plus中SQL注入器有什么作用,如何去进行扩展。2、默认的4个扩展方法各自的作用。3、默认的saveBatch批量新增和通过insertBatchSomeColumn实现的批量新增的底层实现原理的区别,为什么insertBatchSomeColumn性能更好以及存在哪些弊端。4、为insertBatchSomeColumn添加分批处理机制,避免批量插入的insert语句过长问题。
前言前面已经介绍了利用mybatis-plus中默认的雪花算法生成分布式唯一id,但是还是有一些弊端存在,今天聊聊在mybatis-plus中引入分布式ID生成框架idworker,进一步增强实现生成分布式唯一ID。一、官网官方文档:https://baomidou.com/Git地址:https://github.com/baomidou/mybatis-plusidworker官网:https://github.com/imadcn/idworkerTIP⚠️:推荐学习框架的时候,多研究下官网,获取第一手资料。二、默认实现的弊端在雪花算法的实现中,需要用户指定datacenterId和workerId的值。在分布式场景下,如果多台机器上的服务都指定相同的datacenterId和workerId,在高并发请求下,会出现Id重复的风险。如下是一个雪花算法ID出现重复的案例:https://github.com/imadcn/idworker/issues/14三、mybatis-plus中datacenterId和workerId的默认生成规则默认情况下,并不需要我们主动去配置datacenterId和workerId的值。mybatis-plus框架会根据应用所在服务器IP地址来生成datacenterId和workerId。我们来看看DefaultIdentifierGenerator的构造方法://默认的无参构造方法 public DefaultIdentifierGenerator() { this.sequence = new Sequence((InetAddress)null); } public DefaultIdentifierGenerator(InetAddress inetAddress) { this.sequence = new Sequence(inetAddress); } #也可以主动指定datacenterId和workerId的值 public DefaultIdentifierGenerator(long workerId, long dataCenterId) { this.sequence = new Sequence(workerId, dataCenterId); }根据ip地址初始化Sequence:public Sequence(InetAddress inetAddress) { this.inetAddress = inetAddress; this.datacenterId = this.getDatacenterId(31L); this.workerId = this.getMaxWorkerId(this.datacenterId, 31L); }根据ip地址生成datacenterId:protected long getDatacenterId(long maxDatacenterId) { long id = 0L; try { if (null == this.inetAddress) { this.inetAddress = InetAddress.getLocalHost(); } NetworkInterface network = NetworkInterface.getByInetAddress(this.inetAddress); if (null == network) { id = 1L; } else { byte[] mac = network.getHardwareAddress(); if (null != mac) { id = (255L & (long)mac[mac.length - 2] | 65280L & (long)mac[mac.length - 1] << 8) >> 6; id %= maxDatacenterId + 1L; } } } catch (Exception var7) { logger.warn(" getDatacenterId: " + var7.getMessage()); } return id; }根据datacenterId生成workerId:protected long getMaxWorkerId(long datacenterId, long maxWorkerId) { StringBuilder mpid = new StringBuilder(); mpid.append(datacenterId); String name = ManagementFactory.getRuntimeMXBean().getName(); if (StringUtils.isNotBlank(name)) { mpid.append(name.split("@")[0]); } return (long)(mpid.toString().hashCode() & '\uffff') % (maxWorkerId + 1L); }小结:无论是用户自己指定datacenterId和workerId,还是根据IP地址自动生成datacenterId和workerId。显然在大规模的集群环境下都不利于集群的扩展和维护管理,而且容易出现datacenterId和workerId相同而导致出现id重复的问题。那么有没有方法自动管理datacenterId和workerId的生成呢?四、idworker介绍idworker 是一个基于zookeeper和snowflake算法的分布式统一ID生成工具,通过zookeeper自动注册机器(最多1024台),无需手动指定workerId和dataCenterId。在分布式集群中,可能需要部署的大量的机器节点。在节点少的受,可以人工维护。在量大的场景下,手动维护成本高,考虑到自动部署、运维等等问题,节点的命名,最好由系统自动维护。节点的命名,主要是为节点进行唯一编号。主要的诉求是,不同节点的编号,是绝对的不能重复。一旦编号重复,就会导致有不同的节点碰撞,导致集群异常。有以下两个方案,可供生成集群节点编号:(1)使用数据库的自增ID特性,用数据表,存储机器的mac地址或者ip来维护。(2)使用ZooKeeper持久顺序节点的次序特性,来维护节点的编号。这里,我们采用第二种,通过ZooKeeper持久顺序节点特性,来配置维护节点的编号NODEID。集群节点命名服务的基本流程是:(1)启动节点服务,连接ZooKeeper, 检查命名服务根节点根节点是否存在,如果不存在就创建系统根节点。(2)在根节点下创建一个临时顺序节点,取回顺序号做节点的NODEID。如何临时节点太多,可以根据需要,删除临时节点。由于是采用zookeeper顺序节点的特性生成datacenterId和workerId,可以天然的保证datacenterId和workerId的唯一性,减少了人工维护的弊端。五、idworker实战其中mybatis-plus内置的ImadcnIdentifierGenerator方法,就已经提供了对idworker框架的支持。对,你没看错,又又又是内置的,可是你却还不会用。不得不佩服mybatis-plus框架的开发者,太牛了。查看ImadcnIdentifierGenerator的源码,可以发现里面就是通过idworker实现的。1、引入maven依赖<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.2</version> </dependency> <dependency> <groupId>com.imadcn.framework</groupId> <artifactId>idworker</artifactId> <version>1.5.0</version> </dependency>2、添加zookeeper配置mybatis-plus.zookeeper.serverLists=127.0.0.1:21813、指定mybatis-plus的id生成器@Configuration public class IdAutoConfig { @Value("${mybatis-plus.zookeeper.serverLists}") private String zkServerLists; @Bean public IdentifierGenerator idGenerator() { return new ImadcnIdentifierGenerator(zkServerLists); } }4、测试执行单元测试:@Test public void testInsert() { System.out.println(("----- insert method test ------")); User user = new User(); user.setName("test"); user.setAge(13); user.setEmail("101@qq.com"); userMapper.insert(user); System.out.println(user.toString()); }执行结果:Preparing: INSERT INTO user ( id, name, age, email ) VALUES ( ?, ?, ?, ? ) Parameters: 728706665213329499(Long), test(String), 13(Integer), 101@qq.com(String) Updates: 1 User(id=728706665213329499, name=test, age=13, email=101@qq.com)总结本文主要介绍如何在mybatis-plus中引入idworker框架,通过zookeeper管理snowflake算法中workerId和dataCenterId`的生成,保证其唯一性,避免出现id重复的情况。
前言在实际开发过程中,数据库自增主键生成Id能满足大部分的场景。但是随着分布式应用场景的增多,表数据的增大导致分表分库的大量应用。数据库自增主键的生成规则无法满足对应的业务场景,于是诞生了越来越多的分布式ID生成算法,其中雪花算法是目前最为流行的。今天说一下在mybatis-plus中如何使用雪花算法生成Id。一、mybatis-plus官网官方文档:https://baomidou.com/Git地址:https://github.com/baomidou/mybatis-plusTIP⚠️:推荐学习框架的使用的时候,都多研究下官网,获取第一手资料。二、雪花算法实战1.建表DROP TABLE IF EXISTS user; CREATE TABLE user ( id BIGINT(20) NOT NULL COMMENT '主键ID', name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名', age INT(11) NULL DEFAULT NULL COMMENT '年龄', email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱', PRIMARY KEY (id) );注意⚠️:这里的主键字段没有配置自增生成策略,所以执行新增操作的时候,需要给id字段设置值,才能新增成功。类似如下:INSERT INTO user ( id, name, age, email ) VALUES ( 123434, 'test', 13, '101@qq.com')2.新建测试工程相关代码:maven依赖:<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>实体User:@Data public class User { private Long id; private String name; private Integer age; private String email; }mapper: public interface UserMapper extends BaseMapper<User> { }启动类Application:@SpringBootApplication @Slf4j @MapperScan("com.laowan.mybatis_plus.mapper") public class MybatisPlusApplication { public static void main(String[] args) { SpringApplication.run(MybatisPlusApplication.class, args); log.info("mybatis_plus_demo 启动成功"); } }注意⚠️:这里在启动类上配置了@MapperScan(“mapper接口目录”),所以在UserMapper接口上没有添加@Mapper注解。@Mapper配置方法:@Mapper public interface UserMapper extends BaseMapper<User> { }两者任意选择一种方式配置即可,如果都不配置,那么在执行dao层方法进行数据操作时,会出现在spring容器中找不到对应的bean的异常。@Mapper和@MapperScan都不配置调用mapper方法时出现的异常:Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.laowan.mybatis_plus.mapper.UserMapper' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}配置属性:server.port=8080 logging.level.com.laowan.mybatis_plus.mapper=debug spring.datasource.url = jdbc:mysql://localst:3306/seckill?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true spring.datasource.username = root spring.datasource.password = 1234563.单元测试@SpringBootTest class MybatisPlusApplicationTests { @Autowired private UserMapper userMapper; @Test public void testInsert() { System.out.println(("----- insert method test ------")); User user = new User(); user.setName("test"); user.setAge(13); user.setEmail("101@qq.com"); userMapper.insert(user); System.out.println(user.toString()); }执行结果:User(id=728666272023183375, name=test, age=13, email=101@qq.com)多次执行,发现主键ID的确呈趋势递增,并且符合雪花算法的规范。结论:主键id的生成策略已经采用了雪花算法,呈趋势递增。三、实现分析很多人可能疑惑🤔,你这明明啥都没干,怎么就实现了雪花算法生成Id。其实mybatis-plus已经内置雪花算法生成分布式唯一id。在mybatis-plus特性中已经明确说明了这点。我们可以直接在IDEA中双击shift搜索Sequence类查看其具体实现,可以发现其实现就是采用了雪花算法。四、为什么默认就是雪花算法实体User:@Data public class User { private Long id; private String name; private Integer age; private String email; }这里可以看到我们并没有在实体类的id上设置id生成策略。其实mybatis-plus中默认的主键生成策略为DefaultIdentifierGenerator,里面的实现就是采用Sequence生成主键。public class DefaultIdentifierGenerator implements IdentifierGenerator { private final Sequence sequence; public DefaultIdentifierGenerator() { this.sequence = new Sequence((InetAddress)null); } public DefaultIdentifierGenerator(InetAddress inetAddress) { this.sequence = new Sequence(inetAddress); } public DefaultIdentifierGenerator(long workerId, long dataCenterId) { this.sequence = new Sequence(workerId, dataCenterId); } public DefaultIdentifierGenerator(Sequence sequence) { this.sequence = sequence; } public Long nextId(Object entity) { return this.sequence.nextId(); } }五、主动设置Id生成策略可以通过mybatis-plus中的@TableId主键,主动标识主键字段,并配置主键生成策略。@Data public class User { //采用IdentifierGenerator默认的实现类DefaultIdentifierGenerator生成id @TableId(type = IdType.ASSIGN_ID) private Long id; private String name; private Integer age; private String email; }六、内置的雪花算法工具类:IdWorker在mybatis-plus中,已经内置了雪花算法的工具类IdWorker,其实现原理也是通过默认的ID生成器DefaultIdentifierGenerator来实现。如果项目开发中需要主动去获取雪花id通过编码实现业务逻辑,可以使用其中的相关方法。public static void main(String[] args) { // 返回值 1385106677482582018 System.out.println(IdWorker.getId()); // 返回值 "1385106677482582019" System.out.println(IdWorker.getIdStr()); }注意⚠️:在github中有一个很流行的分布式统一ID生成框架也叫idworker,需要和mybatis-plus中自带的Idworker工具类区分开来。idworker 是一个基于zookeeper和snowflake算法的分布式统一ID生成工具,通过zookeeper自动注册机器(最多1024台),无需手动指定workerId和dataCenterId。idworker官网:https://github.com/imadcn/idworkermybatis-plus雪花算法增强idworker:https://laowan.blog.csdn.net/article/details/125607205总结mybatis-plus已经内置了雪花算法生成分布式唯一Id,并且是默认的ID生成策略。大家在实际项目中,可以通过在主键字段上添加@TableId注解来控制主键的生成策略。
一、概念说明myabtis的缓存分为一级缓存和二级缓存,默认开启一级缓存,关闭二级缓存,一级缓存时sqlSession级别,二级缓存是namespace级别。1、一级缓存mybatis的一级缓存默认是开启的,它在一个sqlSession会话里面的所有查询操作都会保存到缓存中,一般来说一个请求中的所有增删改查操作都是在同一个sqlSession里面的,所以我们可以认为每个请求都有自己的一级缓存,如果同一个sqlSession会话中2个查询中间有一个 insert 、update或delete 语句,那么之前查询的所有缓存都会清空。使用MyBatis开启一次和数据库的会话,MyBatis会创建出一个SqlSession对象表示一次数据库会话,在对数据库的一次会话中, 有可能会反复地执行完全相同的查询语句,每一次查询都会去查一次数据库,为了减少资源浪费,mybaits提供了一种缓存的方式(一级缓存)。一级缓存失效:如果同一个sqlSession会话中2 个查询中间有一个 insert 、update或delete 语句,那么之前查询的所有缓存都会清空。因为每次增删改操作都有可能会改变原来的数据,所以必须刷新缓存;2、二级缓存二级缓存是全局的,也就是说;多个请求可以共用一个缓存,二级缓存需要手动开启。二级缓存针对的是同一个namespace,所以建议是在单表操作的Mapper中使用,或者是在相关表的Mapper文件中共享同一个缓存。一级缓存无过期时间,只有生命周期,缓存会先放在一级缓存中,当sqlSession会话提交或者关闭时才会将一级缓存刷新到二级缓存中;开启二级缓存后,用户查询时,会先去二级缓存中找,找不到在去一级缓存中找,然后才去数据库查询;二级缓存失效:所有的update操作(insert,delete,uptede)都会触发缓存的刷新,从而导致二级缓存失效,所以二级缓存适合在读多写少的场景中开启。二级缓存过期时间:需要注意的是,并不是key-value的过期时间,而是这个cache的过期时间,是flushInterval,意味着整个清空缓存cache,所以不需要后台线程去定时检测。每当存取数据的时候,都有检测一下cache的生命时间,默认是1小时,如果这个cache存活了一个小时,那么将整个清空一下。3、比较开启方式一级缓存是默认开启的,二级缓存默认关闭。作用范围一级缓存是会话级别的缓存,即sqlSession级别,会话结束,清除会话中的缓存数据,实际代码中通过通过开启事务让多个数据库操作共享一个sqlSession。二级缓存: 全局级别,也叫namespace级别,会话结束,缓存依然存在,多个请求可以共享缓存数据。缓存位置一级缓存由于是sqlSession级别,本质上是在JVM中创建一个Map集合对象保存缓存数据,所以缓存数据保留的地方是本地JVM内存中。二级缓存默认也是保存在JVM中,但是可以通过配置将缓存数据保存到第三方缓存中,比如ehcache、redis。保存在redis这些的分布式缓存中,能提供更好的分布式场景的支持。缓存过期一级缓存无过期时间,只有生命周期,缓存会先放在一级缓存中,当sqlSession会话提交或者关闭时才会将一级缓存刷新到二级缓存中;开启二级缓存后,用户查询时,会先去二级缓存中找,找不到在去一级缓存中找,然后才去数据库查询;二级缓存的过期时间默认是1小时,如果这个cache存活了一个小时,那么将整个清空一下。需要注意的是,并不是key-value的过期时间,而是这个cache的过期时间,是flushInterval,意味着整个清空缓存cache,所以不需要后台线程去定时检测,每当存取数据的时候,都有检测一下cache的生命时间。小结:一级缓存的作用在我看来在实际业务场景中作用真的非常有限,因为需要在一个事务方法中重复查询的需求场景真的太少,而且由于Mysql数据库的MVCC机制以及事务隔离机制-可重复读的能力,会导致同一个事务方法内多次执行相同的查询必定会得到相同的结果,所以在事务范围内的重复查询基本没什么实际作用。设计一级缓存设计的意义,可能更多的是为二级缓存的实现做铺垫。所以,如果关闭了mybatis的一级缓存,二级缓存将不会生效。二、mybatis缓存的生命周期mybatis的查询缓存会先放在一级缓存中,当sqlSession会话提交或者关闭时才会将一级缓存刷新到二级缓存中;开启二级缓存后,用户查询时,会先去二级缓存中找,找不到在去一级缓存中找,然后才去数据库查询;三、一级缓存的使用spring开启事务管理的方法在方法执行过程中只会创建一个sqlsession。这是因为事务管理下的sql执行方式是BATCH,只会与数据库交互一次,一次执行完所有的sql,所以只会创建一个sqlsession;不开启事务:@Override public GoodsStock getStock(Integer goodsId) { log.info("第1次查询"); GoodsStock goodsStock = goodsStockMapper.getStock(goodsId); log.info("第2次查询"); goodsStock = goodsStockMapper.getStock(goodsId); return goodsStock; }执行结果:开启事务:@Override @Transactional(rollbackFor = Exception.class) public GoodsStock getStock(Integer goodsId) { log.info("第1次查询"); GoodsStock goodsStock = goodsStockMapper.getStock(goodsId); log.info("第2次查询"); goodsStock = goodsStockMapper.getStock(goodsId); return goodsStock; }执行结果:可以看到,第2次查询并没有打印查询SQL,说明命中了缓存。关闭一级缓存:在配置文件中添加如下属性#一级缓存关闭,默认值SESSION,表示开启一级缓存,statement表示关闭一级缓存mybatis.configuration.local-cache-scope=statement再次请求:通过添加配置关闭mybatis的一级缓存后,第2次查询打印了SQL,进行了查库操作。说实话,一级缓存比较鸡肋,一般在同一个方法内,很少会进行重复的查询,在实际业务中的真实使用场景暂时没有想到。目前的实际意义更多的是为mybatis的二级缓存做一个铺垫。关于一级缓存的使用注意:1、一般在一个方法内需要用相同条件查询多次的场景其实非常少见,因为方法体内对象都是可见共享的,没必要再次进行查询。2、如果需要再次查询判断之前的查询结果是否已经出现变更,这时除了需要关闭一级缓存外,还需要注意事务的隔离级别。比如,如果事务隔离级别为可重复读时,在本事务中尽管执行多次查询,由于会对查询的数据在事务范围内加读锁,所以查询的数据一定是相同的,没有重复查下的必要。如果事务隔离级别为读已提交,需要进行重复查询判断数据的变化情况,那么需要关闭mybatis的一级缓存。不过,推荐在需要多次查询做数据比对的场景,最好还是关闭事务,这样就可以保障每次查询都进行查库操作,获取最新的数据。事务为可重复读级别REPEATABLE_READ,没有再次查询的必要,数据一定不会发生变更:@Override @Transactional(rollbackFor = Exception.class,isolation=Isolation.REPEATABLE_READ,readOnly = true) public GoodsStock getStock(Integer goodsId) { log.info("第1次查询"); //由于是REPEATABLE_READ可重复读隔离级别,在事务范围内会对读取到的数据加锁,所以读取到的数据一定不会发生变更,没有再次查询的必要 GoodsStock goodsStock = goodsStockMapper.getStock(goodsId); log.info("第2次查询"); goodsStock = goodsStockMapper.getStock(goodsId); return goodsStock; }事务为读已提交READ_COMMITTED,由于读锁会立即释放,导致事务范围内已经查询到的数据可能被其他事务修改,如果需要重复查库判断数据变化,那么必须关闭mybatis的一级缓存,不然2次查询不会进行查库操作。如果在1次查询和2 次查询中的逻辑处理有一个 insert 、update或delete 操作,那么之前查询的所有缓存都会清空。2次查询也会进行查库操作。@Override @Transactional(rollbackFor = Exception.class,isolation=Isolation.READ_COMMITTED,readOnly = true) public GoodsStock getStock(Integer goodsId) { log.info("第1次查询"); //由于是REPEATABLE_READ可重复读隔离级别,在事务范围内会对读取到的数据加锁,所以读取到的数据一定不会发生变更,没有再次查询的必要 GoodsStock goodsStock1 = goodsStockMapper.getStock(goodsId); log.info("逻辑处理"); log.info("第2次查询,判断变化,需要关闭mybatis的一级缓存"); GoodsStock goodsStock2 = goodsStockMapper.getStock(goodsId); return goodsStock2; }四、二级缓存的使用二级缓存相关的配置有三个地方:1、mybatis-config中有一个全局配置属性,这个不配置也行,因为默认就是true。<setting name="cacheEnabled" value="true"/>spring boot中也可以通过配置项开启二级缓存:#二级缓存开启 mybatis.configuration.cache-enabled=true2、在Mapper映射文件内需要配置缓存标签:<cache eviction="LRU" flushInterval="60000" size="1000"/>配置项说明:eviction 淘汰策略,LRU、FIFOsize 缓存个数flushInterval 刷新频率,即缓存过期时间LruCache LRU淘汰策略缓存(默认淘汰策略) 当缓存达到上限,删除最近最少使用缓存 eviction=“LRU”FifoCache FIFO淘汰策略缓存 当缓存达到上限,删除最先入队的缓存 eviction=“FIFO”3、在select查询语句标签上配置useCache属性,如下:<select id="selectUserAndJob" resultMap="JobResultMap2" useCache="true"> select * from lw_user </select>以上配置第1点是默认开启的,也就是说我们只要配置第2点就可以打开二级缓存了,而第3点是当我们需要针对某一条语句来配置二级缓存的时候则可以使用。4、开启二级缓存后,映射的pojo对象需要实现序列化接口执行结果报序列化异常,那是因为我们映射的pojo对象未实现序列化接口,说明我们从缓存数据中读取数据需要进行反序列化,这是因 为mybatis的二级缓存的缓存介质有多种多样,而并不一定是在内存中,所以需要我们对pojo对象进行序列化,只要实现序列化接口即可。五、自定义二级缓存默认的二级缓存也是存储在本地缓存,对于微服务下是可能出现脏读的情况的,这时可能会需要自定义缓存,比如利用redis来存储缓存,而不是存储在本地内存当中。MyBatis官方也提供了第三方缓存的支持引入pom文件:<dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-redis</artifactId> <version>1.0.0-beta2</version> </dependency>然后缓存配置如下:<cache type="org.mybatis.caches.redis.RedisCache"></cache>然后在默认的resource路径下新建一个redis.properties文件:host=localhost port=6379六、mybatis缓存、spring缓存和redis缓存的使用比较mybatis缓存——dao层的缓存,主要是对select查库操作的结果进行缓存。spring缓存——方法级别的缓存,主要通过在方法上添加缓存注解@CachePut、@CacheEvict来实现。redisTemplate缓存——代码级别的缓存,主要是在方法体内部通过redisTemplate模板类编码控制缓存读写。mybatis缓存只能对sql查询结果进行缓存,spring 缓存只能对方法执行结果进行缓存,不能对方法执行过程中的中间对象进行缓存。这两者的优点都是使用简单,只需简单的配置或添加注解即可实现缓存,基本不需要编码。缺点是缓存的控制范围都是方法级别,不能缓存方法执行过程中的中间对象,有一定的使用局限性。而通过redisTemplate进行编码控制缓存,优点是控制灵活,缺点是需要编码实现。实际项目中大家可以根据需要选择怎么使用缓存,但是要注意,针对单一方法,缓存不要混用,容易造成逻辑混乱,定位问题困难等问题。总结本文主要是针对mybatis的一级缓存和二级缓存的区别和使用进行了详细介绍。重点掌握一下方面1、一级缓存和二级缓存的区别2、mybatis缓存的生命周期3、如何开启二级缓存,二级缓存什么时候失效,如何将本地的二级缓存升级到采用redis支持分布式的二级缓存?4、二级缓存的淘汰策略有哪些5、mybatis缓存、spring缓存和redis缓存的比较。mybatis的一级缓存是为了二级缓存做铺垫,如果采用二级缓存,推荐升级到采用redis存储缓存数据,这样能更好的支持分布式。
前言很多时候,在项目初期都是仅采用mysql数据库作为业务数据库,但是随着数据的增长,当单表的数据超过千万级后,在怎么对查询SQL语句进行优化性能都不理想。这种情况下,我们就可以考虑通过ES来实现项目的读写分离:写操作对Mysql库进行操作,读操作采用ES。那么我们应该如何保证ES和Mysql的数据同步呢?本文给大家介绍通过Logstash实现mysql数据定时增量同步到ES。一、系统配置在本篇文章中,我使用下列产品进行测试:MySQL:8.0.16Elasticsearch:7.1.1Logstash:7.1.1Java:1.8.0_162-b12JDBC 输入插件:v4.3.13JDBC 连接器:Connector/J 8.0.16关于MySQL、Elasticsearch、Logstash的安装过程这里就不作赘述。二、同步步骤整体概览在本篇博文中,我们使用 Logstash 和 JDBC 输入插件来让 Elasticsearch 与 MySQL 保持同步。从概念上讲,Logstash 的 JDBC 输入插件会运行一个循环来定期对 MySQL 进行轮询,从而找出在此次循环的上次迭代后插入或更改的记录。如要让其正确运行,必须满足下列条件:ES和Mysql表的id字段对应关系在将 MySQL 中的文档写入 Elasticsearch 时,Elasticsearch 中的 “_id” 字段必须设置为 MySQL 中的 “id” 字段。这可在 MySQL 记录与 Elasticsearch 文档之间建立一个直接映射关系。如果在 MySQL 中更新了某条记录,那么将会在 Elasticsearch 中覆盖整条相关记录。请注意,在 Elasticsearch 中覆盖文档的效率与更新操作的效率一样高,因为从内部原理上来讲,更新便包括删除旧文档以及随后对全新文档进行索引。因为是根据时间实现增量同步,所以mysql表中必须有一个包含更新或插入时间的字段当在 MySQL 中插入或更新数据时,该条记录必须有一个包含更新或插入时间的字段。通过此字段,便可允许 Logstash 仅请求获得在轮询循环的上次迭代后编辑或插入的文档。Logstash 每次对 MySQL 进行轮询时,都会保存其从 MySQL 所读取最后一条记录的更新或插入时间。在下一次迭代时,Logstash 便知道其仅需请求获得符合下列条件的记录:更新或插入时间晚于在轮询循环中的上一次迭代中所收到的最后一条记录。如果满足上述条件,我们便可配置 Logstash,以定期请求从 MySQL 获得新增或已编辑的全部记录,然后将它们写入 Elasticsearch 中。完成这些操作的 Logstash 代码在本篇博文的后面会列出。整个同步演示步骤如下:在Mysql中新建表在ES中建立索引logstash进行管道配置验证数据同步三.logstash数据同步实战1、新建mysql表可以使用下列代码配置 MySQL 数据库和数据表:CREATE DATABASE es_db; USE es_db; DROP TABLE IF EXISTS es_table; CREATE TABLE es_table ( id BIGINT(20) UNSIGNED NOT NULL, client_name VARCHAR(32) NOT NULL, modification_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, insertion_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY unique_id (id) );在上面的 MySQL 配置中,有几个参数需要特别注意:es_table:这是 MySQL 数据表的名称,数据会从这里读取出来并同步到 Elasticsearch。id:这是该条记录的唯一标识符。请注意 “id” 已被定义为 PRIMARY KEY(主键)和 UNIQUE KEY(唯一键)。这能确保每个 “id” 仅在当前表格中出现一次。其将会转换为 “_id”,以用于更新 Elasticsearch 中的文档及向 Elasticsearch 中插入文档。client_name:此字段表示在每条记录中所存储的用户定义数据。在本篇博文中,为简单起见,我们只有一个包含用户定义数据的字段,但您可以轻松添加更多字段。我们要更改的就是这个字段,从而向大家演示不仅新插入的 MySQL 记录被复制到了 Elasticsearch 中,而且更新的记录也被正确传播到了 Elasticsearch 中。modification_time:在 MySQL 中插入或更改任何记录时,都会将这个所定义字段的值设置为编辑时间。有了这个编辑时间,我们便能提取自从上次 Logstash 请求从 MySQL 获取记录后被编辑的任何记录。insertion_time:此字段主要用于演示目的,并非正确进行同步需满足的严格必要条件。我们用其来跟踪记录最初插入到 MySQL 中的时间。完成上述配置后,可以通过下列语句向 MySQL 中写入记录:INSERT INTO es_table (id, client_name) VALUES (<id>, <client name>);可以通过下列命令更新 MySQL 中的记录:UPDATE es_table SET client_name = <new client name> WHERE id=<id>;2、ES中新建索引在ES中新建索引rdbms_sync_idxPUT rdbms_sync_idx { "settings": { "index": { "refresh_interval": "5s" } }, "mappings": { "_default_": { "properties": { "@timestamp": { "type": "date" }, "insertion_time": { "type": "date" }, "modification_time": { "type": "date" }, "client_name": { "type": "keyword" } } } } }3、Logstash 管道配置新建logstash-7.1.1/sync/logstash-db-sync.conf配置文件:#logstash输入配置 input { #jdbc输入配置,用来指定mysql中需要同步的数据查询SQL及同步周期 jdbc { jdbc_driver_library => "<path>/mysql-connector-java-8.0.16.jar" jdbc_driver_class => "com.mysql.jdbc.Driver" jdbc_connection_string => "jdbc:mysql://<MySQL host>:3306/es_db" jdbc_user => <my username> jdbc_password => <my password> # 是否开启分页 jdbc_paging_enabled => true # 是否开启记录上次追踪的结果,也就是上次更新的时间,这个会记录到 last_run_metadata_path 的文件 use_column_value => true # 用来控制增量更新的字段,一般是自增id或者创建、更新时间,注意这里要采用sql语句中select采用的字段别名 tracking_column => "unix_ts_in_secs" # tracking_column 对应字段的类型 tracking_column_type => "numeric" # 设置定时任务间隔 含义:分、时、天、月、年,全部为*默认含义为每分钟跑一次任务,这里设置为每5分钟同步一次 schedule => "*/5 * * * * *" # 同步数据的查询sql语句 statement => "SELECT *, UNIX_TIMESTAMP(modification_time) AS unix_ts_in_secs FROM es_table WHERE (UNIX_TIMESTAMP(modification_time) > :sql_last_value AND modification_time < NOW()) ORDER BY modification_time ASC" } } #logstash输入数据的字段匹配和数据过滤 filter { mutate { copy => { "id" => "[@metadata][_id]"} remove_field => ["id", "@version", "unix_ts_in_secs"] } } #logstash输出配置 output { # 采用stdout可以将同步数据输出到控制台,主要是调试阶段使用 stdout { codec => "rubydebug"} # 指定输出到ES的具体索引 elasticsearch { index => "rdbms_sync_idx" document_id => "%{[@metadata][_id]}" } }在上述管道中,应该重点强调几个区域:tracking_column:此字段会指定 “unix_ts_in_secs” 字段(用于跟踪 Logstash 从 MySQL 读取的最后一个文档,下面会进行描述),其存储在 .logstash_jdbc_last_run 中的磁盘上。该值将会用来确定 Logstash 在其轮询循环的下一次迭代中所请求文档的起始值。在 .logstash_jdbc_last_run 中所存储的值可以作为 “:sql_last_value” 通过 SELECT 语句进行访问。unix_ts_in_secs:这是一个由上述 SELECT 语句生成的字段,包含可作为标准 Unix 时间戳(自 Epoch 起秒数)的 “modification_time”。我们刚讨论的 “tracking column” 会引用该字段。Unix 时间戳用于跟踪进度,而非作为简单的时间戳;如将其作为简单时间戳,可能会导致错误,因为在 UMT 和本地时区之间正确地来回转换是一个十分复杂的过程。sql_last_value:这是一个内置参数,包括 Logstash 轮询循环中当前迭代的起始点,上面 JDBC 输入配置中的 SELECT 语句便会引用这一参数。该字段会设置为 “unix_ts_in_secs”(读取自 .logstash_jdbc_last_run)的最新值。在 Logstash 轮询循环内所执行的 MySQL 查询中,其会用作所返回文档的起点。通过在查询中加入这一变量,能够确保不会将之前传播到 Elasticsearch 的插入或更新内容重新发送到 Elasticsearch。schedule:其会使用 cron 语法来指定 Logstash 应当以什么频率对 MySQL 进行轮询以查找变更。这里所指定的 “*/5 * * * * *” 会告诉 Logstash 每 5 秒钟联系一次 MySQL。modification_time < NOW():SELECT 中的这一部分是一个较难解释的概念,我们会在下一部分详加解释。filter:在这一部分,我们只需简单地将 MySQL 记录中的 “id” 值复制到名为 “_id” 的元数据字段,因为我们之后输出时会引用这一字段,以确保写入 Elasticsearch 的每个文档都有正确的 “_id” 值。通过使用元数据字段,可以确保这一临时值不会导致创建新的字段。我们还从文档中删除了 “id”、“@version” 和 “unix_ts_in_secs” 字段,因为我们不希望将这些字段写入到 Elasticsearch 中。output:在这一部分,我们指定每个文档都应当写入 Elasticsearch,还需为其分配一个 “_id”(需从我们在筛选部分所创建的元数据字段提取出来)。还会有一个包含被注释掉代码的 rubydebug 输出,启用此输出后能够帮助您进行故障排查。4、启动Logstash./logstash -f /usr/local/logstash-7.1.1/sync/logstash-db-sync.conf后台启动为:nohup ./logstash -f /usr/local/logstash-7.1.1/sync/logstash-db-sync.conf &5、测试可以通过一些简单测试来展示我们的实施方案能够实现预期效果。我们可以使用下列命令向 MySQL 中写入记录:INSERT INTO es_table (id, client_name) VALUES (1, 'Jim Carrey'); INSERT INTO es_table (id, client_name) VALUES (2, 'Mike Myers'); INSERT INTO es_table (id, client_name) VALUES (3, 'Bryan Adams');JDBC 输入计划触发了从 MySQL 读取记录的操作并将记录写入 Elasticsearch 后,我们即可运行下列 Elasticsearch 查询来查看 Elasticsearch 中的文档:GET rdbms_sync_idx/_search其会返回类似下面回复的内容:"hits" : { "total" : { "value" :3, "relation" : "eq" }, "max_score" :1.0, "hits" : [ { "_index" : "rdbms_sync_idx", "_type" : "_doc", "_id" :"1", "_score" :1.0, "_source" : { "insertion_time" :"2019-06-18T12:58:56.000Z", "@timestamp" :"2019-06-18T13:04:27.436Z", "modification_time" :"2019-06-18T12:58:56.000Z", "client_name" :"Jim Carrey" } }, Etc …然后我们可以使用下列命令更新在 MySQL 中对应至 _id=1 的文档:UPDATE es_table SET client_name = 'Jimbo Kerry' WHERE id=1;其会正确更新 _id 被识别为 1 的文档。我们可以通过运行下列命令直接查看 Elasticsearch 中的文档:GET rdbms_sync_idx/_doc/1其会返回一个类似下面的文档:{ "_index" : "rdbms_sync_idx", "_type" : "_doc", "_id" :"1", "_version" :2, "_seq_no" :3, "_primary_term" :1, "found" : true, "_source" : { "insertion_time" :"2019-06-18T12:58:56.000Z", "@timestamp" :"2019-06-18T13:09:30.300Z", "modification_time" :"2019-06-18T13:09:28.000Z", "client_name" :"Jimbo Kerry" } }请注意 _version 现已设置为 2,modification_time 现在已不同于 insertion_time,并且 client_name 字段已正确更新至新值。在本例中,@timestamp 字段的用处并不大,由 Logstash 默认添加。6、删除数据按照目前的配置,如果从 MySQL 中删除一个文档,那么这一删除操作并不会传播到 Elasticsearch。可以考虑通过下列方法来解决这一问题:我们可以通过软删除实现mysql数据删除操作的同步。MySQL 记录可以包含一个 “is_deleted” 字段,用来显示该条记录是否仍有效。这一方法被称为“软删除”。正如对 MySQL 中的记录进行其他更新一样,“is_deleted” 字段将会通过 Logstash 传播至 Elasticsearch。如果实施这一方法,则需要编写 Elasticsearch 和 MySQL 查询,从而将 “is_deleted” 为 “true”(正)的记录/文档排除在外。 最后,可以通过后台作业来从 MySQL 和 Elastic 中移除此类文档。另一种方法是确保负责从 MySQL 中删除记录的任何系统随后也会执行一条命令,从而直接从 Elasticsearch 中删除相应文档。四.SELECT 语句正确性分析注意:增量同步的SQL语句对于增量数据的查询,如果是通过更新时间来查询增量数据,不能简单的通过i.updated_time >= :sql_last_value来控制,会出现临界值的问题.在这一部分,我们会详加解释为什么在 SELECT 语句中添加 modification_time < NOW() 至关重要。为帮助解释这一概念,我们首先给出几个反面例子,向您演示为什么两种最直观的方法行不通。然后会解释为什么添加 modification_time < NOW() 能够克服那两种直观方法所导致的问题。情况一:大于sql_last_value如果仅仅采用UNIX_TIMESTAMP(modification_time) > :sql_last_value 的话,会发生什么情况。在这种情况下,SELECT 语句如下:statement => "SELECT *, UNIX_TIMESTAMP(modification_time) AS unix_ts_in_secs FROM es_table WHERE (UNIX_TIMESTAMP(modification_time) > :sql_last_value) ORDER BY modification_time ASC"乍看起来,上面的方法好像应可以正常运行,但是对于一些边缘情况,其可能会错过一些文档。举例说明,我们假设 MySQL 现在每秒插入两个文档,Logstash 每 5 秒执行一次 SELECT 语句。具体如下图所示,T0 到 T10 分别代表每一秒,MySQL 中的数据则以 R1 到 R22 表示。我们假定 Logstash 轮询循环的第一个迭代发生在 T5,其会读取文档 R1 到 R11,如蓝绿色的方框所示。在 sql_last_value 中存储的值现在是 T5,因为这是所读取最后一条记录 (R11) 的时间戳。我们还假设在 Logstash 从 MySQL 读取完文件后,另一个时间戳为 T5 的文档 R12 立即插入到了 MySQL 中。图表显示读取记录时会错开一条在上述 SELECT 语句的下一个迭代中,我们仅会提取时间晚于 T5 的文档(因为 WHERE (UNIX_TIMESTAMP(modification_time) > :sql_last_value) 就是如此规定的),这也就意味着将会跳过记录 R12。您可以参看下面的图表,其中蓝绿色方框表示 Logstash 在当前迭代中读取的记录,灰色方框表示 Logstash 之前读取的记录。简单来说:在T5时刻执行sql查询时,这一时刻同时插入了多条数据R11,R12等,但是由于并发或者其他网络延迟问题,只读取到了R11这条数据,这样就导致了R12数据不会被同步到ES中。结论:通过(modification_time) > :sql_last_value(modification_time) > sql_last_value查询增量数据,如果在执行同步的时刻发生大量的并发写入,很容易出现数据丢失的情况情况二:大于等于sql_last_value为了解决上面的问题,您可能决定更改 WHERE 子句为 greater than or equals(晚于或等于),具体如下:statement => "SELECT *, UNIX_TIMESTAMP(modification_time) AS unix_ts_in_secs FROM es_table WHERE (UNIX_TIMESTAMP(modification_time) >= :sql_last_value) ORDER BY modification_time ASC"然而,这种实施策略也并不理想。这种情况下的问题是:在最近一个时间间隔内从 MySQL 读取的最近文档会重复发送到 Elasticsearch。尽管这不会对结果的正确性造成任何影响,但的确做了无用功。和前一部分类似,在最初的 Logstash 轮询迭代后,下图显示了已经从 MySQL 读取了哪些文档。当执行后续的 Logstash 轮询迭代时,我们会将时间晚于或等于 T5 的文档全部提取出来。可以参见下面的图表。请注意:记录 11(紫色显示)会再次发送到 Elasticsearch。前面两种情况都不甚理想。在第一种情况中,会丢失数据,而在第二种情况中,会从 MySQL 读取冗余数据并将这些数据发送到 Elasticsearch。结论:如果通过(modification_time) >= :sql_last_value查询增量数据,mysql在查询的时候发生大量的并发写入,会从 MySQL 读取冗余数据并将这些数据发送到 Elasticsearch情况三:modification_time大于sql_last_value并且小于NOW()由于执行statement 查询增量的时刻,mysql可能还在继续写入数据,所以针对NOW()时刻获取的数据是不准确的,需要过滤。鉴于前面两种情况都不太理想,应该采用另一种办法。通过指定 (UNIX_TIMESTAMP(modification_time) > :sql_last_value AND modification_time < NOW()),我们会将每个文档都发送到 Elasticsearch,而且只发送一次。请参见下面的图表,其中当前的 Logstash 轮询会在 T5 执行。请注意,由于必须满足 modification_time < NOW(),所以只会从 MySQL 中读取截至(但不包括)时间段 T5 的文档。由于我们已经提取了 T4 的全部文档,而未读取 T5 的任何文档,所以我们知道对于下一次的Logstash 轮询迭代,sql_last_value 将会被设置为 T4。下图演示了在 Logstash 轮询的下一次迭代中将会发生什么情况。由于 UNIX_TIMESTAMP(modification_time) > :sql_last_value,并且 sql_last_value 设置为 T4,我们知道仅会从 T5 开始提取文档。此外,由于只会提取满足 modification_time < NOW() 的文档,所以仅会提取到截至(含)T9 的文档。再说一遍,这意味着 T9 中的所有文档都已提取出来,而且对于下一次迭代 sql_last_value 将会设置为 T9。所以这一方法消除了对于任何给定时间间隔仅检索到 MySQL 文档的一个子集的风险。五.和监控mysql的binlog日志实现数据同步对比针对mysql数据到ES的数据同步,还有一种典型的实现方式是实时监控mysql的binlog,然后解析sql对ES进行数据同步。典型的实现是cannal监控binlog。实时性Logstash是通过定时轮询查询mysql表的新增数据进行同步,实时性收到同步周期的影响。同步周期最低是分钟级别。通过监控mysql的binlog变化来同步数据到ES,可以实现准实时的数据同步。复杂性通过Logstash实现mysql到es的数据同步相对更简单,通过cannal监控mysql的binlog实现起来更加复杂。全量更新Logstash既可以支持全量数据更新,也支持增量数据更新。而cannal的数据同步更依赖binlog日志,如果没有完整的binlog日志,则没办法实现全量更新。增量更新的限制性Logstash的增量更新依赖于表字段,自增主键或者数据更新时间。如果表中没有能识别增量数据的字段,则无法实现增量更新。cannal数据同步主要依赖binlog日志,对表字段没有限制。总结本文主要是介绍了通过Logstash实现mysql数据定时增量同步到ES。1、在配置Logstash中输入statement 参数SQL读取mysql增量数据时,需要注意临界条件的控制,modification_time大于sql_last_value并且小于NOW(),避免数据漏传或多传。2、LogStash和监控Binlog日志实现数据同步的区别和适用场景。参考文章:1、如何使用 Logstash 和 JDBC 确保 Elasticsearch 与关系型数据库保持同步2、Ingest data from a relational database into Elastic Cloud Enterprise3、如何将mysql数据同步到es4、Logstash 从Mysql同步数据到ES
前言不正确的日志打印不但会降低程序运行性能,还会占用大量IO资源和硬盘存储空间。本文主要总结一些能提高日志打印性能的手段。一、通过AsyncAppender异步输出日志我们通常使用的ConsoleAppender 和 RollingFileAppender都是同步输出日志,会阻塞程序运行。只有当日志打印完毕程序才会继续执行。而通过AsyncAppender实现异步日志输出,会启用单独日志线程去记录日志,并且不会阻塞程序运行,可以极大的增加日志打印的吞吐量。具体实现可以查看:logback异步输出日志详解配置示例:添加一个基于异步写日志的 appender,并指向原先配置的 appender即可。<configuration> <!-- 同步输出 --> <appender name="FILE" class="ch.qos.logback.core.FileAppender"> <file>myapp.log</file> <encoder> <pattern>%logger{35} - %msg%n</pattern> </encoder> </appender> <!-- 异步输出 --> <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender"> <appender-ref ref="FILE" /> </appender> <root level="DEBUG"> <appender-ref ref="ASYNC" /> </root> </configuration>二、禁用即时刷新immediateFlush=false默认情况下,每个日志事件都会立即刷新到基础输出流。这种默认方法更安全,因为如果应用程序退出时没有正确关闭附加程序,那么日志事件就不会丢失。但是,为了显著提高日志吞吐量,我们可以将 immediateFlush 属性设置为 false。<configuration> <appender name="FILE" class="ch.qos.logback.core.FileAppender"> <file>testFile.log</file> <append>true</append> <!-- 禁用即时刷新提高日志吞吐量 --> <immediateFlush>false</immediateFlush> <encoder> <pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern> </encoder> </appender> <root level="DEBUG"> <appender-ref ref="FILE" /> </root> </configuration>原理分析:无论是ConsoleAppender还是 FileAppender,都是继承了OutputStreamAppender。所有的日志输出本质都是I/O写入操作。通过禁用即时刷新可以充分利用IO缓冲区,提高IO性能。三、规范日志输出格式pattern<encoder> <!-- 日志默认输出格式 --> <pattern>%-4relative [%thread] %-5level %logger{35} - %msg %n</pattern> <!-- 采用utf8字符格式输出,避免出现乱码 --> <charset>utf8</charset> </encoder>日志布局介绍:https://logback.qos.ch/manual/layouts.html主要是需要禁止输出下面这些信息:1、禁止输出文件名参数说明F/file输出发出日志记录请求的 Java 源文件的文件名。生成文件名信息并不是特别快。因此,应该避免使用它,除非执行速度不是问题。输出发出日志记录请求的 Java 源文件的文件名。生成文件信息并不是特别快。因此,应该避免使用它,除非执行速度不是问题。2、禁止输出调用方类名参数说明C{length}class{length}输出发出日志记录请求的调用方的完全限定类名。生成调用方类信息并不是特别快。因此,应该避免使用它,除非执行速度不是问题。推荐采用%logger{26}输出日志记录器的名称,而不要输出类名。3、禁止输出行号参数说明L / line输出发出日志记录请求的地方的行号。生成行号信息并不是特别快。因此,应该避免使用它,除非执行速度不是问题。4、禁止输出调用方类名参数说明M / method输出发出日志记录请求的方法名。生成方法名并不是特别快。因此,应该避免使用它,除非执行速度不是问题。输出发出日志记录请求的方法名,速度比较慢,尽量避免。注意⚠️:如果你的项目输出日志非常缓慢,赶紧检测下日志输出格式中是否包含上面这些内容。四、提高生产环境的日志输出级别可以根据项目运行的不同环境来限制日志输出的默认级别。比如开发环境dev由于需要查看更详细的日志信息便于程序调试,可以将root的日志级别设置为DEBUG。而测试环境test、生产环境prod不需要关注调试级别的日志信息,只需要将root日志级别设置为INFO。logback 日志级别排序: TRACE < DEBUG < INFO < WARN < ERROR根据spring环境打印不同级别的日志:<!-- 开发环境日志级别为DEBUG --> <springProfile name="dev"> <root level="DEBUG"> <appender-ref ref="STDOUT"/> <appender-ref ref="FILE"/> </root> </springProfile> <!-- 测试环境日志级别为INFO --> <springProfile name="test"> <root level="INFO"> <appender-ref ref="STDOUT"/> <appender-ref ref="FILE"/> </root> <!-- 生产环境日志级别为INFO --> <springProfile name="prod"> <root level="INFO"> <appender-ref ref="STDOUT"/> <appender-ref ref="FILE"/> </root> </springProfile>另外spring boot下,还可以针对项目中的特定类指定不同的日志输出级别:比如开发环境dev,将mapper目录下的日志设置为trace级别,这样就可以将数据库操作的sql、入参以及返回都打印出来,方便调试。但是针对测试和生产环境,则一定要禁止。application-dev.propertieslogging.level.com.laowan.test.demo.mapper=traceapplication-prod.properties#可以注释掉该行,使其继承root的日志级别,也可以主动设置为info logging.level.com.laowan.test.demo.mapper=info五、非控制台输出严禁彩色打印在上篇文章logback控制台彩色日志输出中提到过,彩色日志只有在控制台输出才能展示彩色效果,如果是文件类型的日志输出,会输出大量彩色渲染的识别码,不利于日志查看,同时也会增加日志输出的压力。彩色日志的文件输出:六、根据日志的输出级别保存到不同文件虽然采用多日志文件输出可以在一定程度避免IO进程对单文件的竞争,但是日志输出文件频繁切换也可能会对日志输出性能造成一定影响,单日志文件的顺序写入有可能性能更高。由于该方式的没有经过具体的性能测试,具体是否能起到优化效果暂未可知,所以只推荐在希望分离出error级别日志,方便问题定位的时候使用。通过配置LevelFilter控制日志输出级别:<appender name="errorlog" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${BUILD_FOLDER:-logs}/error/error.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern>${BUILD_FOLDER:-logs}/error/error-%d{yyyy-MM-dd}.log</FileNamePattern> <MaxHistory>10</MaxHistory> </rollingPolicy> <encoder> <pattern>[%-5level] %d{HH:mm:ss.SSS} [%thread] %logger{36} - %msg%n</pattern> <charset>utf8</charset> </encoder> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>error</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> </appender>七、根据日志具体作用设置对应的日志输出级别只有在程序开发的过程中,在打印日志的时候设置了合适的日志级别,这样我们在logback中根据各个环境控制的不同日志级别才能达到更好的效果。不然调试级别的日志输出采用info级别,异常日志输出又采用info级别,就完全失去了日志级别控制的意义。log.trace("trace级别日志,一般用于调试跟踪,常见的比如mapper层日志打印输出sql执行返回"); log.debug("调试级别日志,一般用于调试跟踪"); log.info("info级别日志输出,一般用于输出一些关键信息,比如请求入参,方法耗时等"); log.warn("告警级别日志输出"); log.error("异常级别日志输出");典型的错误用法:1、胡乱采用error级别输出关键日志信息public void order(OrderReq req) { log.error("请求入参为:{}",req); //具体业务处理 }2、异常日志采用info级别输出try { //逻辑处理 }catch (Exception ex){ log.info("请求失败,异常原因",ex); }八、减少不必要的日志输出分析日志是否有输出的必要,是否可以对输出的日志降级。有时候减少一些不必要的日志输出,可以极大的减少日志输出的IO压力,提高程序性能。九、其他1、字符串连接输出反例:采用+号进行字符串连接public void add(User user) { log.info("新增用户ID:"+user.getId()",年纪:"+user.getAge()); //具体业务处理 }正例:采用{}替换符连接public void add(User user) { log.info("新增用户ID:{},年纪:{}",user.getId(),user.getAge()); //具体业务处理 }2、异常日志输出反例:采用e.printStackTrace()打印日志堆栈try { //逻辑处理 }catch (Exception e){ e.printStackTrace(); }正例:采用log.error(“异常描述”,e)打印日志堆栈try { //逻辑处理 }catch (Exception e){ log.error("请求失败,异常原因",e); }总结本文主要是对logback日志输出的相关优化手段进行了总结说明。1、通过AsyncAppender进行异步输出日志和禁止即时刷新是比较直接有效的手段,但需要注意两者都要日志丢失的风险,在需要进行日志收集归档的情况的要谨慎使用。2、设置日志输出级别,对性能优化至关重要。3、注意日志的打印格式pattern,严禁日志输出中包含文件名,类名,方法名以及行号。4、减少不必要的日志输出。
前言logback应该是目前最流行的日志打印框架了,毕竟Spring Boot中默认的集成的日志框架也是logback。在实际项目开发过程中,常常会遇到由于打印大量日志而导致程序并发降低,QPS降低的问题,而通过logback异步日志输出则能很大程度上解决这个问题。一、什么是Appender?logback官方文档:https://logback.qos.ch/manual/appenders.htmllogback中文文档官方介绍:Logback 将编写日志事件的任务委托给名为 Appenders 的组件,Appenders 必须实现ch.qos.logback.core.Appender的接口。简单来说,Appender就是用来处理logback框架下日志输出事件的组件。Appender接口的核心方法如下:package ch.qos.logback.core; import ch.qos.logback.core.spi.ContextAware; import ch.qos.logback.core.spi.FilterAttachable; import ch.qos.logback.core.spi.LifeCycle; public interface Appender<E> extends LifeCycle, ContextAware, FilterAttachable { public String getName(); public void setName(String name); //核心方法:处理日志事件 void doAppend(E event); }其中doAppend()方法是 logback 框架中最重要的方法。它负责将日志事件以适当的格式输出到适当的输出设备。二、Appender类图说明:OutputStreamAppender 是另外三个附加程序的超类,即 ConsoleAppender 和 FileAppender,后者又是 RollingFileAppender 的超类。下一个图说明了 OutputStreamAppender 及其子类的类图。1、控制台日志输出 ConsoleAppender配置示例:<configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%-4relative [%thread] %-5level %logger{35} - %msg %n</pattern> </encoder> </appender> <root level="DEBUG"> <appender-ref ref="STDOUT" /> </root> </configuration>说明:控制台日志输出主要是在开发环境采用,比如在IDEA中开发时,可以清楚直观得在控制台看到运行日志,更方便程序调试。当应用发布到测试环境、生产环境时,建议关闭控制台日志输出,以提高日志输出的吞吐量,减少不必要的性能开销。2、单日志文件输出 FileAppender配置示例:<configuration> <appender name="FILE" class="ch.qos.logback.core.FileAppender"> <!-- 日志文件名称 --> <file>testFile.log</file> <!-- 是否追加输出 --> <append>true</append> <!-- 立即刷新,设置成false可以提高日志吞吐量 --> <immediateFlush>true</immediateFlush> <encoder> <!-- 日志输出格式 --> <pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern> </encoder> </appender> <root level="DEBUG"> <appender-ref ref="FILE" /> </root> </configuration>弊端:采用单日志文件输出日志,很容易导致日志文件的体积一直膨胀,不利于日志文件的管理和查看。一般很少采用。3、滚动日志文件输出 RollingFileAppender配置示例:<configuration> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!-- 日志文件名称 --> <file>logFile.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- 按天滚动生成历史日志文件 --> <fileNamePattern>logFile.%d{yyyy-MM-dd}.log</fileNamePattern> <!-- 历史日志文件保存的天数和容量大小--> <maxHistory>30</maxHistory> <totalSizeCap>3GB</totalSizeCap> </rollingPolicy> <encoder> <pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern> </encoder> </appender> <root level="DEBUG"> <appender-ref ref="FILE" /> </root> </configuration>说明:通过rollingPolicy 配置日志文件的滚动生成策略,以及历史日志文件保存的天数和总容量大小,是测试环境和生产环境最推荐的日志输出方式。三、同步输出和异步输出比较同步输出传统的日志打印采用的是同步输出的方式,所谓同步日志,即当输出日志时,必须等待日志输出语句执行完毕后,才能执行后面的业务逻辑语句。使用logback的同步日志进行日志输出,日志输出语句与程序的业务逻辑语句将在同一个线程运行。在高并发场景下,日志数量不但激增,作为磁盘IO来说,容易产生瓶颈,导致线程卡顿在生成日志过程中,会影响程序后续的主业务,降低程序的性能。异步输出使用异步日志进行输出时,日志输出语句与业务逻辑语句并不是在同一个线程中运行,而是有专门的线程用于进行日志输出操作,处理业务逻辑的主线程不用等待即可执行后续业务逻辑。这样即使日志没有完成输出,也不会影响程序的主业务,从而提高了程序的性能。四、异步日志实现原理AsyncAppenderlogback异步输出日志是通过AsyncAppender实现的。AsyncAppender可以异步的记录 ILoggingEvents日志事件。但是这里需要注意,AsyncAppender只充当事件分配器,它必须引用另一个Appender才能完成最终的日志输出。示意图:Logback的异步输出采用生产者消费者的模式,将生成的日志放入消息队列中,并将创建一个线程用于输出日志事件,有效的解决了这个问题,提高了程序的性能。logback中的异步输出日志使用了AsyncAppender这个appender,通过看AsyncAppender源码,跟到它的父类AsyncAppenderBase,可以看到它有几个重要的成员变量:AppenderAttachableImpl<E> aai = new AppenderAttachableImpl<E>(); BlockingQueue<E> blockingQueue; AsyncAppenderBase<E>.Worker worker = new AsyncAppenderBase.Worker();lockingQueue是一个队列,Worker是一个消费线程,基本可以判定是个生产者消费者模式。再看消费者(work)的主要代码:while (parent.isStarted()) { try { E e = parent.blockingQueue.take(); //单条循环 aai.appendLoopOnAppenders(e); } catch (InterruptedException ie) { break; } }使用的是while单条循环 ,即logback异步输出是由一个消费者循环单条写入日志文件,工作流程如下图:五、异步日志配置配置示例:配置异步输出日志的方式很简单,添加一个基于异步写日志的 appender,并指向原先配置的 appender即可。<configuration> <appender name="FILE" class="ch.qos.logback.core.FileAppender"> <file>myapp.log</file> <encoder> <pattern>%logger{35} - %msg%n</pattern> </encoder> </appender> <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender"> <appender-ref ref="FILE" /> <!-- 设置异步阻塞队列的大小,为了不丢失日志建议设置的大一些,单机压测时100000是没问题的,应该不用担心OOM --> <queueSize>10000</queueSize> <!-- 设置丢弃DEBUG、TRACE、INFO日志的阀值,不丢失 --> <discardingThreshold>0</discardingThreshold> <!-- 设置队列入队时非阻塞,当队列满时会直接丢弃日志,但是对性能提升极大 --> <neverBlock>true</neverBlock> </appender> <root level="DEBUG"> <appender-ref ref="ASYNC" /> </root> </configuration>核心配置参数说明:属性名类型描述queueSizeintBlockingQueue的最大容量,默认情况下,大小为256。discardingThresholdint设置日志丢弃阈值, 默认情况下,当队列还有20%容量,他将丢弃trace、debug和info级别的日志,只保留warn和error级别的日志。includeCallerDataboolean提取调用方数据可能相当昂贵。若要提高性能,默认情况下,当事件添加到事件队列时,不会提取与事件关联的调用方数据。默认情况下,只复制线程名和 MDC 等“廉价”数据。通过将 includeecallerdata 属性设置为 true,可以指示此附加程序包含调用方数据。maxFlushTimeint根据被引用的 appender 的队列深度和延迟,AsyncAppender 可能需要不可接受的时间来完全刷新队列。当 LoggerContext 停止时,AsyncAppender stop 方法将等待工作线程完成直到超时。使用 maxFlushTime 指定最大队列刷新超时(以毫秒为单位)。无法在此窗口内处理的事件将被丢弃。此值的语义与 Thread.join (long)的语义相同。neverBlockboolean默认是false,代表在队列放满的情况下是否卡住线程。也就是说,如果配置neverBlock=true,当队列满了之后,后面阻塞的线程想要输出的消息就直接被丢弃,从而线程不会阻塞。默认情况下,event queue配置最大容量为256个events。如果队列已经满了,那么应用程序线程将被阻塞,无法记录新事件,直到工作线程有机会分派一个或多个事件。当队列不再达到最大容量时,应用程序线程可以再次开始记录事件。因此,当应用程序在其事件缓冲区的容量或附近运行时,异步日志记录就变成了伪同步。这未必是件坏事,AsyncAppender异步追加器设计目的是允许应用程序继续运行,尽管需要稍微多一点的时间来记录事件,直到附加缓冲区的压力减轻。优化 appenders 事件队列的大小以获得最大的应用程序吞吐量取决于几个因素。下列任何或全部因素都可能导致出现伪同步行为:大量的应用程序线程每个应用程序调用都有大量的日志事件每个日志事件都有大量数据子级appenders的高延迟为了保持事情的进展,增加队列的大小通常会有所帮助,代价是减少应用程序可用的堆。为了减少阻塞,在缺省情况下,当队列容量保留不到20% 时,AsyncAppender 将丢失 TRACE、 DEBUG 和 INFO 级别的事件,只保留 WARN 和 ERROR 级别的事件。这种策略确保了对日志事件的非阻塞处理(因此具有优异的性能) ,同时在队列容量小于20% 时减少 TRACE、 DEBUG 和 INFO 级别的事件。事件丢失可以通过将丢弃阈值属性设置为0(零)来防止。六、性能测试这部分自己还没时间做测试,引用网上的一些测试数据。既然能提高性能的话,必须进行一次测试比对,同步和异步输出日志性能到底能提升多少倍?服务器硬件CPU 六核内存 8G测试工具Apache Jmeter1、同步输出日志线程数:100Ramp-Up Loop(可以理解为启动线程所用时间) :0 可以理解为100个线程同时启用测试结果:重点关注指标 Throughput【TPS】 吞吐量:系统在单位时间内处理请求的数量,在同步输出日志中 TPS 为 44.2/sec2、异步输出日志线程数 100Ramp-Up Loop:0测试结果:TPS 为 497.5/sec , 性能提升了10多倍!!!参考文章:logback那些事儿
项目场景:商品超买超卖是高并发下非常典型的问题,也是面试中秒杀场景常常会问到的问题。常见的问题有:1、怎么设计一个秒杀系统?2、商品超买、超卖问题产生的原因?3、怎么防止商品出现超买|超卖问题?4、乐观锁和悲观锁的适用场景是什么?5、提高事务的隔离级别能解决超买|超卖问题吗?今天和大家一起探究下商品超买、超卖的原因及其解决方案。原因分析:商品下单扣减库存的流程如下:1、根据商品ID查询商品库存信息2、判断商品库存是否大于购买数量3、库存充足则进行下单减库存操作模拟代码如下:@Transactional(rollbackFor = Exception.class) public void secKill(Integer goodsId, Integer num) throws InterruptedException { //1、查询商品库存 GoodsStock goodsStock = goodsStockMapper.getStock(goodsId); //2、判断库存是否充足 if(goodsStock.getNum() >= num){ //3、使用减法计算出剩余库存 int stockNum = goodsStock.getNum() - num; goodsStock.setNum(stockNum); //4、更新商品剩余库存的值 int result = goodsStockMapper.updateByPrimaryKeySelective(goodsStock); if(result<0){ log.error("库存不足"); throw new RuntimeException("秒杀失败"); }else{ log.info("秒杀成功"); } }else{ log.error("库存不足"); throw new RuntimeException("秒杀失败"); } }采用jMeter压测发现,这样的代码不但会出现超买超卖的问题,还会导致商品的剩余库存出现覆盖更新的情况。流程分析:1、在高并发的情况下,会有很多请求同时查询到商品的库存信息,进入到步骤1。2、并通过了库存是否充足的判断,计算出剩余库存。3、通过updateByPrimaryKeySelective方法直接将计算出的剩余库存的值写入到数据库。假设A商品当前剩余库存是10,有10个线程同时进入到步骤1下单购买10个A商品,刚好都通过了步骤2的库存是否充足的判定,经过步骤3计算出剩余库存为0,然后执行更新操作将剩余库存的值写入到数据库。最后我们发现,明明A商品只有10件,但是我们确卖出了100件。这就是商品的超买、超卖问题。下面是模拟2个并发事务购买10个A商品的请求过程:A商品库存只有10个,最后却卖出了20件。原因说明:1、添加事务控制并不能保证减库存方法secKill()执行的原子性,代码仍然会并发执行。2、在并发场景下,先查询库存,再用java代码判定库存是否充足是不正确。3、在并发场景下,如果先通过java代码计算出剩余库存,再把剩余库存的值更新到数据库中会导致出现覆盖更新的情况。解决方案:1、最简单暴力的办法,既然减库存的方法secKill()不支持并发,那么可以将整个方法块做异步处理。如果不考虑分布式,可以直接用synchronized关键字,如果需要支持分布式,可以采用redis加分布式锁。2、采用悲观锁,给数据库记录加锁。3、采用乐观锁,更新记录时通过比对版本号判定是否执行更新库存。4、直接在sql中执行减法来更新库存,并在where调价中判定库存是否充足update table set stock = stock - 10 where goods_id=1 and stock - 10 >=0 ;代码实战:1、商品库存表CREATE TABLE `goods_stock` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `goods_id` varchar(255) DEFAULT NULL COMMENT '商品id', `num` int(11) DEFAULT NULL COMMENT '库存数量', `version` int(11) unsigned DEFAULT NULL COMMENT '版本号', PRIMARY KEY (`id`), UNIQUE KEY `ui_goods_id` (`goods_id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4; INSERT INTO `seckill`.`goods_stock`(`id`, `goods_id`, `num`, `version`) VALUES (1, '1', 10000, NULL);2、控制层代码 SecKillController@RestController @Slf4j public class SecKillController { @Autowired private SecKillService secKillService; @GetMapping(value = "/secKill/{goodsId}/{num}") public void secKill(@PathVariable Integer goodsId, @PathVariable Integer num) throws InterruptedException{ secKillService.secKill(goodsId,num); } }1、synchronized方式只需要在方法头上添加synchronized关键字,不用修改任何代码@Transactional(rollbackFor = Exception.class) public synchronized void secKill(Integer goodsId, Integer num) throws InterruptedException { //1、查询商品库存 GoodsStock goodsStock = goodsStockMapper.getStock(goodsId); //2、判断库存是否充足 if(goodsStock.getNum() >= num){ //3、减库存 int stockNum = goodsStock.getNum() - num; goodsStock.setNum(stockNum); int result = goodsStockMapper.updateByPrimaryKeySelective(goodsStock); if(result < 1){ log.error("库存不足"); throw new RuntimeException("秒杀失败"); }else{ log.info("秒杀成功"); } }else{ log.error("库存不足"); throw new RuntimeException("秒杀失败"); } }采用JMeter压测:线程组:采用500个线程,请求20次。http请求:采用get请求,http://localhost:8080/secKill/1/1,每次购买一个商品执行日志:商品库存依次递减1,说明现在扣减库存的方法secKill()确实是异步执行。优点:简单,只需要在方法头部加上一个synchronized关键字,不用修改其他代码。缺点:不支持分布式,并发能力较弱。2、redis分布式锁 public void secKill(Integer goodsId, Integer num) { RLock rlock = redissonClient.getLock("goods_sku"); try{ //加分布式锁 rlock.lock(); //1、查询商品库存 GoodsStock goodsStock = goodsStockMapper.getStock(goodsId); //2、判断库存是否充足 if(goodsStock.getNum() >= num){ //3、减库存 int stockNum = goodsStock.getNum() - num; goodsStock.setNum(stockNum); int result = goodsStockMapper.updateByPrimaryKeySelective(goodsStock); if(result < 1){ log.error("库存不足"); throw new RuntimeException("秒杀失败"); }else{ log.info("秒杀成功"); } }else{ log.error("库存不足"); throw new RuntimeException("秒杀失败"); } }catch(Exception e){ log.error("秒杀失败"); }finally{ rlock.unlock(); } }优点:支持分布式,能保证方法异步执行,避免超买超卖问题。缺点:引入了redis中间件。3、悲观锁@Transactional(rollbackFor = Exception.class) public void secKill(Integer goodsId, Integer num) throws InterruptedException { //1、查询商品库存 GoodsStock goodsStock = goodsStockMapper.getStockForUpdate(goodsId); //2、判断库存是否充足 if(goodsStock.getNum() >= num){ //3、减库存 int result = goodsStockMapper.secKill(goodsId,num); if(result < 1){ log.error("库存不足"); throw new RuntimeException("秒杀失败"); }else{ log.info("秒杀成功"); } }else{ log.error("库存不足"); throw new RuntimeException("秒杀失败"); } }<select id="getStockForUpdate" resultType="com.laowan.seckill.modle.GoodsStock"> select <include refid="Base_Column_List" /> from goods_stock where goods_id = #{goodsId} for update </select> <select id="getStock" resultType="com.laowan.seckill.modle.GoodsStock"> select <include refid="Base_Column_List" /> from goods_stock where goods_id = #{goodsId} </select> <update id="secKill"> update goods_stock set num = num - #{num} where goods_id = #{goodsId} </update>核心是在执行goodsStockMapper.getStockForUpdate(goodsId)方法时,通过在查询语句后面添加for update,会去尝试给查询的记录添加写锁(排他锁)。这样当第一个事务A进来获取写锁后,后面的事务就不能获取该记录上的写锁,会一直等待事务A执行完毕释放写锁后,再竞争获取写锁,才能执行扣减库存的操作。select * from goods_stock where goods_id = #{goodsId} for update优点:支持分布式,充分利用了数据库的排他锁机制,保证扣减库存的操作串行执行。缺点:并发效率相比synchronized、redis分布式锁会低很多,并且容易导致mysql死锁的问题。悲观锁方式本质上是利用数据库的排他锁的特性,让事务内代码异步串行执行,从而避免了超买超卖问题。上面介绍的几种方式,本质上都是通过加锁的方式,使得扣减库存的操作异步串行执行。那么有没有不用加锁的方式呢?4、乐观锁@Transactional(rollbackFor = Exception.class) public void secKill(Integer goodsId, Integer num) throws InterruptedException { //1、查询商品库存 GoodsStock goodsStock = goodsStockMapper.getStock(goodsId); //2、判断库存是否充足 if(goodsStock.getNum() >= num){ //3、减库存 goodsStock.setNum(num); int result = goodsStockMapper.secKillForVersion(goodsStock); if(result < 1){ log.error("库存不足"); throw new RuntimeException("秒杀失败"); }else{ log.info("秒杀成功"); } }else{ log.error("库存不足"); throw new RuntimeException("秒杀失败"); } }<update id="secKillForVersion"> update goods_stock set num = num - #{num},version=version+1 where goods_id = #{goodsId} and version=#{version} </update>验证:数据准备:INSERT INTO `seckill`.`goods_stock`(`id`, `goods_id`, `num`, `version`) VALUES (1, '1', 1000, 0);JMeter压测:执行结果:乐观锁的版本号增长了779,商品的库存也正好扣减了779,说明成功控制了扣减库存的并发。通过增加了版本号的控制,在扣减库存的时候在where条件进行版本号的比对。实现查询的是哪一条记录,那么就要求更新的是哪一条记录,在查询到更新的过程中版本号不能变动,否则更新失败。改进:增加乐观锁的循环次数,提高请求成功的概率。public void secKill(Integer goodsId, Integer num) throws InterruptedException { int retryCount = 0; int result = 0; //1、查询商品库存 GoodsStock goodsStock = goodsStockMapper.getStock(goodsId); //2、判断库存是否充足 if(goodsStock.getNum() < num){ log.error("库存不足"); throw new RuntimeException("秒杀失败"); } //最多重试3次 while(retryCount < 3 && result == 0){ result = this.reduceStock(goodsId,num); retryCount++; } if(result > 0){ log.info("秒杀成功"); }else{ log.error("库存不足"); throw new RuntimeException("秒杀失败"); } } /** * 减库存 * * 由于默认的事务隔离级别是可重复读,会导致在同一个事务中查询3次goodsStockMapper.getStock() * 得到的数据始终是相同的,所以需要提取reduceStock方法。每次循环都启动新的事务尝试扣减库存操作。 */ @Transactional(rollbackFor = Exception.class) public int reduceStock(Integer goodsId, Integer num){ int result = 0; //1、查询商品库存 GoodsStock goodsStock = goodsStockMapper.getStock(goodsId); //2、判断库存是否充足 if(goodsStock.getNum() >= num){ //3、减库存 goodsStock.setNum(num); result = goodsStockMapper.secKillForVersion(goodsStock); } return result; }注意⚠️:不能在同一个事务里面,重试扣减库存的操作。最后压测发现,扣减库存请求的成功率提高了很多。但请求过程中还是会出现大量版本冲突的问题。乐观锁机制类似java中的cas机制,在查询数据的时候不加锁,只有更新数据的时候才比对数据是否已经发生过改变,没有改变则执行更新操作,已经改变了则进行重试。乐观锁机制在大并发场景下,会出现大量的版本冲突导致重试的情况,而这种重试无疑会增大数据库和程序的压力。 显然乐观锁方式并不适合高并发的场景。5、where条件where条件的方式主要是将库存是否充足的判定放在了更新库存的where条件中,以此保证不会出现超买超卖的情况。@Transactional(rollbackFor = Exception.class) public void secKill(Integer goodsId, Integer num) throws InterruptedException { //1、查询商品库存 GoodsStock goodsStock = goodsStockMapper.getStock(goodsId); //2、判断库存是否充足 if(goodsStock.getNum() >= num){ //3、减库存 int result = goodsStockMapper.secKillForWhere(goodsId,num); if(result < 1){ log.error("库存不足"); throw new RuntimeException("秒杀失败"); }else{ log.info("秒杀成功"); } }else{ log.error("库存不足"); throw new RuntimeException("秒杀失败"); } }在更新语句中扣减库存,并在where调价中判定库存是否充足。<update id="secKillForWhere"> update goods_stock set num = num - #{num} where goods_id = #{goodsId} and num - #{num} >=0 </update>JMeter压测:INSERT INTO `seckill`.`goods_stock`(`id`, `goods_id`, `num`, `version`) VALUES (1, '1', 500, 0);线程数设置:也可以加大线程数,看是否会出现库存数被扣减成负数的情况。压测结果:库存成功被扣完,且没有出现异常,执行效率也非常高。6、unsigned 非负字段限制本质上来说,这种方式和where条件方式类似。where条件是通过限制扣减库存的查询条件来限制超卖unsigned 非负字段限制是如果扣减库存出现负值后,在保存的时候会报错来防止出现超卖。库存字段num增加非负字段限制unsigned,保证只能保存非负整数。CREATE TABLE `goods_stock` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `goods_id` varchar(255) DEFAULT NULL COMMENT '商品id', `num` int(11) unsigned DEFAULT NULL COMMENT '库存数量', `version` int(11) unsigned DEFAULT NULL COMMENT '版本号', PRIMARY KEY (`id`), UNIQUE KEY `ui_goods_id` (`goods_id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;@Transactional(rollbackFor = Exception.class) public void secKill(Integer goodsId, Integer num) throws InterruptedException { //1、查询商品库存 GoodsStock goodsStock = goodsStockMapper.getStock(goodsId); //2、判断库存是否充足 if(goodsStock.getNum() >= num){ //3、减库存 int result = goodsStockMapper.secKill(goodsId,num); if(result < 1){ log.error("库存不足"); throw new RuntimeException("秒杀失败"); }else{ log.info("秒杀成功"); } }else{ log.error("库存不足"); } }压测过程中出现如下异常提示:Data truncation: BIGINT UNSIGNED value is out of range in '(`seckill`.`goods_stock`.`num` - 1)'说明库存不足时,不能继续扣减库存。小结:unsigned 非负字段限制方式算是一种兜底策略,从底层数据库底层数据规则的层面限制出现超买超卖的情况。 并且简单有效,能和其他方式共同使用,防止超卖超卖情况的出现。为什么不通过事务隔离级别控制事务代码的并发简单来说,事务的隔离级别并不能控制事务代码的并发。事务是什么?Transactions are atomic units of work that can be committed or rolled back. When a transaction makes multiple changes to the database, either all the changes succeed when the transaction is committed, or all the changes are undone when the transaction is rolled back.事务是由一组SQL语句组成的原子操作单元,其对数据的变更,要么全都执行成功(Committed),要么全都不执行(Rollback)。InnoDB实现的数据库事务具有常说的ACID属性,即原子性(atomicity),一致性(consistency)、隔离性(isolation)和持久性(durability)。原子性:事务被视为不可分割的最小单元,所有操作要么全部执行成功,要么失败回滚(即还原到事务开始前的状态,就像这个事务从来没有执行过一样)一致性:在成功提交或失败回滚之后以及正在进行的事务期间,数据库始终保持一致的状态。如果正在多个表之间更新相关数据,那么查询将看到所有旧值或所有新值,而不会一部分是新值,一部分是旧值隔离性:事务处理过程中的中间状态应该对外部不可见,换句话说,事务在进行过程中是隔离的,事务之间不能互相干扰,不能访问到彼此未提交的数据。这种隔离可通过锁机制实现。有经验的用户可以根据实际的业务场景,通过调整事务隔离级别,以提高并发能力持久性:一旦事务提交,其所做的修改将会永远保存到数据库中。即使系统发生故障,事务执行的结果也不能丢失。这里我们要注意:事务的原子性指的是在事务范围内执行的insert、update、delete要么全部成功,要么全部失败。而并不能保证添加了@Transactional注解声明事务的方法的执行具有原子性,也不能保证在事务A执行一系列insert、update、delete操作的过程中,事务B不能执行insert、update、delete操作。场景一:在默认的事务隔离级别下,可重复读(Repeatable Read)由于采用了MVCC机制,读不加锁,所以都可以查询到商品的库存信息,进行扣减库存的操作,这里都可以并发执行。直到运行到update的操作,由于需要先获取到数据的排他锁,才能执行更新操作,才变为串行执行。所以,这里如果采用在事务代码中,用java代码的方式先求出剩余库存,再将值更新到数据库,就会出现覆盖更新的情况。场景二: 串行化(Serializable)如果我们将事务的隔离级别提升为串行化(Serializable),执行的情况呢?可以发现,尽管将事务的隔离级别提升为串行化(Serializable),也只是影响了在事务中执行sql时的加锁方式,并不能保证事务范围内的代码异步执行。小结:事务隔离级别串行化(Serializable)并不能保证事务访问内的逻辑在并发场景下的串行化执行,大家不要被其命名所误导。串行化(Serializable)隔离级别只能保证,先查到的,先更新。总结:本文主要对商品超买超卖问题进行了深度分析,并提供了相关解决方案和实战代码,通过JMeter压测对其正确性进行了验证。1、介绍了高并发场景下商品超买超卖问题出现的原因。2、通过代码实战介绍了6种方式解决超买超买问题:synchronized方式redis分布式锁悲观锁乐观锁where条件unsigned 非负限制其中前3种是通过加锁的方式保证扣减库存代码串行执行,后面3种是无锁方式。3、如果事务代码中涉及到复杂计算,需要先通过java代码计算,然后再将计算结果更新到数据库,这种情况推荐采用synchronized方式、redis分布式锁、悲观锁这些加锁的方式,保证整个事务范围内的代码串行执行,防止出现覆盖更新的情况。如果是像扣减库存这样简单的计算操作,可以在update中轻松实现的情况,推荐采用where条件、unsigned 非负限制方式,并发的效率更高。4、强烈建议unsigned 非负限制方式能和其他方式一起使用,保证数据安全性。5、乐观锁为什么不适合在高并发场景下使用6、介绍了为什么不能通过提高事务的隔离级别来解决超买超卖的问题.
问题描述查询全表数据也是日常工作中常见的一种查询场景。在ES如果我们使用match_all查询索引的全量数据时,默认只会返回10条数据。那么在ES如何查询索引的全量数据呢?小实验1、索引和数据准备PUT book { "mappings": { "properties": { "name": { "type": "text", "analyzer": "ik_smart" }, "price": { "type": "double" } } } } PUT /book/_bulk { "create": { } } {"name": "java编程思想","price": 100} { "create": { } } {"name": "ES实战","price": 120} { "create": { } } {"name": "ES从入门到精通","price": 60} { "create": { } } {"name": "微服务架构 设计模式","price": 160} { "create": { } } {"name": "架构真经","price": 90} { "create": { } } {"name": "spring boot实战","price": 50} { "create": { } } {"name": "高性能mysql","price": 80} { "create": { } } {"name": "java进阶1","price": 10} { "create": { } } {"name": "java进阶2","price": 20} { "create": { } } {"name": "java进阶3","price": 30} { "create": { } } {"name": "java进阶4","price": 40} { "create": { } } {"name": "java进阶5","price": 50}2、match_all全匹配查询GET book/_search等同于GET book/_search { "query": { "match_all": {} } }发现只返回了10条记录。这样因为_search查询默认采用的是分页查询,每页记录数size的默认值为10.3、添加size参数GET book/_search { "query": { "match_all": {} }, "size": 100 }将size值设置为100,而我们只添加了13条记录,所以成功返回索引的全量记录。4、size大于10000GET book/_search { "query": { "match_all": {} }, "size": 20000 }返回结果:{ "error" : { "root_cause" : [ { "type" : "illegal_argument_exception", "reason" : "Result window is too large, from + size must be less than or equal to: [10000] but was [20000]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting." } ],异常说明:1、查询结果的窗口太大,from + size的结果必须小于或等于10000,而当前查询结果的窗口为20000。2、可以采用scroll api更高效的请求大量数据集。3、查询结果的窗口的限制可以通过参数index.max_result_window进行设置。index.max_result_windowThe maximum value of from + size for searches to this index. Defaults to 10000. Search requests take heap memory and time proportional to from + size and this limits that memory. See Scroll or Search After for a more efficient alternative to raising this.说明:参数index.max_result_window主要用来限制单次查询满足查询条件的结果窗口的大小,窗口大小由from + size共同决定。不能简单理解成查询返回给调用方的数据量。这样做主要是为了限制内存的消耗。请求大数据集推荐采用Scroll or Search After 。比如:from为1000000,size为10,逻辑意义是从满足条件的数据中取1000000到(1000000 + 10)的记录。这时ES一定要先将(1000000 + 10)的记录(即result_window)加载到内存中,再进行分页取值的操作。尽管最后我们只取了10条数据返回给客户端,但ES进程执行查询操作的过程中确需要将(1000000 + 10)的记录都加载到内存中,可想而知对内存的消耗有多大。这也是ES中不推荐采用(from + size)方式进行深度分页的原因。同理,from为0,size为1000000时,ES进程执行查询操作的过程中确需要将1000000 条记录都加载到内存中再返回给调用方,也会对ES内存造成很大压力。1.参数设置PUT book/_settings { "index.max_result_window" :"5" }注意:1、此方法是设置单索引,如果需要更改索引需要将book换成_all2、即使换成_all,对于新增的索引,还是默认的100002.查看参数查看所有索引中的index.max_result_window值:GET _all/_settings/index.max_result_window查看book索引的_settings配置:GET book/_settings查看_settings配置中的参数index.max_result_window的值:GET book/_settings/index.max_result_windowScroll api实践改动index.max_result_window参数值的大小,只能解决一时的问题,当索引的数据量持续增长时,在查询全量数据时还是会出现问题。而且会增加ES服务器内存大结果集消耗完的风险。最佳实践还是根据异常提示中的采用scroll api更高效的请求大量数据集。1.DSL命令查询1、查询命令中新增scroll=1m,说明采用游标查询,保持游标查询窗口一分钟。2、这里由于测试数据量不够,所以size值设置为2。实际使用中为了减少游标查询的次数,可以将值适当增大,比如设置为1000。GET /book/_search?scroll=1m { "query": { "match_all": {}}, "size": 2 }查询结果:除了返回前2条记录,还返回了一个游标ID值_scroll_id。{ "_scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFm1ab2pDVEpiVGh5ZWthYnRQanB5YlEAAAAAABRP-xZHYWJiZzJGNFJYQ1RPS0dZb1VwejRR", "took" : 0, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, …… }采用游标id查询:GET /_search/scroll { "scroll": "1m", "scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFm1ab2pDVEpiVGh5ZWthYnRQanB5YlEAAAAAABRP5BZHYWJiZzJGNFJYQ1RPS0dZb1VwejRR" }说明:多次根据scroll_id游标查询,直到没有数据返回则结果查询。。采用游标查询索引全量数据,更安全高效,限制了单次对内存的消耗。2.java高级API实现/** * 通过游标查询所有数据 * @return */ public <T> List<T> searchAllData(SearchRequest searchRequest, Class<T> clazz) { List<T> tList = new ArrayList<>(); final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L)); searchRequest.scroll(scroll); try{ SearchResponse searchResponse = highLevelClient.search(searchRequest); String scrollId = searchResponse.getScrollId(); log.info("ES查询DSL语句:\nGET {}\n{}", String.format("/%s/_search", searchRequest.indices()[0]), searchRequest.source()); //打印命中数量 log.info("命中总数量:{}", searchResponse.getHits().getTotalHits()); SearchHit[] searchHits = searchResponse.getHits().getHits(); while (searchHits != null && searchHits.length > 0) { SearchHits hits = searchResponse.getHits(); List<T> resultList = Arrays.stream(hits.getHits()).map(e -> { String jsonStr = e.getSourceAsString(); return JSON.parseObject(jsonStr, clazz); }).collect(Collectors.toList()); if(!CollectionUtils.isEmpty(resultList)){ tList.addAll(resultList); } SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId); scrollRequest.scroll(scroll); searchResponse = highLevelClient.searchScroll(scrollRequest); searchHits = searchResponse.getHits().getHits(); } ClearScrollRequest clearScrollRequest = new ClearScrollRequest(); clearScrollRequest.addScrollId(scrollId); ClearScrollResponse clearScrollResponse = highLevelClient.clearScroll(clearScrollRequest); boolean succeeded = clearScrollResponse.isSucceeded(); }catch (Exception e){ log.error("执行EsService的searchAllData方法出现异常", e); } return tList; }注意:1、尽管通过采用Scroll API提高的查询全量数据的性能,减少了ES服务器的内存消耗。但是当返回结果集过大的时候,也会出现将调用方应用的内存撑爆的风险。2、推荐的做法是:在调用searchAllData方法查询索引全量数据时,在方法内部添加返回记录条数的限制。避免出现返回几百万、甚至上千万的结果集,导致调用方程序由于内存吃完而宕机。总结1、本文主要介绍了如何通过Scroll API查询索引的全量数据。2、介绍了index.max_result_window参数和相关配置方法。3、尽管通过采用Scroll API查询索引全量数据提高了查询效率并减少了ES服务器的内存消耗。当同时要注意不要返回太大的结果集撑爆调用方应用的内存。4、针对ES大结果集的查询,要同时考虑ES服务提供方和请求调用方的内存消耗。
问题描述:在实际项目中,查询Top10数据的场景非常常见,比如查询票房前十的电影,销售榜前十的商品等。那么在ES中如何查询Top10的数据呢?问题分析:一般Top10问题,都可以转化成先排序再取排行前10的问题,那么实现就简单了。数据准备创建索引book,获取价格最贵的3本书的信息。PUT book { "mappings": { "properties": { "name": { "type": "text", "analyzer": "ik_smart" }, "price": { "type": "double" } } } }PUT /book/_bulk { "create": { } } {"name": "java编程思想","price": 100} { "create": { } } {"name": "ES实战","price": 120} { "create": { } } {"name": "ES从入门到精通","price": 60} { "create": { } } {"name": "微服务架构 设计模式","price": 160} { "create": { } } {"name": "架构真经","price": 90} { "create": { } } {"name": "spring boot实战","price": 50} { "create": { } } {"name": "高性能mysql","price": 80}实现方案:1、SQL查询POST /_sql?format=txt { "query": "SELECT * FROM book ORDER BY price DESC", "fetch_size": 3 } POST /_sql?format=txt { "query": "SELECT * FROM (SELECT * FROM book ORDER BY price DESC ) limit 3" }查询结果:2、DSL查询POST /_sql/translate { "query": "SELECT * FROM book ORDER BY price DESC", "fetch_size": 3 }转化结果:{ "size" : 3, "_source" : { "includes" : [ "name", "price" ], "excludes" : [ ] }, "sort" : [ { "price" : { "order" : "desc", "missing" : "_first", "unmapped_type" : "double" } } ] }完整DSL语句:POST /book/_search { "size" : 3, "_source" : { "includes" : [ "name", "price" ], "excludes" : [ ] }, "sort" : [ { "price" : { "order" : "desc", "missing" : "_first", "unmapped_type" : "double" } } ] }
前言前端请求的日期格式的参数,你还在挨个配置@DateTimeFormat注解进行接受吗?后端返回给前端的json响应中的时间格式,你还在挨个用@JsonFormat配置时间格式化吗?本文教大家如何在spring boot下进行全局的日期格式化配置。一、全局属性配置#json格式化全局配置 spring.jackson.time-zone=GMT+8 spring.jackson.date-format=yyyy-MM-dd HH:mm:ss spring.jackson.default-property-inclusion=NON_NULL spring.mvc.date-format=yyyy-MM-dd HH:mm:ss说明:spring.jackson.time-zone 指定将响应结果进行json序列化时采用的默认时区spring.jackson.date-format 指定json序列化时 时间格式化采用的默认格式spring.jackson.default-property-inclusion 指定默认包含的熟悉,NON_NULL表示只序列化非空属性spring.mvc.date-format指定前端请求参数中日期格式参数和后端时间类型字段绑定时默认的格式,相当于@DateTimeFormat的全局配置注意⚠️:这里的spring.jackson.time-zone指定的时区一定要和mysql数据库连接中时区保持一致。二、自定义全局格式化配置经过上面的全局配置,已经能满足大部分场景,个别特殊的时间格式化的场景,我们可以单独采用@JsonFormat和@DateTimeFormat的实体中的日期字段进行配置。如果用户希望自定义控制json格式化,可参考如下配置:1、自定义格式化类CustomDateFormat/** * @Description JSON形式的全局时间类型转换器 * 自定义的格式化类一定要继承SimpleDateFormat */ public class CustomDateFormat extends SimpleDateFormat { private static final long serialVersionUID = -3201781773655300201L; public String defaultDateFormat; public String defaultTimeZone; public CustomDateFormat(String pattern,String defaultTimeZone){ this.defaultDateFormat = pattern; this.defaultTimeZone = defaultTimeZone; } /** * 只要覆盖parse(String)这个方法即可 */ @Override public Date parse(String dateStr, ParsePosition pos) { return getDate(dateStr, pos); } /** * 前端传的日期字符串转Date * @param dateStr * @return */ @Override public Date parse(String dateStr) { ParsePosition pos = new ParsePosition(0); return getDate(dateStr, pos); } //可以根据前端传递的时间格式自动匹配格式化 private Date getDate(String dateStr, ParsePosition pos) { SimpleDateFormat sdf = null; if (StringUtils.isBlank(dateStr)) { return null; } else if (dateStr.matches("^\\d{4}-\\d{1,2}$")) { sdf = new SimpleDateFormat("yyyy-MM"); return sdf.parse(dateStr, pos); } else if (dateStr.matches("^\\d{4}-\\d{1,2}-\\d{1,2}$")) { sdf = new SimpleDateFormat("yyyy-MM-dd"); return sdf.parse(dateStr, pos); } else if (dateStr.matches("^\\d{4}-\\d{1,2}-\\d{1,2} {1}\\d{1,2}:\\d{1,2}$")) { sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm"); return sdf.parse(dateStr, pos); } else if (dateStr.matches("^\\d{4}-\\d{1,2}-\\d{1,2} {1}\\d{1,2}:\\d{1,2}:\\d{1,2}$")) { sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.parse(dateStr, pos); } else if (dateStr.length() == 23) { sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); return sdf.parse(dateStr, pos); } return super.parse(dateStr, pos); } /** * 后端返回的日期格式化指定字符串 * @param date * @param toAppendTo * @param fieldPosition * @return */ @Override public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition){ return new StringBuffer(DateFormatUtils.format(date, defaultDateFormat, TimeZone.getTimeZone(defaultTimeZone))); } }2、自定义表单日期转化器/** * @Description 表单形式的全局时间类型转换器 */ @Configuration public class DateConverter implements Converter<String, Date> { private static final List<String> FORMARTS = new ArrayList<String>(4); static{ FORMARTS.add("yyyy-MM"); FORMARTS.add("yyyy-MM-dd"); FORMARTS.add("yyyy-MM-dd HH:mm"); FORMARTS.add("yyyy-MM-dd HH:mm:ss"); } //可以根据前端传递的时间格式自动匹配格式化 @Override public Date convert(String source) { String value = source.trim(); if ("".equals(value)) { return null; } if(source.matches("^\\d{4}-\\d{1,2}$")){ return parseDate(source, FORMARTS.get(0)); }else if(source.matches("^\\d{4}-\\d{1,2}-\\d{1,2}$")){ return parseDate(source, FORMARTS.get(1)); }else if(source.matches("^\\d{4}-\\d{1,2}-\\d{1,2} {1}\\d{1,2}:\\d{1,2}$")){ return parseDate(source, FORMARTS.get(2)); }else if(source.matches("^\\d{4}-\\d{1,2}-\\d{1,2} {1}\\d{1,2}:\\d{1,2}:\\d{1,2}$")){ return parseDate(source, FORMARTS.get(3)); }else { throw new IllegalArgumentException("Invalid boolean value '" + source + "'"); } } /** * 功能描述:格式化日期 * @param dateStr String 字符型日期 * @param format String 格式 * @return Date 日期 */ public Date parseDate(String dateStr, String format) { Date date=null; try { DateFormat dateFormat = new SimpleDateFormat(format); date = (Date) dateFormat.parse(dateStr); } catch (Exception e) { } return date; } }3、注册自定义的日期转化器@Slf4j @Configuration public class WebMvcConfigurer implements WebMvcConfigurer { private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss"); @Value("${spring.jackson.date-format:yyyy-MM-dd HH:mm:ss}") private String defaultDateFormat; @Value("${spring.jackson.time-zone:UTC}") private String defaultTimeZone; /** * JSON全局日期转换器 */ public MappingJackson2HttpMessageConverter getMappingJackson2HttpMessageConverter() { MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); //设置日期格式 ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setDateFormat(new CustomDateFormat(defaultDateFormat,defaultTimeZone)); //支持LocalDateTime、LocalDate、LocalTime的序列化 JavaTimeModule javaTimeModule = new JavaTimeModule(); javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER)); javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DATE_FORMATTER)); javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(TIME_FORMATTER)); objectMapper.registerModule(javaTimeModule); // 设置时区 objectMapper.setTimeZone(TimeZone.getTimeZone(defaultTimeZone)); mappingJackson2HttpMessageConverter.setObjectMapper(objectMapper); //忽略不能识别的字段 objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); //忽略非空字段 objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); //设置中文编码格式 List<MediaType> list = new ArrayList<MediaType>(); list.add(MediaType.APPLICATION_JSON); mappingJackson2HttpMessageConverter.setSupportedMediaTypes(list); return mappingJackson2HttpMessageConverter; } /** * 注册转化器 * 注意:List<HttpMessageConverter>的转化器是按顺序生效,前面的执行了,后面的就不会执行了 * @param converters */ @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { //将fastJson添加到视图消息转换器列表内 converters.add(0,getMappingJackson2HttpMessageConverter()); } /** * 表单全局日期转换器 */ @Bean public ConversionService getConversionService(DateConverter dateConverter){ ConversionServiceFactoryBean factoryBean = new ConversionServiceFactoryBean(); Set<Converter> converters = new HashSet<>(); converters.add(dateConverter); factoryBean.setConverters(converters); return factoryBean.getObject(); } }这样采用自定义的方式,主要完成了自动根据时间参数的格式去匹配时间格式化完成Date类型的参数绑定。四、补充前端传递到后端的时间,一般会带有时区,这就导致传递的是CST时区,保存到数据库自动转化成了UTC时区。出现传入时间和保存到数据库的时间不一致的问题。注意:保证数据库连接的时区也采用GMT+8能解决大部分时间不一致的问题。解决方案如下:方案一: (经验证,解决我的问题)将以下行添加到 application.properties 文件:spring.jackson.deserialization.ADJUST_DATES_TO_CONTEXT_TIME_ZONE = false原理主要是不将前端的时区传到后端。方式二:(参考,待验证)在 Application.java(带有 main 方法的类)中设置全局时区:@PostConstruct void started() { TimeZone.setDefault(TimeZone.getTimeZone("Etc/UTC")); }总结本文主要介绍了spring boot项目中,如何进行全局日期格式化的配置。并更进一步介绍了通过自定义格式化类,实现自动根据时间参数的格式去匹配时间格式化完成Date类型的参数绑定,帮助提高日常开发效率。
前言网上关于ES性能优化的文章太多,这里参考官网收集整理下。官网优化说明:how-to一、常规建议1、不要返回数据量非常大的结果集2、避免出现大文档,即单条索引记录的体积不要过大。默认情况下, http.max_content_length 设置为100mb,限制单个索引记录的大小不能超过100mb。二、优化索引速度1.尽量使用批请求bulk2.采用从多个线程或进程发送数据到ES3.增加索引刷新时间大小index.refresh_interval,默认1s刷新一次,设置为-1表示关闭索引刷新。4.初始加载数据时禁用副本,将index.number_of_replicas 设置为0。一般我们在进行大量数据的同步任务和加载的时候,可以先设置index.refresh_interval=-1,index.number_of_replicas=0,关闭自动刷新并将索引的副本数设置为0。待完成数据同步后,再调整回正常值。5.禁用内存交换swapping6.分配足够的内存给文件系统缓存文件系统缓存将用于缓冲 I/O 操作。应该确保将运行 Elasticsearch 的计算机的至少一半内存提供给文件系统缓存。7.使用自动生成id在索引具有显式 id 的文档时,Elasticsearch 需要检查具有相同 id 的文档是否已存在于同一个分片中,这是一项代价高昂的操作,并且随着索引的增长而变得更加昂贵。通过使用自动生成的 id,Elasticsearch 可以跳过此检查,从而加快索引速度。8.使用更快的硬件,比如使用SSD固态硬盘9.索引缓冲区大小index_buffer_size索引缓冲区index.memory.index_buffer_size默认值是 10%,例如,如果 JVM 10GB 的内存,它将为索引缓冲区提供 1GB。10.使用跨集群复制,避免争抢资源。11.尽量避免使用深度分页,实在不能避免可以采用Scroll 遍历查询或Search After 查询。三、优化查询速度1.分配足够的内存给文件系统缓存Elasticsearch 严重依赖文件系统缓存来加快搜索速度。 通常,您应该确保至少有一半的可用内存进入文件系统缓存,以便 Elasticsearch 可以将索引的热点区域保留在物理内存中。2.使用更快的硬件如果搜索速度受限I/O,可以使用读写速度更快的固态硬盘SSD;如果搜索速度受限CPU,那么可以购买速度更快的CPU。3.优化索引文档结构,避免使用连接查询4.搜索尽可能少的字段可以通过copy_to将多个字段的值合并到一个字段,这样减少搜索过程匹配的字段。5.预处理索引数据,减少查询过程中的计算消耗6.考虑将索引的mapping中的标识符字段(如id字段)设置为keyword类型numeric 类型适合范围查询range querieskeyword 类型适合等值查询term queries.当然,你也可以使用多字段multi-field适配多种场景下的查询。7.避免使用脚本尽量避免使用脚本排序,脚本计算得分,脚本聚合查询。8.合并只读索引9.热身全局序数,会占用部分JVM 堆空间,可以优化聚合查询性能。10.预热文件系统缓存如果重新启动运行 Elasticsearch 的机器,文件系统缓存将是空的,因此操作系统将索引的热点区域加载到内存中需要一些时间,以便快速搜索操作。 您可以使用 index.store.preload 设置根据文件扩展名明确告诉操作系统哪些文件应该立即加载到内存中。11.设置索引存储时的排序方式加快连接查询性能。12.使用首选项帮助优化缓存的使用。主要是集群中各个节点上的缓存配置可能存在差异,通过首选项的配置可以统一配置、优化缓存的使用。13.设置正确的副本数来提高吞吐量。那么正确的副本数量是多少?如果您的集群有 num_nodes 个节点、num_primaries 主分片,并且您希望最多同时处理 max_failures 个节点故障,那么适合您的副本数为 max(max_failures, ceil(num_nodes / num_primaries) - 1)。14.使用更高性能的查询API。比如多用filter少用query。15.使用constant_keyword字段类型提升filter速度。四、优化磁盘使用1.禁用你不需要的特性比如:1)不作为查询条件的属性,可以添加在mapping中声明:"index": false;2)text类型字段如果你只匹配而不关注匹配的分数,可以将类型声明为match_only_text,此字段类型通过删除评分和位置信息来节省大量空间。2.不要使用默认的动态字符串映射默认的动态字符串映射会将字符串字段同时作为text和keyword进行索引。 如果您只需要其中之一,这是一种浪费。 通常,id 字段只需要作为关键字索引,而 body 字段只需要作为文本字段索引。3.注意你的分片大小说明:更大的分片在存储数据方面会更有效。 要增加分片的大小,可以通过创建具有较少主分片的索引、创建更少的索引(例如通过利用 Rollover API)或使用 Shrink API 修改现有索引来减少索引中主分片的数量。但是注意,大的分片也有缺点,例如完整的恢复时间长。4.禁用 _source说明:_source 字段存储文档的原始 JSON 正文。 如果您不需要访问它,您可以禁用它。 但是,需要访问 _source 的 API(例如 update 和 reindex)将不起作用。5.使用压缩器best_compression说明:_source 和 stored 字段可以很容易地占用不可忽略的磁盘空间量。 可以使用 best_compression 编解码器更积极地压缩它们。6.强制合并Force merge说明:Elasticsearch 中的索引存储在一个或多个分片中。 每个分片都是一个 Lucene 索引,由一个或多个段segment组成——磁盘上的实际文件。 更大的段segment对于存储数据更有效。强制合并 API 可用于减少每个分片的段数。 在许多情况下,可以通过设置 max_num_segments=1 将段数减少到每个分片一个。7.收缩索引分片数量 Shrink indexShrink API 允许您减少索引中的分片数量。 与上面的强制合并 API 一起,这可以显着减少索引的分片和段的数量。8.使用足够的最小数字类型有利于节约磁盘空间说明:数字数据选择的字段类型会对磁盘使用产生重大影响。 特别是,整数应使用整数类型(字节、短整型、整数或长整型)存储,浮点数应存储在 scaled_float 中(如果合适)或适合用例的最小类型:使用 float over double,或 half_float over float 将有助于节省存储空间。9.使用索引排序来整理相似的文档,提高压缩率说明:Elasticsearch 在存储_source 时,会一次压缩多个文档,以提高整体压缩率。 例如,文档共享相同的字段名是很常见的,而且它们共享一些字段值也很常见,特别是在基数较低或 zipfian 分布的字段上。默认情况下,文档按照添加到索引的顺序压缩在一起。 如果您启用了索引排序,那么它们将按排序顺序压缩。 将具有相似结构、字段和值的文档排序在一起应该可以提高压缩率。10.在文档中以相同的顺序放置字段说明:由于多个文档被一起压缩成块,如果字段总是以相同的顺序出现,则更有可能在那些 _source 文档中找到更长的重复字符串。11.汇总历史数据说明:保留较旧的数据对以后的分析很有用,但由于存储成本,通常会避免。 您可以使用数据汇总以原始数据存储成本的一小部分来汇总和存储历史数据。12.对于时序数据可以采用数据流data_stream和索引生命周期管理ILM总结本文主要是对ES性能调优的一些方案进行了总结。
一、script脚本的作用通过使用脚本,可以在 Elasticsearch 计算自定义表达式。例如,可以使用脚本作为字段返回计算值,或者计算查询的自定义得分。小结:1、字段的提取2、表达式计算二、支持哪些script脚本语言默认的脚本语言采用的是painless。三、script脚本使用示例1、查询中使用script脚本PUT my-index-000001/_doc/1 { "my_field": 5 }GET my-index-000001/_search { "script_fields": { "my_doubled_field": { "script": { "source": "doc['my_field'].value * params['multiplier']", "params": { "multiplier": 2 } } } } }2、创建单独的脚本说明:通过_scripts命令创建单独的脚本,并通过lang属性指定脚本的语言是painless。source标签中,可以获取到索引文档中的值。通过id指定脚本名称来使用独立的脚本。创建脚本:POST _scripts/calculate-score { "script": { "lang": "painless", "source": "Math.log(_score * 2) + params['my_modifier']" } }获取脚本:GET _scripts/calculate-score使用脚本:GET my-index-000001/_search { "query": { "script_score": { "query": { "match": { "message": "some message" } }, "script": { "id": "calculate-score", "params": { "my_modifier": 2 } } } } }删除脚本:DELETE _scripts/calculate-score3.通过脚本更新索引字段信息PUT my-index-000001/_doc/1 { "counter" : 1, "tags" : ["red"] } POST my-index-000001/_update/1 { "script" : { "source": "ctx._source.counter += params.count", "lang": "painless", "params" : { "count" : 4 } } }4.在mappings属性中定义运行时字段查询的时候,可以直接使用mappings属性中定义的运行时字段day_of_week。PUT my-index-000001/ { "mappings": { "runtime": { "day_of_week": { "type": "keyword", "script": { "source": "emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT))" } } }, "properties": { "@timestamp": {"type": "date"} } } }5.在查询请求中定义运行时字段如果没有在索引的mappings属性中定义运行时字段,那么也可以通过_search查询时,通过runtime_mappings来定义运行时字段。GET my-index-000001/_search { "runtime_mappings": { "day_of_week": { "type": "keyword", "script": { "source": "emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT))" } } }, "aggs": { "day_of_week": { "terms": { "field": "day_of_week" } } } }6.使用脚本自定义计算得分GET index/_search { "query": { "script_score": { "query": { "match": { "body": "elasticsearch" } }, "script": { "source": "_score * saturation(doc['pagerank'].value, 10)" } } } }四、script脚本使用的安全性1、脚本类型限制:script.allowed_typesElasticsearch 支持2中类型的脚本: inline and stored.可以通过在elasticsearch.yml配置文件中的script.allowed_types属性来指定允许执行的脚本类型。相关配置选项:both 同时支持inline和storedinline 内联脚本stored 存储脚本none 都不支持2、脚本上下文限制:script.allowed_contexts默认情况下,所有脚本上下文都是允许的。使用 script.allowed_contexts 设置指定允许的上下文。若要指定不允许上下文,请将 script.allowe_contexts 设置为 none。示例:允许脚本仅在评分和更新上下文中运行:script.allowed_contexts: score, update总结本文主要介绍了ES中script脚本的使用。其主要作用是:提取字段属性,进行表达式计算。最典型的使用场景是:定义运行时字段。
一、Elasticsearch SQL简介Elasticsearch SQL 是一个 X-Pack 组件,它允许对 Elasticsearch 实时执行类似 SQL 的查询。无论是使用 REST 接口、命令行还是 JDBC,任何客户机都可以使用 SQL 在 Elasticsearch 中本地搜索和聚合数据。我们可以把 Elasticsearch SQL 看作一个翻译器,它同时理解 SQL 和 Elasticsearch,并且通过 Elasticsearch 的功能,可以方便地实时读取和处理数据。官方文档:根据版本级别的特征支持说明:https://www.elastic.co/cn/subscriptions免费开源的版本中,已经提供了对Elasticsearch SQL API功能的支持。我们通过对官网链接的版本号修改会发现:1、6.3版本还能正常访问到sql-overview相关介绍https://www.elastic.co/guide/en/elasticsearch/reference/6.3/sql-overview.html#sql-introduction2、切换成6.2后,出现页面不可用的提示。https://www.elastic.co/guide/en/elasticsearch/reference/6.2/sql-overview.html#sql-introduction可以初步得出结论,ES6.3之后的版本才提供免费的Elasticsearch SQL的特性。二、X-Pack 组件说明2019年5月21日,Elastic官方发布消息: Elastic Stack 新版本6.8.0 和7.1.0的核心安全功能现免费提供。这意味着用户现在能够对网络流量进行加密、创建和管理用户、定义能够保护索引和集群级别访问权限的角色,并且使用 Spaces 为 Kibana提供全面保护。免费提供的核心安全功能如下:1)TLS 功能。 可对通信进行加密;2)文件和原生 Realm。 可用于创建和管理用户;3)基于角色的访问控制。 可用于控制用户对集群 API 和索引的访问权限;通过针对 Kibana Spaces 的安全功能,还可允许在Kibana 中实现多租户。1、X-Pack演变1)5.X版本之前:没有x-pack,是独立的:security安全,watch查看,alert警告等独立单元。2)5.X版本:对原本的安全,警告,监视,图形和报告做了一个封装,形成了x-pack。3)6.3 版本之前:需要额外安装。4)6.3版本及之后:已经集成在一起发布,无需额外安装,基础安全属于付费黄金版内容。5)6.8.0和7 .1版本:基础安全免费。2、X-Pack包含的特性2018年2月28日X-Pack 特性的所有代码开源,主要包含:Security、Monitoring、Alerting、Graph、Reporting、专门的 APM UI、Canvas、Elasticsearch SQL、Search Profiler、Grok Debugger、Elastic Maps Service zoom levels 以及 Machine Learning。3、开源!=免费2019年5月21日免费开放了文章开头的基础安全功能,在这之前的版本都是仅有1个月的适用期限的。如下功能点仍然是收费的。付费黄金版&白金版提供功能:审核日志IP 筛选LDAP、PKI*和活动目录身份验证Elasticsearch 令牌服务付费白金版提供安全功能:单点登录身份验证(SAML、Kerberos*)基于属性的权限控制字段和文档级别安全性第三方整合(自定义身份验证和授权 Realm)授权 Realm静态数据加密支持三、Elasticsearch SQL入门使用1、创建索引PUT /library/book/_bulk?refresh {"index":{"_id": "Leviathan Wakes"}} {"name": "Leviathan Wakes", "author": "James S.A. Corey", "release_date": "2011-06-02", "page_count": 561} {"index":{"_id": "Hyperion"}} {"name": "Hyperion", "author": "Dan Simmons", "release_date": "1989-05-26", "page_count": 482} {"index":{"_id": "Dune"}} {"name": "Dune", "author": "Frank Herbert", "release_date": "1965-06-01", "page_count": 604}2、使用sql查询索引数据POST /_sql?format=txt { "query": "SELECT * FROM library WHERE release_date < '2000-01-01'" }响应结果:author | name | page_count | release_date ---------------+---------------+---------------+------------------------ Dan Simmons |Hyperion |482 |1989-05-26T00:00:00.000Z Frank Herbert |Dune |604 |1965-06-01T00:00:00.000Z3、响应的数据格式化主要有如下格式化类型:其中用的最多的主要是csv、json、text。JSON: POST /_sql?format=json { "query": "SELECT * FROM library ORDER BY page_count DESC", "fetch_size": 5 }响应结果:{ "columns": [ {"name": "author", "type": "text"}, {"name": "name", "type": "text"}, {"name": "page_count", "type": "short"}, {"name": "release_date", "type": "datetime"} ], "rows": [ ["Peter F. Hamilton", "Pandora's Star", 768, "2004-03-02T00:00:00.000Z"], ["Vernor Vinge", "A Fire Upon the Deep", 613, "1992-06-01T00:00:00.000Z"], ["Frank Herbert", "Dune", 604, "1965-06-01T00:00:00.000Z"], ["Alastair Reynolds", "Revelation Space", 585, "2000-03-15T00:00:00.000Z"], ["James S.A. Corey", "Leviathan Wakes", 561, "2011-06-02T00:00:00.000Z"] ], "cursor": "sDXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAAEWWWdrRlVfSS1TbDYtcW9lc1FJNmlYdw==:BAFmBmF1dGhvcgFmBG5hbWUBZgpwYWdlX2NvdW50AWYMcmVsZWFzZV9kYXRl+v///w8=" }CSV:POST /_sql?format=csv { "query": "SELECT * FROM library ORDER BY page_count DESC", "fetch_size": 5 }响应结果:author,name,page_count,release_date Peter F. Hamilton,Pandora's Star,768,2004-03-02T00:00:00.000Z Vernor Vinge,A Fire Upon the Deep,613,1992-06-01T00:00:00.000Z Frank Herbert,Dune,604,1965-06-01T00:00:00.000Z Alastair Reynolds,Revelation Space,585,2000-03-15T00:00:00.000Z James S.A. Corey,Leviathan Wakes,561,2011-06-02T00:00:00.000Z4、sql查询中使用filterPOST /_sql?format=txt { "query": "SELECT * FROM library ORDER BY page_count DESC", "filter": { "range": { "page_count": { "gte" : 100, "lte" : 200 } } }, "fetch_size": 5 }5、参数传递可以直接将参数组装成完整的SQL语句,也可以使用?占位符来传参。POST /_sql?format=txt { "query": "SELECT YEAR(release_date) AS year FROM library WHERE page_count > ? AND author = ? GROUP BY year HAVING COUNT(*) > ?", "params": [300, "Frank Herbert", 0] }6、使用运行时字段POST _sql?format=txt { "runtime_mappings": { "release_day_of_week": { "type": "keyword", "script": """ emit(doc['release_date'].value.dayOfWeekEnum.toString()) """ } }, "query": """ SELECT * FROM library WHERE page_count > 300 AND author = 'Frank Herbert' """ }响应结果:author | name | page_count | release_date |release_day_of_week ---------------+---------------+---------------+------------------------+------------------- Frank Herbert |Dune |604 |1965-06-01T00:00:00.000Z|TUESDAY7、Sql查询语句转为DSL查询语句POST /_sql/translate { "query": "SELECT * FROM library ORDER BY page_count DESC", "fetch_size": 10 }响应结果:{ "size": 10, "_source": false, "fields": [ { "field": "author" }, { "field": "name" }, { "field": "page_count" }, { "field": "release_date", "format": "strict_date_optional_time_nanos" } ], "sort": [ { "page_count": { "order": "desc", "missing": "_first", "unmapped_type": "short" } } ] }8、Sql支持的函数查看支持的所有函数:SHOW FUNCTIONS;查看支持的天数相关函数:SHOW FUNCTIONS LIKE '%DAY%'; name | type ---------------+--------------- DAY |SCALAR DAYNAME |SCALAR DAYOFMONTH |SCALAR DAYOFWEEK |SCALAR DAYOFYEAR |SCALAR DAY_NAME |SCALAR DAY_OF_MONTH |SCALAR DAY_OF_WEEK |SCALAR DAY_OF_YEAR |SCALAR HOUR_OF_DAY |SCALAR ISODAYOFWEEK |SCALAR ISO_DAY_OF_WEEK|SCALAR MINUTE_OF_DAY |SCALAR TODAY |SCALARElasticsearch SQL 提供了一整套内置的操作符和函数:官网说明:sql-functions9、子查询支持使用子选择(SELECT x FROM (SELECT y))在很小程度上是受支持的: Elasticsearch SQL 可以将任何子选择“扁平化”为单个 SELECT。SELECT * FROM (SELECT first_name, last_name FROM emp WHERE last_name NOT LIKE '%a%') WHERE first_name LIKE 'A%' ORDER BY 1; first_name | last_name ---------------+--------------- Alejandro |McAlpine Anneke |Preusig Anoosh |Peyn Arumugam |Ossenbruggen注意⚠️:如果子查询中包含 GROUP BY 或 HAVING 或封闭的 SELECT语句,这些比 SELECT X FROM (SELECT ...) WHERE [simple_condition]更复杂的查询,目前不支持。更多ES中SQL查询的限制,可以查看官网说明SQL Limitations10、SQL分页查询支持1)使用limit限制返回记录数:POST /_sql?format=txt { "query": "SELECT * FROM library limit 2" }2)使用top函数限制返回记录数:POST /_sql?format=txt { "query": "SELECT top 2 * FROM library" }3)使用fetch_size参数限制返回记录数:POST /_sql?format=txt { "query": "SELECT * FROM library", "fetch_size":2 }4)采用limit结合自查询进行分页查询:POST /_sql?format=txt { "query": "SELECT * FROM (SELECT * FROM library limit 2) limit 1" }5)通过游标访问下一页:说明:在采用CSV, TSV 和 TXT 格式化返回时, 会返回一个游标值cursor,通过游标值我们可以继续访问下一页。这种方式非常时候大数据量的分页返回。POST /_sql?format=json { "query": "SELECT * FROM library ORDER BY page_count DESC", "fetch_size": 5 }响应结果:```csharp { "columns": [ {"name": "author", "type": "text"}, {"name": "name", "type": "text"}, {"name": "page_count", "type": "short"}, {"name": "release_date", "type": "datetime"} ], "rows": [ ["Peter F. Hamilton", "Pandora's Star", 768, "2004-03-02T00:00:00.000Z"], ["Vernor Vinge", "A Fire Upon the Deep", 613, "1992-06-01T00:00:00.000Z"], ["Frank Herbert", "Dune", 604, "1965-06-01T00:00:00.000Z"], ["Alastair Reynolds", "Revelation Space", 585, "2000-03-15T00:00:00.000Z"], ["James S.A. Corey", "Leviathan Wakes", 561, "2011-06-02T00:00:00.000Z"] ], "cursor": "sDXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAAEWWWdrRlVfSS1TbDYtcW9lc1FJNmlYdw==:BAFmBmF1dGhvcgFmBG5hbWUBZgpwYWdlX2NvdW50AWYMcmVsZWFzZV9kYXRl+v///w8=" }通过游标访问下一页:POST /_sql?format=json { "cursor": "sDXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAAEWYUpOYklQMHhRUEtld3RsNnFtYU1hQQ==:BAFmBGRhdGUBZgVsaWtlcwFzB21lc3NhZ2UBZgR1c2Vy9f///w8=" }四、Elasticsearch和SQL对应关系虽然 SQL 和 Elasticsearch 对于数据的组织方式(以及不同的语义)有不同的术语,但本质上它们的用途是相同的。SQLElasticsearch说明columnfield在 Elasticsearch 字段时,SQL 将这样的条目调用为 column。注意,在 Elasticsearch,一个字段可以包含同一类型的多个值(本质上是一个列表) ,而在 SQL 中,一个列可以只包含一个表示类型的值。Elasticsearch SQL 将尽最大努力保留 SQL 语义,并根据查询的不同,拒绝那些返回多个值的字段。rowdocument列和字段本身不存在; 它们是行或文档的一部分。两者的语义略有不同: 行row往往是严格的(并且有更多的强制执行),而文档往往更灵活或更松散(同时仍然具有结构)。tableindex在 SQL 还是 Elasticsearch 中查询针对的目标schema无在关系型数据库中,schema 主要是表的名称空间,通常用作安全边界。Elasticsearch没有为它提供一个等价的概念。总结本文主要介绍了Elasticsearch SQL的使用。如果你对DSL查询语句不熟悉,那么采用SQL查询索引数据将是一个非常简单,0门槛入门的好方法。1、注意ES在6.3版本之后才原生支持SQL查询。2、可以通过translate API将sql语句转为DSL语句。3、ES的SQL查询提供对自查询的简单支持。4、通过SHOW FUNCTIONS可以查看ES的SQL查询支持的函数。5、ES的SQL查询可以通过游标cursor实现分页查询。
前言前面介绍了ES的简单使用,并说明了ES聚合查询主要分为3类:指标聚合、桶聚合和管道聚合。本文主要是介绍其中桶聚合的相关使用。一、桶聚合Bucket 聚合不像Metric聚合那样计算字段上的指标,而是创建多个“存放“文档的桶。每个桶都与一个标准相关联(取决于聚合类型) ,该标准确定当前上下文中的文档是否“落入”它。换句话说,bucket 有效地定义了文档集。除了 bucket 本身之外,bucket 聚合还计算并返回“落入”每个 bucket 中的文档数量。与Metric聚合不同的是,Bucket 聚合可以包含子聚合。这些子聚合将聚合它们的“父”bucket 聚合创建的 bucket。不同的桶式聚合器有不同的“分桶”策略。有些定义单个桶,有些定义固定数量的多个桶,还有一些在聚合过程中动态创建桶。二、使用场景桶聚合查询主要用来做分组统计。其中每个桶代表一个分组。三、桶聚合的分类官方文档:桶聚合四、典型使用1、单值聚合统计 terms一个基于多桶值源的聚合,其中桶是动态构建的——每个唯一值一个桶。主要使用做分组个数统计,类似mysql中的 select type , count(type) from table group by type;示例:统计每个类型的记录数,基于genre的唯一值个数创建多个桶,并统计每个桶的记录数。GET /_search { "aggs": { "genres": { "terms": { "field": "genre" } } } }执行结果:{ ... "aggregations": { "genres": { "doc_count_error_upper_bound": 0, "sum_other_doc_count": 0, "buckets": [ { "key": "electronic", "doc_count": 6 }, { "key": "rock", "doc_count": 3 }, { "key": "jazz", "doc_count": 2 } ] } } }如果想基于分组的个数排序,可以使用如下语句:GET /_search { "aggs": { "genres": { "terms": { "field": "genre", "order": { "_count": "asc" } } } } }如果希望基于分组的key值排序,可以使用如下语句:GET /_search { "aggs": { "genres": { "terms": { "field": "genre", "order": { "_key": "asc" } } } } }使用 min_doc_count 选项可以只返回匹配超过配置的命中数的结果:GET /_search { "aggs": { "tags": { "terms": { "field": "tags", "min_doc_count": 10 } } } }2、时间范围分组date_range统计10个月前和近10个月的记录数。POST /sales/_search?size=0 { "aggs": { "range": { "date_range": { "field": "date", "format": "MM-yyyy", "ranges": [ { "to": "now-10M/M" }, { "from": "now-10M/M" } ] } } } }3、多值分组Multi Terms根据多个字段进行分组统计,类比sql:select genre ,product, count(*) from products group by genre,product;GET /products/_search { "aggs": { "genres_and_products": { "multi_terms": { "terms": [{ "field": "genre" }, { "field": "product" }] } } } }4、自定义分组查询 Range AggregationRange Aggregation基于多桶值源的聚合,使用户能够定义一组范围——每个范围代表一个桶。在聚合过程中,将根据每个 bucket 范围检查从每个文档中提取的值,并对相关/匹配文档进行“ bucket”检查。请注意,这个聚合包括 from 值,并为每个范围排除 to 值。示例:根据自定义的价格范围统计买卖订单数。GET sales/_search { "aggs": { "price_ranges": { "range": { "field": "price", "ranges": [ { "to": 100.0 }, { "from": 100.0, "to": 200.0 }, { "from": 200.0 } ] } } } }5、日期分组聚合 Date Histogram按月分组聚合:POST /sales/_search?size=0 { "aggs": { "sales_over_time": { "date_histogram": { "field": "date", "calendar_interval": "month" } } } }按2天的日历单元聚合:POST /sales/_search?size=0 { "aggs": { "sales_over_time": { "date_histogram": { "field": "date", "calendar_interval": "2d" } } } }6、自动日期分组聚合 Auto Date Histogram自动根据日期分组聚合,还提供了一个目标桶数,表示所需桶数,并自动选择桶的间隔,以最好地实现该目标。返回的桶数总是小于或等于这个目标数。Bucket 字段是可选的,如果没有指定,则默认为10个 bucket。示例:要求10桶的目标。POST /sales/_search?size=0 { "aggs": { "sales_over_time": { "auto_date_histogram": { "field": "date", "buckets": 10 } } } }7、聚合中使用过滤器filterPOST /sales/_search?size=0&filter_path=aggregations { "aggs": { "avg_price": { "avg": { "field": "price" } }, "t_shirts": { "filter": { "term": { "type": "t-shirt" } }, "aggs": { "avg_price": { "avg": { "field": "price" } } } } } }8、稀有值查询 Rare terms示例:查询桶中最多包含2条记录的类型genre。GET /_search { "aggs": { "genres": { "rare_terms": { "field": "genre", "max_doc_count": 2 } } } }9、嵌套聚合 Nested Aggregation创建嵌套索引PUT /products { "mappings": { "properties": { "resellers": { "type": "nested", "properties": { "reseller": { "type": "keyword" }, "price": { "type": "double" } } } } } }添加数据:PUT /products/_doc/0?refresh { "name": "LED TV", "resellers": [ { "reseller": "companyA", "price": 350 }, { "reseller": "companyB", "price": 500 } ] }根据嵌套聚合查询:GET /products/_search?size=0 { "query": { "match": { "name": "led tv" } }, "aggs": { "resellers": { "nested": { "path": "resellers" }, "aggs": { "min_price": { "min": { "field": "resellers.price" } } } } } }执行结果:{ ... "aggregations": { "resellers": { "doc_count": 2, "min_price": { "value": 350.0 } } } }10、缺少聚合 Missing aggregationMissing聚合属于单桶聚合,对空值数据统计。示例:统计没有价格的产品总数。POST /sales/_search?size=0 { "aggs": { "products_without_a_price": { "missing": { "field": "price" } } } }11、全局聚合 Global aggregators定义搜索执行上下文中所有文档的单个存储桶。此上下文由您正在搜索的索引和文档类型定义,但不受搜索查询本身的影响。示例:聚合查询all_products中的平均价格统计不受外层query中的查询添加的影响。POST /sales/_search?size=0 { "query": { "match": { "type": "t-shirt" } }, "aggs": { "all_products": { "global": {}, "aggs": { "avg_price": { "avg": { "field": "price" } } } }, "t_shirts": { "avg": { "field": "price" } } } }总结本文主要是对ES中典型的桶聚合查询进行了介绍,特别要注意以下几个分组统计。1、单值分组统计 Terms aggregation2、多值分组统计 Multi Terms3、自定义分组统计 Range Aggregation4、时间范围分组 date_range
前言本文主要介绍ES中的聚合查询。一、聚合查询简介聚合查询可以将数据汇总为度量、统计或其他分析。聚合查询主要分为三个类别:Metric 指标聚合Bucket 桶聚合Pipeline 管道聚合二、聚合函数的使用1、如何运行一个聚合查询GET /my-index-000001/_search { "aggs": { "my-agg-name": { "terms": { "field": "my-field" } } } }说明:aggs 说明采用的是聚合查询my-agg-name 是聚合查询的名称terms 说明采用的是Terms aggregation多值聚合:一个基于多桶值源的聚合,其中桶是动态构建的——每个唯一值一个桶。统计每个唯一值的个数。field 指定需要统计的字段。2、限制聚合查询的范围GET /my-index-000001/_search { "query": { "range": { "@timestamp": { "gte": "now-1d/d", "lt": "now/d" } } }, "aggs": { "my-agg-name": { "terms": { "field": "my-field" } } } }3、仅返回聚合结果默认情况下,包含聚合的查询会同时返回搜索命中的结果和聚合结果。若要只返回聚合结果,请将大小设置为0GET /my-index-000001/_search { "size": 0, "aggs": { "my-agg-name": { "terms": { "field": "my-field" } } } }4、运行多个聚合GET /my-index-000001/_search { "aggs": { "my-first-agg-name": { "terms": { "field": "my-field" } }, "my-second-agg-name": { "avg": { "field": "my-other-field" } } } }5、子聚合统计索引中my-field字段的每个唯一值的记录数,并计算每组记录中my-other-field字段的平均值。典型的场景:先分组,再计算GET /my-index-000001/_search { "aggs": { "my-agg-name": { "terms": { "field": "my-field" }, "aggs": { "my-sub-agg-name": { "avg": { "field": "my-other-field" } } } } } }执行结果:{ ... "aggregations": { "my-agg-name": { "doc_count_error_upper_bound": 0, "sum_other_doc_count": 0, "buckets": [ { "key": "foo", "doc_count": 5, "my-sub-agg-name": { "value": 75.0 } } ] } } }6、聚合查询中使用scripts脚本采用script脚本提取运行时字段,并对运行时字段message.length进行聚合。GET /my-index-000001/_search?size=0 { "runtime_mappings": { "message.length": { "type": "long", "script": "emit(doc['message.keyword'].value.length())" } }, "aggs": { "message_length": { "histogram": { "interval": 10, "field": "message.length" } } } }7、聚合结果分页GET /my-index-000001/_search { "size":0, "aggs" : { "group_account" : { "terms" : { "size": 2, "field" : "account", "order": { "sum_gmv" : "desc" } }, "aggs": { "sum_gmv": { "sum": {"field": "bus_ep_gmv"} } } } } }8、聚合查询缓存说明为了获得更快的响应,Elasticsearch 将频繁运行的聚合结果缓存到切分请求缓存中。若要获取缓存结果,请对每次搜索使用相同的首选项字符串。如果您不需要搜索命中、只返回聚合结果,请将大小设置为0,以避免填充缓存。总结本文主要是聚合查询进行了简单的介绍。1、聚合查询主要使用场景:数据的统计分析。2、聚合查询主要分为三个类别:Metric 指标聚合Bucket 桶聚合Pipeline 管道聚合3、聚合查询的简单使用示例。
前言logback应该是目前最主流的日志框架了,在实际使用中经常遇到打印的日志文件不会自动删除,导致日志文件占有大量磁盘空间的问题。本文主要介绍logback日志文件自动删除的实现机制。一、官方文档介绍官方文档地址:logback1、ConsoleAppender说明:如果希望日志打印到控制台,需要配置ConsoleAppender控制台日志追加。<configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <!-- encoders are assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder by default --> <encoder> <pattern>%-4relative [%thread] %-5level %logger{35} - %msg %n</pattern> </encoder> </appender> <root level="DEBUG"> <appender-ref ref="STDOUT" /> </root> </configuration>2、RollingFileAppender说明:如果希望打印生成的日志文件根据日志大小和时间自动滚动生成新的日志文件,需要配置RollingFileAppender滚动日志追加。滚动策略选择SizeAndTimeBasedRollingPolicy基于日志大小和时间滚动。下面的配置是限制每个文件最多100mb,保存30天的历史记录,日志总大小最多20gb。<configuration> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_FILE}</file> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <!--日志文件输出的文件名--> <fileNamePattern>${LOG_FILE}-%d{yyyy-MM-dd}.%i.gz</fileNamePattern> <!--日志大小--> <maxFileSize>100MB</maxFileSize> <!--日志保留时长--> <maxHistory>30</maxHistory> <totalSizeCap>20GB</totalSizeCap> <cleanHistoryOnStart>true</cleanHistoryOnStart> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-4relative [%thread] %-5level %logger{35} - %msg%n</pattern> <charset>utf8</charset> </encoder> </appender> <root level="DEBUG"> <appender-ref ref="FILE" /> </root> <configuration>参数说明:file 生成的日志名称rollingPolicy 滚动策略,这里采用的SizeAndTimeBasedRollingPolicy,基于日志文件大小和时间滚动。fileNamePattern 定义翻转(归档)日志文件的名称。它的值应该包括文件的名称以及适当放置的% d 转换说明符。% d 转换说明符可能包含日期和时间模式。如果省略了日期和时间模式,则假定使用默认模式 yyyy-MM-dd。翻转周期是从 fileNamePattern 的值推断出来的。这里的滚动周期需要和maxHistory配合使用。maxFileSize 单个日志文件的最大体积,到达最大体积后就会触发日志滚动操作,生成新的日志文件maxHistory 要保存的归档文件的最大数量,以异步方式删除旧文件。例如,如果通过fileNamePattern中的%d{yyyy-MM}指定滚动周期为月度滚动,并将 maxHistory 设置为6,那么存档文件中超过6个月的文件将被删除。totalSizeCap 控制所有归档日志文件的总大小。当超出总大小上限时,将异步删除最早的归档日志文件。设置totalSizeCap 属性还要求设置 maxHistory 属性。优先“最大历史”限制,其次是“总大小上限”的限制。按照实际业务情况配置 totalSizeCap ,可以有效避免占用过大的磁盘空间。比如你希望 maxHistory 保留7天日志文件,但是可能这7个文件总大小超出磁盘容量,所以可以通过 totalSizeCap 来控制总大小,这样系统会判断大于此值时进行自动覆盖。注意⚠️:单独配置totalSizeCap是没有意义的,一定要同时配置maxHistory属性,且大于0,才能实现超过总大小上限异步删除。cleanHistoryOnStart 是否在应用启动的时候删除历史日志。如果设置为真,将在启动应用程序时执行档案删除。默认情况下,此属性设置为 false。归档日志移除通常在滚动期间执行。但是,有些应用程序的存活时间可能不够长,无法触发滚动。因此,对于如此短命的应用程序,删除存档可能永远没有机会执行。通过将 cleanHistoryOnStart 设置为 true,将在启动 appender 时执行档案删除。encoder 控制输出日志的格式和编码。二、效果测试说明:为了测试效果,修改配置,通过fileNamePattern属性中的%d{yyyy-MM-dd_HH-mm}指定滚动周期为分钟。maxFileSize控制日志文件超过10kb就触发滚动。maxHistory属性结合fileNamePattern中解析出现的滚动周期,实现最多保留近3分钟的归档日志。totalSizeCap控制所有归档日志文件的总大小不超过20kb。<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_FILE}</file> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <!--日志文件输出的文件名--> <fileNamePattern>${LOG_FILE}-%d{yyyy-MM-dd_HH-mm}.%i.gz</fileNamePattern> <!--日志大小--> <maxFileSize>10KB</maxFileSize> <!--日志保留时长--> <maxHistory>3</maxHistory> <totalSizeCap>20KB</totalSizeCap> <cleanHistoryOnStart>true</cleanHistoryOnStart> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-4relative [%thread] %-5level %logger{35} - %msg%n</pattern> <charset>utf8</charset> </encoder> </appender>测试一:项目启动后,持续调用方法生成日志文件:执行结果:归档日志(带时间后缀的这些滚动生成的日志文件)总大小超过20kb后,就会触发删除操作。说明totalSizeCap配置属性生效。测试二:间隔3分钟后,在调用方法生成日志。执行结果:3分钟前的日志文件全部被删除,说明fileNamePattern和maxHistory配置最多保留近3分钟的归档日志生效。测试三:生成一批归档日志文件后,3分钟后重启项目,校验是否在项目启动是清理日志。这里为了验证结果的正确性,需要保证项目重启过程中生成的日志大小不超过maxFileSize,不触发日志滚动。项目启动生成日志:刚好9kb,没有触发滚动。调用方法,生成日志文件:为避免触发滚动,删除log文件,3分钟后重启项目:执行结果:3分钟前的日志文件全部被删除,说明cleanHistoryOnStart配置生效,在项目启动的时候会检查是否有需要删除的归档日志文件。总结本文主要介绍了logback过期的日志文件的自动删除机制如何配置。有如下参数需要注意:1、RollingFileAppender控制滚动日志文件追加。2、SizeAndTimeBasedRollingPolicy用来配置采用基于大小和时间的滚动策略。3、fileNamePattern既控制滚动日志的命名模式,也控制maxHistory的滚动周期。4、maxFileSize控制日志最大多少触发滚动。5、maxHistory控制归档日志的保留时长,需要和fileNamePattern中的%d{yyyy-MM-dd}解析的滚动周期一起使用。6、totalSizeCap控制归档日志的最大体积是多少,超过会触发删除归档日志操作。需要和maxHistory属性一起使用,只配置totalSizeCap属性但是maxHistory=0时不会触发自动删除操作。7、cleanHistoryOnStart控制是否在项目启动的时候检查是否需要删除归档日志。
前言在采用ELK分布式日志采集平台的时候,一般都会采用ES来存储采集的日志信息。日志信息一般都是持续增长的,是典型的时序数据。如果不对采集的日志数据做生命周期管理,很容易导致单个索引体积持续增长、查询速度越来越慢、过期的日志信息浪费空间等问题。本节主要介绍如果通过ES中的索引生命周期管理机制ILM来实现对日志数据的管理。一、说明es可以用来存储日志,一般日志存储只是短期保存,超过一定时间日志要是能自动删除最好,这样保证索引文档不会过多,查询时效性也能得到保证。索引的生命周期分为四个阶段:HOT->WARM->COLD->Frozen->DELETE。上面除了HOT为必须的阶段外,其他为非必须阶段,可以任意选择配置。因为日志索引只要满足自己删除功能,所以下文只配置了HOT与DELETE阶段。三步实现完成es生命周期管理:配置策略(policy)->索引模版(template)->索引(index)二、实战1.配置策略说明:创建策略log_policy,包含2个阶段hot和delete。hot阶段:数据写入首先进入hot阶段,包含一个回滚的动作,当记录条数达到3时滚动一次,创建一个新的后备索引。delete阶段:滚动发送后30s,执行删除动作。PUT _ilm/policy/log_policy { "policy": { "phases": { "hot": { "actions": { "rollover": { "max_docs":3 } } }, "delete": { "min_age": "30s", "actions": { "delete": {} } } } } }2.创建索引模板PUT _index_template/log_template { "index_patterns": ["log"], "data_stream": { }, "template": { "settings": { "number_of_shards": 1, "number_of_replicas": 1, "index.lifecycle.name": "log_policy" } } }参数说明:index_patterns 索引匹配模式data_stream 声明是一个数据流template 配置模板的settings和mappings属性index.lifecycle.name 生命周期策略名称,最核心的属性,要和上文定义的生命周期名称保持一致。3.创建索引模板方式一:创建并添加数据创建数据流timeseries,同时向数据流中写入一条数据。数据中必须包含@timestamp字段信息POST log/_doc { "message": "logged the request", "@timestamp": "1633677855467" }方式二:仅创建数据流PUT _data_stream/log注意⚠️:1、数据流名称必须和索引模板中的index_patterns匹配。2、写入的数据中必须包含@timestamp字段信息4.获取数据流信息GET _data_stream/log结果:generation 第一代,说明还没有执行滚动template 采用的索引模板为log_templateilm_policy 采用的生命周期管理策略为log_policy{ "data_streams" : [ { "name" : "log", "timestamp_field" : { "name" : "@timestamp" }, "indices" : [ { "index_name" : ".ds-log-000001", "index_uuid" : "kRMp8y_2SYyw0mh1Sm713A" } ], "generation" : 1, "status" : "YELLOW", "template" : "log_template", "ilm_policy" : "log_policy" } ] }5.生命周期信息拉取频率集群参数indices.lifecycle.poll_interval 用来控制索引生命周期管理检查符合策略标准的索引的频率,默认为10m。所以在索引生命周期策略中配置的动作和条件,并不是即使触发的,而且定期检查触发。打个比方:尽管在策略中设置了hot阶段索引中数据记录大于3条就进行滚动,但其实并不是超过3条就立刻进行滚动。而且ES集群定期检查,当发现索引满足滚动条件后才进行滚动操作。这里为了方便验证,将检查频率改为10s。PUT /_cluster/settings { "transient": { "indices.lifecycle.poll_interval": "10s" } }6.验证向数据流中写入4条记录,查看数据流生命周期的变化情况。GET .ds-log-*/_ilm/explain结果:{ "indices" : { ".ds-log-000001" : { "index" : ".ds-log-000001", "managed" : true, "policy" : "log_policy", "lifecycle_date_millis" : 1633763085182, "age" : "4.06m", "phase" : "hot", "phase_time_millis" : 1633763085834, "action" : "rollover", "action_time_millis" : 1633763096800, "step" : "check-rollover-ready", "step_time_millis" : 1633763096800, "phase_execution" : { "policy" : "log_policy", "phase_definition" : { "min_age" : "0ms", "actions" : { "rollover" : { "max_docs" : 3 } } }, "version" : 1, "modified_date_in_millis" : 1633762512190 } } } }添加数据:PUT log/_bulk?refresh { "create":{ } } {"message": "logged the request1","@timestamp": "1633677862467"} { "create":{ } } {"message": "logged the request2","@timestamp": "1633677872468"} { "create":{ } } {"message": "logged the request3","@timestamp": "1633682619628"} { "create":{ } } {"message": "logged the request4","@timestamp": "1633682619628"}再次查看索引生命周期情况:GET .ds-log-*/_ilm/explain执行结果:{ "indices" : { ".ds-log-000001" : { "index" : ".ds-log-000001", "managed" : true, "policy" : "log_policy", "lifecycle_date_millis" : 1633763525979, "age" : "11.91s", "phase" : "hot", "phase_time_millis" : 1633763085834, "action" : "complete", "action_time_millis" : 1633763528120, "step" : "complete", "step_time_millis" : 1633763528120, "phase_execution" : { "policy" : "log_policy", "phase_definition" : { "min_age" : "0ms", "actions" : { "rollover" : { "max_docs" : 3 } } }, "version" : 1, "modified_date_in_millis" : 1633762512190 } }, ".ds-log-000002" : { "index" : ".ds-log-000002", "managed" : true, "policy" : "log_policy", "lifecycle_date_millis" : 1633763525996, "age" : "11.89s", "phase" : "delete", "phase_time_millis" : 1633763527252, "action" : "rollover", "action_time_millis" : 1633763536853, "step" : "check-rollover-ready", "step_time_millis" : 1633763536853, "phase_execution" : { "policy" : "log_policy", "phase_definition" : { "min_age" : "0ms", "actions" : { "rollover" : { "max_docs" : 3 } } }, "version" : 1, "modified_date_in_millis" : 1633762512190 } } } }过段时间再次查看,发现只有ds-log-000002。ds-log-000001已经被删除。{ "indices" : { ".ds-log-000002" : { "index" : ".ds-log-000002", "managed" : true, "policy" : "log_policy", "lifecycle_date_millis" : 1633763696125, "age" : "1.45m", "phase" : "hot", "phase_time_millis" : 1633763697292, "action" : "rollover", "action_time_millis" : 1633763706877, "step" : "check-rollover-ready", "step_time_millis" : 1633763706877, "phase_execution" : { "policy" : "log_policy", "phase_definition" : { "min_age" : "0ms", "actions" : { "rollover" : { "max_docs" : 3 } } }, "version" : 1, "modified_date_in_millis" : 1633762512190 } } } }说明:执行结果说明,随着日志数据的写入,log索引能够自动滚动生存新的索引,滚动操作后超过30s的索引数据会被删除。注意⚠️:索引生命周期管理,控制的粒度是索引级别,而不是索引记录级别。所以采用_bulk指令批量写入数据时,都是向当前的最新的写索引写入数据。不会出现写了3条数据就自动触发滚动然后向新的索引写入第4条的情况。查询的时候也会发现所有的数据都是在索引ds-log-000001中。当触发delete阶段的删除操作后,会直接删除满足条件的整个ds-log-000001。不会说只删除3条,还有一条保留在ds-log-000002中的情况。总结本文主要介绍了通过索引生命周期管理ILM机制实现对日志数据的生命周期的自动化管理。1、索引生命周期管理的控制粒度是索引级别,而不是索引记录级别。2、索引生命周期的触发并不是实时的,而是定时周期触发检查机制,检查频率大小由indices.lifecycle.poll_interval控制。3、日志数据满足时序数据、连续不断持续增长,只读属性的特点,适合采用数据流保存。
一、什么是数据流官方定义:Data streams 数据流数据流是可以跨多个索引存储仅限于追加存储的时间序列数据,同时为请求提供单个命名资源。在 Elasticsearch 7.9之前,通常会使用带写索引权限的索引别名来管理时间序列数据。数据流取代了这一功能,需要更少的维护,并自动与数据层集成。所以可以把数据流看作是带有写索引权限的索引别名的升级特性。官网说明:Data Stream从定义中我们可以看出,数据流的一些特点:1、可以跨多个索引存储2、仅限追加存储,不支持删除、修改操作3、时间序列数据4、为请求提供单个命名资源,可以理解成天然具有公共的别名使用场景:数据流非常适合于日志、事件、指标和其他连续生成的数据。可以将索引和搜索请求直接提交到数据流,流会自动将请求路由到存储流数据的备份索引。也可以使用索引生命周期管理(ILM)来对数据流中的备份索引的自动化管理。例如,您可以使用 ILM 自动将较旧的支持索引移动到较便宜的硬件,并删除不需要的索引。ILM 可以帮助您在数据增长时降低成本和开销。二、核心概念1、后备索引 Backing indices数据流由一个或多个隐藏的自动生成的后备索引组成。1、数据流需要匹配的索引模板。模板包含用于配置流的后台索引的映射和设置。2、索引到数据流的每个文档必须包含一个@timestamp 字段,映射为 date 或 date _ nanos 字段类型。如果索引模板没有为@timestamp 字段指定映射,Elasticsearch 将@timestamp 映射为带有默认选项的日期字段。3、同一索引模板可用于多个数据流。不能删除数据流正在使用的索引模板。2、读请求 Read request当您向数据流提交一个读请求时,数据流将请求路由到它的所有后备索引。3、写索引 write index最近创建的备份索引是数据流的写索引。流仅向此索引添加新文档。不能将新文档添加到其他后备索引中,即使是直接向索引发送请求也不行。也不能对写入索引执行可能阻碍索引的操作,比如删除、克隆、冻结4、滚动 Rollover滚动操作将创建一个新的后备索引,该索引将成为流的新写索引。一般会使用 ILM 在写索引达到指定的年龄或大小时自动滚动数据流。如果需要,还可以手动滚动数据流。5、自增序号 generation每触发一次滚动操作就会生成新的后备索引,generation后备索引的自增序号。每个数据流跟踪id的生成规则:一个六位数的零填充整数,作为流的滚动累计计数,从000001开始。创建备份索引时,索引使用以下约定命名:.ds-<data-stream>-<yyyy.MM.dd>-<generation>说明:data-stream是数据流名称yyyy.MM.dd是后备索引创建时间generation自增序号6、仅限追加 Append Only数据流是为现有数据很少更新的用例而设计的。不能将现有文档的更新或删除请求直接发送到数据流。如果需要,可以通过直接向文档的后备索引提交请求来更新或删除文档。如果经常更新或删除现有的时间序列数据,请使用具有写索引权限的索引别名,而不是数据流。三、创建数据流官网说明:创建数据流1.创建索引生命周期策略PUT _ilm/policy/timeseries_policy { "policy": { "phases": { "hot": { "actions": { "rollover": { "max_docs":3 } } }, "delete": { "min_age": "30s", "actions": { "delete": {} } } } } }2.创建索引模板说明:通过data_stream属性说明这是一个数据流的索引模板。PUT _index_template/timeseries_template { "index_patterns": ["timeseries"], "data_stream": { }, "template": { "settings": { "number_of_shards": 1, "number_of_replicas": 1, "index.lifecycle.name": "timeseries_policy" } } }3.创建数据流方式一:创建并添加数据创建数据流timeseries,同时向数据流中写入一条数据。数据中必须包含@timestamp字段信息POST timeseries/_doc { "message": "logged the request", "@timestamp": "1633677855467" }方式二:仅创建数据流PUT _data_stream/timeseries注意⚠️:1、数据流名称必须和索引模板中的index_patterns匹配。2、写入的数据中必须包含@timestamp字段信息4.向数据流写入数据批量写入:PUT timeseries/_bulk { "create":{ } } {"message": "logged the request1","@timestamp": "1633677862467"} { "create":{ } } {"message": "logged the request2","@timestamp": "1633677872468"} { "create":{ } } {"message": "logged the request3","@timestamp": "1633682619628"}单条写入:POST timeseries/_doc { "message": "logged the request", "@timestamp": "1633677872468" }5.将索引别名转化成数据流POST _data_stream/_migrate/my-time-series-data6.获取数据流信息GET _data_stream/my-data-stream四、删除数据流DELETE _data_stream/my-data-stream注意⚠️:不同于删除别名,该命令会同时删除数据流及其包含的后备索引数据,一定要非常谨慎的操作。五、查看数据流的生命周期GET .ds-timeseries-*/_ilm/explain六、采用update_by_query更新数据POST /my-data-stream/_update_by_query { "query": { "match": { "user.id": "l7gk7f82" } }, "script": { "source": "ctx._source.user.id = params.new_id", "params": { "new_id": "XgdX0NoX" } } }七、采用delete_by_query删除数据POST /my-data-stream/_delete_by_query { "query": { "match": { "user.id": "vlb44hny" } } }八、直接向后备索引发起删除或更新请求也可以通过向包含文档的后备索引直接发送请求来更新或删除数据流中的文档。1、先查询索引记录的信息主要是为了获取后备索引的名称_index,文档id号_id,序列号_seq_no,主分片号_seq_no。GET /my-data-stream/_search { "seq_no_primary_term": true, "query": { "match": { "user.id": "yWIumJd7" } } }响应:"hits": [ { "_index": ".ds-my-data-stream-2099.03.08-000003", "_type": "_doc", "_id": "bfspvnIBr7VVZlfp2lqX", "_seq_no": 0, "_primary_term": 1, "_score": 0.2876821, "_source": { "@timestamp": "2099-03-08T11:06:07.000Z", "user": { "id": "yWIumJd7" }, "message": "Login successful" } } ]2、更新PUT /.ds-my-data-stream-2099-03-08-000003/_doc/bfspvnIBr7VVZlfp2lqX?if_seq_no=0&if_primary_term=1 { "@timestamp": "2099-03-08T11:06:07.000Z", "user": { "id": "8a4f500d" }, "message": "Login successful" }3、删除DELETE /.ds-my-data-stream-2099.03.08-000003/_doc/bfspvnIBr7VVZlfp2lqX总结数据流并不是什么高深莫测的东西,只是ES中对具有写权限的索引别名机制的一种升级,使用过程中可以类比索引别名来理解。1、数据流适用于仅限追加存储的时间序列数据,比如日志、事件、指标等。2、不支持对数据流直接发起更新、删除请求,但是可以直接向数据流中包含的后备索引发起更新、删除请求。3、数据流中必须包含@timestamp字段信息4、数据流中会包含一系列后备索引,读请求会发送到所有后备索引,写请求会发送到最新的后备索引。5、数据流一般都会结合索引生命周期管理ILM一起使用,实现对索引数据生命周期的自动化管理。6、数据流的创建一般是先创建索引生命周期管理策略,再创建索引模板,然后创建索引。
一、为什么需要滚动索引当索引时间序列数据(如日志或指标)时,您不能无限期地写入单个索引。为了满足索引和搜索性能要求并管理资源使用,您可以写入一个索引,直到达到某个阈值,然后创建一个新的索引并开始写入该索引。滚动索引的作用:在高性能热节点上优化高摄取率的活动指标。优化暖节点的搜索性能将较旧、较少访问的数据转移到较便宜的冷节点通过删除整个索引来根据保留策略删除数据。二、如何使用滚动索引ILM 使您能够根据索引大小、文档数量或年龄自动转移到新的索引。触发滚动时,创建一个新索引,更新写别名以指向新索引,并将所有后续更新写入新索引。通常建议使用数据流来管理时间序列数据,数据流自动跟踪写索引,同时将配置保持在最低限度。每个数据流都需要一个索引模板,其中包含:数据流的名称或通配符(*)模式。数据流的时间戳字段映射mappings和设置settingsILM 允许您根据索引大小、文档数量或年龄自动转移到新的索引。触发滚动时,创建一个新索引,更新写别名以指向新索引,并将所有后续更新写入新索引。滚动的触发条件:“max_primary_shard_size”: “50GB” 依据主分片大小“max_age”: “30d” 依据索引数据年龄“max_docs”:3 索引记录数“max_size”: “5gb” 索引大小TIP:滚动到基于索引大小、文档数量或年龄的新索引比基于时间的滚动更可取。在任意时间滚动通常会导致许多小的索引,这可能会对性能和资源使用产生负面影响。为了创建一个滚动索引,你需要:创建生命周期策略创建一个索引模板验证索引生命周期阶段的转变官网说明文档: Rollover三、实战1、创建生命周期策略方式一:通过Kibana定义通过 Kibana 或使用 create 或 update policy API 创建策略。要从 Kibana 创建策略,打开菜单,进入堆栈管理 > 索引生命周期策略。点击创建策略。方式二:通过API定义说明:定义一个包含两个阶段的索引生命周期管理策略 timeseries_policy。1、在hot阶段,定义了翻转动作,该阶段指定当索引的主分片的最大存储容量到达50g 或者最大年龄到达30天进行滚动。2、delete阶段,定义滚动后90天删除索引。PUT _ilm/policy/timeseries_policy { "policy": { "phases": { "hot": { "actions": { "rollover": { "max_primary_shard_size": "50GB", "max_age": "30d" } } }, "delete": { "min_age": "90d", "actions": { "delete": {} } } } } }参数介绍:1、rollover 设置索引滚动的触发条件2、max_primary_shard_size 最大主分片数3、max_age 索引最多保留多少时间4、min_age多长时间后,进入下一个阶段5、actions滚动到该阶段后需要执行的动作注意⚠️:1、滚动动作只有在hot阶段才能配置。2、min_age时间是从执行滚动后开始算起,而不是创建索引开始算起。例如,下列策略在索引滚动一天后删除该索引。它不会在创建索引后一天删除索引。PUT /_ilm/policy/rollover_policy { "policy": { "phases": { "hot": { "actions": { "rollover": { "max_size": "50G" } } }, "delete": { "min_age": "1d", "actions": { "delete": {} } } } } }2、创建索引模板要设置数据流,首先创建一个索引模板来指定生命周期策略。因为模板是用于数据流的,所以它还必须包含数据流定义。例如,您可以创建一个 timeseries _ template,用于将来名为 timeseries 的数据流。为了使 ILM 能够管理数据流,模板配置了一个 ILM 设置:指定要应用于数据流的生命周期策略的名称方式一:通过Kibana创建索引模板您可以使用 Kibana Create 模板向导添加模板。从 Kibana 打开菜单,进入 Stack Management > Index Management。在 Index Templates 选项卡中,单击 Create template。方式二:通过API创建索引模板PUT _index_template/timeseries_template { "index_patterns": ["timeseries"], "data_stream": { }, "template": { "settings": { "number_of_shards": 1, "number_of_replicas": 1, "index.lifecycle.name": "timeseries_policy" } } }说明:创建名称为timeseries_template的索引模板。index_patterns 用于设置索引名称的匹配模式,当索引名称与 timeseries 目标匹配时应用该模板index.lifecycle.name 用于管理数据流的 ILM 策略的名称。通过data_stream声明是一个数据流模板。3、创建数据流如果索引名称与索引模板的 index _ patterns 中定义的名称或通配符模式匹配,只要现有的数据流、索引或索引别名尚未使用该名称,索引请求就会自动创建带有单个后备索引的对应数据流。Elasticsearch 自动将请求的文档索引到这个支持索引中,该索引也充当流的写索引。例如,下面的请求将创建 timeseries 数据流和名为. ds-timeseries-2099.03.08-00001的第一代支持索引。POST timeseries/_doc { "message": "logged the request", "@timestamp": "1591890611" }当满足生命周期策略中的滚动条件时,滚动操作:创建名为 .ds-timeseries-2099.03.08-000002 的第二代后备索引。 因为它是 timeseries 数据流的后备索引,所以来自 timeseries_template 索引模板的配置将应用于新索引。由于是timeseries数据流的最新一代索引,新创建的backing index .ds-timeseries-2099.03.08-000002成为数据流的写索引。每次满足滚动条件时,这个过程都会重复。你可以搜索所有数据流的支持索引,这些索引由 timeseries_ policy 管理,并带有时间串数据流名称。写操作被路由到当前的写索引。读操作将由所有支持索引处理。注意⚠️:1、数据流是缩放和管理时间序列数据的一种方便方式,但是它们仅用于附加。很多场景下数据需要更新或删除的用例,而数据流不支持直接删除和更新请求,因此索引 api 将需要直接用于数据流的支持索引。2、如果索引数据需要支持直接删除、更新那么就不能采用数据流了,这时可以使用索引别名来管理包含时间序列数据的索引,并定期转移到新的索引。4、检索生命周期的进度要获取托管索引的状态信息,可以使用 ILM 解释 API:索引处于什么阶段以及何时进入这个阶段当前操作和正在执行的步骤如果发生任何错误或进度被阻塞例如,下面的请求获取有关 timeseries 数据流的支持索引的信息:GET .ds-timeseries-*/_ilm/explain总结1、滚动索引使用场景2、如何使用滚动索引,配置策略(policy)、创建索引模版(template)、创建索引(index)
前言在使用ES的过程中,你是否遇到过这样的问题:1、单个索引数据量持续增长,导致查询速度降低,运维困难2、希望能根据时间周期自动生成新的索引,比如天、周、月自动生成新的索引3、希望能定期自动删除过期的历史数据,比如3年前的订单信息。4、自动控制数据的冷热数据分层存储。其实ES早就提供了相关的处理机制,那就是索引生命周期管理ILM(index lifecycle management )。一、ILM介绍通过配置索引生命周期管理 (ILM)策略实现根据性能、弹性和保留要求自动管理索引。使用场景:当索引达到特定大小或文档数量时启动新索引每天、每周或每月创建一个新索引并存档以前的索引删除陈旧索引以执行数据保留标准我们可以通过 Kibana Management 或 ILM API 创建和管理索引生命周期策略。当您为 Beats 或 Logstash Elasticsearch 输出插件启用索引生命周期管理时,会自动配置默认策略。索引生命周期策略可以触发如下操作:可以触发的操作:Rollover: 当索引达到特定大小、文档数量或年龄时,创建一个新的写入索引Shrink: 减少索引中主分片的数量Force merge: 触发强制合并以减少索引分片中的segments段数Freeze: 冻结索引并使其只读Delete: 永久删除索引,包括其所有数据和元数据。ILM 可以更轻松地管理热-温-冷架构中的索引,这在您处理日志和指标等时间序列数据时很常见。可以指定:您想要滚动到新索引的最大分片大小、文档数或年龄。不再更新索引并且可以减少主分片数量的点。何时强制合并以永久删除标记为删除的文档。索引可以移动到性能较低的硬件的点。可用性不那么重要并且可以减少副本数量的点。何时可以安全删除索引。例如,如果您将 ATM 机群中的指标数据索引到 Elasticsearch 中,您可以定义一个策略,说明:1、当索引的主分片总大小达到 50GB 时,滚动到新索引。2、将旧索引移至热阶段,将其标记为只读,并将其缩小为单个分片。3、7 天后,将索引移至冷阶段并将其移至较便宜的硬件。4、达到所需的 30 天保留期后,删除索引。简单来说,索引生命周期管理ILM通过自定义索引生命周期策略(Index lifecycle policies),可以实现自动控制索引的滚动,分片压缩,强制合并,冻结,删除等操作。注意⚠️:要使用 ILM,集群中的所有节点必须运行相同的版本。二、索引生命周期ILM将索引生命周期定义为5个阶段:Hot 热阶段,正在更新和查询的索引Warm 暖阶段,不再更新,但仍在查询的索引Cold 冷阶段,索引不再更新,并且不经常查询。信息仍需要可搜索,但是查询比较慢也没关系。Frozen 冻结阶段,索引不再更新,很少被查询。信息仍需要搜索,但是查询非常慢也没关系。Delete 删除阶段,索引不再需要,可以安全地删除。索引的生命周期策略(Index lifecycle policies)指定适用于哪些阶段,在每个阶段执行哪些操作,以及在阶段之间转换的时间。阶段转变 phase transitionsILM 根据索引的年龄在生命周期中移动索引。为了控制这些转换的时间,可以为每个阶段设置最小年龄。要使索引进入下一阶段,当前阶段的所有操作都必须完成,并且索引的年龄必须大于下一阶段的最小年龄。规定的最低年龄必须在以后各阶段之间增加,例如,最低年龄为10天的”暖”阶段之后只能是最低年龄未设定或大于10天的”冷”阶段。索引生命周期阶段的转变主要是根据索引的年龄。阶段执行 phase executionILM 控制一个阶段中的动作执行的顺序,以及执行哪些步骤以对每个动作执行必要的索引操作。当索引进入一个阶段时,ILM 将阶段定义缓存到索引元数据中。这可以确保策略更新不会将索引置于它永远不能退出阶段的状态。如果可以安全地应用更改,ILM 将更新缓存的阶段定义。如果不能,则使用缓存的定义继续阶段执行。ILM 定期运行,检查索引是否满足策略标准,并执行所需的任何步骤。为了避免竞态条件,ILM 可能需要运行多次以执行完成操作所需的所有步骤。例如,如果 ILM 确定某个索引已经满足滚动条件,它将开始执行完成滚动动作所需的步骤。阶段行动 phase actionsILM 在每个阶段支持以下操作。Hot阶段设置优先级 Set Priority取消跟随 Unfollow滚动 Rollover只读 Read-Only分片压缩 Shrink强制合并 Force Merge可搜索快照 Searchable SnapshotWarm阶段设置优先级 Set Priority取消跟随 Unfollow只读 Read-Only分配 Allocate迁移 Migrate分片压缩 Shrink强制合并 Force MergeCold阶段设置优先级 Set Priority取消跟随 Unfollow只读 Read-Only可搜索快照 Searchable Snapshot分配 Allocate迁移 Migrate强制合并 Force MergeFrozen阶段可搜索快照 Searchable SnapshotDelete阶段等待快照 Wait For Snapshot删除 Delete总结本节主要介绍了ES中索引生命周期管理ILM的相关功能和使用场景。1、索引生命周期管理的概念以及使用场景。2、索引生命周期的5个阶段,Hot—>Warm—>Cold—>Frozen—>Delete。3、可以通过定义索引生命周期策略(Index lifecycle policies)来实现索引生命周期的管理。4、索引在每个生命周期阶段,可以执行的相关动作。
一、function_score介绍主要用于让用户自定义查询相关性得分,实现精细化控制评分的目的。在ES的常规查询中,只有参与了匹配查询的字段才会参与记录的相关性得分score的计算。但很多时候我们希望能根据搜索记录的热度、浏览量、评分高低等来计算相关性得分,提高用户体验。官网介绍:function_score哪些信息是用户真正关心的?搜索引擎本质是一个匹配过程,即从海量数据中找到匹配用户需求的内容。除了根据用户输入的查询关键字去检索外,还应根据用户的使用习惯、浏览记录、最近关注、搜索记录的热度等进行更加智能化的匹配。常见的一些场景:1、在百度、谷歌中搜索内容;2、在淘宝、京东上面搜索商品;3、在抖音上搜索用户和短视频。二、实战演示1、创建索引说明:创建博客blog索引,只有2个字段,博客名title和访问量access_num。用户根据博客名称搜索的时候,既希望名称能尽可能匹配,也希望访问量越多的排在最前面,因为一般访问量越多的博客质量会越好,这样可以提高用户的检索体验。DELETE /blog PUT /blog { "mappings": { "properties": { "title": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" }, "access_num": { "type": "integer" } } } }2、添加测试数据PUT blog/_doc/2 { "title": "java入门到精通", "access_num":30 } PUT blog/_doc/3 { "title": "es入门到精通", "access_num":50 } PUT blog/_doc/4 { "title": "mysql入门到精通", "access_num":30 } PUT blog/_doc/5 { "title": "精通spark", "access_num":40 }3、常规检索直接使用match查询,只会根据检索关键字和title字段值的相关性检索排序。GET /blog/_search { "query": { "match": { "title": "java入门" } } }查询结果L"hits" : [ { "_index" : "blog", "_type" : "_doc", "_id" : "1", "_score" : 1.3739232, "_source" : { "title" : "java入门", "access_num" : 20 } }, { "_index" : "blog", "_type" : "_doc", "_id" : "2", "_score" : 1.0552295, "_source" : { "title" : "java入门到精通", "access_num" : 30 } }, …… ]4、采用function_score自定义评分除了match匹配查询计算相关性得分,还引入了根据浏览量access_num计算得分。GET /blog/_search { "query": { "function_score": { "query": { "match": { "title": "java入门" } }, "functions": [ { "script_score": { "script": { "params": { "access_num_ratio": 2.5 }, "lang": "painless", "source": "doc['access_num'].value * params.access_num_ratio " } } } ] } } }查询结果:说明:尽管博客名为java入门的名称和搜索词更加匹配,但由于博客名为java入门到精通的博客访问量更高,最终检索品分更高,排名更靠前。"hits" : [ { "_index" : "blog", "_type" : "_doc", "_id" : "2", "_score" : 79.14222, "_source" : { "title" : "java入门到精通", "access_num" : 30 } }, { "_index" : "blog", "_type" : "_doc", "_id" : "1", "_score" : 68.69616, "_source" : { "title" : "java入门", "access_num" : 20 } },三、自定义评分类型function_score 查询提供了多种类型的评分函数。script_score script脚本评分weight 字段权重评分random_score 随机评分field_value_factor 字段值因子评分decay functions: gauss, linear, exp 衰减函数说明:decay functions衰减函数太过复杂,这里暂时不作介绍。1、script脚本评分script_score 函数允许您包装另一个查询并选择性地使用脚本表达式从文档中的其他数字字段值派生的计算自定义它的评分。 这是一个简单的示例:GET /_search { "query": { "function_score": { "query": { "match": { "message": "elasticsearch" } }, "script_score" : { "script" : { "source": "Math.log(2 + doc['likes'].value)" } } } } }请注意,与 custom_score 查询不同,查询的分数乘以脚本评分的结果。 如果你想禁止这个,设置 “boost_mode”: “replace”2、weight 权重评分GET /_search { "query": { "function_score": { "query": { "match": { "message": "elasticsearch" } }, "functions":[ { "weight":1.5 , "filter": { "term": { "description": "hadoop" }} }, { "weight":3 , "filter": { "term": { "description": "flink" }} } ] } } }weight函数是最简单的分支,它将得分乘以一个常数。请注意,普通的boost字段按照标准化来增加分数。而weight函数却真真切切地将得分乘以确定的数值。下面的例子意味着,在description字段中匹配了hadoop词条查询的文档,他们的分数将被乘以1.5.3、random_score随机评分random_score 生成从 0 到但不包括 1 的均匀分布的分数。默认情况下,它使用内部 Lucene doc id 作为随机源。如果您希望分数可重现,可以提供种子和字段。 然后将基于此种子、所考虑文档的字段最小值以及基于索引名称和分片 id 计算的盐计算最终分数,以便具有相同值但存储在不同索引中的文档得到 不同的分数。请注意,位于同一个分片内且具有相同字段值的文档将获得相同的分数,因此通常希望使用对所有文档具有唯一值的字段。 一个好的默认选择可能是使用 _seq_no 字段,其唯一的缺点是如果文档更新,分数会改变,因为更新操作也会更新 _seq_no 字段的值。GET /_search { "query": { "function_score": { "random_score": { "seed": 10, "field": "_seq_no" } } } }4、field_value_factor 字段值因子评分field_value_factor 函数允许您使用文档中的字段来影响分数。 它类似于使用 script_score 函数,但是,它避免了脚本的开销。 如果用于多值字段,则在计算中仅使用该字段的第一个值。举个例子,假设你有一个用数字 likes 字段索引的文档,并希望用这个字段影响文档的分数,一个这样做的例子看起来像:GET /_search { "query": { "function_score": { "field_value_factor": { "field": "likes", "factor": 1.2, "modifier": "sqrt", "missing": 1 } } } }得分计算公式: sqrt(1.2 * doc['likes'].value)参数说明:field要从文档中提取的字段。factor与字段值相乘的可选因子,默认为 1。modifier应用于字段值的计算修饰符, none, log, log1p, log2p, ln, ln1p, ln2p, square, sqrt, or reciprocal,默认 none.missing如果文档没有该字段,则使用的值。 修饰符和因子仍然适用于它,就好像它是从文档中读取的一样。5、Decay functions 衰减函数衰减函数使用一个函数对文档进行评分,该函数根据文档的数字字段值与用户给定原点的距离而衰减。 这类似于范围查询,但具有平滑的边缘而不是框。要对具有数字字段的查询使用距离评分,用户必须为每个字段定义原点和比例。 需要原点来定义计算距离的“中心点”,以及定义衰减率的比例尺。好吧,一脸懵逼,这里就不继续介绍了。放一个示例,大家有兴趣可以参考官方文档继续研究下。GET /_search { "query": { "function_score": { "functions": [ { "gauss": { "price": { "origin": "0", "scale": "20" } } }, { "gauss": { "location": { "origin": "11, 12", "scale": "2km" } } } ], "query": { "match": { "properties": "balcony" } }, "score_mode": "multiply" } } }四、合并得分GET /_search { "query": { "function_score": { "query": { "match_all": {} }, "boost": "5", "functions": [ { "filter": { "match": { "test": "bar" } }, "random_score": {}, "weight": 23 }, { "filter": { "match": { "test": "cat" } }, "weight": 42 } ], "max_boost": 42, "score_mode": "max", "boost_mode": "multiply", "min_score" : 42 } } }参数说明:max_boost可以通过设置 max_boost 参数将新分数限制为不超过某个限制。 max_boost 的默认值是 FLT_MAX。min_score默认情况下,修改分数不会更改匹配的文档。 要排除不满足某个分数阈值的文档,可以将 min_score 参数设置为所需的分数阈值。参数 score_mode 指定如何组合计算的分数:multiply 相乘 (default)sum 求和avg 平均分first 使用具有匹配过滤器的第一个函数的得分max 使用最高分min 使用最低分boost_mode定义新计算的分数与查询的分数相结合。 具体选项:multiply 查询得分和函数得分相乘,默认replace 仅使用函数得分,查询得分被忽略sum 查询得分和函数得分求和avg 查询得分和函数得分取平均值max 取查询得分和函数得分的最大值min 取查询得分和函数得分的最小值总结本文主要介绍了ES中自定义评分函数function_score的使用场景以及各种评分函数的用法。
前言我们都知道,在ES中一旦声明了字段名称,就不能对字段名称进行修改了。只能新增字段,不能删除、修改已经声明的mapping字段。那么,如果我们需要修改mapping中的字段名称,需要怎么操作呢?一、分析不能直接修改原索引中的mapping字段,那么只能在新索引中重命名索引字段,然后将数据导入到新索引。而ES中重建索引命令_reindex正好能很好的支持这一点。官网说明:docs-reindex-change-name二、实战1、创建索引test并插入数据POST test/_doc/1?refresh { "text": "words words", "flag": "foo" }2、通过reindex重命名字段名称说明:将原索引test中的字段flag重命名为tagPOST _reindex { "source": { "index": "test" }, "dest": { "index": "test2" }, "script": { "source": "ctx._source.tag = ctx._source.remove(\"flag\")" } }3、查看结果##根据id查看记录 GET test2/_doc/1 ## 返回结果 { "found": true, "_id": "1", "_index": "test2", "_type": "_doc", "_version": 1, "_seq_no": 44, "_primary_term": 1, "_source": { "text": "words words", "tag": "foo" } }总结本文主要介绍如何通过索引重建reindex+script脚本实现修改索引字段名称。
一、使用场景1.分片数变更:当你的数据量过大,而你的索引最初创建的分片数量不足,导致数据入库较慢的情况,此时需要扩大分片的数量,此时可以尝试使用Reindex。2. mapping字段变更:当数据的mapping需要修改,但是大量的数据已经导入到索引中了,重新导入数据到新的索引太耗时;但是在ES中,一个字段的mapping在定义并且导入数据之后是不能再修改的,所以这种情况下也可以考虑尝试使用Reindex。3. 分词规则修改,比如使用了新的分词器或者对分词器自定义词库进行了扩展,而之前保存的数据都是按照旧的分词规则保存的,这时候必须进行索引重建。二、_reindex官方说明地址:reindexES提供了_reindex这个API。相比于我们重新导入数据肯定会快不少,实测速度大概是bulk导入数据的5-10倍。reindex的核心做跨索引、跨集群的数据迁移。Reindex 不会尝试设置目标索引。 它不会复制源索引的设置。 您应该在运行 _reindex 操作之前设置目标索引,包括设置映射、分片计数、副本等。先根据复制源索引创建新的目标索引,然后执行reindex命令。基础使用命令:POST _reindex { "source": { "index": "old_index" }, "dest": { "index": "new_index" } }三、实战1、覆盖更新说明:"version_type": "internal",internal表示内部的,省略version_type或version_type设置为 internal 将导致 Elasticsearch 盲目地将文档转储到目标中,覆盖任何具有相同类型和 ID 的文件。这也是最常见的重建方式。POST _reindex { "source": { "index": "twitter" }, "dest": { "index": "new_twitter", "version_type": "internal" } }2、创建丢失的文档并更新旧版本的文档说明:"version_type": "external",external表示外部的,将 version_type 设置为 external 将导致 Elasticsearch 保留源中的版本,创建任何丢失的文档,并更新目标索引中版本比源索引中版本旧的任何文档。id不存在的文档会直接更新;id存在的文档会先判断版本号,只会更新版本号旧的文档。POST _reindex { "source": { "index": "twitter" }, "dest": { "index": "new_twitter", "version_type": "external" } }3、仅创建丢失的文档要创建的 op_type 设置将导致 _reindex 仅在目标索引中创建丢失的文档,所有存在的文档都会引起版本冲突。只要两个索引中存在id相同的记录,就会引起版本冲突。POST _reindex { "source": { "index": "twitter" }, "dest": { "index": "new_twitter", "op_type": "create" } }4、冲突处理默认情况下,版本冲突会中止 _reindex 进程。 “冲突”请求正文参数可用于指示 _reindex 继续处理有关版本冲突的下一个文档。 需要注意的是,其他错误类型的处理不受“冲突”参数的影响。当"conflicts": "proceed"在请求正文中设置时,_reindex 进程将继续处理版本冲突并返回遇到的版本冲突计数。POST _reindex { "conflicts": "proceed", "source": { "index": "twitter" }, "dest": { "index": "new_twitter", "op_type": "create" } }5、source中添加查询条件POST _reindex { "source": { "index": "twitter", "query": { "term": { "user": "kimchy" } } }, "dest": { "index": "new_twitter" } }6、source中包含多个源索引源中的索引可以是一个列表,允许您在一个请求中从多个源中复制。 这将从 twitter 和 blog 索引中复制文档:POST _reindex { "source": { "index": ["twitter", "blog"] }, "dest": { "index": "all_together" } }也支持*号来匹配多个索引。POST _reindex { "source": { "index": "twitter*" }, "dest": { "index": "all_together" } }7、限制处理的记录数通过设置size大小来限制处理文档的数量。POST _reindex { "size": 10000, "source": { "index": "twitter", "sort": { "date": "desc" } }, "dest": { "index": "new_twitter" } }8、从远程ES集群中重建索引POST _reindex { "source": { "remote": { "host": "http://otherhost:9200", "username": "user", "password": "pass", "socket_timeout": "1m", "connect_timeout": "10s" }, "index": "source", "query": { "match": { "test": "data" } } }, "dest": { "index": "dest" } }9、提取随机子集说明:从源索引中随机取10条数据到新索引中。POST _reindex { "size": 10, "source": { "index": "twitter", "query": { "function_score" : { "query" : { "match_all": {} }, "random_score" : {} } }, "sort": "_score" }, "dest": { "index": "random_twitter" } }10、修改字段名称原索引POST test/_doc/1?refresh { "text": "words words", "flag": "foo" }重建索引,将原索引中的flag字段重命名为tag字段。POST _reindex { "source": { "index": "test" }, "dest": { "index": "test2" }, "script": { "source": "ctx._source.tag = ctx._source.remove(\"flag\")" } }结果:GET test2/_doc/1 { "found": true, "_id": "1", "_index": "test2", "_type": "_doc", "_version": 1, "_seq_no": 44, "_primary_term": 1, "_source": { "text": "words words", "tag": "foo" } }四、性能优化常规的如果我们只是进行少量的数据迁移利用普通的reindex就可以很好的达到要求,但是当我们发现我们需要迁移的数据量过大时,我们会发现reindex的速度会变得很慢。数据量几十个G的场景下,elasticsearch reindex速度太慢,从旧索引导数据到新索引,当前最佳方案是什么?原因分析:reindex的核心做跨索引、跨集群的数据迁移。慢的原因及优化思路无非包括:1)批量大小值可能太小。需要结合堆内存、线程池调整大小;2)reindex的底层是scroll实现,借助scroll并行优化方式,提升效率;3)跨索引、跨集群的核心是写入数据,考虑写入优化角度提升效率。可行方案:1)提升批量写入的大小值size2)通过设置sliced提高写入的并行度1、提升批量写入大小值默认情况下 _reindex 使用 1000 的滚动批次。可以使用源元素source中的 size 字段更改批次大小:POST _reindex { "source": { "index": "source", "size": 5000 }, "dest": { "index": "dest" } }2、提高scroll的并行度Reindex 支持 Sliced Scroll 来并行化重新索引过程。 这种并行化可以提高效率并提供一种将请求分解为更小的部分的便捷方式。每个Scroll请求,可以分成多个Slice请求,可以理解为切片,各Slice独立并行,利用Scroll重建或者遍历要快很多倍。slicing的设定分为两种方式:手动设置分片、自动设置分片。自动设置分片如下:POST _reindex?slices=5&refresh { "source": { "index": "twitter" }, "dest": { "index": "new_twitter" } }slices大小设置注意事项:1)slices大小的设置可以手动指定,或者设置slices设置为auto,auto的含义是:针对单索引,slices大小=分片数;针对多索引,slices=分片的最小值。2)当slices的数量等于索引中的分片数量时,查询性能最高效。slices大小大于分片数,非但不会提升效率,反而会增加开销。3)如果这个slices数字很大(例如500),建议选择一个较低的数字,因为过大的slices 会影响性能。效果实践证明,比默认设置reindex速度能提升10倍+。五、超时问题es中的请求超时时间默认是1分钟,当重建索引的数据量太大时,经常会出现超时。这种情况可以增大超时时间,也可以添加wait_for_completion=false参数将请求转为异步任务。POST _reindex?slices=9&refresh&wait_for_completion=false { "source": { "index": "twitter" }, "dest": { "index": "new_twitter" } }1、获取reindex任务列表GET _tasks?detailed=true&actions=*reindex2、根据任务id查看任务GET /_tasks/r1A2WoRbTwKZ516z6NEs5A:366193、取消任务POST _tasks/r1A2WoRbTwKZ516z6NEs5A:36619/_cancel总结本文主要介绍了ES索引重建的常见使用场景以及典型的使用方法,并说明了相关性能优化的技巧和请求超时问题的处理方法。
一、场景说明索引中有几千万的数据,现在需要每次查询随机抽样返回10条数据,怎么实现?二、实现方式DSL语句执行如下:GET myIndex/_search { "from": 0, "size": 20, "timeout": "10s", "sort": { "_script": { "script": "Math.random()", "type": "number", "order": "asc" } } }java代码实现:private void randomSort(SearchSourceBuilder sourceBuilder){ Script script = new Script("Math.random()"); ScriptSortBuilder sortBuilder = new ScriptSortBuilder(script, ScriptSortBuilder.ScriptSortType.NUMBER); sourceBuilder.sort(sortBuilder); }三、注意事项实际开发过程中发现,如果对索引中的全量数据进行随机抽样查询是非常消耗查询性能的。我遇到的情况:生产环境上,8千万多万数据的索引进行随机抽样查询耗时5s,这种查询速度显然是不能接受的。优化改进:sort排序是针对匹配的所有数据进行排序,而8000多万数据的随机排序,显然非常耗时。我们可以在查询条件中增加一些随机查询条件,比如主键id的随机前缀匹配,数据产生时间的随机范围匹配,从而减轻随机匹配的性能损耗。由于我的索引数据中id的前缀都是在0~9,所以我在每次查询时,先生成0~9的随机数,然后去匹配主键id做前缀匹配。{ "query": { "wildcard": { "id": { "value": "1*" } } } ,"sort":[{"_script":{"script":{"source":"Math.random()","lang":"painless"},"type":"number","order":"asc"}}] }可以发现,每次随机查询匹配的数据量total的直显著下降,消耗的查询时间took也明显降低,完全能满足生产要求。总结本文主要介绍了ES中如何实现随机抽样查询,并强调了随机抽样查询的性能损耗问题以及对应的解决方案。
一、parallelStream说明Java 8引入了流的概念去对数据进行复杂的操作,而且使用并行流(Parallel Steams)支持并发,大大加快了运行效率。parallelStream默认使用了fork-join框架,其默认线程数是CPU核心数。二、parallelStream默认的并发数@Test public void testParallelism1() throws ExecutionException, InterruptedException { int cupNum = Runtime.getRuntime().availableProcessors(); log.info("CPU num:{}",cupNum); long firstNum = 1; long lastNum = 10000; List<Long> aList = LongStream.rangeClosed(firstNum, lastNum).boxed() .collect(Collectors.toList()); aList.parallelStream().forEach(e->{ log.info("输出:{}",e); }); }执行结果:说明:可以发现有8个线程参与任务执行,分别是main主线程、ForkJoinPool.commonPool-worker-1 到ForkJoinPool.commonPool-worker-7,正好与CPU的核心数8匹配。默认情况下,parallelStream使用的是ForkJoinPool.commonPool(),这是一个公用的线程池,被整个程序所使用。可以通过以下方法修改parallelStream默认的多线程数量:三、设置ForkJoinPool.commonPool()的并发数设置parallelStream默认的公用线程池的全局并发数:System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "4");实战:@Test public void testParallelism3() throws ExecutionException, InterruptedException { int cupNum = Runtime.getRuntime().availableProcessors(); log.info("CPU num:{}",cupNum); long firstNum = 1; long lastNum = 10000; List<Long> aList = LongStream.rangeClosed(firstNum, lastNum).boxed() .collect(Collectors.toList()); System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "4"); aList.parallelStream().forEach(e->{ log.info("输出:{}",e); }); }执行结果:说明:执行结果中出现ForkJoinPool.commonPool-worker-0到ForkJoinPool.commonPool-worker-3,说明公共线程池的并发数配置的并发数4确实生效了。此时任务的并发数是5, mian主线程 + ForkJoinPool.commonPool(0~4)推荐:由于主线程也会参与任务抢占CPU,所以ForkJoinPool.commonPool的线程数尽量设置为(CPU核心数*N - 1)四、通过ForkJoinPool定义私有线程池@Test public void testParallelism4() { int cupNum = Runtime.getRuntime().availableProcessors(); log.info("CPU num:{}",cupNum); long firstNum = 1; long lastNum = 10000; List<Long> aList = LongStream.rangeClosed(firstNum, lastNum).boxed() .collect(Collectors.toList()); ForkJoinPool forkJoinPool = new ForkJoinPool(8); try{ List<Long> longs = forkJoinPool.submit(() -> aList.parallelStream().map(e->{ return e+1; }).collect(Collectors.toList())).get(); //通过调用get方法,等待任务执行完毕 System.out.println(longs.size()); System.out.println("执行结束"); }catch (InterruptedException e) { e.printStackTrace(); } catch (Exception e){ e.printStackTrace(); }finally { forkJoinPool.shutdown(); } }执行结果:###|||2021-08-27 15:03:31.080|||INFO|||-|||-|||main|||SimpleTest--->CPU num:8 10000 执行结束说明:采用自定义的forkJoinPool线程池去提交任务,主线程不会参与计算。forkJoinPool线程池采用submit异步提交任务,通过get方法阻塞主线程,直到任务执行完成,再调用shutdown方法关闭线程池。注意,等待提交任务执行完毕不能采用awaitTermination()方法,该方法是等待指定时间后强制关闭线程池。五、ForkJoinPool的错误使用错误一:通过forkJoinPool线程池结合parallelStream.forEach并发提交任务,get方法不能起到阻塞主线程、等待任务执行完毕的作用。@Test public void testParallelism5() { long firstNum = 1; long lastNum = 1000; List<Long> aList = LongStream.rangeClosed(firstNum, lastNum).boxed() .collect(Collectors.toList()); ForkJoinPool forkJoinPool = new ForkJoinPool(8); try{ ForkJoinTask future = forkJoinPool.submit(() -> aList.parallelStream().forEach(e->{ log.info("输出:{}",e); })); //通过调用get方法,等待任务执行完毕 future.get(10, TimeUnit.MINUTES); //这里不能使用log打印日志 //log.info("执行结束"); System.out.println("执行结束"); }catch (InterruptedException e) { e.printStackTrace(); } catch (Exception e){ e.printStackTrace(); }finally { forkJoinPool.shutdown(); } }执行结果:ForkJoinPool线程池中的任务没有执行完成,主线程中就打印执行结束。get方法没有起到阻塞任务、等待任务执行完成的作用。错误二:通过forkJoinPool线程池的awaitTermination()方法是等待指定时间后关闭线程池,而不是等待任务结束后关闭线程池。@Test public void testParallelism5() { long firstNum = 1; long lastNum = 1000; List<Long> aList = LongStream.rangeClosed(firstNum, lastNum).boxed() .collect(Collectors.toList()); ForkJoinPool forkJoinPool = new ForkJoinPool(8); try{ ForkJoinTask future = forkJoinPool.submit(() -> aList.parallelStream().forEach(e->{ log.info("输出:{}",e); })); long startTime = System.currentTimeMillis(); forkJoinPool.awaitTermination(20,TimeUnit.SECONDS); long totalTime = (System.currentTimeMillis() - startTime)/1000; System.out.println("耗时:"+totalTime); System.out.println("执行结束"); }catch (InterruptedException e) { e.printStackTrace(); } catch (Exception e){ e.printStackTrace(); }finally { forkJoinPool.shutdown(); } }执行结果:任务几秒就执行完毕了,但是却等待了20秒才继续往下执行。显然用awaitTermination来等待任务执行完成是不合适的。小结:采用get方法阻塞主线程,等待ForkJoinPool线程池中的任务执行完毕,适合聚合运算有结果返回的情况,不适合forEach这样的遍历操作。六、执行效率对比@Test public void testParallelism2() throws ExecutionException, InterruptedException { long firstNum = 1; long lastNum = 1_000_000; List<Long> aList = LongStream.rangeClosed(firstNum, lastNum).boxed() .collect(Collectors.toList()); //4 055755750 //8 091087375 //20 250872750 ForkJoinPool customThreadPool = new ForkJoinPool(8); StopWatch stopWatch = new StopWatch("task"); stopWatch.start("parallelStream"); long actualTotal = customThreadPool.submit( () -> aList.parallelStream().reduce(0L, Long::sum)).get(); stopWatch.stop(); log.info("result:{}",actualTotal); log.info("耗时统计:\n" +stopWatch.prettyPrint()); }执行结果:并发数耗时4055755750809108737520250872750说明:针对这类高密度的CPU计算任务,提高线程池的并发数,反而会降低任务的执行效率,因为CPU抢占和大量线程频繁切换会增加任务的耗时。总结本文主要介绍了java8中如何修改parallelStream的默认并发数。主要有以下两种方式:1、设置ForkJoinPool.commonPool公共池的全局并发数。2、自定义ForkJoinPool线程池指定并发数。然后分析了ForkJoinPool并发任务如何阻塞等待任务执行完毕。最后通过一个简单的测试,说明了针对CPU密集型计算任务,线程池的并发数越大,任务执行效率反而更低。
一、场景说明我们在使用ES进行查询时常常遇到这样的场景:需要根据用户输入的查询关键字同时去匹配多个字段,并且希望对匹配字段的权重做不同的设置,比如同时去匹配公司名称和公司简介,这里一般需要提升公司名称匹配的权重,这样得出的相关性评分才会更准确。在ES中,我们可以通过boost参数来控制多字段查询的权重。二、权重参数boost官网链接boost是一个用来修改文档的相关性的参数,默认值是1。可以通过设置不同的值,提升该字段在相关性评分的权重。boost有两种类型:索引期间boost查询期间boost创建索引时指定的boost参数是存储在索引中的,修改boost值唯一的方法是重新索引这篇文档。鉴于此,推荐用户使用查询期间的boost,这样会更加灵活,用户可以在不重新索引数据的前提下改变字段的权 重。1、索引期间boost创建索引说明:在创建索引的过程中,在mappings中给字段设置boost的值为3,提升company的相关性权重。PUT my-index-000001 { "mappings": { "properties": { "company": { "type": "text", "analyzer": "ik_smart", "boost":"3" }, "desc": { "type": "text", "analyzer": "ik_smart" } } } }添加数据:PUT my-index-000001/_doc/1 { "company": "北京京东世纪股份有限公司" } PUT my-index-000001/_doc/2 { "desc": "北京京东世纪股份有限公司" }通过multi_match进行多字段匹配查询:GET my-index-000001/_search { "query": { "multi_match": { "query": "京东", "fields": ["company","desc"] } } }执行结果:"hits" : [ { "_index" : "my-index-000001", "_type" : "_doc", "_id" : "1", "_score" : 0.68324494, "_source" : { "company" : "北京京东世纪股份有限公司" } }, { "_index" : "my-index-000001", "_type" : "_doc", "_id" : "2", "_score" : 0.2876821, "_source" : { "desc" : "北京京东世纪股份有限公司" } } ]结论:对相同的字段串进行匹配,由于company的权重boost是desc的3倍,最后的相关性得分也高接近3倍。。2、查询期间boostDELETE my-index-000001 PUT my-index-000001 { "mappings": { "properties": { "company": { "type": "text", "analyzer": "ik_smart" }, "desc": { "type": "text", "analyzer": "ik_smart" } } } } PUT my-index-000001/_doc/1 { "company": "北京京东世纪股份有限公司" } PUT my-index-000001/_doc/2 { "desc": "北京京东世纪股份有限公司" }正常查询,2条记录的得分一样:GET my-index-000001/_search { "query": { "multi_match": { "query": "京东", "fields": ["company","desc"] } } }提高company字段的权重:GET my-index-000001/_search { "query": { "multi_match": { "query": "京东", "fields": ["company^3","desc"] } } }说明:通过字段名称后面添加“^”符号和boost的值,提升指定字段的评分权重。三、ES java API中权重控制 Map<String,Float> fields = new HashMap(2); fields.put("company", 3.0f); fields.put("desc", 1.0f); queryBuilder.must(QueryBuilders.multiMatchQuery(paramsDto.getKeyword()).fields(fields).analyzer("ik_smart"));说明:通过封装fields对象,指定需要匹配的字段和字段的权重boost。总结看完本文,你学会了如何通过boost参考提高多字段匹配时的评分权重了吗?1、权重参数boost的2种用法,创建索引时指定字段的权重以及查询时指定字段权重。2、在ES java API查询中,如何指定字段的权重参数boost。
前言我们都知道ES是一款近实时的搜索引擎产品。那么为什么是近实时而不是实时呢?为什么新添加的数据开始查询不到,后来又可以检索到?有哪些办法能够提高ES的实时性呢?今天让我们一起来探究ES查询的实时性问题。一、实战演示1、新建索引在settings中通过refresh_interval参数指定索引每60s刷新一次。PUT my-index-000001 { "mappings": { "properties": { "city": { "type": "keyword" } } }, "settings": {"refresh_interval": "60s"} }2、添加数据PUT my-index-000001/_doc/1 { "city": "北京" } PUT my-index-000001/_doc/2 { "city": "天津" } PUT my-index-000001/_doc/3 { "city": "武汉" } POST my-index-000001/_doc/4?refresh=true { "city": "成都" }3、GET API通过索引id查询记录GET my-index-000001/_doc/1执行结果:{ "_index" : "my-index-000001", "_type" : "_doc", "_id" : "1", "_version" : 1, "_seq_no" : 0, "_primary_term" : 1, "found" : true, "_source" : { "city" : "北京" } }说明新增的数据立刻被检索到,说明GET API是实时的。GET API主要包含以下请求:GET <index>/_doc/<_id> HEAD <index>/_doc/<_id> GET <index>/_source/<_id> HEAD <index>/_source/<_id>默认情况下,Get API 是实时的,不受索引刷新率的影响(当数据对搜索可见时)。 如果请求存储的字段(请参阅 stored_fields 参数)并且文档已更新但尚未刷新,则 get API 将必须解析和分析源以提取存储的字段。 为了禁用实时 GET,可以将 realtime 参数设置为 false。GET my-index-000001/_doc/1?realtime=false4、query查询GET my-index-000001/_search { "query": { "bool": { "filter": [ {"term": { "city": "北京" }} ] } } } ## 新增数据并刷新 POST my-index-000001/_doc/4?refresh=true { "city": "成都" } GET my-index-000001/_search { "query": { "bool": { "filter": [ {"term": { "city": "成都" }} ] } } }执行结果:在新增北京记录操作的60s内执行查询操作,没有查询到新增的记录,因为还没有执行refresh动作。而新增成都记录时,由于添加了refresh=true参数,会在执行添加操作后立刻执行refresh动作,所以可以根据条件即时查询到成都的记录。二、ES写入过程分析ES写入过程说明:1.不断将 Document 写入到 In-memory buffer (内存缓冲区)。2.当满足一定条件后内存缓冲区中的 Documents刷新到 高速缓存(cache)。3.生成新的 segment ,这个 segment 还在 cache 中。 (在cache中生成的segment就可以被检索到了)4.这时候还没有 commit,但是已经可以被读取了。5.translog 事务日志主要用来失败恢复,防止服务器宕机出现内存中没有刷写到磁盘的数据丢失。数据从 buffer 到 cache 的过程是定期每秒刷新一次。所以新写入的 Document 最慢 1 秒就可以在 cache 中被搜索到。而 Document 从 buffer 到 cache 的过程叫做 refresh ,默认是 1 秒刷新一次。这也就是为什么说 Elasticsearch 是准实时的。Elasticsearch通过引入translog,多副本,以及定期执行flush,merge等操作保证了数据可靠性和较高的存储性能。document同时写入In-memory buffer (内存缓冲区)和translog的过程类似mysql中的double write,主要目的也是失败恢复,防止数据丢失。translog是顺序追加写入。使文档立即可见:PUT /test/_doc/1?refresh {"test": "test"} // 或者 PUT /test/_doc/2?refresh=true {"test": "test"}三、Get API的实时性简单来说,就是通过get请求直接根据id获取索引记录,是实时操作。#默认情况下,get API是实时的 GET my-index-000001/_doc/0 #通过realtime=false设置请求为非实时 GET my-index-000001/_doc/0?realtime=false(注: 如果是realtime=true, 则先从translog中读取source, 没有读取到才从索引中读取)四、java API中实时性在ES 的java API中,如何控制数据写入操作后,即时刷新,使得新增的数据立即可见呢?答案是通过刷新策略WriteRequest.RefreshPolicy。下面的代码演示,采用highLevelClient.bulk实现批量插入数据中,通过bulkRequest.setRefreshPolicy指定刷新策略为即使刷新。public boolean bulk(String indice, List<String> jsonStrList) { boolean result = true; try { BulkRequest bulkRequest = new BulkRequest(); //设置刷新策略 bulkRequest.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); for (String jsonStr : jsonStrList) { IndexRequest indexRequest = new IndexRequest(indice,"_doc"); indexRequest.source(jsonStr, XContentType.JSON); bulkRequest.add(indexRequest); } BulkResponse bulkResponse = highLevelClient.bulk(bulkRequest); if (bulkResponse.hasFailures()) { result = false; } } catch (Exception e) { result = false; } return result; }查看源码发现:public static enum RefreshPolicy implements Writeable { NONE("false"), IMMEDIATE("true"), WAIT_UNTIL("wait_for"); …… }可知有以下三种刷新策略:1.RefreshPolicy#IMMEDIATE:请求向ElasticSearch提交了数据,立即进行数据刷新,然后再结束请求。优点:实时性高、操作延时短。缺点:资源消耗高。2.RefreshPolicy#WAIT_UNTIL:请求向ElasticSearch提交了数据,等待数据完成刷新,然后再结束请求。优点:实时性高、操作延时长。缺点:资源消耗低。3.RefreshPolicy#NONE:默认策略。请求向ElasticSearch提交了数据,不关系数据是否已经完成刷新,直接结束请求。优点:操作延时短、资源消耗低。缺点:实时性低。总结文本主要对ES数据写入后查询的实时性问题进行了分析。1、通过分析ES的写入流程,说明了ES查询数据为什么会产生延迟,即为什么说ES是准实时的2、说明了Get API默认是实时的原因,主要是Get API默认情况下优先读取的是translog中的数据。3、可以通过设置refresh_interval参数,缩短索引refresh的间隔时间,增大实时性。4、可以通过写入操作后添加refresh参数,让写入的数据被即时检索到。5、介绍了ES java API中通过设置写入操作的刷新策略RefreshPolicy,改变写入数据的实时性。
前言ES如果采用单节点部署,不用考虑什么节点角色,默认就好。但是在大规模的ES集群中,一定要根据服务器配置,数据冷热,并发情况等合理配置节点的角色,才能让ES集群节点更好的协调合作,对外提供稳定的服务。一、ES节点有哪些角色?ES节点有如下角色:master 主节点data 数据节点data_content 内容数据节点data_hot 热点数据节点data_warm 暖数据节点data_cold 冷数据节点data_frozen 冻结数据节点ingest 摄取节点ml 机器学习节点remote_cluster_client 远程集群客户端节点transform 转换节点voting_only 仅投票节点coordinating 仅协调节点注意⚠️:1、如果你设置了 node.roles,则节点只会分配你指定的角色。 如果不设置 node.roles,节点将被分配以上所有角色(除了voting_only 和coordinating角色例外)。2、节点不等同于服务器,一个服务器上可以部署多个节点。3、启动一个Elasticsearch 实例时,都在启动一个节点。 连接节点的集合称为集群。4、集群中的每个节点都可以处理 HTTP 和传输流量。 传输层专门用于节点之间的通信; HTTP 层由 REST 客户端使用。5、所有节点都知道集群中的所有其他节点,并且可以将客户端请求转发到适当的节点。二、怎么设置节点的角色只需要在elasticsearch.yml通过node.roles属性设置即可可以同时指定多个角色node.roles: [ data, master, voting_only ]注意⚠️:一个ES集群中,必须有以下角色:masterdata_content and data_hotORdata三、节点角色介绍1、master 主节点功能说明:主节点负责轻量级集群范围的操作,例如创建或删除索引、跟踪哪些节点是集群的一部分以及决定将哪些分片分配给哪些节点。任何不是仅投票节点的主合格节点都可以通过主选举过程选举成为主节点。主节点必须有一个path.data目录,其内容在重启后仍然存在,就像数据节点一样,因为这是存储集群元数据的地方。集群元数据描述了如何读取存储在数据节点上的数据,因此如果丢失,则无法读取存储在数据节点上的数据。如果小型或轻负载集群的主节点具有其他角色和职责,则其可能运行良好,但是一旦您的集群包含多个节点,使用专用的主节点通常是有意义的。角色配置:要创建一个专用的主节点,请设置:node.roles: [ master ]2、voting_only 仅投票节点功能说明:只能参与主节点的投票选举环节,但是自己不能被选举为master。高可用性 (HA) 集群需要至少三个符合主节点的节点,其中至少两个不是仅投票节点。这样即使其中一个节点发生故障,集群也能够选举出一个主节点。所有符合主节点的节点,包括仅投票节点,都需要相当快的持久存储以及与集群其余部分的可靠且低延迟的网络连接,因为它们处于发布集群状态更新的关键路径上 。角色配置:要创建仅投票节点,请设置:node.roles: [ master, voting_only ]即是数据节点,也是仅投票节点。node.roles: [ data, master, voting_only ]注意⚠️:只有具有master角色的节点才能被标记为具有 voting_only角色。3、data 数据节点功能说明:数据节点保存包含已编入索引的文档的分片。数据节点处理数据相关操作,如 CRUD、搜索和聚合。这些操作是 I/O 密集型、内存密集型和 CPU 密集型的。监控这些资源并在它们过载时添加更多数据节点非常重要。拥有专用数据节点的主要好处是主角色和数据角色的分离。在多层部署架构,您可以使用专门的数据角色分配数据节点到指定等级:data_content,data_hot,data_warm, data_cold,或data_frozen。一个节点可以属于多个层,但具有其中一个专用数据角色的节点不能具有通用data角色。作用:1、保存索引数据2、处理数据相关操作,如 CRUD、搜索和聚合。角色配置:要创建专用数据节点,请设置:node.roles: [ data ]4、data_content 内容数据节点内容数据节点容纳用户创建的内容。它们支持 CRUD、搜索和聚合等操作。要创建专用内容节点,请设置:node.roles: [ data_content ]5、data_hot 热点数据节点热数据节点在进入 Elasticsearch 时存储时间序列数据。热层必须快速读取和写入,并且需要更多的硬件资源(例如 SSD 驱动器)。要创建专用热节点,请设置:node.roles: [ data_hot ]6、data_warm 暖数据节点暖数据节点存储不再定期更新但仍在查询的索引。查询量的频率通常低于索引处于热层时的频率。性能较低的硬件通常可用于此层中的节点。要创建专用的暖节点,请设置:node.roles: [ data_warm]7、data_cold 冷数据节点冷数据节点存储访问频率较低的只读索引。此层使用性能较低的硬件,并且可以利用可搜索的快照索引来最小化所需的资源。要创建专用冷节点,请设置:node.roles: [ data_cold]8、data_frozen 冻结数据节点冻结层 专门存储部分安装的索引。我们建议您在冻结层中使用专用节点。要创建专用的冻结节点,请设置:node.roles: [ data_frozen]9、ingest 摄取节点摄取节点可以执行由一个或多个摄取处理器组成的预处理管道。根据摄取处理器执行的操作类型和所需资源,拥有仅执行此特定任务的专用摄取节点可能是有意义的。要创建专用摄取节点,请设置:node.roles: [ingest]10、coordinating 仅协调节点如果您取消了处理主职责、保存数据和预处理文档的能力,那么您就剩下一个只能路由请求、处理搜索减少阶段和分发批量索引的协调节点。本质上,仅协调节点的行为就像智能负载均衡器。通过从数据和符合主节点的节点卸载协调节点角色,仅协调节点可以使大型集群受益。他们加入集群并接收完整的集群状态,就像其他每个节点一样,他们使用集群状态将请求直接路由到适当的地方。要创建专用协调节点,请设置:node.roles:[ ]11、remote 远程集群客户端节点远程集群客户端节点充当跨集群客户端并连接到 远程集群。连接后,您可以使用跨集群搜索来搜索远程集群。您还可以使用跨集群复制在集群之间同步数据。node.roles:[ remote_cluster_client ]12、ml 机器学习节点机器学习节点运行作业并处理机器学习 API 请求。要创建专用机器学习节点,请设置:node.roles:[ml,remote_cluster_client]注意⚠️:一般开启ml角色的节点,推荐同时开启remote_cluster_client角色。13、transform 转换节点转换节点运行转换并处理转换 API 请求。要创建专用变换节点,请设置:node.roles: [ transform, remote_cluster_client ]注意⚠️:一般开启transform角色的节点,推荐同时开启remote_cluster_client角色。小结:重点理解master节点和data节点即可。master节点负责轻量级集群范围的操作,比如创建或删除索引,跟踪集群中节点位置以及分片分配。data节点负责存储数据,并处理数据相关操作,如 CRUD、搜索和聚合。四、改变节点角色存储数据说明1、data数据节点分配给该节点的每个分片的分片数据分配给该节点的每个分片对应的索引元数据集群范围的元数据,例如设置和索引模板2、master主节点集群中每个索引的索引元数据集群范围的元数据,例如设置和索引模板数据检查机制每个节点在启动时会检查其数据路径的内容。如果它发现意外数据,它将拒绝启动。这是为了避免导入可能导致红色集群运行状况的不需要的悬空索引。更准确地说,没有data角色的节点在启动时如果在磁盘上找到任何分片数据将拒绝启动,而没有角色master和data角色的节点如果在启动时在磁盘上有任何索引元数据将拒绝启动。更改节点角色可以通过调整其elasticsearch.yml文件并重新启动它来更改节点的角色 。这称为重新调整节点的用途。为了满足上述对意外数据的检查,您必须执行一些额外的步骤来准备节点,以便在没有data或master角色的情况下启动节点时重新调整用途。如果您想通过删除data角色来重新调整数据节点的用途,那么您应该首先使用分配过滤器将所有分片数据安全地迁移到集群中的其他节点上。如果您想重新调整节点的用途,使其既没有data也没有master角色,那么最简单的方法是启动一个带有空数据路径和所需角色的全新节点。您可能会发现首先使用分配过滤器将分片数据迁移到集群中的其他位置是最安全的 。如果无法执行这些额外步骤,那么您可以使用该elasticsearch-node repurpose工具删除任何阻止节点启动的多余数据。五、节点数据路径设置每个数据和主节点都需要配置数据存储目录,其中存储分片、索引和集群元数据的path.data默认为$ES_HOME/data,用户可以在elasticsearch.yml中自己配置。配置文件中指定:path.data: /var/elasticsearch/data启动命令中指定:./bin/elasticsearch -Epath.data=/var/elasticsearch/data说明:更推荐在配置文件中指定的方式。总结本节主要是对ES集群中的节点角色进行了说明。1、一个可用的ES集群中,必须具备master节点和data节点。2、详细说明了各个节点角色的作用以及配置方法3、介绍了master节点和data节点负责的功能:master节点负责轻量级集群范围的操作,比如创建或删除索引,跟踪集群中节点位置以及分片分配。data节点负责存储数据,并处理数据相关操作,如 CRUD、搜索和聚合。
一、ES支持的三种分页查询方式From + Size 查询Scroll 遍历查询Search After 查询说明:官方已经不再推荐采用Scroll API进行深度分页。如果遇到超过10000的深度分页,推荐采用search_after + PIT。官方文档地址二、分布式系统中的深度分页问题为什么分布式存储系统中对深度分页支持都不怎么友好呢?首先我们看一下分布式存储系统中分页查询的过程。假设在一个有 4 个主分片的索引中搜索,每页返回10条记录。当我们请求结果的第1页(结果从 1 到 10 ),每一个分片产生前 10 的结果,并且返回给 协调节点 ,协调节点对 40 个结果排序得到全部结果的前 10 个。当我们请求第 99 页(结果从 990 到 1000),需要从每个分片中获取满足查询条件的前1000个结果,返回给协调节点, 然后协调节点对全部 4000 个结果排序,获取前10个记录。当请求第10000页,每页10条记录,则需要先从每个分片中获取满足查询条件的前100010个结果,返回给协调节点。然后协调节点需要对全部(100010 * 分片数4)的结果进行排序,然后返回前10个记录。可以看到,在分布式系统中,对结果排序的成本随分页的深度成指数上升。这就是 web 搜索引擎对任何查询都不要返回超过 10000 个结果的原因。三、From + Size 查询1、准备数据PUT user_index { "mappings": { "properties": { "id": {"type": "integer"}, "name": {"type": "keyword"} } } } POST user_index/_bulk { "create": { "_id": "1" }} { "id":1,"name":"老万"} { "create": { "_id": "2" }} { "id":2,"name":"老王"} { "create": { "_id": "3" }} { "id":3,"name":"老刘"} { "create": { "_id": "4" }} { "id":4,"name":"小明"} { "create": { "_id": "5" }} { "id":5,"name":"小红"}2、查询演示无条件查询POST user_index/_search默认返回前10个匹配的匹配项。其中:from:未指定,默认值是 0,注意不是1,代表当前页返回数据的起始值。size:未指定,默认值是 10,代表当前页返回数据的条数。指定from+size查询POST user_index/_search { "from": 0, "size": 10, "query": { "match_all": {} }, "sort": [ {"id": "asc"} ] }3、max_result_windowes 默认采用的分页方式是 from+ size 的形式,在深度分页的情况下,这种使用方式效率是非常低的。比如from = 5000, size=10, es 需要在各个分片上匹配排序并得到5000*10条有效数据,然后在结果集中取最后10条数据返回,这种方式类似于mongo的 skip + size。除了效率上的问题,还有一个无法解决的问题是,es 目前支持最大的 skip 值是 max_result_window ,默认为 10000 。也就是当 from + size > max_result_window 时,es 将返回错误。POST user_index/_search { "from": 10000, "size": 10, "query": { "match_all": {} }, "sort": [ {"id": "asc"} ] }这是ElasticSearch最简单的分页查询,但以上命令是会报错的。报错信息,指window默认是10000。"root_cause": [ { "type": "illegal_argument_exception", "reason": "Result window is too large, from + size must be less than or equal to: [10000] but was [10001]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting." } ], "type": "search_phase_execution_exception",怎么解决这个问题,首先能想到的就是调大这个window。PUT user_index/_settings { "index" : { "max_result_window" : 20000 } }然后这种方式只能暂时解决问题,当es 的使用越来越多,数据量越来越大,深度分页的场景越来越复杂时,如何解决这种问题呢?官方建议:避免过度使用 from 和 size 来分页或一次请求太多结果。不推荐使用 from + size 做深度分页查询的核心原因:搜索请求通常跨越多个分片,每个分片必须将其请求的命中内容以及任何先前页面的命中内容加载到内存中。对于翻页较深的页面或大量结果,这些操作会显著增加内存和 CPU 使用率,从而导致性能下降或节点故障。四、Search After 查询search_after 参数使用上一页中的一组排序值来检索下一页的数据。使用 search_after 需要具有相同查询和排序值的多个搜索请求。 如果在这些请求之间发生刷新,结果的顺序可能会发生变化,从而导致跨页面的结果不一致。 为防止出现这种情况,您可以创建一个时间点 (PIT) 以保留搜索中的当前索引状态。时间点 Point In Time(PIT)保障搜索过程中保留特定事件点的索引状态。注意⚠️:es 给出了 search_after 的方式,这是在 >= 5.0 版本才提供的功能。Point In Time(PIT)是 Elasticsearch 7.10 版本之后才有的新特性。PIT的本质:存储索引数据状态的轻量级视图。如下示例能很好的解读 PIT 视图的内涵。#1、给索引user_index创建pit POST /user_index/_pit?keep_alive=5m #2、统计当前记录数 5 POST /user_index/_count #3、根据pit统计当前记录数 5 GET /_search { "query": { "match_all": {} }, "pit": { "id": "i6-xAwEKdXNlcl9pbmRleBZYTXdtSFRHeVJrZVhCby1OTjlHMS1nABZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3AAAAAAAAIODBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EBFlhNd21IVEd5UmtlWEJvLU5OOUcxLWcAAA==", "keep_alive": "5m" }, "sort": [ {"id": "asc"} ] } #4、插入一条数据 POST user_index/_bulk { "create": { "_id": "6" }} { "id":6,"name":"老李"} #5、数据总量 6 POST /user_index/_count #6、根据pit统计数据总量还是 5 ,说明是根据时间点的视图进行统计。 GET /_search { "query": { "match_all": {} }, "pit": { "id": "i6-xAwEKdXNlcl9pbmRleBZYTXdtSFRHeVJrZVhCby1OTjlHMS1nABZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3AAAAAAAAIODBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EBFlhNd21IVEd5UmtlWEJvLU5OOUcxLWcAAA==", "keep_alive": "5m" }, "sort": [ {"id": "asc"} ] }有了 PIT,search_after 的后续查询都是基于 PIT 视图进行,能有效保障数据的一致性。search_after 分页查询可以简单概括为如下几个步骤。1、获取索引的pitPOST /user_index/_pit?keep_alive=5m2、根据pit首次查询说明:根据pit查询的时候,不用指定索引名称。GET /_search { "query": { "match_all": {} }, "pit": { "id": "i6-xAwEKdXNlcl9pbmRleBZYTXdtSFRHeVJrZVhCby1OTjlHMS1nABZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3AAAAAAAAIODBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EBFlhNd21IVEd5UmtlWEJvLU5OOUcxLWcAAA==", "keep_alive": "1m" }, "sort": [ {"id": "asc"} ] }查询结果:返回的sort值为2.hits" : [ { "_index" : "user_index", "_type" : "_doc", "_id" : "2", "_score" : null, "_source" : { "id" : 2, "name" : "老王" }, "sort" : [ 2 ] } ]3、根据search_after和pit进行翻页查询说明:search_after指定为上一次查询返回的sort值。要获得下一页结果,请使用最后一次命中的排序值(包括 tiebreaker)作为 search_after 参数重新运行先前的搜索。 如果使用 PIT,请在 pit.id 参数中使用最新的 PIT ID。 搜索的查询和排序参数必须保持不变。 如果提供,则 from 参数必须为 0(默认值)或 -1。GET /_search { "size": 1, "query": { "match_all": {} }, "pit": { "id": "i6-xAwEKdXNlcl9pbmRleBZYTXdtSFRHeVJrZVhCby1OTjlHMS1nABZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3AAAAAAAAIOJ7FmdBWEd2UmFVVGllZldNdnhPZDJmX0EBFlhNd21IVEd5UmtlWEJvLU5OOUcxLWcAAA==", "keep_alive": "5m" }, "sort": [ {"id": "asc"} ], "search_after": [ 2 ] }优缺点分析search_after 查询仅支持向后翻页。不严格受制于 max_result_window,可以无限制往后翻页。单次请求值不能超过 max_result_window;但总翻页结果集可以超过。思考🤔???1、为什么采用search_after 查询能解决深度分页的问题?2、search_after + pit分页查询过程中,PIT视图过期怎么办?3、search_after 查询,如果需要回到前几页怎么办?五、Scroll 遍历查询ES官方不再推荐使用Scroll API 进行深度分页。 如果您需要在分页超过 10,000 个点击时保留索引状态,请使用带有时间点 (PIT) 的 search_after 参数。相比于 From + size 和 search_after 返回一页数据,Scroll API 可用于从单个搜索请求中检索大量结果(甚至所有结果),其方式与传统数据库中游标(cursor)类似。Scroll API 原理上是对某次查询生成一个游标 scroll_id , 后续的查询只需要根据这个游标去取数据,直到结果集中返回的 hits 字段为空,就表示遍历结束。scroll_id 的生成可以理解为建立了一个临时的历史快照,在此之后的增删改查等操作不会影响到这个快照的结果。所有文档获取完毕之后,需要手动清理掉 scroll_id 。虽然es 会有自动清理机制,但是 srcoll_id 的存在会耗费大量的资源来保存一份当前查询结果集映像,并且会占用文件描述符。所以用完之后要及时清理。使用 es 提供的 CLEAR_API 来删除指定的 scroll_id1、首次查询,并获取_scroll_idPOST /user_index/_search?scroll=1m { "size": 1, "query": { "match_all": {} } }返回结果:{ "_scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EAAAAAACDlQBZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3", "took" : 0, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 6, "relation" : "eq" }, "max_score" : 1.0, "hits" : [ { "_index" : "user_index", "_type" : "_doc", "_id" : "1", "_score" : 1.0, "_source" : { "id" : 1, "name" : "老万" } } ] } }2、根据scroll_id遍历数据POST /_search/scroll { "scroll" : "1m", "scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EAAAAAACDlKxZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3" }3、删除游标scrollDELETE /_search/scroll { "scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EAAAAAACDlKxZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3" }优缺点scroll查询的相应数据是非实时的,这点和PIT视图比较类似,如果遍历过程中插入新的数据,是查询不到的。并且保留上下文需要足够的堆内存空间。适用场景全量或数据量很大时遍历结果数据,而非分页查询。官方文档强调:不再建议使用scroll API进行深度分页。如果要分页检索超过 Top 10,000+ 结果时,推荐使用:PIT + search_after。六、业务层面优化很多时候,技术解决不了的问题,可以通过业务层面变通下来解决!比如,针对分页场景,我们可以采用如下优化方案。1、增加默认的筛选条件通过尽可能的增加默认的筛选条件,如:时间周期和最低评分,减少满足条件的数据量,避免出现深度分页的情况。2、采用滚动增量显示典型场景比如手机上面浏览微博,可以一直往下滚动加载。示例:如下列表展示中,取消了分页按钮,通过滚动条增量加载数据。3、小范围跳页通过对分页组件的设计,禁止用户直接跳转到非常大的页码中。比如直接跳转到最后一页这种操作。示例:google搜索的小范围跳页总结分布式存储引擎的深度分页目前没有完美的解决方案。比如针对百度、google这种全文检索的查询,通过From+ size返回Top 10000 条数据完全能满足使用需求,末尾查询评分非常低的结果一般参考意义都不大。From+ size:需要随机跳转不同分页(类似主流搜索引擎)、Top 10000 条数据之内分页显示场景。search_after:仅需要向后翻页的场景及超过Top 10000 数据需要分页场景。Scroll:需要遍历全量数据场景 。max_result_window:调大治标不治本,不建议调过大。PIT:本质是视图。ElasticSearch分页与深度分页问题解决全方位深度解读 Elasticsearch 分页查询
前言本节主要介绍在ES中关联关系的处理方式。一、方案汇总根据《Elasticsearch权威指南》以及官网中的介绍,ES针对关联关系的处理主要有如下方式:1.应用层关联2.非规划化数据3.嵌套对象4.父子关系文档5.Terms lookup跨索引查询Join、Nested、Object、Flattened字段类型对比二、应用层关联对索引数据不进行特殊处理,而是在应用程序中通过多次查询实现数据的关联查询。例如,比方下面的例子,一个问题会有多个答案,且问题数据和答案数据在不同的索引中。1、创建问题索引question_indexPUT question_index { "mappings": { "properties": { "id":{"type": "keyword"}, "text":{"type": "keyword"} } } } PUT question_index/_doc/1?refresh { "id":"1", "text": "我是第一个问题" } PUT question_index/_doc/2?refresh { "id":"2", "text": "我是第二个问题" }2、创建答案索引question_index说明:其中pid是问题id。PUT answer_index { "mappings": { "properties": { "pid":{"type": "keyword"}, "text":{"type": "keyword"} } } } PUT answer_index/_doc/1?refresh { "pid":"1", "text": "问题一的答案1" } PUT answer_index/_doc/2?refresh { "pid":"1", "text": "问题一的答案2" }3、业务场景现在需要查询第一个问题的对应答案信息,我们可以这样做:首先,根据问题名称查出对应记录的id。GET question_index/_search { "query": { "term": { "text": { "value": "我是第一个问题" } } } }然后,将第一个查询得到的结果将填充到 terms 过滤器中,从answer_index中查询出答案数据。GET answer_index/_search { "query": { "terms": { "pid": [ "1" ] } } }优缺点分析:采用应用层关联的主要优点是简单,不需要对数据结构做额外的处理,可以对任意两个不同的索引进行关联查询。缺点是必须进行多次查询。适用场景:应用层关联适用于关联数据较少的情况,原因是terms对大量数据的进行多值匹配查询性能会比较差。使用terms可以进行多值查询, 只要目标文档匹配terms查询中的一个值, 此文档就会被标记为查询结果中的一个, 但terms的参数值是有限制的, 默认65535个元素, 你可以通过设置index.max_terms_count来进行更改。还可以通过terms lookup语法来解决terms参数元素过多的情况。三、非规范化数据为了获得较好的检索性能,最好的方法是在索引建模时进行非规范化数据存储,通过对文档数据字段的冗余保存避免访问时进行关联查询。比如下面的例子,希望通过用户姓名找到他写的博客文章。常规的方法索引结构如下,在blog_index索引中只保存user_id,用来关联用户信息。PUT user_index { "mappings": { "properties": { "id": {"type": "keyword"}, "name": {"type": "keyword"}, "email": {"type": "keyword"} } } } PUT blog_index { "mappings": { "properties": { "title":{"type": "keyword"}, "body":{"type": "keyword"}, "user_id":{"type": "keyword"} } } }非规范化数据处理:说明:将用户信息直接通过Object字段类型保存在博客索引数据中,这样通过数据的冗余保存,就避免了关联查询。PUT blog_index { "mappings": { "properties": { "title":{"type": "keyword"}, "body":{"type": "keyword"}, "user":{ "properties": { "id": {"type": "keyword"}, "name": {"type": "keyword"}, "email": {"type": "keyword"} } } } } }查询用户名称为老万的博客数据:GET blog_index/_search { "query": { "bool": { "must": [ { "term": { "user.name": "老万"}} ] } } }优缺点分析:数据非规范化的优点是速度快。因为每个文档都包含了所需的所有信息,当这些信息需要在查询进行匹配时,并不需要进行昂贵的联接操作。缺点是由于对大量数据进行了冗余存储,会占用更大的存储空间,且对关联数据的更新操作会更复杂。四、嵌套对象通过nested构建嵌套数据类型,也可以实现数据的关联关系。在上面的非规范化数据中,已经演示了通过Object字段类型来冗余保存关联数据避免数据关联查询。那么两者有什么区别呢?Object fileds 和nested fileds的区别:官方说明:Object fileds 和nested fileds的区别如果需要索引对象数组并保持数组中每个对象的独立性,请使用嵌套数据类型而不是对象数据类型。在内部,嵌套对象将数组中的每个对象作为单独的隐藏文档进行索引,这意味着可以使用嵌套查询独立于其他对象查询每个嵌套对象。简单来说:Object fileds适合保存简单对象,不能用来保存对象数组,因为它不能保证多个对象查询时的独立性。nested fileds适合保存对象数组。## 1、创建索引,指定user字段为嵌套对象 PUT my-index-000001 { "mappings": { "properties": { "user": { "type": "nested" } } } } ## 2、添加数据 PUT my-index-000001/_doc/1 { "group" : "fans", "user" : [ { "first" : "John", "last" : "Smith" }, { "first" : "Alice", "last" : "White" } ] } ## 3、查询数据;查询姓Alice,名Smith的用户。如果是user是Object类型可以查询到记录。 ## 而nested类型由于每个对象相互隔离,没有满足条件的记录 GET my-index-000001/_search { "query": { "nested": { "path": "user", "query": { "bool": { "must": [ { "match": { "user.first": "Alice" }}, { "match": { "user.last": "Smith" }} ] } } } } }优缺点分析:Object fileds 适合一对一的关联关系nested fileds 适合一对多的关联关系。两者都是通过非规范化数据,利用数据的冗余保存来避免关联查询。无论是Object fileds 还是nested fileds ,都是在同一条记录中保存数据的关联关系。五、父子关系文档通过join字段类型,构建索引记录间的父子关联关系。ES中通过join类型字段构建父子关联官网地址:Join field typejoin类型的字段主要用来在同一个索引中构建父子关联关系。通过relations定义一组父子关系,每个关系都包含一个父级关系名称和一个子级关系名称。示例:创建索引my_index,并在mappings中指定关联字段my_join_field的type类型为join,并通过relations属性指定关联关系,父级关系名称为question,子级关系名称为answer。这里的父子级关系的名称可以自己定义,在向索引中添加数据时,需要根据定义的关系名称指定my_join_field字段的值。my_join_field关联字段的名称也可以自定义。PUT my_index { "mappings": { "properties": { "text":{"type": "keyword"}, "my_join_field": { "type": "join", "relations": { "question": "answer" } } } } }优缺点分析:通过Join字段构建的父子关联关系,数据保存在同一索引的相同分片下,但是父记录和子记录分别保存的不同的索引记录中。而通过Object fileds和nested fileds构建的关联关系都是在同一条索引记录中。所以,Join字段构建的父子关联关系更适合保存关联数据比较多的场景。并且由于父子关联关系都是独立的记录存储,所以可以更方便的对父、子级数据单独进行新增、更新、删除等操作。缺点主要是has_child 或 has_parent 查询的查询性能会比较差。注意⚠️:Join字段不能像关系型数据库中的join使用,在ES中为了保证良好的查询性能,最佳的实践是将数据模型设置为非规范化文档,也就是通过字段冗余构造宽表。针对每一个join字段,has_child 或 has_parent 查询都会对您的查询性能造成重大影响。六、Terms lookup跨索引查询目前,只发现通过Terms lookup可以实现跨索引的关联查询。如果有其他方面,欢迎留言交流。说明:Terms lookup查询通过id获取现有文档的字段值,然后使用这些值作为搜索词进行二次查询。1、创建参数索引,并添加数据PUT params_index/_doc/1 { "group" : "fans", "name" : [ "老万", "小明" ] }2、创建博客索引,并添加数据## DELETE blog_index PUT blog_index { "mappings": { "properties": { "title":{"type": "keyword"}, "body":{"type": "keyword"}, "user_name":{"type": "keyword"} } } } PUT blog_index/_doc/1?refresh { "title": "老万的第一篇博客", "body":"开始es学习的第一天……", "user_name": "老万" } PUT blog_index/_doc/2?refresh { "title": "老万的第二篇博客", "body":"学习ES的关联查询……", "user_name": "老万" } PUT blog_index/_doc/3?refresh { "title": "三亚旅游日记", "body":"海边打卡", "user_name": "小明" } PUT blog_index/_doc/4?refresh { "title": "王者日记", "body":"今天5杀上王者", "user_name": "小王" }3、根据参数索引params_index查询blog_index中的记录GET blog_index/_search { "query": { "terms": { "user_name" : { "index" : "params_index", "id" : "1", "path" : "name" } } } }总结本文主要对ES中关联关系处理方式进行了汇总说明。主要有如下方式:1.应用层关联2.非规划化数据3.嵌套对象4.父子关系文档5.Terms lookup跨索引查询根据每种方式的优缺点和适用场景,在实际项目中正确选用。
问题描述:ES中如何实现空值和非空值的查询?实现方案:ES中可以通过exists查询实现空值和非空值的查询ES中exists查询官方说明:https://www.elastic.co/guide/en/elasticsearch/reference/7.9/query-dsl-exists-query.htmlexists用来查询指定字段存在数据的记录。以下这些情况被认为字段值为空:1.在json中字段的值为为null 或 [ ]2.字段的mapping属性中,“index” 被设置为 false3.字段值的长度超过映射中的 ignore_above 设置4.字段值格式错误并且在映射中定义了ignore_malformed实战演示:1、数据准备创建订单索引order_index,并添加测试数据。## 删除索引 DELETE order_index ## 新建索引,通过参数ignore_above设置长度大于2的姓名会被认为是空值 PUT order_index { "mappings": { "properties": { "name": { "type": "keyword", "ignore_above": 2 }, "amount": { "type": "integer" } } } } ## 添加数据 POST order_index/_bulk?refresh { "create": { } } { "amount": 100} { "create": { } } { "name": null,"amount": 80} { "create": { } } { "name": "东方不败", "amount": 15} { "create": { } } { "name": [],"amount": 80} { "create": { } } { "name": "老万", "amount": 300} { "create": { } } { "name": "老王", "amount": 45} { "create": { } } { "name": "小明", "amount": 15}2、空值查询说明:查询name字段为空的记录。GET order_index/_search { "query": { "bool": { "must_not": { "exists": { "field": "name" } } } } }3、非空值查询说明:查询name字段不为空的记录。GET order_index/_search { "query": { "exists": { "field": "name" } } }4、sql查询## 空值查询 POST /_sql?format=txt { "query": "SELECT name,amount FROM order_index Where name is null" } ## 非空查询 POST /_sql?format=txt { "query": "SELECT name,amount FROM order_index Where name is not null" }总结本文主要介绍了ES中如何实现空值和非空值的查询。主要是通过exists query实现。注意exists query中会被判定为空值的4种情况。
问题描述:在ES中如何实现in和not in查询?实现方案:ES中可以通过terms进行多值匹配查询,实现in和not in查询逻辑。比如:"query": { "terms": { "name": [ "老万", "小明" ] } }实战演示:1、数据准备创建订单索引order_index,并添加测试数据。## 删除索引 ## DELETE order_index ## 新建索引 PUT order_index { "mappings": { "properties": { "name": { "type": "keyword" }, "amount": { "type": "integer" } } } } ## 添加数据 POST order_index/_bulk?refresh { "create": { } } { "name": "老万", "amount": 100} { "create": { } } { "name": "老万", "amount": 80} { "create": { } } { "name": "老万", "amount": 300} { "create": { } } { "name": "老王", "amount": 45} { "create": { } } { "name": "小明", "amount": 15} { "create": { } } { "name": "小明", "amount": 50} { "create": { } } { "name": "小红", "amount": 300}2、实现IN查询说明:查询姓名为老万和小明的订单记录。GET order_index/_search { "query": { "terms": { "name": [ "老万", "小明" ] } } }3、实现NOT IN查询说明:通过bool查询,结合must_not和terms实现not in查询。GET order_index/_search { "query": { "bool": { "must_not": [ { "terms": { "name": [ "老万", "小明" ] } } ] } } }4、通过SQL查询实现## in查询 POST /_sql?format=txt { "query": "SELECT name FROM order_index where name in ('老万','小明') " } ## not in查询 POST /_sql?format=txt { "query": "SELECT name FROM order_index where name not in ('老万','小明') " } ## 将not in查询sql语句转为dsl语句 POST /_sql/translate { "query": "SELECT name FROM order_index where name not in ('老万','小明') " }总结本文主要介绍ES中通过terms进行多值匹配查询实现in和not in查询逻辑。当然,如果你对DSL查询语句不熟悉,现在ES中也支持直接采用SQL语句查询。
问题描述:我们都知道ES针对复杂的多添加组合查询非常强大,也知道通过match可以实现全文检索查询(分词查询),但是如果现在我只需要实现类似mysql中的like全匹配模糊查询,该怎么实现呢?业务场景:从content_index表中查询字段content中包含ES的记录。在关系型数据库中对应的SQL语句:SELECT content FROM content_index WHERE content like '%ES%'数据准备:## 删除索引 ## DELETE content_index ## 新建索引 PUT content_index { "mappings": { "properties": { "content": { "type": "wildcard" } } } } ## 添加数据 POST content_index/_bulk?refresh { "create": { } } { "content": "老万最近正在学习ES"} { "create": { } } { "content": "老万精通JAVA"} { "create": { } } { "content": "ES从入门到放弃"}说明:ElasticSearch 5.0以后,String字段被拆分成两种新的数据类型: text用于全文搜索,会分词,而keyword用于关键词搜索,不进行分词。补充:官网对wildcard字段类型的说明说明:1、采用wildcard通配符查询的字段推荐采用字段type设置为wildcard。2、text字段会进行分词,wildcard通配符查询检索的是分词后的数据。3、keyword字段虽然不会进行分词,但执行通配符wildcard查询(特别是带有前导通配符的模式)很慢。实现方案:1、sql实现POST /_sql?format=txt { "query": "SELECT content FROM content_index Where content like '%ES%'" }查询结果:转为DSL查看底层实现:POST /_sql/translate { "query": "SELECT content FROM content_index Where content like '%ES%'" }执行结果:底层就是基于wildcard的通配符查询,其中?和*分别代替一个和多个字符。{ "size" : 1000, "query" : { "wildcard" : { "content" : { "wildcard" : "*ES*", "boost" : 1.0 } } }, "_source" : false, "stored_fields" : "_none_", "docvalue_fields" : [ { "field" : "content" } ], "sort" : [ { "_doc" : { "order" : "asc" } } ] }2、dsl实现利用wildcard通配符查询实现,其中?和*分别代替一个和多个字符。GET content_index/_search { "query": { "wildcard": { "content": { "value": "*ES*" } } } }查询结果:{ "took" : 1, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 2, "relation" : "eq" }, "max_score" : 1.0, "hits" : [ { "_index" : "content_index", "_type" : "_doc", "_id" : "E3E0BnsBxW9JEct2L-d4", "_score" : 1.0, "_source" : { "content" : "老万最近正在学习ES" } }, { "_index" : "content_index", "_type" : "_doc", "_id" : "FXE0BnsBxW9JEct2L-d4", "_score" : 1.0, "_source" : { "content" : "ES从入门到放弃" } } ] } }总结本文主要介绍了ES中通过wildcard通配符查询实现like模糊查询。而sql查询的方式显然适合大众口味。使用wildcard通配符查询的目标字段的type类型需要设置为wildcard。
一、问题描述:在mysql数据库中,我们可以很方面的通过having关键字实现对聚合结果的过滤查询。那么,在ES中该如何实现类似having的先聚合再过滤查询呢?二、业务场景:需要找出下单次数大于等于2单,并且平均下单金额大于等于100的客户在关系型数据库中对应的SQL语句:SELECT userId, AVG(amount) avgAmount, count(*) orderCount FROM order GROUP by userId HAVING avgAmount >= 100 and orderCount >=2三、数据准备创建订单索引order_index,并添加测试数据。## 删除索引 ## DELETE order_index ## 新建索引 PUT order_index { "mappings": { "properties": { "name": { "type": "keyword" }, "amount": { "type": "integer" } } } } ## 添加数据 POST order_index/_bulk?refresh { "create": { } } { "name": "老万", "amount": 100} { "create": { } } { "name": "老万", "amount": 80} { "create": { } } { "name": "老万", "amount": 300} { "create": { } } { "name": "老王", "amount": 45} { "create": { } } { "name": "小明", "amount": 15} { "create": { } } { "name": "小明", "amount": 50} { "create": { } } { "name": "小红", "amount": 300}四、具体实现1、SQL实现方式说明:由于ES6.3以后已经支持sql查询,所有首先尝试大家最熟悉的sql查询方案能否实现。POST /_sql?format=txt { "query": "SELECT name,AVG(amount) avgAmount,count(*) orderCount FROM order_index group by name having avgAmount >= 100 and orderCount >=2 " }查询结果:用户名为老万,满足平均订单金额大于100,且下单数大于2。查询结果正确。2、DSL实现方式GET order_index/_search { "size": 0, "aggs": { "groupName": { "terms": { "field": "name" }, "aggs": { "avgAmount": { "avg": { "field": "amount" } }, "having": { "bucket_selector": { "buckets_path": { "orderCount": "_count", "avgAmount": "avgAmount" }, "script": { "source": "params.avgAmount >= 100 && params.orderCount >=2 " } } } } } } }查询结果:{ "took" : 1, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 7, "relation" : "eq" }, "max_score" : null, "hits" : [ ] }, "aggregations" : { "groupUserId" : { "doc_count_error_upper_bound" : 0, "sum_other_doc_count" : 0, "buckets" : [ { "key" : "老万", "doc_count" : 3, "avgAmount" : { "value" : 160.0 } } ] } } }sql语句底层实现分析:POST /_sql/translate { "query": "SELECT name,AVG(amount) avgAmount,count(*) orderCount FROM order_index group by name having avgAmount >= 100 and orderCount >=2 " }执行结果:分析sql转化的DSL语句,和上面DSL语句的实现,说明两者底层实现原理一致。mysql中通过having实现根据聚合结果进行过滤,ES中使用 bucket_selector 来实现此功能。{ "size" : 0, "_source" : false, "stored_fields" : "_none_", "aggregations" : { "groupby" : { "composite" : { "size" : 1000, "sources" : [ { "7e80e5b2" : { "terms" : { "field" : "name", "missing_bucket" : true, "order" : "asc" } } } ] }, "aggregations" : { "d8415567" : { "avg" : { "field" : "amount" } }, "having.having.d8415567_&_having.b26c7698" : { "bucket_selector" : { "buckets_path" : { "a0" : "d8415567", "a1" : "_count" }, "script" : { "source" : "InternalQlScriptUtils.nullSafeFilter(InternalQlScriptUtils.and(InternalQlScriptUtils.nullSafeFilter(InternalQlScriptUtils.gte(params.a0,params.v0)),InternalQlScriptUtils.nullSafeFilter(InternalQlScriptUtils.gte(params.a1,params.v1))))", "lang" : "painless", "params" : { "v0" : 100, "v1" : 2 } }, "gap_policy" : "skip" } } } } } }具体实现本文主要介绍了ES中如何实现类似having的先聚合再过滤查询。1、介绍了基于sql和dsl的两种实现方式,但是二者的底层原理其实都是一样的。2、实际项目中,更推荐直接采用sql来实现,代码简单,sql语句相比dsl上手更容易,也更容易理解。3、mysql中通过having实现根据聚合结果进行过滤,ES中使用 bucket_selector 来实现此功能。
一、功能场景在mysql数据库中查询数据时,我们可以采用dinstinct关键字去重。那么,在ES中如何实现查询结果去重呢?二、原理分析DISTINCT关键字去重的sql语句等价于对需要去重的字段进行GROUP BY。以下2个sql都能实现对name,age字段查询结果的去重。SELECT DISTINCT name,age FROM distinct_index SELECT name,age FROM distinct_index GROUP BY name,age三、方案汇总根据上面的分析,总结出以下2种实现方案:1、使用GROUP BY的SQL查询2、使用Aggregation(聚合)查询说明:ES6.3之后的版本以及支持SQL查询四、数据准备## 删除索引 ## DELETE distinct_index ## 新建索引 PUT distinct_index { "mappings": { "properties": { "name": { "type": "keyword" }, "age": { "type": "integer" } } } } ## 添加数据 POST distinct_index/_bulk?refresh { "create": { } } { "name": "小天", "age": 25} { "create": { } } { "name": "小天", "age": 25} { "create": { } } { "name": "老万", "age": 35} { "create": { } } { "name": "老王", "age": 45} { "create": { } } { "name": "小明", "age": 15} { "create": { } } { "name": "小明", "age": 15} { "create": { } } { "name": "小红", "age": 12} { "create": { } } { "name": "乐乐", "age": 18}五、方案实战1、SQL查询方案##可以通过format参数控制返回结果的格式,txt表示文本格式,看起来更直观点,默认为json格式。 POST _sql?format=txt { "query": "SELECT name,age FROM distinct_index group by name,age" }查询结果:和预期相符,查询结果达到去重的效果。SQL语句转DSL:POST /_sql/translate { "query": "SELECT name,age FROM distinct_index group by name,age" }结果:{ "size" : 0, "_source" : false, "stored_fields" : "_none_", "aggregations" : { "groupby" : { "composite" : { "size" : 1000, "sources" : [ { "f5b401c4" : { "terms" : { "field" : "name", "missing_bucket" : true, "order" : "asc" } } }, { "f557e07b" : { "terms" : { "field" : "age", "missing_bucket" : true, "order" : "asc" } } } ] } } } }说明:通过将sql语句转为dsl语句可以发现,sql语句中的group by查询底层原理是转化成了dsl中的aggregations聚合查询。2、aggregations聚合查询POST distinct_index/_search { "from": 0, "size": 0, "aggregations": { "name": { "terms": { "field": "name", "size": 2147483647 }, "aggregations": { "age": { "terms": { "field": "age", "size": 2147483647 } } } } } }查询结果:{ "took" : 3, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 8, "relation" : "eq" }, "max_score" : null, "hits" : [ ] }, "aggregations" : { "name" : { "doc_count_error_upper_bound" : 0, "sum_other_doc_count" : 0, "buckets" : [ { "key" : "小天", "doc_count" : 2, "age" : { "doc_count_error_upper_bound" : 0, "sum_other_doc_count" : 0, "buckets" : [ { "key" : 25, "doc_count" : 2 } ] } }, { "key" : "小明", "doc_count" : 2, "age" : { "doc_count_error_upper_bound" : 0, "sum_other_doc_count" : 0, "buckets" : [ { "key" : 15, "doc_count" : 2 } ] } }, { "key" : "乐乐", "doc_count" : 1, "age" : { "doc_count_error_upper_bound" : 0, "sum_other_doc_count" : 0, "buckets" : [ { "key" : 18, "doc_count" : 1 } ] } }, { "key" : "小红", "doc_count" : 1, "age" : { "doc_count_error_upper_bound" : 0, "sum_other_doc_count" : 0, "buckets" : [ { "key" : 12, "doc_count" : 1 } ] } }, { "key" : "老万", "doc_count" : 1, "age" : { "doc_count_error_upper_bound" : 0, "sum_other_doc_count" : 0, "buckets" : [ { "key" : 35, "doc_count" : 1 } ] } }, { "key" : "老王", "doc_count" : 1, "age" : { "doc_count_error_upper_bound" : 0, "sum_other_doc_count" : 0, "buckets" : [ { "key" : 45, "doc_count" : 1 } ] } } ] } } }六、异常记录1、ES中的sql查询不支持DISTINCT关键字POST _sql?format=txt { "query": "SELECT DISTINCT name,age FROM distinct_index" }报错:{ "error" : { "root_cause" : [ { "type" : "verification_exception", "reason" : "Found 1 problem\nline 1:8: SELECT DISTINCT is not yet supported" } ], "type" : "verification_exception", "reason" : "Found 1 problem\nline 1:8: SELECT DISTINCT is not yet supported" }, "status" : 400 }2、ES的sql查询中表名中不能有中划线,比如my-index-000001PUT my-index-000001 { "mappings": { "properties": { "name": { "type": "keyword" }, "age": { "type": "integer" } } } } POST _sql?format=txt { "query": "SELECT name FROM my-index-000001" }报错:{ "error" : { "root_cause" : [ { "type" : "parsing_exception", "reason" : "line 1:20: extraneous input '-' expecting {<EOF>, ',', 'ANALYZE', 'ANALYZED', 'AS', 'CATALOGS', ……遇到这种情况,最简单方法是给索引添加别名。POST /_aliases { "actions" : [ { "add" : { "index" : "my-index-000001", "alias" : "my_index" } } ] } POST _sql?format=txt { "query": "SELECT name FROM my_index" }总结本文主要介绍了ES中如何实现类似dinstinct的数据去重功能。1、首先通过通过dinstinct和group by的等价sql语句,说明可以通过分组函数实现数据去重。2、分别介绍了ES中通过sql语句查询和aggregations聚合查询实现对查询结果的去重。3、ES中的sql查询不支持DISTINCT关键字4、ES中的sql查询中表名不能包含中划线,比如my-index-000001
项目场景:Spring boot文件下载调用接口下载spring boot工程的resources目录下的excel模板文件,非常常见的一个文件下载功能,但是却容易遇到很多坑,下面总结记录下。问题一:下载的文件名称出现中文乱码的问题解决方案:response.setHeader("Content-Disposition", "attachment;filename=" + new String("下载模板".getBytes("UTF-8"), "ISO8859-1"));说明:这是网上最常见的解决方案,经过这样的修改后,在浏览器上调用get请求下载的文件确实没有出现文件名中文乱码了。但是在swagger里面测试接口,下载的问题还是会出现中文乱码。问题二:在swagger中测试下载接口,点击下载的文件,发现文件名是乱码的问题这里我项目中使用的是springdoc-openapi-ui 1.5.9,基于的是openapi3.0的协议。整体使用方式和界面和swagger类似。swagger中下载的文件,点击开发后,文件名乱码问题:解决方案:response.setHeader("Content-Disposition", "attachment;fileName=" + URLEncoder.encode("线索导入模板.xlsx","utf8"));说明:通过URLEncoder.encode函数对文件名称处理后,无论是在浏览器调用GET请求下载文件,还是Swagger中调用下载接口,都不会出现文件名乱码问题。问题三:下载的excel文件打开时总是提示部分内容有问题,尝试恢复。问题原因:一般有2种情况:1、由于没有找到文件,下载的文件字节大小为0,这种情况文件完全打不开2、读取的文件大小和元素文件的大小不一致,这种情况会提升自动修复。解决办法:网上最多的解决方案是主动在response的Header中设置Content-Length大小。但这种方式其实是错误的。文件的Content-Length其实可以从返回流中直接获取,并不需要用户主动去设置。这里的问题核心应该是思考:为什么下载的文件和元素文件的大小会不一致?下面的2个获取inputStream的长度的API,只有在读取磁盘上具体文件中才比较适用。如果是jar包中的文件,是获取不到大小。/加上设置大小 response.addHeader("Content-Length",String.valueOf(file.length())); //response.addHeader("Content-Length",String.valueOf(inputStream.available()));问题四:采用BufferedInputStream缓冲流读写文件导致输出文件和原始文件体积差异的问题由于下载的文件体积总是比元素文件体积大一点点,导致文件打开提示异常修复。outputStream = response.getOutputStream(); bis = new BufferedInputStream(inputStream); //缓冲数组,每次读取1024 byte[] buff = new byte[1024]; while (bis.read(buff)!= -1) { //异常代码行 outputStream.write(buff, 0, buff.length); } outputStream.flush();原因分析:出现问题的原因就是buff.length,数组声明后长度就是固定的,而不是获取里面读取的内容的字节长度,所以导致这里的buff.length的值始终是1024。解决:outputStream = response.getOutputStream(); bis = new BufferedInputStream(inputStream); //缓冲流,每次读取1024 byte[] buff = new byte[1024]; int readLength = 0; while (( readLength = bis.read(buff)) != -1) { //每次写入缓冲流buff读到的字节长度,而不是buff.length outputStream.write(buff, 0, readLength); } outputStream.flush();问题五:开发环境下载成功,打成jar包发布到服务器上部署就出现下载失败问题java.io.FileNotFoundException: class path resource [template/template.xlsx] cannot be resolved to absolute file path because it does not reside in the file system: jar:file原因:Resource下的文件是存在于jar这个文件里面,在磁盘上是没有真实路径存在的,它其实是位于jar内部的一个路径。所以通过ResourceUtils.getFile或者this.getClass().getResource("")方法无法正确获取文件。解决:通过ClassPathResource读取文件流InputStream inputStream = getClass().getClassLoader().getResourceAsStream("template/template.xlsx");完整代码1、控制层代码@Operation(summary = "下载模版",description = "下载模版") @GetMapping("/download") public void download(HttpServletResponse response){ templateService.download(response); }2、下载方法实现方式一:经典的缓冲流BufferedInputStream读取法,一般读取比较大的文件,优先考虑缓冲流读取方式方式二:利用spring的FileCopyUtils工具类,小文件优先考虑此方式,代码更简单不易出错。/** * 下载线索模板 * @param response */ public void download(HttpServletResponse response) { InputStream inputStream = null; BufferedInputStream bis = null; OutputStream outputStream = null; try { inputStream=getClass().getClassLoader().getResourceAsStream("template/template.xlsx"); response.setContentType("application/octet-stream"); response.setHeader("content-type", "application/octet-stream"); //待下载文件名 String fileName = URLEncoder.encode("模板.xlsx","utf8"); response.setHeader("Content-Disposition", "attachment;fileName=" + fileName); outputStream = response.getOutputStream(); //加上设置大小 下载下来的excel文件才不会在打开前提示修复 //这里流的长度很难在开始读取前获取,特别是打成jar包后,读取inputStream长度经常失败 //response.addHeader("Content-Length",String.valueOf(classPathResource.getFile().length())); //response.addHeader("Content-Length",String.valueOf(inputStream.available())); //方式一:经典的缓冲流BufferedInputStream读取法 // bis = new BufferedInputStream(inputStream); //缓冲流,每次读取1024 // byte[] buff = new byte[1024]; // int readLength = 0; // while (( readLength = bis.read(buff)) != -1) { // outputStream.write(buff, 0, readLength); // } // outputStream.flush(); //方式二:利用spring的FileCopyUtils工具类 if(inputStream!=null){ byte[] results = FileCopyUtils.copyToByteArray(inputStream); outputStream.write(results); outputStream.flush(); } } catch ( IOException e ) { log.error("文件下载失败,e"); } finally { IOUtils.closeQuietly(outputStream); IOUtils.closeQuietly(inputStream); IOUtils.closeQuietly(bis); } }参考:https://blog.csdn.net/Hi_Boy_/article/details/107198371
前言很多人在Spring boot项目中都已经习惯采用Spring家族封装的spring-data-elasticsearch来操作elasticsearch,而官方更推荐采用rest-client。今天给大家介绍下在spring boot中如何整合rest-client操作elasticsearch。一、为什么不使用Spring家族封装的spring-data-elasticsearch?主要是灵活性和更新速度。spring将elasticsearch过度封装,让开发者很难跟ES的DSL查询语句进行关联。再者就是更新速度,ES的更新速度是非常快的,但是spring-data-elasticsearch更新速度比较缓慢。另外,spring-data-elasticsearch底层依赖的是TransportClient。而TransportClient在ES7中已被弃用,取而代之的是 Java 高级 REST 客户端,并将在 Elasticsearch 8.0 中删除。ElasticsearchRepository的优缺点:优点: 简单,SpringBoot无缝对接,配置简单缺点: 基于即将废弃的TransportClient, 不能支持复杂的业务基于此,强烈建议采用官方推出的java客户端elasticsearch-rest-high-level-client,它的代码写法跟DSL语句很相似,懂ES查询的使用其上手很快。二、低级REST客户端和高级REST客户端的关系使用Elasticsearch服务,则要先获取一个Elasticsearch客户端。获取Elasticsearch客户端的方法很简单,最常见的就是创建一个可以连接到集群的传输客户端对象。在Elasticsearch中,客户端有初级客户端和高级客户端两种,它们均使用Elasticsearch提供了RESTful风格的API,在使用RESTful API时,一般通过9200端口与Elasticsearch进行通信。初级客户端是Elasticsearch为用户提供的官方版初级客户端。初级客户端允许通过HTTP与Elasticsearch集群进行通信,它将请求封装发给Elasticsearch集群,将Elasticsearch集群的响应封装返回给用户。初级客户端与所有Elasticsearch版本都兼容。高级客户端是用于弹性搜索的高级客户端,它基于初级客户端。高级客户端公开了API特定的方法,并负责处理未编组的请求和响应。官网对java-rest客户端的特性介绍:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-low.htmljava low-level client特性:最小依赖针对可用节点,负载均衡针对错误和故障节点可以进行故障转移针对某个节点连接失败次数越多,客户端就会等待更长的时间,才会再次尝试这个节点持久连接请求日志追踪原子性操作java High-level client特性:在 Java 低级 REST 客户端之上运行主要目标是公开 API 特定的方法每个API都有同步和异步调用Java High Level REST Client 依赖于 Elasticsearch 核心项目简单来说:low-level client 最小依赖,兼容性更好,使用更灵活。High-level client基于low-level client ,它的主要目标是公开 API 特定的方法,封装性更好,使用更方便。三、实战示例主要演示low-level client 和High-level client的基本用法,所以只包含客户端的获取和简单的查询示例。1、添加maven依赖<properties> <java.version>1.8</java.version> <es.version>7.10.2</es.version> <monitor.version>1.2.7.5.RELEASE</monitor.version> <skipTests>true</skipTests> </properties> <!-- es的starter, 主要是为了实现自动化配置,方便快捷的获取rest client --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> <exclusions> <exclusion> <groupId>org.springframework.data</groupId> <artifactId>spring-data-elasticsearch</artifactId> </exclusion> </exclusions> </dependency> <!-- low-level client --> <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-client</artifactId> <version>${es.version}</version> </dependency> <!-- high-level client ,默认依赖的elasticsearch存在版本差异,排除后添加统一的es版本--> <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> <version>${es.version}</version> <exclusions> <exclusion> <groupId>org.elasticsearch</groupId> <artifactId>elasticsearch</artifactId> </exclusion> </exclusions> </dependency> <!-- 使用low-level client不需要,但是high-level client需要依赖--> <dependency> <groupId>org.elasticsearch</groupId> <artifactId>elasticsearch</artifactId> <version>${es.version}</version> </dependency>2、配置ES连接属性#es配置 spring.elasticsearch.rest.uris = http://localhost:9200 spring.elasticsearch.rest.username = elastic spring.elasticsearch.rest.password = 123456 spring.elasticsearch.rest.connection-timeout = 10s spring.elasticsearch.rest.read-timeout = 30s3、RestClient、RestHighLevelClient使用@SpringBootTest @Slf4j public class EsTest { @Autowired private RestClient restClient; @Autowired private RestHighLevelClient highLevelClient; /** * low-rest client测试类 * 说明:根据DSL语句发起请求 * 同步请求 performRequest * 异步请求 performRequestAsync */ @Test public void lowClientTest() { try{ //DSL查询语句 String queryString = "{\"query\":{\"match_all\":{}},\"sort\":{\"crawler_time\":{\"order\":\"desc\"}},\"from\":0,\"size\":2}"; HttpEntity entity = new NStringEntity(queryString, ContentType.APPLICATION_JSON); //指定请求方法和索引 Request request = new Request("GET", "/user_info_*/_search"); request.setEntity(entity); Map<String, String> params = Collections.emptyMap(); request.addParameters(params); //发起请求 Response response = restClient.performRequest(request); if(response!=null && HttpStatus.OK.value() == response.getStatusLine().getStatusCode()){ String responseBody = EntityUtils.toString(response.getEntity()); JSONObject jsonObject = JSON.parseObject(responseBody); JSONObject jsonObject1 = (JSONObject)((JSONObject)jsonObject.get("hits")).get("total"); if(jsonObject1.get("value")!=null){ Integer total = (Integer) jsonObject1.get("value"); System.out.println("总记录数:" + total); } //处理返回结果 JSONArray jsonArray = (JSONArray)((JSONObject)jsonObject.get("hits")).get("hits"); List<UserInfo> infoList = jsonArray.stream().map(e->{ JSONObject source = (JSONObject) ((JSONObject)e).get("_source"); return JSON.parseObject(JSON.toJSONString(source), UserInfo.class); }).collect(Collectors.toList()); infoList.forEach(e->{ System.out.println(e.toString()); }); } }catch (Exception e){ log.error("lowClientTest fail",e); } } /** * high-rest Client测试类 * 说明:通过SearchRequest、SearchSourceBuilder、QueryBuilders构建请求,API封装的更丰富 */ @Test public void highRestClientTest(){ String indice = "user_info_*"; SearchRequest searchRequest = new SearchRequest(indice); SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); sourceBuilder.query(QueryBuilders.matchAllQuery()); sourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS)); log.info("ES查询DSL语句:\nGET {}\n{}", String.format("/%s/_search",searchRequest.indices()[0]),sourceBuilder); searchRequest.source(sourceBuilder); try { SearchResponse response = highLevelClient.search(searchRequest, RequestOptions.DEFAULT); Arrays.stream(response.getHits().getHits()) .forEach(i -> { //索引名称 System.out.println(i.getIndex()); System.out.println(i.getSourceAsString()); }); System.out.println("记录总数:" + response.getHits().getTotalHits().value); } catch (Exception e) { log.error("highRestClientTest fail",e); } } }4、spring-boot-starter-data-elasticsearch说明RestClientAutoConfiguration自动化配置类:RestClientConfigurations核心源码class RestClientConfigurations { /** * 声明RestClient的bean */ @Bean @ConditionalOnMissingBean RestClient elasticsearchRestClient(RestClientBuilder builder) { return builder.build(); } } @Configuration( proxyBeanMethods = false ) @ConditionalOnClass({RestHighLevelClient.class}) static class RestHighLevelClientConfiguration { RestHighLevelClientConfiguration() { } /** * 声明RestHighLevelClient */ @Bean @ConditionalOnMissingBean RestHighLevelClient elasticsearchRestHighLevelClient(RestClientBuilder restClientBuilder) { return new RestHighLevelClient(restClientBuilder); } /** * 声明RestClient的bean */ @Bean @ConditionalOnMissingBean RestClient elasticsearchRestClient(RestClientBuilder builder, ObjectProvider<RestHighLevelClient> restHighLevelClient) { RestHighLevelClient client = (RestHighLevelClient)restHighLevelClient.getIfUnique(); return client != null ? client.getLowLevelClient() : builder.build(); } }说明:引入spring-boot-starter-data-elasticsearch自动化配置类,主要是简化配置,更方便的获取RestClient和RestHighLevelClient。总结本文主要介绍类如下内容:1、为什么弃用spring-data-elasticsearch?2、es中低级RestClient和高级RestHighLevelClient的特性和区别3、spring boot中如何通过RestClient和RestHighLevelClient访问elasticsearch
问题从mysql数据库查询出来的时间数据,返回给前端后,如果采用yyyy-MM-dd HH:mm:ss的格式进行时间格式化,会相差8小时。而如果采用yyyy-MM-dd的格式,会相差一天。实体中的created_at字段:/** * 创建时间 */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") private Date createdAt;mysql数据库时间:返回前端时间:问题:希望的结构是后端返回给前端的创建时间应该和数据库里面数据保持一致,但是实际上返回给前端创建时间相差8小时。一、原因分析所有返回的创建时间都相差8小时,很明显和createdAt字段的时区有关。因为默认的时区UTC和北京时间(GMT+8)正好相差8小时。但是这里我们在createdAt字段序列化的时候,已经通过@JsonFormat注解指定了序列化的格式为yyyy-MM-dd HH:mm:ss,时区为北京时间GMT+8。@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")继续通过debug断点分析,查看从数据库查询出的结果是什么?发现从数据库中查询出来的结果,已经相差了8小时,排除JSON序列化的问题。其中CST表示使用的是北京时区。而存储在mysql数据库里面的时间字段,是没有时区概念的,时间字段的时区是由建立mysql的连接决定的。spring.datasource.jdbc-url=jdbc:mysql://localhost:3306/user?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC&zeroDateTimeBehavior=convertToNull其中serverTimezone=UTC指定时区。问题产生原因:由于在建立mysql连接时,通过serverTimezone=UTC指定了时区为UTC,所以认为mysql数据库里面的时间字段的对应时区都是UTC,而通过mybatis获取时间字段数据和实体绑定时,由于时区会被转为北京时区CST,所以时间数值相差8小时。二、问题解决为了保证获取的时间字段的数据值保持一致,一定要保证mysql数据库连接中指定的时区serverTimezone和时间字段JSON格式化时指定的时区保持一致。所以,有2种修复方案:方案一:由于mysql连接中使用的时区为UTC,在JSON格式化时,也采用UTC时区(采用这种方案的原因主要是由于当前项目中不方便修改mysql连接配置信息,所以只能通过JSON格式化指定时区对时间数据的时差进行修正。)/** * 创建时间 */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "UTC") private Date createdAt;结果:方案二:修改mysql连接中serverTimezone=CST(北京时间),JSON格式化中也使用CST时区这也是更推荐采用的方案。能保证程序中获取的时间都是正确的北京时间,避免出现时差问题。关于北京时区的表示方式,CST,GMT+8,Asia/Shanghai都表示北京时区。mysql连接:spring.datasource.jdbc-url=jdbc:mysql://localhost:3306/user?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=CST&zeroDateTimeBehavior=convertToNull 1 java实体: /** * 创建时间 */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "CST") private Date createdAt;三、时区说明常见的时区有UTC、GMT、CST、GMT+8、Asia/Shanghai具体说明:UTC,世界标准时间 (UTC, Coordinated Universal Time)是由国际无线电咨询委员会规定和推荐,并由国际时间局(BIH)负责保持的以秒为基础的时间标度,当今民用时间的基础。它使用一天 24 小时时间制,并结合了地球的自转时间与原子钟的高精度度量。UTC相当于本初子午线(即经度0度)上的平均太阳时,过去曾用格林威治平均时(GMT)来表示,北京时间比UTC时间早8小时。UTC是一个标准,而不是一个时区。UTC 是一个全球通用的时间标准。全球各地都同意将各自的时间进行同步协调 (coordinated),这也是 UTC 名字的来源:Universal Coordinated Time。GMT(Greenwich Mean Time) 格林尼治平时由于地球轨道并非圆形,其运行速度又随着地球与太阳的距离改变而出现变化,因此视太阳时欠缺均匀性。视太阳日的长度同时亦受到地球自转轴相对轨道面的倾斜度所影响。为着要纠正上述的不均匀性,天文学家计算地球非圆形轨迹与极轴倾斜对视太阳时的效应。平太阳时就是指经修订后的视太阳时。在格林尼治子午线上的平太阳时称为世界时(UT0),又叫格林尼治平时(GMT)。 为了确保协调世界时与世界时(UT1)相差不会超过0.9秒,有需要时便会在协调世界时内加上正或负闰秒。因此协调世界时与国际原子时(TAI)之间会出现若干整数秒的差别。位于巴黎的国际地球自转事务中央局(IERS)负责决定何时加入闰秒。我们可以认为格林威治时间就是世界协调时间(GMT=UTC),格林威治时间和UTC时间均用秒数来计算的。CST:北京时间(中国标准时间),北京处于东八区,所以也可以表示成GMT+8。Asia/Shanghai:上海时区原因是1949年以前,中国一共分了5个时区,以哈尔滨 ( Asia/Harbin)、上海(Asia/Shanghai)、重庆(Asia/Chongqing)、乌鲁木齐(Asia/Urumqi)、喀什(Asia/Kashgar)为代表——分别是:长白时区GMT+8:30、中原标准时区 GMT+8、陇蜀时区GMT+7、新藏时区GMT+6和昆仑时区GMT+5:30。它是1912年北京观象台制订,后由内政部批准过。而且从国际标准本身的角度来看,北京和上海处于同一时区,只能保留一个,而作为时区代表上海已经足够具有代表性。所以目前还没有Asia/beijing。CST、GMT+8、Asia/Shanghai三个时区等同,都表示东八区北京时间。三、为什么@JsonFormat指定了时区还是相差8小时由于mysql连接属性中通过serverTimezone=UTC指定了服务器的时区是UTC,所以认为数据库存储的时间字段的值对应的时区都是UTC时区。而java程序中的Date字段默认对应的是CST北京时区。这样就导致了java程序中获取的日期字段的值与数据库中的日期数据相差8小时。而我们通常的json格式化都会将时区指定为东八区北京时间,即@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "CST")或者@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")所以最终导致JSON格式化后返回给前端的时间相差了8小时。四、为什么采用@JSONField进行时间格式化没有起作用@JSONField(format = "yyyy-MM-dd") private Date createdAt;原因:@JSONField注解是fastjson框架下的注解@JsonFormat是jackson框架下的注解而spring boot默认使用的是jackson序列化框架。五、使用@JSONField注解如何指定时区@JsonFormat注解可以通过timezone属性指定时区,但是fastjson中的时间格式注解@JSONField并没有发现对应的时区属性字段,那么使用@JSONField注解时,需要怎么指定时区呢?解决:通过JSON.defaultTimeZone指定JSON对应的默认时区。JSON.defaultTimeZone=TimeZone.getTimeZone("UTC"); log.info(JSON.toJSONString(followInfoDto));实体字段上添加@JSONField注解指定时间格式/** * 创建时间 */ @JSONField(format = "yyyy-MM-dd HH:mm:ss") private Date createdAt;输出结果:六、@JsonFormat、@JSONField、@DateTimeFormat注解的区别@DatetimeFormat是Spring框架下的注解,将String转换成Date,主要用于前台给后台传参时用Date类型参数绑定使用。@JsonFormat是jackson框架下的注解,jackson框架是Spring Boot项目下默认的json序列化框架。主要用于指定json序列化格式, 一般用于后台返回数据给前台时,指定实体的序列化格式。@JSONField是fastjson框架下的注解,主要用于指定实体序列化后面的字段名称和序列化格式。查看源码发现,@JsonFormat和@JSONField默认时区都是是DEFAULT_TIMEZONE = “##default”,对应的默认时区是UTC,而不是系统默认时区。JsonFormat.DEFAULT_TIMEZONE is NOT the system default, as the documentation and SO answer suggest, but actually defaults to UTC.七、spring boot替换序列化框架采用fastjson替换spring boot默认的json序列化框架jackson。一般情况下,不推荐替换成fastjson框架。这是由于fastjson虽然在序列化速度上有一定的优势,但是一直以来都频繁爆出安全漏洞,相比之下默认的序列化框架jackson在速度和安全性方面都更有保障。1、引入fastjson依赖库:<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.75</version> </dependency>2、配置fastjson序列化:spring boot支持两种配置方法:第一种方法:(1)启动类继承extends WebMvcConfigurerAdapter(2)覆盖方法configureMessageConverters或extendMessageConverters具体代码如下:@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { // 网上这种单独清除默认的Jackson2转换器的方法,还是会出问题 //converters.removeIf(httpMessageConverter -> httpMessageConverter instanceof MappingJackson2HttpMessageConverter); //清空所有默认的转换器,不然容易受StringHttpMessageConverter的影响 converters.clear(); //指定时区 JSON.defaultTimeZone= TimeZone.getTimeZone("GMT+08"); FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter(); List<MediaType> fastMediaTypes = new ArrayList<>(); fastMediaTypes.add(MediaType.APPLICATION_JSON); fastMediaTypes.add(MediaType.TEXT_PLAIN); fastMediaTypes.add(MediaType.ALL); fastConverter.setSupportedMediaTypes(fastMediaTypes); FastJsonConfig fastJsonConfig = new FastJsonConfig(); fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat); fastConverter.setFastJsonConfig(fastJsonConfig); converters.add(fastConverter); } }说明:消息转换器converters的列表执行有先后次序,先匹配先处理,后面的转化器就不会起作用了。如果不清空默认的转化器,直接添加的fastConverter会一直不起作用。这里重写configureMessageConverters方法和extendMessageConverters方法效果是一样的。常见异常:Response body Unrecognized response type; displaying content as text.由于没有清空默认的转换器,走了其他的转换器,导致的。注意⚠️:网上教程大多都是通过WebMvcConfigurerAdapter对象来配置,但是WebMvcConfigurerAdapter已经过期,尽量不要使用。第二种方法:在配置类中通过@Bean注解声明FastJsonHttpMessageConverter。优点配置简单,不会出现中文乱码问题。@Bean public HttpMessageConverters fastJsonHttpMessageConverters() { FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter(); FastJsonConfig fastJsonConfig = new FastJsonConfig(); //指定时区 JSON.defaultTimeZone= TimeZone.getTimeZone("GMT+08"); fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat); fastConverter.setFastJsonConfig(fastJsonConfig); HttpMessageConverter<?> converter = fastConverter; return new HttpMessageConverters(converter); }源码分析:消息转换自动化配置类 HttpMessageConvertersAutoConfiguration@Configuration( proxyBeanMethods = false ) @ConditionalOnClass({HttpMessageConverter.class}) @Conditional({HttpMessageConvertersAutoConfiguration.NotReactiveWebApplicationCondition.class}) @AutoConfigureAfter({GsonAutoConfiguration.class, JacksonAutoConfiguration.class, JsonbAutoConfiguration.class}) @Import({JacksonHttpMessageConvertersConfiguration.class, GsonHttpMessageConvertersConfiguration.class, JsonbHttpMessageConvertersConfiguration.class}) public class HttpMessageConvertersAutoConfiguration { static final String PREFERRED_MAPPER_PROPERTY = "spring.http.converters.preferred-json-mapper"; public HttpMessageConvertersAutoConfiguration() { } @Bean @ConditionalOnMissingBean public HttpMessageConverters messageConverters(ObjectProvider<HttpMessageConverter<?>> converters) { return new HttpMessageConverters((Collection)converters.orderedStream().collect(Collectors.toList())); }在声明HttpMessageConverters的方法上发现@ConditionalOnMissingBean注解,也就是当程序中没有声明HttpMessageConverters的bean时,才会加载默认的这些converters。如果程序中声明了自己的HttpMessageConverters,那么就不会加载默认的这些converters。总结本文主要通过json格式化出现时差问题,扩展介绍了json时间格式化的相关注意事项。1、mysql中日期字段数据的时区由连接属性中的serverTimezone属性决定。2、介绍了常见的时区UTC、GMT、CST、GMT+8、Asia/Shanghai3、@JsonFormat、@JSONField、@DateTimeFormat注解的区别4、@JSONField可以通过JSON.defaultTimeZone指定时区5、介绍如何使用FastJson框架替换spring boot默认的JSON序列号框架jackson。
前言在es的早期版本中,没有免费提供安全认证的相关功能。为了防止数据安全问题,一般的措施都是采用IP黑白名单,网络防火墙,Nginx代理权限控制。而从es6.8和7.1版本开始,es给我们免费提供了安全功能,也就是今天要介绍的X-Pack功能。一、ES版本的安全性各版本支持的特性:https://www.elastic.co/cn/subscriptions安全性配置的官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/7.13/security-minimal-setup.html二、ES的安全层级说明:目前ES的安全层级可以分为4层:1、Minimal security (Elasticsearch Development)最低安全方案,主要是开发环境使用。此配置通过为内置用户设置密码来防止对本地集群的未授权访问。对于生产模式集群,最低安全方案是不够的。 如果您的集群有多个节点,您必须启用最低安全性,然后在节点之间配置传输层安全性 (TLS)。2、basic security(Elasticsearch Production)此方案通过添加传输层安全性 (TLS) 用于节点之间的通信,以最低安全要求为基础。 适用于生产环境的ES使用。这一附加层要求节点验证安全证书,以防止未经授权的节点加入您的 Elasticsearch 集群。Elasticsearch 和 Kibana 之间的外部 HTTP 流量不会被加密,但节点间通信将受到保护。ES节点直接会验证安装证书,但是Elasticsearch 和 Kibana 之间的外部 HTTP 流量没有用证书保护。3、basic security + TLS for REST此方案建立在基本安全方案的基础上,并使用 TLS 保护所有 HTTP 流量。 除了在 Elasticsearch 集群的传输接口上配置 TLS 之外,您还可以在 HTTP 接口上为 Elasticsearch 和 Kibana 配置 TLS。如果您需要在 HTTP 层上使用双向(双向)TLS,那么您需要配置双向认证加密。然后,您将 Kibana 和 Beats 配置为使用 TLS 与 Elasticsearch 通信,以便对所有通信进行加密。 此级别的安全性很强,可确保进出集群的任何通信都是安全的。总结:ES的安全认证主要有3个级别的配置方案,高级的安全方案都是以低一级的安全方案为基础。最低方案:基于用户名和密码认证。basic方案:ES节点间添加传输层安全性 (TLS) ,也就是节点采用证书认证更高的方案:TLS for REST,即http请求也都添加证书认证。三、X-Pack开启步骤1.Minimal security配置在elasticsearch.yml配置文件中添加如下配置:xpack.security.enabled: true如果你只有es只有单个节点,还可以添加如下配置:discovery.type: single-node重启elasticsearch:启动日志中发现,安全认证已经启动。执行curl localhost:9200,提示权限不够{ "error": { "root_cause": [ { "type": "security_exception", "reason": "missing authentication credentials for REST request [/]", "header": { "WWW-Authenticate": "Basic realm=\"security\" charset=\"UTF-8\"" } } ], "type": "security_exception", "reason": "missing authentication credentials for REST request [/]", "header": { "WWW-Authenticate": "Basic realm=\"security\" charset=\"UTF-8\"" } }, "status": 401 }内置用户配置密码:在es进程启动的情况下,执行elasticsearch-setup-passwords工具类,为内置用户配置密码。如果你希望手动设置密码,可以执行如下命令:./bin/elasticsearch-setup-passwords interactive如果你希望自动生产密码,可以执行如下命令:./bin/elasticsearch-setup-passwords auto执行结果如下:Changed password for user apm_system PASSWORD apm_system = GAkyp0bLv60B1VTqE8KH Changed password for user kibana_system PASSWORD kibana_system = u021T4dZgwBNTeAGg4k8 Changed password for user kibana PASSWORD kibana = u021T4dZgwBNTeAGg4k8 Changed password for user logstash_system PASSWORD logstash_system = 1xSET6NRTMwuts8ey2pD Changed password for user beats_system PASSWORD beats_system = ZgumYOBDA0RiYnv8L5WH Changed password for user remote_monitoring_user PASSWORD remote_monitoring_user = z1joG8rlS7ZwNvB2Jacj Changed password for user elastic PASSWORD elastic = AndTGV1WmKblRS9oSILp注意⚠️:为elastic用户设置密码后,不能再次运行elasticsearch-setup-passwords命令。尝试使用内置用户elastic访问:[root@Mobile ~]# curl -X GET -u "elastic:AndTGV1WmKblRS9oSILp" "localhost:9200/?pretty" { "name" : "node-1", "cluster_name" : "my-test", "cluster_uuid" : "M5EGtEmzRQG5ri3Yy_Wpxg", "version" : { "number" : "7.10.2", "build_flavor" : "default", "build_type" : "tar", "build_hash" : "747e1cc71def077253878a59143c1f785afa92b9", "build_date" : "2021-01-13T00:42:12.435326Z", "build_snapshot" : false, "lucene_version" : "8.7.0", "minimum_wire_compatibility_version" : "6.8.0", "minimum_index_compatibility_version" : "6.0.0-beta1" }, "tagline" : "You Know, for Search" }请求成功,说明我们的认证配置正确。2.配置kibanatar -zxvf kibana-7.10.2-linux-x86_64.tar.gz ##kibana进程需要和es进程通信,都采用esuser用户启动 chown -R esuser:esuser kibana-7.10.2-linux-x86_64 cd kibana-7.10.2-linux-x86_64/config vi kibana.yml采用cat命名查看启动的配置项:cat kibana.yml | egrep -v "^$|#" server.port: 5601 server.host: "0.0.0.0" server.name: "my-kibana" elasticsearch.hosts: ["http://localhost:9200"] kibana.index: ".kibana" elasticsearch.username: "kibana_system" elasticsearch.password: "u021T4dZgwBNTeAGg4k8" #kibana的界面使用中文显示 i18n.locale: "zh-CN"注意⚠️:启用 Elasticsearch 安全功能后,用户必须使用有效的用户名和密码登录 Kibana。您将配置 Kibana 以使用内置的 kibana_system 用户和您之前创建的密码。 Kibana 执行一些需要使用 kibana_system 用户的后台任务。此帐户不适用于个人用户,并且无权从浏览器登录 Kibana。 相反,您将以elastic超级用户身份登录 Kibana,使用此超级用户帐户来管理空间、创建新用户和分配角色。采用kibana-keystore存储用户密码:1、执行create创建kibana-keystore2、执行add命令添加elasticsearch.password密钥到kibana-keystore中3、修改kibana.yml 配置文件,去除elasticsearch.password配置。[esuser@Mobile bin]$ ./kibana-keystore create Created Kibana keystore in /usr/local/test/kibana-7.10.2-linux-x86_64/config/kibana.keystore [esuser@Mobile bin]$ ./kibana-keystore add elasticsearch.password Enter value for elasticsearch.password: ******************** [esuser@Mobile bin]$ cd ../config/ [esuser@Mobile config]$ vi kibana.yml切换成esuser用户,启动kibana:su esuser ./bin/kibana后台启动,可以执行如下命令:nohup ./bin/kibana >>/dev/null 2>&1 &访问服务器上kibana地址:http://192.168.100.240:5601/使用内置用户elastic登录登录成功:在Management中打开开发工具界面:GET _search { "query": { "match_all": {} } }能正常进行请求操作。3.basic security配置在最低安全配置中添加密码保护后,您需要配置传输层安全 (TLS)。 传输层处理集群中节点之间的所有内部通信。如果您的集群有多个节点,那么您必须在节点之间配置 TLS。 如果您不启用 TLS,生产模式集群将不会启动。配置证书认证:1、生成certificate authority (CA)./bin/elasticsearch-certutil ca说明:可以选择输入一个ca的密码,在使用ca生成证书和私钥的时候需要用到。集群必须验证这些证书的真实性。 推荐的方法是信任特定的证书颁发机构 (CA)。 当节点添加到您的集群时,它们必须使用由同一 CA 签署的证书。2、生成证书和私钥执行命令:./bin/elasticsearch-certutil cert --ca elastic-stack-ca.p12需要输入CA的密码,证书的输出位置,证书的密码Enter password for CA (elastic-stack-ca.p12) : Please enter the desired output file [elastic-certificates.p12]: Enter password for elastic-certificates.p12 :3、拷贝证书elastic-certificates.p12到集群每个节点下的ES下的config/目录中4、集群中每个节点上配置TLS也就是配置SSL证书修改elasticsearch.yml配置文件,新增如下配置:#1、配置集群名称 cluster.name: my-test #2、配置节点名称,注意唯一性 node.name: node-1 #3、配置证书 xpack.security.transport.ssl.enabled: true xpack.security.transport.ssl.verification_mode: certificate xpack.security.transport.ssl.client_authentication: required xpack.security.transport.ssl.keystore.path: elastic-certificates.p12 xpack.security.transport.ssl.truststore.path: elastic-certificates.p12说明:transport的安全认证只对节点间通信有效,采用rest风格的http请求时,只受用户名、密码认证控制。5、设置密钥库密码如果生成证书的时候使用了密码,那么一定要执行以下命令。./bin/elasticsearch-keystore add xpack.security.transport.ssl.keystore.secure_password ./bin/elasticsearch-keystore add xpack.security.transport.ssl.truststore.secure_password4.配置HTTPS经过之前的步骤,我们实现了http请求的用户名、密码认证,es集群节点间的ssl证书认证通信,基本能满足我们的生产环境安全要求。如果希望加强外部http请求的网络安全,可以配置ES的HTTPS访问。4.1 生成http证书./bin/elasticsearch-certutil http1、是否生成CSR,输入n2、是否使用已经存在的CA,输入y3、输入CA的路径,这里注意要使用绝对路径4、输入CA的密码5、输入证书过期时间,默认是5年6、是给每个节点生成一个证书,还是所有节点公用一个证书。这里由于是本地测试,我选择公用一个证书,输入n如果输入y,则会每个节点生成一个证书,每个证书拥有自己的私钥,对应自己的hostname和IP。7、输入节点名称8、输入hostname和IP9、确认是否选项是否正确,输入y10、是否开始生成证书,输入y11、是否希望给证书设置密码,输入密码12、生成zip文件保存的位置,默认就好13、重复在每个节点上执行如上步骤最后会在es的根目录,生成一个elasticsearch-ssl-http.zip文件,使用unzip命令解压后,是如下结构:/elasticsearch |_ README.txt |_ http.p12 |_ sample-elasticsearch.yml/kibana |_ README.txt |_ elasticsearch-ca.pem |_ sample-kibana.yml4.2 配置ES的https拷贝http.p12文件到ES每个节点上的config目录中修改elasticsearch.yml配置文件,新增如下配置:xpack.security.http.ssl.enabled: true xpack.security.http.ssl.keystore.path: http.p12如果在生成http证书的时候设置了密码,一定要执行下面的命令保存密码到elasticsearch-keystore中。./bin/elasticsearch-keystore add xpack.security.http.ssl.keystore.secure_password重启ES后,会发现原来的curl命令不可用curl -X GET -u "elastic:AndTGV1WmKblRS9oSILp" "http://localhost:9200/?pretty" curl: (52) Empty reply from server改为https还是请求失败,原因是curl命令默认不支持https请求。curl -X GET -u "elastic:AndTGV1WmKblRS9oSILp" "https://localhost:9200/?pretty" curl: (60) Peer's certificate issuer has been marked as not trusted by the user. More details here: http://curl.haxx.se/docs/sslcerts.html curl performs SSL certificate verification by default, using a "bundle" of Certificate Authority (CA) public keys (CA certs). If the default bundle file isn't adequate, you can specify an alternate file using the --cacert option. If this HTTPS server uses a certificate signed by a CA represented in the bundle, the certificate verification probably failed due to a problem with the certificate (it might be expired, or the name might not match the domain name in the URL). If you'd like to turn off curl's verification of the certificate, use the -k (or --insecure) option.更加提示,可以通过在curl请求中添加-k的参数,关闭证书认证。curl -X GET -k -u "elastic:AndTGV1WmKblRS9oSILp" "https://localhost:9200/?pretty" { "name" : "node-1", "cluster_name" : "my-test", "cluster_uuid" : "M5EGtEmzRQG5ri3Yy_Wpxg", "version" : { "number" : "7.10.2", "build_flavor" : "default", "build_type" : "tar", "build_hash" : "747e1cc71def077253878a59143c1f785afa92b9", "build_date" : "2021-01-13T00:42:12.435326Z", "build_snapshot" : false, "lucene_version" : "8.7.0", "minimum_wire_compatibility_version" : "6.8.0", "minimum_index_compatibility_version" : "6.0.0-beta1" }, "tagline" : "You Know, for Search" }也可以浏览器上访问https://192.168.100.240:9200/成功访问,说明ES的https配置成功。4.3 配置Kibana到ES的https1、将kibanaelasticsearch-ca.pem拷贝到kibana安装根目录下的config目录中2、修改kibana.yml配置文件#注意这里要使用绝对目录 elasticsearch.ssl.certificateAuthorities: /usr/local/test/kibana-7.10.2-linux-x86_64/config/elasticsearch-ca.pem #这里改为https elasticsearch.hosts: ["https://localhost:9200"]3、重启kibanalsof -i:5601 kill -9 pid /kibana4、尝试重新使用elastic用户登录kibana,访问正常,说明kibana到es的https请求配置成功。4.4 配置HTTPS访问Kibana1、生成p12证书采用如下命令,生成p12证书./bin/elasticsearch-certutil cert -name kibana-server -dns kibana.com,localhost,192.168.100.241根据提示输入证书密码,这里需要注意⚠️,不能输入完全是数字的密码,一定要带有字符。将生成的证书kibana-server.p12拷贝到kibana的config目录下2、保存证书密码./bin/kibana-keystore add server.ssl.keystore.password3、修改kibana.yml配置文件server.ssl.keystore.path: “/usr/local/test/kibana-7.10.2-linux-x86_64/config/kibana-server.p12” server.ssl.enabled: true4、重启kibana,使用https访问成功。官网还有一种证书配置方式,根据csr生成crt证书。但是由于具体生成命令不清楚,没有成功。./bin/elasticsearch-certutil csr -name kibana-server -dns kibana.com生成csr-bundle.zip解压 unzip csr-bundle.zipArchive: csr-bundle.zip creating: kibana-server/ inflating: kibana-server/kibana-server.csr inflating: kibana-server/kibana-server.key这里需要根据csr和key值从CA获取crt证书文件。?????server.ssl.enabled: true server.ssl.certificate: KBN_PATH_CONF/kibana-server.crt server.ssl.key: KBN_PATH_CONF/kibana-server.key四、异常记录:问题一keystore password was incorrect原因:密码库密码不正确解决:./bin/elasticsearch-keystore add xpack.security.transport.ssl.keystore.secure_password ./bin/elasticsearch-keystore add xpack.security.transport.ssl.truststore.secure_password问题二:ERROR: [1] bootstrap checks failed [1]: the default discovery settings are unsuitable for production use; at least one of [discovery.seed_hosts, discovery.seed_providers, cluster.initial_master_nodes] must be configured原因:discovery.seed_hosts, discovery.seed_providers, cluster.initial_master_nodes三个配置至少要配置一个。解决:修改elasticsearch.yml配置文件discovery.seed_hosts: ["127.0.0.1"] cluster.initial_master_nodes: ["node-1"]问题三:启动xpack.security.enabled=true后,必须设置xpack.security.transport.ssl.enabled=trueERROR: [1] bootstrap checks failed [1]: Transport SSL must be enabled if security is enabled on a [basic] license. Please set [xpack.security.transport.ssl.enabled] to [true] or disable security by setting [xpack.security.enabled] to [false] ERROR: Elasticsearch did not exit normally - check the logs at /usr/local/test/elasticsearch-7.10.2/logs/my-test.log解决:修改elasticsearch.yml配置文件xpack.security.transport.ssl.enabled=true问题四:failed to establish trust with server at [127.0.0.1]; the server provided a certificate with subject name [CN=Elastic Certificate Tool Autogenerated CA] and fingerprint [50757f9c9dc63ef848c122261c4ab06a78119430]; the certificate does not have any subject alternative names; the certificate is self-issued; the [CN=Elastic Certificate Tool Autogenerated CA] certificate is not trusted in this ssl context ([(shared)])说明:这是一个警告信息,意思是提醒证书是自行签发的; [CN=Elastic Certificate Tool Autogenerated CA] 证书在此 ssl 上下文中不受信任 ([(shared)])。对功能没有影响,可以忽略。问题五:sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target说明:不能够发现证书路径。检测证书路径配置是否正确。总结主要介绍了通过开启X-Pack组件,实现对ES安全控制。1、最初级的实现是用户名、账号2、其次配置TLS,提升ES集群节点间的安全通信3、最后,配置HTTPS的证书,提升所有外部http请求的安全性。
2022年10月