暂时未有相关云产品技术能力~
前言本文是基于Docker安装的Nginx,并且假设已经配置好证书的Nginx进行项目部署正文一、https证书下载并配置1、项目基于Springboot内置Tomcat,启动,下载Tomcat证书2、解压之后放入项目中的根目录中 3、修改application.yml配置文件server: port: 9100 ssl: key-store: classpath:123_www.example.pfx #证书的路径 key-store-password: 666666 #密码4、如果最后访问时候报错,可以尝试把该证书上传到服务器和jar包同一目录下。二、SpringBoot项目配置1、tomcat 配置类 import org.apache.catalina.Context; import org.apache.catalina.connector.Connector; import org.apache.tomcat.util.descriptor.web.SecurityCollection; import org.apache.tomcat.util.descriptor.web.SecurityConstraint; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.servlet.server.ServletWebServerFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @author xiaojie * @version 1.0 * @description: tomcat配置http转htttps * @date 2022/5/7 8:52 */ @Configuration public class TomcatConfig { @Value("${my.httpServer.port}") private Integer httpServerPort; //http的端口 @Value("${server.port}") private Integer serverPort;//https的端口,也是配置文件中配置的端口 @Bean public ServletWebServerFactory servletContainer() { TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() { @Override protected void postProcessContext(Context context) { SecurityConstraint securityConstraint = new SecurityConstraint(); securityConstraint.setUserConstraint("CONFIDENTIAL"); SecurityCollection collection = new SecurityCollection(); collection.addPattern("/*"); securityConstraint.addCollection(collection); context.addConstraint(securityConstraint); } }; tomcat.addAdditionalTomcatConnectors(redirectConnector()); return tomcat; } private Connector redirectConnector() { Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol"); connector.setScheme("http"); connector.setPort(httpServerPort); connector.setSecure(false); connector.setRedirectPort(serverPort); return connector; } } 配置文件1. my: 2. httpServer: 3. port: 81002、然后将文件打包,并命名为xxx_9100.jar,上传到服务器。将上面的端口8100修改为8101,9100端口不修改,在启动参数中修改,打包后上传到服务器。打俩个jar包是为了做主备,也可以只打一个jar。三、Nginx配置文件1、myapp.confupstream myapp{ server ip:9100; #此处的ip写服务器的真实ip,因为是docker构建的,不然可能访问不到 server ip:9101 backup; #备机 } server { listen 443 ssl; server_name www.example.com; ssl_certificate certs/1_www.example.com.pem; ssl_certificate_key certs/1_www.example.com.key; ssl_session_timeout 5m; ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_session_cache shared:SSL:1m; fastcgi_param HTTPS on; fastcgi_param HTTP_SCHEME https; location / { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_connect_timeout 10s; proxy_send_timeout 60s; proxy_read_timeout 60s; proxy_ignore_client_abort on; proxy_pass https://myapp/; #此处与上面的upstream处对应 } error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } }2、启动项目1. nohup java -jar -Dserver.port=9100 abc_9100.jar >log_9100.log & 2. nohup java -jar -Dserver.port=9101 abc_9101.jar >log_9101.log &说明:9100和9101端口是nginx映射的端口,也是项目application.yml中配置项目的端口3、启动项目
正文一、搭建Nginx1、创建用户组和用户#创建用户组 groupadd nginx #创建用户 useradd -g nginx nginx #第一个参数用户组,第二个参数用户名 #设置密码 passwd nginx 删除用户 userdel nginx #删除用户组 groupdel nginx #删除用户所在目录 rm -rf nginx如果在创建用户过程总发生 Creating mailbox file: File exists错误执行如下命令rm -rf /var/spool/mail/nginx #nginx为对应的用户名2、从阿里云申请ssl证书a、b、c、 d、 e、这里我们选择SSL证书这个,有免费的证书 f、点击申请证书,填写你的信息,等待审核通过后,下载证书 g、下载证书,我们选择nginx证书下载 3、阿里云域名解析配置a、请确保你的域名已经实名认证 b、添加记录 A是将域名指向一个ip 。CNAME :当需要将域名指向另一个域名,再由另一个域名提供 IP 地址,就需要添加 CNAME 记录,最常用到 CNAME 的场景包括做 CDN、企业邮箱、全局流量管理等。c、配置完成后,ping一下你的域名,如果能ping通,证明你的域名DNS解析成功 4、docker创建Nginxmkdir -p /data/nginx/{conf,conf.d,html,logs,certs}a、将上面下载的证书解压之后,上传到/data/nginx/certs目录下b、在/data/conf文件下创建nginx.conf文件user nginx; worker_processes auto; #一般为cpu核数 error_log /var/log/nginx/error.log notice; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; #log格式 log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; #tcp_nopush on; keepalive_timeout 65; gzip on; #开启压缩 include /etc/nginx/conf.d/*.conf; }c、在/data/html文件下创建html文件 index.html<!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> <style> html { color-scheme: light dark; } body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } </style> </head> <body> <h1>Welcome to nginx!</h1> <p>If you see this page, the nginx web server is successfully installed and working. Further configuration is required.</p> <p>For online documentation and support please refer to <a href="http://nginx.org/">nginx.org</a>.<br/> Commercial support is available at <a href="http://nginx.com/">nginx.com</a>.</p> <p><em>Thank you for using nginx.</em></p> </body> </html>d、在/data/nginx/conf.d/目录创建default.confserver { listen 80; listen [::]:80; server_name www.example.com; #填写域名 #将所有HTTP请求通过rewrite指令重定向到HTTPS rewrite ^(.*) https://$server_name$1 permanent; } #配置443端口 server { listen 443 ssl; # 1.1版本后这样写 server_name www.example.com; #填写域名 ssl_certificate certs/1_www.example.com.pem; #需要将cert-file-name.pem替换成已上传的证书文件的名称。 ssl_certificate_key certs/1_www.example.com.key; #需要将cert-file-name.key替换成已上传的证书私钥文件的名称。 ssl_session_timeout 5m; #表示使用的加密套件的类型。 ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3; #表示使用的TLS协议的类型。 ssl_prefer_server_ciphers on; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_session_cache shared:SSL:1m; fastcgi_param HTTPS on; fastcgi_param HTTP_SCHEME https; location / { proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; root html; index index.html index.htm; } }e、授权文件给nginx用户chown -R nginx:nginx /data/nginxf、创建容器并启动docker run --name nginx -d -p 80:80 \ -p 443:443 \ -v /data/nginx/conf/nginx.conf:/etc/nginx/nginx.conf \ -v /data/nginx/conf.d/:/etc/nginx/conf.d \ -v /data/nginx/html:/etc/nginx/html \ -v /data/nginx/logs:/var/log/nginx \ -v /data/nginx/certs:/etc/nginx/certs \ -v /etc/localtime:/etc/localtime:ro \ nginx:1.21.4二、创建图片服务器 1、在/data/nginx/html目录下创建images目录并将图片文件上传到该目录 2、添加images.confserver { listen 8090 ssl ; #图片服务器监听的端口 server_name images.example.com; #域名 #新的证书,需要将新的证书上传到/certs目录下 ssl_certificate certs/8_images.example.com.pem; ssl_certificate_key certs/8_images.example.com.key; ssl_session_timeout 5m; ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_session_cache shared:SSL:1m; fastcgi_param HTTPS on; fastcgi_param HTTP_SCHEME https; location /images/ { expires 24h; root html; autoindex on; } error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } }3、重新创建nginx容器docker run --name nginx -d -p 80:80 \ -p 443:443 \ -p 8090:8090 \ -v /data/nginx/conf/nginx.conf:/etc/nginx/nginx.conf \ -v /data/nginx/conf.d/:/etc/nginx/conf.d \ -v /data/nginx/html:/etc/nginx/html \ -v /data/nginx/logs:/var/log/nginx \ -v /data/nginx/certs:/etc/nginx/certs \ -v /etc/localtime:/etc/localtime:ro \ nginx:1.21.48090为图片服务器监听的端口,切记一定要映射出该端口!!!4、访问为https访问,http访问会报400的错误。
正文一、什么是SkyWalkingSkyWalking是一个开源的观测平台,用于从服务和云原生等基础设施中收集、分析、聚合以及可视化数据。SkyWalking 提供了一种简便的方式来清晰地观测分布式系统。相比较zipkin而言,skywalking利用agent字节码增强技术实现代码无侵入,通信方式采用GRPC,性能较好,实现方式是java探针,支持告警,支持JVM监控,支持全局调用统计,UI界面更加强大等优点。二、SkyWalking的搭建skywalking使用Agent代理技术,相当于JVM层面的AOP 技术,在执行我们项目的时候,skywalking经过代理,然后收集我们的系统数据,然后经过可视化界面展示。安装skywalking从8.8.0开始,Java Agent从原始的主存储库中分离出来。所以我们需要下载两个文件Apache Downloads 主程序Apache Downloads agent代理在apache-skywalking-apm-9.0.0\apache-skywalking-apm-bin\bin路径下启动,在webapp中的webapp.yml中可以修改启动端口,默认是8080 然后访问 ip:8080agent模块解压后有skywalking-agent.jar三、整合springboot1、整合skywalking由于skywalking是无代码侵入式的,启动项目时需要配置jvm参数#skywalking-agent.jar的路径 -javaagent:E:\java-tools\skywalking\skywalking-agent\skywalking-agent.jar #启动服务的名称 -Dskywalking.agent.service_name=xiaojie-sso #连接到skywalking的地址 -Dskywalking.collector.backend_service=127.0.0.1:118002、初识skywalking界面 安装好之后可以随意点点,提供了CPU、JVM、垃圾回收,数据库、线程池、日志、拓扑图等等很多信息。3、上报日志maven依赖<dependency> <groupId>org.apache.skywalking</groupId> <artifactId>apm-toolkit-logback-1.x</artifactId> <version>8.10.0</version> </dependency>logback.yml<?xml version="1.0" encoding="utf-8" ?> <!---scan这个属性是用来查看配置信息的,scanPeriod的值是固定多长时间扫描一次,周期内新生成的文件会覆盖旧文件--> <configuration> <property name="LOG_FILE_LOCATION" value="./log/"/> <property name="CONSOLE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{50}) - %highlight(%msg) %n"/> <property name="FILE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/> <appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender"> <layout class="ch.qos.logback.classic.PatternLayout"> <pattern>${CONSOLE_LOG_PATTERN}</pattern> </layout> </appender> <appender name="fileInfoLog" class="ch.qos.logback.core.rolling.RollingFileAppender"> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>ERROR</level> <!--匹配就舍去--> <onMatch>DENY</onMatch> <onMismatch>ACCEPT</onMismatch> </filter> <file>${LOG_FILE_LOCATION}/info.log</file> <encoder> <pattern> ${FILE_LOG_PATTERN} </pattern> </encoder> <!--滚动策略--> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <!--日志文件输出的文件名--> <FileNamePattern>${LOG_FILE_LOCATION}/bak/info.%d{yyyy-MM-dd}.%i.log.gz</FileNamePattern> <!--日志文件保留天数--> <MaxHistory>30</MaxHistory> <MaxFileSize>10MB</MaxFileSize> </rollingPolicy> </appender> <appender name="fileErrorLog" class="ch.qos.logback.core.rolling.RollingFileAppender"> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>ERROR</level> </filter> <file>${LOG_FILE_LOCATION}/error.log</file> <encoder> <pattern> <pattern>${FILE_LOG_PATTERN} </pattern> </pattern> </encoder> <!--滚动策略--> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <!--日志文件输出的文件名--> <FileNamePattern>${LOG_FILE_LOCATION}/bak/error.%d{yyyy-MM-dd}.%i.log.gz</FileNamePattern> <!--日志文件保留天数--> <MaxHistory>30</MaxHistory> <MaxFileSize>10MB</MaxFileSize> </rollingPolicy> </appender> <!--打印tranceid--> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder"> <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.mdc.TraceIdMDCPatternLogbackLayout"> <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{tid}] [%thread] %-5level %logger{36} -%msg%n</Pattern> </layout> </encoder> </appender> <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender"> <discardingThreshold>0</discardingThreshold> <queueSize>1024</queueSize> <neverBlock>true</neverBlock> <appender-ref ref="STDOUT"/> </appender> <!--上报日志--> <appender name="grpc-log" class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender"> <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder"> <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.mdc.TraceIdMDCPatternLogbackLayout"> <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{tid}] [%thread] %-5level %logger{36} -%msg%n</Pattern> </layout> </encoder> </appender> <root level="INFO"> <appender-ref ref="grpc-log"/> <appender-ref ref="ASYNC"/> <appender-ref ref="consoleLog"/> <appender-ref ref="fileInfoLog"/> <appender-ref ref="fileErrorLog"/> </root> </configuration>4、持久化配置 修改apache-skywalking-apm-9.0.0\apache-skywalking-apm-bin\config路径下的application.yml第一处、修改storage为mysql,我这里以mysql为例,支持多种修改第二处、修改mysql的连接 需要手动在数据库创建swtest的数据库,然后启动skywalking,表自动创建。四、报警系统 apache-skywalking-apm-9.0.0\apache-skywalking-apm-bin\config路径下的alarm-settings.yml配置了告警策略rules: # Rule unique name, must be ended with `_rule`. service_resp_time_rule: metrics-name: service_resp_time op: ">" threshold: 1000 period: 10 count: 3 silence-period: 5 #过去3分钟内服务平均响应时间超过1秒 message: Response time of service {name} is more than 1000ms in 3 minutes of last 10 minutes. service_sla_rule: # Metrics value need to be long, double or int metrics-name: service_sla op: "<" threshold: 8000 # The length of time to evaluate the metrics period: 10 # How many times after the metrics match the condition, will trigger alarm count: 2 # How many times of checks, the alarm keeps silence after alarm triggered, default as same as period. silence-period: 3 #服务成功率在过去2分钟内低于80% message: Successful rate of service {name} is lower than 80% in 2 minutes of last 10 minutes service_resp_time_percentile_rule: # Metrics value need to be long, double or int metrics-name: service_percentile op: ">" threshold: 1000,1000,1000,1000,1000 period: 10 count: 3 silence-period: 5 #再过去的3分钟内有超过50%,75%,90%,95%,99%响应时间大于1000毫秒 message: Percentile response time of service {name} alarm in 3 minutes of last 10 minutes, due to more than one condition of p50 > 1000, p75 > 1000, p90 > 1000, p95 > 1000, p99 > 1000 service_instance_resp_time_rule: metrics-name: service_instance_resp_time op: ">" threshold: 1000 period: 10 count: 2 silence-period: 5 #最近2分钟内服务实例的平均响应时间超过1秒 message: Response time of service instance {name} is more than 1000ms in 2 minutes of last 10 minutes database_access_resp_time_rule: metrics-name: database_access_resp_time threshold: 1000 op: ">" period: 10 count: 2 #最近2分钟内数据库的平均响应时间超过1秒 message: Response time of database access {name} is more than 1000ms in 2 minutes of last 10 minutes endpoint_relation_resp_time_rule: metrics-name: endpoint_relation_resp_time threshold: 1000 op: ">" period: 10 count: 2 #端点平均响应时间过去2分钟超过1秒,断点可以理解为某个路径 message: Response time of endpoint relation {name} is more than 1000ms in 2 minutes of last 10 minutes # Active endpoint related metrics alarm will cost more memory than service and service instance metrics alarm. # Because the number of endpoint is much more than service and instance. # # endpoint_resp_time_rule: # metrics-name: endpoint_resp_time # op: ">" # threshold: 1000 # period: 10 # count: 2 # silence-period: 5 # message: Response time of endpoint {name} is more than 1000ms in 2 minutes of last 10 minutes webhooks: - http://127.0.0.1:8090/notify/ #告警路径,需要我们自定义接口实现,post接口 # - http://127.0.0.1/go-wechat/属性参照如下 定义告警实体类@Data public class AlarmMessageDto { private String scopeId; private String name; private String id0; private String id1; private String alarmMessage; private long startTime; }定义接口 @Override public void send(List<AlarmMessageDto> alarmMessageList) { //实际生产中应当单独建立一个监控系统的服务,使用mq 发送短信,邮件或者微信公众号模板方式解决,此处只是演示 for (AlarmMessageDto alarm: alarmMessageList) { log.info("报警信息为>>>>>>>>{}",alarm.getAlarmMessage()); } }完整代码参考 spring-boot: Springboot整合redis、消息中间件等相关代码、
正文一、产生背景在微服务系统中,随着业务的发展,系统会变得越来越大,那么各个服务之间的调用关系也就变得越来越复杂。一个 HTTP 请求会调用多个不同的微服务来处理返回最后的结果,在这个调用过程中,可能会因为某个服务出现网络延迟或发送错误导致请求失败,这个时候,对请求调用的监控就显得尤为重要了。Spring Cloud Sleuth+zipkin 提供了分布式服务链路监控的解决方案。二、Sleuth&zipkin介绍Sleuth1、基本术语Span:基本工作单元,发送一个远程调度任务 就会产生一个Span,Span是一个64位ID唯一标识的,Trace是用另一个64位ID唯一标识的,Span还有其他数据信息,比如摘要、时间戳事件、Span的ID、以及进度ID。例如在微服务中我们的一个服务。root span:开始一个 Trace 的初始Span,root span的ID的值等于trace ID。Trace:一系列Span组成的一个树状结构。请求一个微服务系统的API接口,这个API接口,需要调用多个微服务,调用每个微服务都会产生一个新的Span,所有由这个请求产生的Span组成了这个Trace。可以理解为就是微服务中的一条请求链,如订单服务→支付服务→积分服务就是一个trace,其中的每个服务都可以是一个span。Annotation:用来及时记录一个事件的,一些核心注解用来定义一个请求的开始和结束 。这些注解包括以下:cs - Client Sent -客户端发送一个请求,这个注解描述了这个Span的开始。sr - Server Received -服务端获得请求并准备开始处理它,如果将其sr减去cs时间戳便可得到网络传输的时间。ss - Server Sent (服务端发送响应)–该注解表明请求处理的完成(当请求返回客户端),如果ss的时间戳减去sr时间戳,就可以得到服务器请求的时间。cr - Client Received (客户端接收响应)-此时Span的结束,如果cr的时间戳减去cs时间戳便可以得到整个请求所消耗的时间。2、微服务间传递跟踪信息当一个请求被跟踪时,在传播中会添加spanid或者traceid到请求头header。比如spanId,traceId等,但是不会传输详细信息,比如操作名称,传输数据。在请求头信息中多了 4 个属性:x-b3-spanid:一个工作单元(rpc 调用)的唯一标识。x-b3-parentspanid:当前工作单元的上一个工作单元,Root Span(请求链路的第一个工作单元)的值为空。x-b3-traceid:一条请求链条(trace) 的唯一标识。x-b3-sampled:是否被抽样被导出(采样)的标志,1 为需要被导出,0 为不需要被导出。ZipkinZipkin是Twitter的一个开源项目,我们可以使用它来收集各个服务器上请求链路的跟踪数据,并通过它提供的API接口来辅助查询跟踪数据,或者通过UI组件可视化地展示服务调用链路中各个服务节点的是否异常和处理耗时。Reporter: 应用程序中向zipkin发送跟踪数据的组件。Transport: Reporter发送数据给zipkin收集器的传输方式,三种主要传输方式:HTTP、Kafka 和 ScribeStorage:zipkin将跟踪数据保存至Storage中。API 将查询存储以向UI提供数据。zipkin的4个组件:Collector(收集器组件):主要负责收集外部系统跟踪信息,跟踪数据到达Zipkin收集器守护进程后,将对其进行验证、存储和索引,以便Zipkin收集器进行查找。Storage(存储组件):主要负责收到的跟踪信息的存储,默认为存储在内存中,同时支持存储到Mysql、Cassandra以及ElasticSearch。API(Query): 负责查询Storage中存储的数据,提供简单的JSON API获取数据,这个API主要提供给web UI使用。Web UI(展示组件):提供简单的web界面,方便进行跟踪信息的查看以及查询,同时进行相关的分析。工作流程更多内容请看官网OpenZipkin · A distributed tracing system 三、安装下载jar包然后启动java -jar zipkin.jar java -jar zipkin.jar下载地址链接:https://pan.baidu.com/s/1ohHc_7ybNy0V41MpBRcibw 提取码:r9tm 访问地址 your ip :9411四、整合springbootmaven依赖 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> <version>3.0.5</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-sleuth-zipkin --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-sleuth-zipkin</artifactId> <version>3.0.5</version> </dependency> application.yml配置server: port: 8085 spring: application: name: xiaojie-sso cloud: nacos: server-addr: 127.0.0.1:8848 discovery: enabled: true zipkin: base-url: http://127.0.0.1:9411 #一定要加前面的http sender: type: web #以http形式发送 service: name: xiaojie-sso sleuth: sampler: probability: 1.0 #采集所有 rate: 10 #每秒采集数量完整demo参考spring-boot: Springboot整合redis、消息中间件等相关代码 中zipkin模块五、简单使用执行查询可以查看调用的服务,然后有路径,调用服务消耗时间,报错信息等还有支持下载具体的json数据如下面这个样子。[{ "traceId": "2751e7f7bb8c2a11", "parentId": "2751e7f7bb8c2a11", "id": "c06891d9b13ebd18", "kind": "SERVER", "name": "get /send/{name}", "timestamp": 1651306249672816, "duration": 27835, "localEndpoint": { "serviceName": "xiaojie-message", "ipv4": "192.168.1.3" }, "remoteEndpoint": { "ipv4": "192.168.1.3", "port": 55399 }, "tags": { "http.method": "GET", "http.path": "/send/admin", "mvc.controller.class": "MessageServiceImpl", "mvc.controller.method": "send" }, "shared": true }, { "traceId": "2751e7f7bb8c2a11", "parentId": "2751e7f7bb8c2a11", "id": "c06891d9b13ebd18", "kind": "CLIENT", "name": "get", "timestamp": 1651306249618939, "duration": 90664, "localEndpoint": { "serviceName": "xiaojie-sso", "ipv4": "192.168.1.3" }, "tags": { "http.method": "GET", "http.path": "/send/admin" } }, { "traceId": "2751e7f7bb8c2a11", "id": "2751e7f7bb8c2a11", "kind": "SERVER", "name": "get /login", "timestamp": 1651306249533616, "duration": 197531, "localEndpoint": { "serviceName": "xiaojie-sso", "ipv4": "192.168.1.3" }, "remoteEndpoint": { "ipv4": "127.0.0.1", "port": 57738 }, "tags": { "http.method": "GET", "http.path": "/login", "mvc.controller.class": "LoginServiceImpl", "mvc.controller.method": "login" } }]参考 : SpringCloud-07-sleuth+zipkin_慕课手记
正文一、Jvisual Vm的安装1、插件安装Java VisualVM是一个工具,它提供了一个可视化界面,用于查看基于Java技术的应用程序(Java应用程序)在Java虚拟机(JVM)上运行时的详细信息。但是jdk8之后的版本,jdk工具包中不再带有visualvm工具,因此需要我们自行下载。下载地址下载后解压缩,文件结构目录如下修改/etc/visualvm.conf文件,修改为自己的JAVAHOME路径然后进入bin目录启动。 注意:请保证你的JAVAHOME中没有jre目录,不然visaulvm启动不起来,也不显示报错信息2、idea插件方式安装 然后启动,配置vivuusalvm.exe路径和JAVAHOME路径之后启动。二、远程连接本文以docker构建的springboot项目为例设置jmx方式远程连接1、修改dockerfile文件FROM openjdk:17 VOLUME /tmp ADD *.jar app.jar ENV JAVA_OPTS="\ -server \ -Xmx512m \ -Xms512m \ -Xmn64m \ -XX:+UseG1GC \ -XX:GCTimeRatio=99 \ -XX:MaxGCPauseMillis=20 \ -XX:MetaspaceSize=256m \ -XX:MaxMetaspaceSize=256m \ -XX:+PrintGC \ -XX:+PrintGCDetails \ -Xloggc:/var/log/gc-%t.log \ -Dcom.sun.management.jmxremote \ -Dcom.sun.management.jmxremote.rmi.port=10086 \ #远程连接的接口 -Dcom.sun.management.jmxremote.port=10086 \ -Dcom.sun.management.jmxremote.ssl=false \ -Dcom.sun.management.jmxremote.authenticate=false \ -Djava.rmi.server.hostname=192.168.139.163 " #远程主机的ip ENTRYPOINT java ${JAVA_OPTS} --add-opens java.base/java.lang=ALL-UNNAMED -Djava.security.egd=file:/dev/./urandom -jar /app.jar #--add-opens java.base/java.lang=ALL-UNNAMED 解决jdk9之后模块化反射失败问题 #-Djava.security.egd=file:/dev/./urandom 添加随机数使tomcat可以快速启动2、修改jenkins的启动脚本 3、4、效果图 5、有一个不足GC插件不能使用,应该是jdk版本的问题,修改jdk版本之后即可。
正文一、安装Jekins 1、安装docker-compose 2、编辑docker-compose.ymlversion: '3.1' services: jenkins: image: jenkins/jenkins:2.324-centos7 volumes: - /data/jenkins/:/var/jenkins_home - /var/run/docker.sock:/var/run/docker.sock - /usr/bin/docker:/usr/bin/docker - /usr/lib/x86_64-linux-gnu/libltdl.so.7:/usr/lib/x86_64-linux-gnu/libltdl.so.7 ports: - "8080:8080"#jenkins启动的端口 - "8085:8085" #对应你的项目的端口 expose: - "8080" - "50000" privileged: true user: root restart: always container_name: jenkins environment: JAVA_OPTS: '-Djava.util.logging.config.file=/var/jenkins_home/log.properties'3、进入/usr/local/bin目录启动,然后等待安装完成 docker-compose up -d二、jekins配置1、访问yourip:8080/后出现如下页面,因为是docker容器部署需要进入到容器内部获取密码docker exec -it jenkins /bin/bash #jenkins为容器名称,或者写容器id获取密码登录2、默认安装推荐的插件之外还需安装Maven插件 Maven Integration plugin。发布插件 Deploy to container Plugin,安装到tomcat的插件 Publish Over SSH :部署到远程服务上的插件3、环境准备在容器内上安装git 、maven和jdk1、yum -y install git #安装git git version #检查是否安装成功 2、yum install maven -y #安装maven mvn -version #检查是否安装成功 由于本人是jdk17,将jdk17的安装包上传到宿主机上然后复制到容器内安装,解压配置java环境docker cp /root/jdk-17_linux-x64_bin.tar.gz jenkins:/root #复制到容器内 修改变量环境 vim /etc/profile ,如果不支持请在容器上安装vim指令 yum install vim,然后执行如下 source /etc/profile使环境变量生效。环境变量配置如下JAVA_HOME=/usr/local/jdk-17.0.1 CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar PATH=$JAVA_HOME/bin:$PATH export PATH JAVA_HOME CLASSPATH4、配置全局环境a、maven配置 b、jdk配置 c、git配置4、其他配置 三、构建项目1、新建一个maven项目,源码配置,可以全局配置证书,也可以就一个项目配置,此处是单个项目配置2、maven构建配置 clean install -Dmaven.test.skip=true -Ptest 3、启动脚本配置 SERVER_NAME=test-jenkins #镜像和服务的名称,请对应项目pom.xml中的配置 TAG=1.0-SNAPSHOT #信息 #容器id CID=$(docker ps | grep "$SERVER_NAME" | awk '{print $1}') #镜像id IID=$(docker images | grep "$SERVER_NAME" | awk '{print $3}') # 构建docker镜像 if [ -n "$IID" ]; then echo "存在$SERVER_NAME镜像,IID=$IID" docker stop $SERVER_NAME # 停止运行中的容器 docker rm $SERVER_NAME ##删除原来的容器 docker rmi $IID ## 删除原来的镜像 else echo "不存在$SERVER_NAME镜像,开始构建镜像" fi mvn docker:build echo "当前docker 镜像:" docker images | grep $SERVER_NAME echo "启动容器----->" docker run --name $SERVER_NAME -p 8090:8085 -d $SERVER_NAME:$TAG echo "启动服务成功!"4、Dockerfile配置,docker建立在main下的docker文件中 FROM openjdk:17 ##拉取镜像 VOLUME /tmp ADD *.jar app.jar ENV JAVA_OPTS="\ ##jvm参数 -server \ -Xmx512m \ -Xms512m \ -Xmn64m \ -XX:+UseG1GC \ -XX:GCTimeRatio=99 \ -XX:MaxGCPauseMillis=20 \ -XX:MetaspaceSize=256m \ -XX:MaxMetaspaceSize=256m \ -XX:+PrintGC \ -XX:+PrintGCDetails \ -Xloggc:/var/log/gc-%t.log" ENTRYPOINT java ${JAVA_OPTS} --add-opens java.base/java.lang=ALL-UNNAMED -Djava.security.egd=file:/dev/./urandom -jar /app.jar #--add-opens java.base/java.lang=ALL-UNNAMED 解决jdk9之后模块化反射失败问题 #-Djava.security.egd=file:/dev/./urandom 添加随机数使tomcat可以快速启动5、项目pom.xml添加docker-maven-plugin插件 <build> <finalName>jenkins</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <mainClass>com.xiaojie.JenkinsApp</mainClass> </configuration> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> <!-- Docker maven plugin start --> <plugin> <groupId>com.spotify</groupId> <artifactId>docker-maven-plugin</artifactId> <version>1.2.2</version> <configuration> <imageName>${project.artifactId}:${project.version}</imageName> <dockerDirectory>${project.basedir}/src/main/docker</dockerDirectory> <!--复制到容器内的地址--> <resources> <resource> <targetPath>/</targetPath> <directory>${project.build.directory}</directory> <include>${project.build.finalName}.jar</include> </resource> </resources> </configuration> </plugin> <!-- Docker maven plugin end --> </plugins> </build>
正文一、什么是OpenRestyOpenResty是一个基于Nginx与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。OpenResty的目标是让你的Web服务直接跑在Nginx服务内部,充分利用Nginx的非阻塞 I/O 模型,不仅仅对 HTTP 客户端请求,甚至于对远程后端诸如 MySQL、PostgreSQL、Memcached 以及 Redis 等都进行一致的高性能响应。由于OpenResty自身内部有nginx,也可以理解为加强后的nginx。二、OpenResty安装传统方式安装1、获取预编译包wget https://openresty.org/package/centos/openresty.repo 2、如果安装过程中报如下错-bash: wget: command not found 先安装wgetyum -y install wget如果需要更新资源包执行如下命令进行更新sudo yum check-update 列出所有openresty仓库里头的软件包sudo yum --disablerepo="*" --enablerepo="openresty" list available3、安装软件包,默认安装在/usr/local/openresty目录下sudo yum install -y openresty4、 启动nginx#启动 /usr/local/openresty/nginx/sbin/nginx #重启 /usr/local/openresty/nginx/sbin/nginx -s reload #停止 /usr/local/openresty/nginx/sbin/nginx -s stop 5、如果看到如下界面安装成功 6、查看版本信息openresty -vDocker方式安装1、首先需要有docker环境2、获取镜像文件docker pull openresty/openresty 3、创建容器并运行docker run -id --name openresty -p 80:80 openresty/openresty4、以宿主机挂在方式运行 a、创建挂载目录mkdir -p /data/openresty b、将容器中的配置复制到宿主机docker cp openresty:/usr/local/openresty /datac、停止并删除openresty容器 1. docker stop openresty #停止 2. docker rm openresty #删除d、重新启动容器docker run --name openresty \ --restart always \ --privileged=true \ -d -p 80:80 \ -v /data/openresty/nginx/conf/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf \ -v /data/openresty/nginx/logs:/usr/local/openresty/nginx/logs \ -v /etc/localtime:/etc/localtime \ openresty/openresty三、结合lua简单使用1、输出helloworda、在宿主机编辑文件 hello.luangx.say("hello world mayikt");b、将文件拷贝到openresty容器进入容器创建文件docker exec -it openresty /bin/bash #进入容器 mkdir -p /root/lua #创建文件 sudo docker cp /data/openresty/lua/hello.lua openresty:/root/lua/hello.lua #拷贝文件到openresty容器c、修改nginx配置文件如下# nginx.conf -- docker-openresty # # This file is installed to: # `/usr/local/openresty/nginx/conf/nginx.conf` # and is the file loaded by nginx at startup, # unless the user specifies otherwise. # # It tracks the upstream OpenResty's `nginx.conf`, but removes the `server` # section and adds this directive: # `include /etc/nginx/conf.d/*.conf;` # # The `docker-openresty` file `nginx.vh.default.conf` is copied to # `/etc/nginx/conf.d/default.conf`. It contains the `server section # of the upstream `nginx.conf`. # # See https://github.com/openresty/docker-openresty/blob/master/README.md#nginx-config-files # #user nobody; user root root; #建议使用非root用户 worker_processes 1; # Enables the use of JIT for regular expressions to speed-up their processing. pcre_jit on; #error_log logs/error.log; #error_log logs/error.log notice; #error_log logs/error.log info; #pid logs/nginx.pid; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; # Enables or disables the use of underscores in client request header fields. # When the use of underscores is disabled, request header fields whose names contain underscores are marked as invalid and become subject to the ignore_invalid_headers directive. # underscores_in_headers off; #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' # '$status $body_bytes_sent "$http_referer" ' # '"$http_user_agent" "$http_x_forwarded_for"'; #access_log logs/access.log main; # Log in JSON Format # log_format nginxlog_json escape=json '{ "timestamp": "$time_iso8601", ' # '"remote_addr": "$remote_addr", ' # '"body_bytes_sent": $body_bytes_sent, ' # '"request_time": $request_time, ' # '"response_status": $status, ' # '"request": "$request", ' # '"request_method": "$request_method", ' # '"host": "$host",' # '"upstream_addr": "$upstream_addr",' # '"http_x_forwarded_for": "$http_x_forwarded_for",' # '"http_referrer": "$http_referer", ' # '"http_user_agent": "$http_user_agent", ' # '"http_version": "$server_protocol", ' # '"nginx_access": true }'; # access_log /dev/stdout nginxlog_json; # See Move default writable paths to a dedicated directory (#119) # https://github.com/openresty/docker-openresty/issues/119 client_body_temp_path /var/run/openresty/nginx-client-body; proxy_temp_path /var/run/openresty/nginx-proxy; fastcgi_temp_path /var/run/openresty/nginx-fastcgi; uwsgi_temp_path /var/run/openresty/nginx-uwsgi; scgi_temp_path /var/run/openresty/nginx-scgi; sendfile on; #tcp_nopush on; #keepalive_timeout 0; keepalive_timeout 65; #gzip on; server { listen 80; server_name localhost; charset utf-8; set $template_root /root/html; #access_log logs/host.access.log main; # 添加 location /lua { default_type 'text/html'; content_by_lua_file /root/lua/hello.lua; #当访问/lua路径时就会访问到lua脚本 } # redirect server error pages to the static page /50x.html error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } #include /etc/nginx/conf.d/*.conf; #注释掉这句,不然默认加载的是容器内 /etc/nginx/conf.d/*.conf的配置文件 # Don't reveal OpenResty version to clients. # server_tokens off; }d、访问 http://ip/lua 思考:利用上面的特性就可以将我们有些不需要变化的数据预热到服务器中,而减少数据库访问的压力,降低服务器带宽的占用,然后利用lua脚本去维护少量变化的数据。例如商品详情页中的图片,文字描述,页面上的广告位,轮播图等。2、限流a、修改nginx.conf如下# nginx.conf -- docker-openresty # # This file is installed to: # `/usr/local/openresty/nginx/conf/nginx.conf` # and is the file loaded by nginx at startup, # unless the user specifies otherwise. # # It tracks the upstream OpenResty's `nginx.conf`, but removes the `server` # section and adds this directive: # `include /etc/nginx/conf.d/*.conf;` # # The `docker-openresty` file `nginx.vh.default.conf` is copied to # `/etc/nginx/conf.d/default.conf`. It contains the `server section # of the upstream `nginx.conf`. # # See https://github.com/openresty/docker-openresty/blob/master/README.md#nginx-config-files # #user nobody; user root root; #建议使用非root用户 worker_processes 1; # Enables the use of JIT for regular expressions to speed-up their processing. pcre_jit on; #error_log logs/error.log; #error_log logs/error.log notice; #error_log logs/error.log info; #pid logs/nginx.pid; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; # Enables or disables the use of underscores in client request header fields. # When the use of underscores is disabled, request header fields whose names contain underscores are marked as invalid and become subject to the ignore_invalid_headers directive. # underscores_in_headers off; #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' # '$status $body_bytes_sent "$http_referer" ' # '"$http_user_agent" "$http_x_forwarded_for"'; #access_log logs/access.log main; # Log in JSON Format # log_format nginxlog_json escape=json '{ "timestamp": "$time_iso8601", ' # '"remote_addr": "$remote_addr", ' # '"body_bytes_sent": $body_bytes_sent, ' # '"request_time": $request_time, ' # '"response_status": $status, ' # '"request": "$request", ' # '"request_method": "$request_method", ' # '"host": "$host",' # '"upstream_addr": "$upstream_addr",' # '"http_x_forwarded_for": "$http_x_forwarded_for",' # '"http_referrer": "$http_referer", ' # '"http_user_agent": "$http_user_agent", ' # '"http_version": "$server_protocol", ' # '"nginx_access": true }'; # access_log /dev/stdout nginxlog_json; # See Move default writable paths to a dedicated directory (#119) # https://github.com/openresty/docker-openresty/issues/119 client_body_temp_path /var/run/openresty/nginx-client-body; proxy_temp_path /var/run/openresty/nginx-proxy; fastcgi_temp_path /var/run/openresty/nginx-fastcgi; uwsgi_temp_path /var/run/openresty/nginx-uwsgi; scgi_temp_path /var/run/openresty/nginx-scgi; sendfile on; #tcp_nopush on; #keepalive_timeout 0; keepalive_timeout 65; #限流设置 允许每秒有2个请求,超过将会限流 limit_req_zone $binary_remote_addr zone=contentRateLimit:1m rate=2r/s; #gzip on; server { listen 80; server_name localhost; charset utf-8; set $template_root /root/html; #access_log logs/host.access.log main; # 添加 location /lua { default_type 'text/html'; content_by_lua_file /root/lua/hello.lua; #当访问/lua路径时就会访问到lua脚本 } location /rate { #使用限流配置 default_type 'text/html'; limit_req zone=contentRateLimit; content_by_lua_file /root/lua/hello.lua; } # redirect server error pages to the static page /50x.html error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } #include /etc/nginx/conf.d/*.conf; #注释掉这句,不然默认加载的是容器内 /etc/nginx/conf.d/*.conf的配置文件 # Don't reveal OpenResty version to clients. # server_tokens off; }b、重启openresty容器,访问,当快速访问时页面报错,错误码为503 c、处理突发流量控制# nginx.conf -- docker-openresty # # This file is installed to: # `/usr/local/openresty/nginx/conf/nginx.conf` # and is the file loaded by nginx at startup, # unless the user specifies otherwise. # # It tracks the upstream OpenResty's `nginx.conf`, but removes the `server` # section and adds this directive: # `include /etc/nginx/conf.d/*.conf;` # # The `docker-openresty` file `nginx.vh.default.conf` is copied to # `/etc/nginx/conf.d/default.conf`. It contains the `server section # of the upstream `nginx.conf`. # # See https://github.com/openresty/docker-openresty/blob/master/README.md#nginx-config-files # #user nobody; user root root; #建议使用非root用户 worker_processes 1; # Enables the use of JIT for regular expressions to speed-up their processing. pcre_jit on; #error_log logs/error.log; #error_log logs/error.log notice; #error_log logs/error.log info; #pid logs/nginx.pid; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; # Enables or disables the use of underscores in client request header fields. # When the use of underscores is disabled, request header fields whose names contain underscores are marked as invalid and become subject to the ignore_invalid_headers directive. # underscores_in_headers off; #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' # '$status $body_bytes_sent "$http_referer" ' # '"$http_user_agent" "$http_x_forwarded_for"'; #access_log logs/access.log main; # Log in JSON Format # log_format nginxlog_json escape=json '{ "timestamp": "$time_iso8601", ' # '"remote_addr": "$remote_addr", ' # '"body_bytes_sent": $body_bytes_sent, ' # '"request_time": $request_time, ' # '"response_status": $status, ' # '"request": "$request", ' # '"request_method": "$request_method", ' # '"host": "$host",' # '"upstream_addr": "$upstream_addr",' # '"http_x_forwarded_for": "$http_x_forwarded_for",' # '"http_referrer": "$http_referer", ' # '"http_user_agent": "$http_user_agent", ' # '"http_version": "$server_protocol", ' # '"nginx_access": true }'; # access_log /dev/stdout nginxlog_json; # See Move default writable paths to a dedicated directory (#119) # https://github.com/openresty/docker-openresty/issues/119 client_body_temp_path /var/run/openresty/nginx-client-body; proxy_temp_path /var/run/openresty/nginx-proxy; fastcgi_temp_path /var/run/openresty/nginx-fastcgi; uwsgi_temp_path /var/run/openresty/nginx-uwsgi; scgi_temp_path /var/run/openresty/nginx-scgi; sendfile on; #tcp_nopush on; #keepalive_timeout 0; keepalive_timeout 65; #限流设置 允许每秒有2个请求,超过将会限流 limit_req_zone $binary_remote_addr zone=contentRateLimit:1m rate=2r/s; #gzip on; server { listen 80; server_name localhost; charset utf-8; set $template_root /root/html; #access_log logs/host.access.log main; # 添加 location /lua { default_type 'text/html'; content_by_lua_file /root/lua/hello.lua; #当访问/lua路径时就会访问到lua脚本 } location /rate { #使用限流配置 default_type 'text/html'; #平均每秒允许不超过2个请求,突发不超过5个请求,并且处理突发5个请求的时候,没有延迟,等到完成之后,按照正常的速率处理 limit_req zone=contentRateLimit burst=5 nodelay; content_by_lua_file /root/lua/hello.lua; } # redirect server error pages to the static page /50x.html error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } #include /etc/nginx/conf.d/*.conf; #注释掉这句,不然默认加载的是容器内 /etc/nginx/conf.d/*.conf的配置文件 # Don't reveal OpenResty version to clients. # server_tokens off; }d、控制并发连接数# nginx.conf -- docker-openresty # # This file is installed to: # `/usr/local/openresty/nginx/conf/nginx.conf` # and is the file loaded by nginx at startup, # unless the user specifies otherwise. # # It tracks the upstream OpenResty's `nginx.conf`, but removes the `server` # section and adds this directive: # `include /etc/nginx/conf.d/*.conf;` # # The `docker-openresty` file `nginx.vh.default.conf` is copied to # `/etc/nginx/conf.d/default.conf`. It contains the `server section # of the upstream `nginx.conf`. # # See https://github.com/openresty/docker-openresty/blob/master/README.md#nginx-config-files # #user nobody; user root root; #建议使用非root用户 worker_processes 1; # Enables the use of JIT for regular expressions to speed-up their processing. pcre_jit on; #error_log logs/error.log; #error_log logs/error.log notice; #error_log logs/error.log info; #pid logs/nginx.pid; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; # Enables or disables the use of underscores in client request header fields. # When the use of underscores is disabled, request header fields whose names contain underscores are marked as invalid and become subject to the ignore_invalid_headers directive. # underscores_in_headers off; #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' # '$status $body_bytes_sent "$http_referer" ' # '"$http_user_agent" "$http_x_forwarded_for"'; #access_log logs/access.log main; # Log in JSON Format # log_format nginxlog_json escape=json '{ "timestamp": "$time_iso8601", ' # '"remote_addr": "$remote_addr", ' # '"body_bytes_sent": $body_bytes_sent, ' # '"request_time": $request_time, ' # '"response_status": $status, ' # '"request": "$request", ' # '"request_method": "$request_method", ' # '"host": "$host",' # '"upstream_addr": "$upstream_addr",' # '"http_x_forwarded_for": "$http_x_forwarded_for",' # '"http_referrer": "$http_referer", ' # '"http_user_agent": "$http_user_agent", ' # '"http_version": "$server_protocol", ' # '"nginx_access": true }'; # access_log /dev/stdout nginxlog_json; # See Move default writable paths to a dedicated directory (#119) # https://github.com/openresty/docker-openresty/issues/119 client_body_temp_path /var/run/openresty/nginx-client-body; proxy_temp_path /var/run/openresty/nginx-proxy; fastcgi_temp_path /var/run/openresty/nginx-fastcgi; uwsgi_temp_path /var/run/openresty/nginx-uwsgi; scgi_temp_path /var/run/openresty/nginx-scgi; sendfile on; #tcp_nopush on; #keepalive_timeout 0; keepalive_timeout 65; #限流设置 允许每秒有2个请求,超过将会限流 limit_req_zone $binary_remote_addr zone=contentRateLimit:1m rate=2r/s; #根据IP地址来限制,存储内存大小10M limit_conn_zone $binary_remote_addr zone=perip:10m; limit_conn_zone $server_name zone=perserver:10m; #gzip on; server { listen 80; server_name localhost; charset utf-8; set $template_root /root/html; #access_log logs/host.access.log main; # 添加 location /lua { default_type 'text/html'; content_by_lua_file /root/lua/hello.lua; #当访问/lua路径时就会访问到lua脚本 } location /rate { #使用限流配置 default_type 'text/html'; #平均每秒允许不超过2个请求,突发不超过5个请求,并且处理突发5个请求的时候,没有延迟,等到完成之后,按照正常的速率处理 limit_req zone=contentRateLimit burst=5 nodelay; content_by_lua_file /root/lua/hello.lua; } location /rate1{ limit_conn perip 0;#单个客户端ip与服务器的连接数 ,设置为0然后连接不上 limit_conn perserver 10; #限制与服务器的总连接数 default_type 'text/html'; content_by_lua_file /root/lua/hello.lua; } # redirect server error pages to the static page /50x.html error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } #include /etc/nginx/conf.d/*.conf; #注释掉这句,不然默认加载的是容器内 /etc/nginx/conf.d/*.conf的配置文件 # Don't reveal OpenResty version to clients. # server_tokens off; }
前言Apache ShardingSphere 是一套开源的分布式数据库解决方案组成的生态圈,旨在充分合理地在分布式的场景下利用关系型数据库的计算和存储能力。具体内容请看官方ShardingSphere。本文主要记录一下Springboot整合ShardingSphere,并实现精确分片算法、范围分片算法、复合分片算法、读写分离、读写分离+分表的配置记录。正文SpringBoot整合ShardingSpheremaven依赖 <dependencies> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>sharding-jdbc-spring-boot-starter</artifactId> <version>4.1.1</version> </dependency> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>sharding-jdbc-spring-namespace</artifactId> <version>4.1.1</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.shardingsphere/sharding-jdbc-core --> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>sharding-jdbc-core</artifactId> <version>4.1.1</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.12</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>5.3.14</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.4</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> </dependencies>行表达式分片策略行表达式分辨策略使用Groovy的表达式,提供对SQL语句中的=和IN的分片操作并且只支持单分片(针对一个字段分片例如id)的操作。例如tb_user_$->{id%2}表示通过id对2取模,实现的效果是tb_user_0存放id为偶数的数据,tb_user_1存放id为奇数的数据。配置文件如下#基于行策略实现的分表分库 spring: shardingsphere: datasource: #数据源名称,多个值用逗号隔开 names: ds0,ds1 ds0: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/test_1?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai username: root password: root ds1: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/test_2?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai username: root password: root sharding: tables: tb_user: #逻辑表名,需要与mapper中sql语句中的表名一致 actual-data-nodes: ds$->{0..1}.tb_user_$->{0..1} #实际的节点名称 例如 ds0.tb_user_0,ds0.tb_user_1,ds1.tb_user_0,ds1.tb_user_1 table-strategy: inline: sharding-column: id #分片字段 algorithm-expression: tb_user_$->{id % 2} #分表表达式 database-strategy: inline: sharding-column: id algorithm-expression: ds$->{id % 2} #分库表达式 key-generator: column: id #id生成策略,雪花算法,uuid type: SNOWFLAKE default-data-source-name: ds0 #不进行分表分库的表,操作的默认数据源 props: sql: show: true #显示sql #注意 #没有分库,只分表的情况下,不需要配置分库策略,配置如下 。结果是ds0.tb_user_0,ds0.tb_user_1 #sharding: # tables: # tb_user: #逻辑表名,需要与mapper中sql语句中的表名一致 # actual-data-nodes: ds0.tb_user_$->{0..1} #实际的节点名称 例如 ds0.tb_user_0,ds0.tb_user_1 # table-strategy: # inline: # sharding-column: id #分片字段 # algorithm-expression: tb_user_$->{id % 2} #分表表达式 # key-generator: # column: id #id生成策略,雪花算法,uuid # type: SNOWFLAKE #如果只分库,不分表,那么需要每个库中的表名称表结构是一样的,那么配置格式如下 #sharding: # tables: # tb_user: #逻辑表名,需要与mapper中sql语句中的表名一致 # actual-data-nodes: ds$->{0..1}.tb_user #实际的节点名称 例如 ds0.tb_user,ds1.tb_user # database-strategy: # inline: # sharding-column: id # algorithm-expression: ds$->{id % 2} #分库表达式 # key-generator: # column: id #id生成策略,雪花算法,uuid # type: SNOWFLAKE #如果按照最上面的配置,结果将是ds0.tb_user_0,ds1.tb_user_1两张表有数据,其他的表将不会存放数据,此时就需要标准分片算法来实现。 标准分片策略标准分片策略提供对SQL语句中的=, >, <, >=, <=, IN和BETWEEN AND的分片操作支持。StandardShardingStrategy只支持单分片键,提供PreciseShardingAlgorithm和RangeShardingAlgorithm两个分片算法。PreciseShardingAlgorithm是必选的,用于处理=和IN的分片。RangeShardingAlgorithm是可选的,用于处理BETWEEN AND, >, <, >=, <=分片,如果不配置RangeShardingAlgorithm,SQL中的BETWEEN AND将按照全库路由处理。配置文件#标准分片策略 spring: shardingsphere: datasource: names: ds0,ds1 ds0: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/test_1?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai username: root password: root ds1: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/test_2?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai username: root password: root sharding: tables: tb_user: #逻辑表名 actual-data-nodes: ds$->{0..1}.tb_user_$->{0..1} #实际的数据库节点 key-generator: column: id type: SNOWFLAKE database-strategy: standard: #自定义数据库分片算法 sharding-column: age range-algorithm-class-name: com.xiaojie.sharding.sphere.shardingalgorithm.MyDBShardingAlgorithm precise-algorithm-class-name: com.xiaojie.sharding.sphere.shardingalgorithm.MyDBShardingAlgorithm table-strategy: standard: #自定义表分片算法 sharding-column: id range-algorithm-class-name: com.xiaojie.sharding.sphere.shardingalgorithm.MyTableShardingAlgorithm precise-algorithm-class-name: com.xiaojie.sharding.sphere.shardingalgorithm.MyTableShardingAlgorithm default-data-source-name: ds0 #不使用分表分库策略的数据源 props: sql: show: true #显示sql自定义算法类自定义分片算法可以根据自己的需要,如按照年、季度、月、星期、天、或者地区等等需要自己实现规则。数据库自定义分片算法package com.xiaojie.sharding.sphere.shardingalgorithm; import com.google.common.collect.Range; import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm; import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue; import org.apache.shardingsphere.api.sharding.standard.RangeShardingAlgorithm; import org.apache.shardingsphere.api.sharding.standard.RangeShardingValue; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.Collection; /** * @Description:自定义数据库分片算法 数据库的分片字段可以和分表分片字段一样,也可以不一样 * 下面配置 分库字段按照age字段,如果年龄超过30那么放在ds0,如果没有超过放在ds1, * 分表字段按照id字段存放。偶数存放到tb_user_0、奇数存放到tb_user_1 * @author: yan * @date: 2022.03.12 */ @Component public class MyDBShardingAlgorithm implements PreciseShardingAlgorithm<Integer>, RangeShardingAlgorithm<Long> { @Override public String doSharding(Collection<String> dbNames, PreciseShardingValue<Integer> shardingValue) { // for (String dbName : dbNames) { // /** // * 取模算法,分片健 % 表数量 数据库 // */ // Integer age = shardingValue.getValue(); // String tableIndex = age%dbNames.size()+""; // if (dbName.endsWith(tableIndex)) { // return dbName;//返回数据库名称 比如db0,db1 // } // } //如果大于30岁放在db0,小于等于30放在db1 if (shardingValue.getValue() > 30) { return (String) dbNames.toArray()[0]; } else { return (String) dbNames.toArray()[1]; } // throw new IllegalArgumentException(); } @Override public Collection<String> doSharding(Collection<String> dbNames, RangeShardingValue<Long> shardingValue) { Collection<String> collect = new ArrayList<>();//数据库节点名称 Range valueRange = shardingValue.getValueRange();//查询返回 String lowerPoint = String.valueOf(valueRange.hasLowerBound() ? valueRange.lowerEndpoint() : "");//下限 String upperPoint = String.valueOf(valueRange.hasUpperBound() ? valueRange.upperEndpoint() : "");//上限 //判断上限,下限值是否存在,如果不存在赋给默认值。用于处理查询条件中只有 >或<一个条件,不是一个范围查询的情况 long lowerEndpoint = 0; //最小值 long lupperEndpoint = 0;//最大值 if (!lowerPoint.isEmpty() && !upperPoint.isEmpty()) { lowerEndpoint = Math.abs(Long.parseLong(lowerPoint)); lupperEndpoint = Math.abs(Long.parseLong(upperPoint)); } else if (lowerPoint.isEmpty() && !upperPoint.isEmpty()) { lupperEndpoint = Math.abs(Long.parseLong(upperPoint)); lowerEndpoint = 0; } else if (!lowerPoint.isEmpty() && upperPoint.isEmpty()) { lowerEndpoint = Math.abs(Long.parseLong(lowerPoint)); lupperEndpoint = 40; } // 循环范围计算分库逻辑 for (long i = lowerEndpoint; i <= lupperEndpoint; i++) { for (String db : dbNames) { if (db.endsWith(i % dbNames.size() + "")) { collect.add(db); } } } return collect; } }表自定义分片算法package com.xiaojie.sharding.sphere.shardingalgorithm; import com.google.common.collect.Range; import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm; import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue; import org.apache.shardingsphere.api.sharding.standard.RangeShardingAlgorithm; import org.apache.shardingsphere.api.sharding.standard.RangeShardingValue; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.Collection; /** * @Description:自定义表分片算法 #范围分片算法类名称,用于BETWEEN,可选。该类需实现RangeShardingAlgorithm接口并提供无参数的构造器 * shardingsphare默认查询只支持=,between and 这种查询,像>,<,>=,<=这种查询目前不支持, * 除非通过继承自定义接口RangeShardingAlgorithm实现,否则无法使用>,<,>=,<=。 * 同时也需要实现PreciseShardingAlgorithm<String>接口 * @author: yan * @date: 2022.03.12 */ @Component public class MyTableShardingAlgorithm implements PreciseShardingAlgorithm<String>, RangeShardingAlgorithm<Long> { @Override public Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<Long> shardingValue) { Range<Long> valueRange = shardingValue.getValueRange();//获得输入的查询条件范围 String slowerEndpoint = String.valueOf(valueRange.hasLowerBound() ? valueRange.lowerEndpoint() : "");//查询条件下限 String supperEndpoint = String.valueOf(valueRange.hasUpperBound() ? valueRange.upperEndpoint() : "");//查询条件上限 //处理只有下限或上限的范围 long lowerEndpoint = 0; long lupperEndpoint = 0; if (!slowerEndpoint.isEmpty() && !supperEndpoint.isEmpty()) { lowerEndpoint = Math.abs(Long.parseLong(slowerEndpoint)); lupperEndpoint = Math.abs(Long.parseLong(supperEndpoint)); } else if (slowerEndpoint.isEmpty() && !supperEndpoint.isEmpty()) { lupperEndpoint = Math.abs(Long.parseLong(supperEndpoint)); lowerEndpoint = 18; } else if (!slowerEndpoint.isEmpty() && supperEndpoint.isEmpty()) { lowerEndpoint = Math.abs(Long.parseLong(slowerEndpoint)); lupperEndpoint = 40; } Collection<String> collect = new ArrayList<>(); // 逐个读取查询范围slowerEndpoint~lupperEndpoint的值,得对应的表名称 for (long i = lowerEndpoint; i <= lupperEndpoint; i++) { for (String each : availableTargetNames) { if (each.endsWith("_" + (i % availableTargetNames.size()))) { if (!collect.contains(each)) { collect.add(each); } } } } return collect; } @Override public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<String> shardingValue) { for (String each : availableTargetNames) { { String hashCode = String.valueOf(shardingValue.getValue());//配置文件中,分表字段对应的值,也是查询条件中输入的查询条件 long segment = Math.abs(Long.parseLong(hashCode)) % availableTargetNames.size(); if (each.endsWith("_" + segment + "")) {// return each; } } } throw new RuntimeException(shardingValue + "没有匹配到表"); } }符合分片策略复合分片策略提供对SQL语句中的=, >, <, >=, <=, IN和BETWEEN AND的分片操作支持。ComplexShardingStrategy支持多分片键,自定义分片规则需要实现ComplexKeysShardingAlgorithm接口配置文件#复合分片策略 spring: shardingsphere: datasource: names: ds0,ds1 ds0: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/test_1?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai username: root password: root ds1: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/test_2?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai username: root password: root sharding: tables: tb_user: actual-data-nodes: ds$->{0..1}.tb_user_$->{0..1} key-generator: column: id type: SNOWFLAKE database-strategy: #库分片策略 complex: sharding-columns: age,id #分片字段 algorithm-class-name: com.xiaojie.sharding.sphere.shardingalgorithm.MyDBComplexShardingStrategy table-strategy: #表分片策略 complex: sharding-columns: age,id #分片字段 algorithm-class-name: com.xiaojie.sharding.sphere.shardingalgorithm.MyTableComplexShardingStrategy default-data-source-name: ds0 props: sql: show: true分库算法package com.xiaojie.sharding.sphere.shardingalgorithm; import com.google.common.collect.Range; import org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingAlgorithm; import org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingValue; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.Collection; import java.util.Map; /** * @Description: 复合分片 分库算法 * @author: yan * @date: 2022.03.12 */ @Component public class MyDBComplexShardingStrategy implements ComplexKeysShardingAlgorithm { @Override public Collection<String> doSharding(Collection availableTargetNames, ComplexKeysShardingValue shardingValue) { //分片的字段集合 Map<String, Collection> columnMap = shardingValue.getColumnNameAndShardingValuesMap(); //分片的范围规则 Map<String, Range> rangeValuesMap = shardingValue.getColumnNameAndRangeValuesMap(); //获取分片字段的集合 Collection<Integer> agesColumn = columnMap.get("age"); Collection<Long> idColumn = columnMap.get("id"); ArrayList<String> list = new ArrayList(); for (Integer age : agesColumn) { for (Long id : idColumn) { String suffix = null; if (age > 30) { suffix = id % age % availableTargetNames.size() + ""; } else { suffix = (id + age) % availableTargetNames.size() + ""; } for (Object db : availableTargetNames) { String dbName = (String) db; if (dbName.endsWith(suffix)) { list.add(dbName); } } } } return list; } }读写分离支持提供一主多从的读写分离配置,可独立使用,也可配合分库分表使用。独立使用读写分离支持SQL透传。同一线程且同一数据库连接内,如有写入操作,以后的读操作均从主库读取,用于保证数据一致性。基于Hint的强制主库路由。不支持主库和从库的数据同步。主库和从库的数据同步延迟导致的数据不一致。主库双写或多写。跨主库和从库之间的事务的数据不一致。主从模型中,事务中读写均用主库。配置文件##读写分离配置 spring: shardingsphere: datasource: names: master,slave0 master: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/my_test?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai username: root password: root slave0: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:33060/my_test?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai username: root password: root masterslave: load-balance-algorithm-type: round_robin name: ms master-data-source-name: master slave-data-source-names: slave0 props: sql: show: true读写分离+数据库分表由于只有一个主库,只实现了分表功能,分库策略同非读写分离的配置一样。配置文件#主从复制+分表 spring: shardingsphere: datasource: names: master0,master0slave0 master0: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/my_test?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai username: root password: root master0slave0: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/my_test?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai username: root password: root sharding: tables: tb_user: #逻辑表名 actual-data-nodes: ds0.tb_user_$->{0..1} table-strategy: #分表策略 inline: sharding-column: id algorithm-expression: tb_user_$->{id % 2} key-generator: column: id type: SNOWFLAKE #绑定表,指分片规则一致的主表和子表。例如:t_order表和t_order_item表,均按照order_id分片,则此两张表互为绑定表关系。 #绑定表之间的多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升 # binding-tables: t_order,t_order_item #广播表,适用于数据量不大且需要与海量数据的表进行关联查询的场景 #广播表概念只存在有分库的情况,如果只是分表或主从,不涉及这个概念,配置了也没啥意义。以下论述均依据存在分库的情况 #广播表在每个数据库都有一个,且数据一样,适合字典表场景,数据量少。 #当插入一条数据时,所有库的tb_user表都会插入一条一模一样数据(可能出现分布式事务问题) # broadcast-tables:t_config #分库策略 # default-database-strategy: # inline: # sharding-column: id # algorithm-expression: master$->{id % 2} master-slave-rules: ds0: master-data-source-name: master0 slave-data-source-names: master0slave0 props: sql: show: true #显示sql 注意:1、数据库如图 2、本整合基于shardingsphere4.1.1;jdk173、如果你的jdk是大于8的运行过程可能报如下错误 这种情况一般在使用高于 Java 8 版本的 JDK 时会出现,原因是在 Java 9 及之后的版本对源码进行了模块化重构,public 不再意味着完全开放,而是使用了 export 的机制来更细粒度地控制可见性。解决方法在JVM启动参数上添加如下参数 --add-opens java.base/java.lang=ALL-UNNAMED完整项目和sql文件请自取
前言又到了金三银四的找工作阶段,你一定被问过MQ是如何保证消息可靠性的或者MQ是如何保证消息不丢失的。我们都知道MQ发送消息一般分为三个阶段分别是生产者发送消息到MQ、MQ存储消息到内存或者硬盘,消费者消费消息。但是这三个过程都有可能因为种种原因导致消息丢失。例如在生产者发送阶段,这个阶段可能由于网络延迟导致mq消息丢失;存储阶段,Broker将消息先放到内存,然后再根据刷盘策略持久化到硬盘上,但是刚收到消息,还没持久化到硬盘服务器宕机了,那么消息就会丢失。在消费端消费时,mq由于网络原因在传输过程中把消息传丢了,而此时MQ也从队列中把消息删除了,或者消费者消费失败消息丢失了等等。那么MQ是如何保证消息不丢失的呢,下面总结一下rocketmq、rabbitmq、kafka是如何保证消息不丢失的。正文RocketMQProducer保证消息不丢失1、RocketMQ发送消息有三种模式,即同步发送,异步发送、单向发送。同步发送消息时会同步阻塞等待Broker返回发送结果,如果发送失败不会收到发送结果SendResult,这种是最可靠的发送方式。异步发送消息可以在回调方法中得知发送结果。单向发送是消息发送完之后就不管了,不管发送成功没成功,是最不可靠的一种方式。 /** * @description: 单向发送 * 这种方式主要用在不特别关心发送结果的场景,例如日志发送。 * @param: * @return: void * @author xiaojie * @date: 2021/11/9 23:39 */ public void sendMq() { for (int i = 0; i < 10; i++) { rocketMQTemplate.convertAndSend("xiaojie-test", "测试发送消息》》》》》》》》》" + i); } } /***********************************************************************************/ /** * @description: 同步发送 * 这种可靠性同步地发送方式使用的比较广泛,比如:重要的消息通知,短信通知。 * @param: * @return: void * @author xiaojie * @date: 2021/11/10 22:25 */ public void sync() { SendResult sendResult = rocketMQTemplate.syncSend("xiaojie-test", "sync发送消息。。。。。。。。。。"); log.info("发送结果{}", sendResult); } /***********************************************************************************/ /** * @description: 异步发送 * 异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待Broker的响应。 * @param: * @return: void * @author xiaojie * @date: 2021/11/10 22:29 */ public void async() { String msg = "异步发送消息。。。。。。。。。。"; log.info(">msg:<<" + msg); rocketMQTemplate.asyncSend("xiaojie-test", msg, new SendCallback() { @Override public void onSuccess(SendResult var1) { log.info("异步发送成功{}", var1); } @Override public void onException(Throwable var1) { //发送失败可以执行重试 log.info("异步发送失败{}", var1); } }); }2、生产者的重试机制 mq为生产者提供了失败重试机制,同步发送和异步发送默认都是失败重试两次当然可以修改重试次数,如果多次还是失败,那么可以采取记录这条信息,然后人工采取补偿机制。Broker保证消息不丢失1、刷盘策略RocketMq持久化消息有两种策略即同步刷盘和异步刷盘。默认情况下是异步刷盘,此模式下当生产者把消息发送到broker,消息存到内存之后就认为消息发送成功了,就会返回给生产者消息发送成功的结果。但是如果消息还没持久化到硬盘,服务器宕机了,那么消息就会丢失。同步刷盘是当Broker接收到消息并且持久化到硬盘之后才会返回消息发送成功的结果,这样就会保证消息不会丢失,但是同步刷盘相对于异步刷盘来说效率上有所降低,大概降低10%,具体情况根据业务需求设定吧。修改配置文件中刷盘方式,ASYNC_FLUSH=异步刷盘,SYNC_FLUSH=同步刷盘2、集群模式#主从复制方式ASYNC_MASTER异步复制,SYNC_MASTER同步复制 brokerRole=SYNC_MASTER #刷盘方式,ASYNC_FLUSH=异步刷盘,SYNC_FLUSH=同步刷盘 flushDiskType=SYNC_FLUSH此模式是broker保证消息不丢失的配置,主从复制同步复制,刷盘模式同步刷盘,但是这种模式下性能会有所降低。Consumer保证消息不丢失1、手动ack/** * @author xiaojie * @version 1.0 * @description: 消费端确认消息消费成功的消费者 * @date 2022/3/8 23:23 */ @Component @Slf4j public class MqConsumerAck implements MessageListenerConcurrently { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) { for (MessageExt msg:msgs){ log.info("接收到的消息是>>>>>>>{}",new String(msg.getBody())); } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }2、消费者消费失败重试机制消费者消费失败会自动重试,如果消费失败没有手动ack则会自动重试15次。RabbitMQProducer保证消息不丢失1、rabbitMQ引入了事务机制和确认机制(confirm)事务机制开启之后,相当于同步执行,必然会降低系统的性能,因此一般我们不采用这种方式。确实机制,是当mq收到生产者发送的消息时,会返回一个ack告知生产者,收到了这条消息,如果没有收到,那就采取重试机制后者其他方式补偿。事务模式 public static void main(String[] args) { try { System.out.println("生产者启动成功.."); // 1.创建连接 connection = MyConnection.getConnection(); // 2.创建通道 channel = connection.createChannel(); channel.queueDeclare(QUEUE_NAME, false, false, false, null); String msg = "测试事务机制保证消息发送可靠性。。。。"; channel.txSelect(); //开启事务 channel.basicPublish("", QUEUE_NAME, null, msg.getBytes(StandardCharsets.UTF_8)); //发生异常时,mq中并没有新的消息入队列 //int i=1/0; //没有发生异常,提交事务 channel.txCommit(); System.out.println("生产者发送消息成功:" + msg); } catch (Exception e) { e.printStackTrace(); //发生异常则回滚事务 try { if (channel != null) { channel.txRollback(); } } catch (IOException ioException) { ioException.printStackTrace(); } } finally { try { if (channel != null) { channel.close(); } if (connection != null) { connection.close(); } } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } }comfirm模式 #开启生产者确认模式 publisher-confirm-type: correlated # 打开消息返回,如果投递失败,会返回消息 publisher-returns: true #publisher-confirm-type有3种取值 #NONE值是禁用发布确认模式,是默认值 #CORRELATED值是发布消息成功到交换器后会触发回调方法 #SIMPLE值经测试有两种效果,其一效果和CORRELATED值一样会触发回调方法回调函数方法类@Component @Slf4j public class ConfirmCallBackListener implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback { @Autowired private RabbitTemplate rabbitTemplate; @PostConstruct public void init() { //指定 ConfirmCallback rabbitTemplate.setConfirmCallback(this); rabbitTemplate.setReturnsCallback(this); } @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { log.info("correlation>>>>>>>{},ack>>>>>>>>>{},cause>>>>>>>>{}", correlationData, ack, cause); if (ack) { //确认收到消息 } else { //收到消息失败,可以自定义重试机制,或者将失败的存起来,进行补偿 } } /* * * @param returnedMessage * 消息是否从Exchange路由到Queue, 只有消息从Exchange路由到Queue失败才会回调这个方法 * @author xiaojie * @date 2021/9/29 13:53 * @return void */ @Override public void returnedMessage(ReturnedMessage returnedMessage) { log.info("被退回信息是》》》》》》{}", returnedMessage.getMessage()); log.info("replyCode》》》》》》{}", returnedMessage.getReplyCode()); log.info("replyText》》》》》》{}", returnedMessage.getReplyText()); log.info("exchange》》》》》》{}", returnedMessage.getExchange()); log.info("routingKey>>>>>>>{}", returnedMessage.getRoutingKey()); } }2、重试机制rabbitmq同样为生产者设置了重试机制默认是3次,同样可以修改重试次数,超过了最大重试次数限制采取人工补偿机制。 Broker保证消息不丢失1、rabbitMq持久化机制消息到达mq之后,mq宕机了,然后消息又没有进行持久化,这时消息就会丢失。开启mq的持久化机制,消息队列,交换机、消息都要开启持久化。开启持久化操作请参考 RabbitMq确认机制&SpringBoot整合RabbitMQ_熟透的蜗牛的博客-CSDN博客2、使用镜像集群3、如果队列满了,多余的消息发送到Broker时可以使用死信队列保证消息不会被丢弃Consumer保证消息不丢失1、开启消费端的手动ack manual-手动ackauto 自动none 不使用ack手动ack代码@Component @Slf4j public class SnailConsumer { @RabbitListener(queues = "snail_direct_queue") public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception { // 获取消息Id String messageId = message.getMessageProperties().getMessageId(); String msg = new String(message.getBody(), "UTF-8"); log.info("获取到的消息>>>>>>>{},消息id>>>>>>{}", msg, messageId); try { int result = 1 / 0; System.out.println("result" + result); // // 手动ack Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG); // 手动签收 channel.basicAck(deliveryTag, false); } catch (Exception e) { //拒绝消费消息(丢失消息) 给死信队列 channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false); } } }2、同样可以使用消费者的重试机制,重试超过最大次数还没成功则采取人工补偿机制。KafkaProducer保证消息不丢失1、producer的ack机制kafka的生产者确认机制有三种取值分别为0、1、-1(all)acks = 0 如果设置为零,则生产者将不会等待来自服务器的任何确认,该记录将立即添加到套接字缓冲区并视为已发送。在这种情况下,无法保证服务器已收到记录,并且重试配置将不会生效(因为客户端通常不会知道任何故障)。acks = 1 这意味着leader会将记录写入其本地日志,但无需等待所有follwer服务器的完全确认即可做出回应,在这种情况下,当leader还没有将数据同步到Follwer宕机,存在丢失数据的可能性。acks = -1代表所有的所有的分区副本备份完成,不会丢失数据这是最强有力的保证。但是这种模式往往效率相对较低。2、producer重试机制Broker保证消息不丢失kafka的broker使用副本机制保证数据的可靠性。每个broker中的partition我们一般都会设置有replication(副本)的个数,生产者写入的时候首先根据分发策略(有partition按partition,有key按key,都没有轮询)写入到leader中,follower(副本)再跟leader同步数据,这样有了备份,也可以保证消息数据的不丢失。Consumer保证消息不丢失1、手动ack/* * * @param message * @param ack * @手动提交ack * containerFactory 手动提交消息ack * errorHandler 消费端异常处理器 * @author xiaojie * @date 2021/10/14 * @return void */ @KafkaListener(containerFactory = "manualListenerContainerFactory", topics = "xiaojie-topic", errorHandler = "consumerAwareListenerErrorHandler" ) public void onMessageManual(List<ConsumerRecord<?, ?>> record, Acknowledgment ack) { for (int i=0;i<record.size();i++){ System.out.println(record.get(i).value()); } ack.acknowledge();//直接提交offset }2、offset commit消费者通过offset commit 来保证数据的不丢失,kafka自己记录了每次消费的offset数值,下次继续消费的时候,会接着上次的offset进行消费。kafka并不像其他消息队列,消费完消息之后,会将数据从队列中删除,而是维护了一个日志文件,通过时间和储存大小进行日志删除策略。如果offset没有提交,程序启动之后,会从上次消费的位置继续消费,有可能存在重复消费的情况。Offset Reset 三种模式 earliest(最早):当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,从头开始消费。latest(最新的):当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,消费新产生的该分区下的数据。none(没有):topic各分区都存在已提交的offset时,从offset后开始消费;只要有一个分区不存在已提交的offset,则抛出异常。以上有完整的代码可以自行取用 请点我!走你
正文如何查看一个表的索引如何查看一个表中有哪些索引呢?可以通过如下sql查看SHOW INDEX FROM tb_user; #tb_user是表名 各个字段表示含义如下 table列表示表名;Non_unique-表示是否是唯一索引,0代表是唯一索引,1代表不是唯一索引;key_name表示索引名称;seq_in_index序号;column_name 表示在哪列上创建索引;collation表示列以什么方式存储在索引中,可以是A或者null,B+Tree的索引总是A,即排序的,如果使用hash索引的话,就是null;Cardinality表示索引中不重复数量的预估值,也可以理解数据等值的数量比较少,一般这个数据值越大越比较适合创建索引,如果特别小那就不适合建索引,如性别(gender)字段。sub_part是否是列的部分索引,如上面在address前15个字符创建了索引。packed 关键字如何被压缩,如果没有被压缩则为null;null 表示是否索引的列含有空值(null);Index_type 表示索引类型,因为表是Innodb存储引擎,所以是Btree;Comment表示注释;Cardinality对这个字段说明一下,Cardinality表示索引中不重复记录数量的预估值。Cardinality是一个预估值而不是一个准确的值。mysql优化器会根据这个值来判断是否使用索引,但是在某些情况下Cardinality可能为null,在这种情况下可能导致明明创建了索引,而索引却没生效,可以尝试执行如下sql尝试。ANALYZE TABLE tb_user; Cardinality在更新通常发生在insert和update。但是不能每次执行insert和update都更新这个值,这样很明显不合理。在Innodb引擎中更新Cardinality值的策略为表中1/16的数据已经发生过变化。stat_modified_counter>20亿。考虑到如果对同一条数据进行修改,那么在Innodb存储内部会有一个计数器stat_modified_counter,如果该数值超过了20亿就会更新Cardinality。Innodb存储引擎通过采样的方法来统计这个数值。默认情况下Innodb通过采样的方式来统计该数值,采取8个叶子结点的数值。采样过程如下采取B+Tree中叶子节点的数量 记为A。随机获取8个叶子结点。统计每个页不同的记录数记为P1,P2.......计算出预估值Cardinality(P1+P2+....P8)*A/8。由于Cardinality是通过采样计算出的数值,所以每次执行SHOW INDEX FROM tb_user;查询出的结果值可能不同。索引优化本文主要对联合索引讨论。慢查询日志如何查看慢查询日志呢?可以通过如下sqlSHOW VARIABLES LIKE '%query%' ; 默认情况下慢查询是关闭状态(图上是我已经开启过的),默认超过10s(long_query_time)定义为慢查询,slow_query_log_file慢查询日志的存放目录。可以通过如下sql开启慢查询set global slow_query_log =1; #开启慢查询或者set global slow_query_log ='on'; set global slow_query_log =0; #关闭慢查询或者set global slow_query_log ='off'; set global long_query_time = 1 ;#修改慢查询时间1s show variables like '%dir%'; #查看日志存放目录 注意设置完之后,需要关闭navicat连接重新连接才会生效,这只是临时开启慢查询,如果想要永久开启需要修改配置文件编辑mysql配置文件/etc/my.cnf [mysqld] slow_query_log = 1 #是否开启慢查询日志,1表示开启,0表示关闭,也可以使用off和on long_query_time = 1 #慢查询时间 log-slow-queries=/var/log/slowquery.log #mysql5.6以下版本 slow-query-log-file=/var/log/slowquery.log #mysql5.6及以上版本 慢查询会记录查询缓慢的sql记录,以及索引使用情况。优化规则全值匹配全值匹配是指在查询条件中尽量条件都是索引中的字段,而且索引都使用。另外尽量创建联合索引,因为每一个索引都是一颗B+Tree,在增删改的时候,需要维护这颗B+Tree;EXPLAIN SELECT * FROM tb_user WHERE `name`='行言孙'; EXPLAIN SELECT * FROM tb_user WHERE `name`='行言孙' AND age=22 ; EXPLAIN SELECT * FROM tb_user WHERE `name`='行言孙' AND age=22 AND position='武汉信息经理' ;最左前缀匹配顾名思义就是最左优先,查找会按照索引的顺序依次查找,索引的最左前列开始并且不跳过索引中的列。EXPLAIN SELECT * FROM tb_user WHERE `name`='行言孙' AND age=22 AND position='武汉信息经理' ; EXPLAIN SELECT * FROM tb_user WHERE age=22 AND position='武汉信息经理' ; 上面sql的执行计划如下图第一个查询使用了索引,第二个索引中没有使用索引而使用了全表扫描,可以判断出,如果索引中第一个列没使用上,那么索引就不能使用,而会全表扫描。EXPLAIN SELECT * FROM tb_user WHERE `name`='行言孙' AND age>22 AND position='武汉信息经理' ;实际上这三个索引的key_len应该是221,而实际上是68,证明position这个索引没有用上。可以判断出,如果索引在遇到范围(> 、<、Like 不包括>=、<= )查询时后面的索引就会失效。 如果等值查询中索引顺序变了,索引会生效吗?EXPLAIN SELECT * FROM tb_user WHERE `name`='行言孙' AND position='武汉信息经理' AND age=22; EXPLAIN SELECT * FROM tb_user WHERE age=22 AND position='武汉信息经理' AND name='行言孙'; EXPLAIN SELECT * FROM tb_user WHERE position='武汉信息经理' AND age=22 AND name='行言孙';这三个sql的执行计划是一模一样的,可知在等值查询中如果索引顺序跟索引的顺序不一致依然会使用索引,原因可能是Mysql优化器会将我们的sql进行优化而使用索引。likeEXPLAIN SELECT * FROM tb_user WHERE `name`LIKE '%行言孙' AND age=22 AND position='武汉信息经理' ; EXPLAIN SELECT * FROM tb_user WHERE `name`LIKE '行言孙%' AND age=22 AND position='武汉信息经理' ; 通过执行计划可知%或者_(%匹配一个或者多个字符;_匹配一个字符)放在左边的时候是不会使用索引的,如果在右边则可以使用到索引。如果非要把%放在左边则可以使用索引覆盖,这样同样会使用索引。EXPLAIN SELECT name,age,position FROM tb_user WHERE `name`LIKE '%行言孙' AND age=22 AND position='武汉信息经理' ; 可以看到同样使用到了索引不在索引列做计算、函数、类型转换EXPLAIN SELECT * FROM tb_user WHERE LTRIM(name)= '行言孙' AND age=21 AND position='武汉信息经理' ; 可知如果在索引列上执行函数户或者计算不会使用到索引。索引覆盖从辅助索引中就可以得到查询记录,而不需要查询聚集索引中的记录(回表),辅助索引不包含整行数据,其大小远小于聚集索引,可以减少大量IO操作。尽量使用覆盖索引(只访问索引的查询(索引列包含查询列)),减少select *语句。EXPLAIN SELECT name,age,position FROM tb_user WHERE name= '行言孙' AND age=21 AND position='武汉信息经理' ;<>、!=不等号EXPLAIN SELECT * FROM tb_user WHERE name!='行言孙' AND age=21 AND position='武汉信息经理' ; EXPLAIN SELECT * FROM tb_user WHERE name='行言孙' AND age<>21 AND position='武汉信息经理' ; 当不等号在索引的开头使用时,不会使用索引,而改为全表扫描。不在开头时索引是会使用到索引的,但是<>之后的字段的索引不会使用(同范围查询一样)。null&is not nullEXPLAIN SELECT * FROM tb_user WHERE name IS NOT NULL AND age<>21 AND position='武汉信息经理' ; EXPLAIN SELECT * FROM tb_user WHERE name IS NULL AND age<>21 AND position='武汉信息经理' ; 可以看出在使用is not null时,并没有使用索引,而在is null时使用了索引。那么为什么呢?通过执行计划可知is not null 的数据大概有1282633条,Mysql优化器认为如果使用索引然后再回表查询 不如直接全表扫描快,而is null 时才有大概6614条数据。由可以得出结论MySQL中决定使不使用某个索引执行查询的依据很简单:就是成本够不够小。而不是是否在WHERE子句中用了 is null 或者 is not null 。你可能不信 那么我证明给你看。可以使用optimizer_trace 分析sql是否选择使用索引。执行以下sql可以开启trace,默认是关闭的,不建议开启,会消耗mysql的性能,使用完之后记得关闭。#开启 SET optimizer_trace='enabled=on',end_markers_in_json=on; #关闭 SET session optimizer_trace="enabled=off"; 注意两条语句要全选中同时执行。SELECT * FROM tb_user WHERE name IS NOT NULL AND age<>21 AND position='武汉信息经理' ; SELECT * FROM information_schema.OPTIMIZER_TRACE;{ "steps": [ { "join_preparation": { /*sql准备阶段*/ "select#": 1, "steps": [ { "expanded_query": "/* select#1 */ select `tb_user`.`id` AS `id`,`tb_user`.`name` AS `name`,`tb_user`.`age` AS `age`,`tb_user`.`position` AS `position`,`tb_user`.`address` AS `address`,`tb_user`.`create_time` AS `create_time`,`tb_user`.`update_time` AS `update_time`,`tb_user`.`delete_flag` AS `delete_flag` from `tb_user` where ((`tb_user`.`name` is not null) and (`tb_user`.`age` <> 21) and (`tb_user`.`position` = '武汉信息经理'))" } ] /* steps */ } /* join_preparation */ }, { "join_optimization": {/*sql优化*/ "select#": 1, "steps": [ { "condition_processing": { /*sql条件*/ "condition": "WHERE", "original_condition": "((`tb_user`.`name` is not null) and (`tb_user`.`age` <> 21) and (`tb_user`.`position` = '武汉信息经理'))", "steps": [ { "transformation": "equality_propagation", "resulting_condition": "((`tb_user`.`name` is not null) and (`tb_user`.`age` <> 21) and (`tb_user`.`position` = '武汉信息经理'))" }, { "transformation": "constant_propagation", "resulting_condition": "((`tb_user`.`name` is not null) and (`tb_user`.`age` <> 21) and (`tb_user`.`position` = '武汉信息经理'))" }, { "transformation": "trivial_condition_removal", "resulting_condition": "((`tb_user`.`name` is not null) and (`tb_user`.`age` <> 21) and (`tb_user`.`position` = '武汉信息经理'))" } ] /* steps */ } /* condition_processing */ }, { "substitute_generated_columns": { } /* substitute_generated_columns */ }, { "table_dependencies": [ /*表依赖*/ { "table": "`tb_user`", "row_may_be_null": false, "map_bit": 0, "depends_on_map_bits": [ ] /* depends_on_map_bits */ } ] /* table_dependencies */ }, { "ref_optimizer_key_uses": [ ] /* ref_optimizer_key_uses */ }, { "rows_estimation": [/*预估访问成本*/ { "table": "`tb_user`", "range_analysis": { /*全表扫描*/ "table_scan": { "rows": 1282633,/*扫描行数*/ "cost": 142326/*花费时间*/ } /* table_scan */, "potential_range_indexes": [ /*查询可能使用的索引*/ { "index": "PRIMARY",/*主键索引*/ "usable": false,/*没用主键索引*/ "cause": "not_applicable"/*不适用*/ }, { "index": "idx_name_age_position",/*辅助索引*/ "usable": true, "key_parts": [ "name", "age", "position", "id" ] /* key_parts */ }, { "index": "address", /*address索引*/ "usable": false, "cause": "not_applicable" } ] /* potential_range_indexes */, "setup_range_conditions": [ ] /* setup_range_conditions */, "group_index_range": { "chosen": false, "cause": "not_group_by_or_distinct" } /* group_index_range */, "skip_scan_range": { "potential_skip_scan_indexes": [ { "index": "idx_name_age_position", "usable": false, "cause": "query_references_nonkey_column" } ] /* potential_skip_scan_indexes */ } /* skip_scan_range */, "analyzing_range_alternatives": {/*分析使用索引的成本*/ "range_scan_alternatives": [ { "index": "idx_name_age_position", "ranges": [ "NULL < name" ] /* ranges */, "index_dives_for_eq_ranges": true, "rowid_ordered": false, "using_mrr": true, "index_only": false, "in_memory": 0, "rows": 641316,/*索引扫描的行数*/ "cost": 476980, /*使用idx_name_age_position索引的成本*/ "chosen": false,/*没有使用该索引*/ "cause": "cost" } ] /* range_scan_alternatives */, "analyzing_roworder_intersect": { "usable": false, "cause": "too_few_roworder_scans" } /* analyzing_roworder_intersect */ } /* analyzing_range_alternatives */ } /* range_analysis */ } ] /* rows_estimation */ }, { "considered_execution_plans": [ /*考虑的执行计划*/ { "plan_prefix": [ ] /* plan_prefix */, "table": "`tb_user`", /*全表扫描*/ "best_access_path": { "considered_access_paths": [ { "rows_to_scan": 1282633, "access_type": "scan", "resulting_rows": 1.28263e+06, "cost": 142324, /*执行成本*/ "chosen": true/*选择这个*/ } ] /* considered_access_paths */ } /* best_access_path */, "condition_filtering_pct": 100, "rows_for_plan": 1.28263e+06, "cost_for_plan": 142324, "chosen": true } ] /* considered_execution_plans */ }, { "attaching_conditions_to_tables": { "original_condition": "((`tb_user`.`name` is not null) and (`tb_user`.`age` <> 21) and (`tb_user`.`position` = '武汉信息经理'))", "attached_conditions_computation": [ ] /* attached_conditions_computation */, "attached_conditions_summary": [ { "table": "`tb_user`", "attached": "((`tb_user`.`name` is not null) and (`tb_user`.`age` <> 21) and (`tb_user`.`position` = '武汉信息经理'))" } ] /* attached_conditions_summary */ } /* attaching_conditions_to_tables */ }, { "finalizing_table_conditions": [ { "table": "`tb_user`", "original_table_condition": "((`tb_user`.`name` is not null) and (`tb_user`.`age` <> 21) and (`tb_user`.`position` = '武汉信息经理'))", "final_table_condition ": "((`tb_user`.`name` is not null) and (`tb_user`.`age` <> 21) and (`tb_user`.`position` = '武汉信息经理'))" } ] /* finalizing_table_conditions */ }, { "refine_plan": [ { "table": "`tb_user`" } ] /* refine_plan */ } ] /* steps */ } /* join_optimization */ }, { "join_execution": { /*sql执行*/ "select#": 1, "steps": [ ] /* steps */ } /* join_execution */ } ] /* steps */ }可以看出使用索引消耗的成本要远比全表扫描要高,所以选择了全表扫描。如果感觉索引没有执行的话,都可以使用这个工具来具体分析一下。in&exsits&or少用in、exsits、or用它查询时,mysql不一定使用索引,mysql内部优化器会根据检索比例、表大小等多个因素整体评估是否使用索引。字符串不加引号字符串上未加引号时是不会使用索引的,但是在mysql8.0.27如果字符串不加引号是会报错的。索引下推Index Condition PushDown(ICP)从Mysql5.6开始。使用ICP时,当进行索引查询时,Mysql在取出索引的同时会进行where条件的过滤,当然where可以过滤的条件是该索引可以覆盖到的范围。这样过滤完之后可以减少大量的回表操作,提高查询效率。默认是开启ICP优化的。EXPLAIN SELECT * FROM tb_user WHERE name='行言孙' OR name='德进药' OR name='比张国' AND age=21 AND position='武汉信息经理' ; Using index condition表示使用了ICP优化。MRR从Mysql5.6开始支持Multi_Range_Read(多范围读),MRR的是为了磁盘的随机访问次数。我们通过辅助索引能够使用到B+树的有序性,但是查出来的主键id未必是有序的。此时通过无序的id主键进行回表扫描的话,此时的IO是随机IO。磁盘IO主要分为两大类:一个随机IO,一个顺序IO。顺序IO的效率大概是随机IO的两个数量级。因此MySQL提出了一个名为Disk-Sweep Multi-Range Read (MRR,多范围 读取)的优化措施,即先读取一部分二级索引记录,将它们的主键值排好序之后再统一执行回表操作。因为磁盘读取数据通过扇区为单位进行读取的。一颗B+树它是有序的。比如说id 1到50 它是第一个扇区的。51到100是第二个扇区的以此类推。假设回表的id是这样的96、23、105、12、88等这些。那么磁盘读取的时候先读取第二个扇区,拿到96的数据,发现没有23的数据,在读取第一个扇区,拿到23的数据,发现没有105的数据,在读取第三个扇区。这样的IO就是随机IO。如果说通过MRR机制排好序了:12、23、88、96、105。读取第一个扇区的时候就能拿到12、23的数据,读取第二个扇区的时候就能拿到88、96的数据,减少了回表的次数。总结MRR可适用于range、ref、eq_ref类型的查询,使用MRR的好处MRR使数据访问变得较为有顺序,在查找辅助索引时,首先根据得到的结果按照主键排序,并按照主键的顺序进行回表查找。批量处理对键值的操作。MRR还可以将某些范围查询,拆分为键值对,以此来进行批量的数据查询。这样做的好处是可以在拆分过程中,直接过滤一些不符合查询条件的数据。减少缓冲池中页被替换的次数。数据读取之后是首先存放在一个缓冲池中的,如果缓冲池不够大,此时频繁的离散读取导致缓冲池中的数据被替换出缓冲池,若是按照主键排序进行访问则可以将这种重复行为降到最低。那么如何开启MRR模式呢?可以通过参数optimizer_switch中的flag来控制。当MRR为on时,表示启用MRR优化。mrr_cost_based表示是否通过costbased的方式来选择是否启用mrr。若设置mrr=on,mrr_cost_based=off,则总是启用MRR优化。SET @@OPTIMIZER_switch='mrr=on,mrr_cost_based=off';EXPLAIN SELECT * FROM tb_user WHERE name='行言孙' OR name='德进药' OR name='比张国' AND age=21 AND position='武汉信息经理' ; 可以看到已经使用了MRR优化。缓存区的大小默认是256K(262144/1024)当超过该值时则执行器对已经缓存的数据进行排序,排序之后获取数据。长字段索引如果字段过长,整个字段建立索引就会很浪费空间了,所以可以考虑对字段的前面几个字段建立索引。ALTER TABLE tb_user ADD INDEX address (address(15));分页优化如果要查询LMIT 1000000,10,虽然只取了10条数据,但是mysql并不能跳跃取值,而是要取出1000010条数据,然后从这里面取出10条。优化方式一使用主键id SELECT * FROM tb_user LIMIT 1000000,10; SELECT * FROM tb_user WHERE id >1000000 LIMIT 10; 优化方式二非主键索引的方式分页优化EXPLAIN select * from tb_user u inner join (select id from tb_user limit 1000000,10) uid on u.id = uid.id; 优化方式三 分库分表如果一个标的数据量特别大,可以考虑分表分库,阿里开发手册中推荐,如果单表行数超过500万行或者单表容量超过2G,推荐使用分表分库。排序优化1、EXPLAIN SELECT * FROM tb_user WHERE name='下里国' AND position='天津信息经理' ORDER BY age;可以看到用到了索引,age索引列用在排序过程中,因为Extra字段里没有using filesort。根据最左前缀原则,position索引没有用上。2、EXPLAIN SELECT * FROM tb_user WHERE NAME = '下里国' ORDER BY position;由于索引跳过了age所以排序索引没有使用上而显示Using filesort。可知在排序中同样需要满足最左前缀原则,如果索引中断,那么将不会使用后面的索引。3、EXPLAIN SELECT * FROM tb_user WHERE name='下里国' ORDER BY age,position; EXPLAIN SELECT * FROM tb_user WHERE name='下里国' ORDER BY position,age; EXPLAIN SELECT * FROM tb_user WHERE name='下里国' ORDER BY age DESC ,position DESC; EXPLAIN SELECT * FROM tb_user WHERE name='下里国' ORDER BY age ASC,position DESC; 第一个sql很显然用到了name列上的索引,而extra是null,null意味着用到了索引,但是部分字段未被索引覆盖,必须通过“回表”来实现,不是纯粹地用到了索引,也不是完全没用到索引。第二个sql由于不符合最左前缀原则(sql优化器没有像等值查询时优化),所以没有使用到索引。第三个sql Backward index scan是mysql8.0之后才出现的,也是一种mysql的优化方式,叫降序索引。第四个sql 由于age使用正序,position使用逆序,与创建索引时的顺序不同,所以也没有使用到索引排序。EXPLAIN SELECT name,age,position FROM tb_user WHERE name='下里国' ORDER BY age,position; 当使用覆盖索引时,排序字段用到了索引。Using filesort排序原理Using filesort分为单路排序和双路排序 单路排序:是一次性取出满足条件行的所有字段,然后在sort buffer中进行排序。双路排序(又叫回表排序模式):是首先根据相应的条件取出相应的排序字段和可以直接定位行 数据的行 ID,然后在 sort buffer 中进行排序,排序完后根据主键id获取其他字段的数据-- 相当于要查询两次。通过trace工具可以看到sort_mode可以通过改变 max_length_for_sort_data变量的值来影响mysql选择的算法。因为单路排序为将要排序的每一行创建了固定的缓冲区。如果查询字段总的长度大小比设定的max_length_for_sort_data 要小,则使用单路排序方式;如果查询字段总的长度大小比设定的max_length_for_sort_data 要大,则使用多路排序方式 。注意从 MySQL 8.0.20 开始不推荐使用此变量(max_length_for_sort_data),因为优化器更改使其过时且无效。在MySQL 8.0.20之后该值默认为4096而不是1024。通过以下sql可以查看。SHOW VARIABLES LIKE '%max_length_for_sort_data%'; SET max_length_for_sort_data = 1024; #max_length_for_sort_data默认值是4096,最小值是4,最大值是8388608小结1、order by语句使用索引最左前缀原则。2、使用where子句与order by子句条件列组合满足索引最左前缀原则。3、尽量在索引列上完成排序,遵循索引建立(索引创建的顺序)时的最左前缀原则。4、如果order by的条件不在索引列上,就会产生Using filesort。5、能用覆盖索引尽量用覆盖索引。6、如果和where 条件查询冲突,那优先where条件上的索引。 count优化-- 临时关闭缓存 set global query_cache_size=0; set global query_cache_type=0; EXPLAIN select count(1) from tb_user; EXPLAIN select count(id) from tb_user; EXPLAIN select count(name) from tb_user; EXPLAIN select count(*) from tb_user; 上面的执行计划1、2、4,没有使用主键的聚簇索引,而使用了address列上的索引。原因是Innodb会优先考虑使用索引树较小的索引来查询,因为address比主键索引和idx_name_age_address都小,所以选择了address索引,如果不存在辅助索引,则使用聚簇索引(一般是主键索引)来查询。 sql3使用了辅助索引 其实上面的执行效率基本是相同的,阿里开发手册中规定不要使用count(列名)或者count(1)来替代count(*)。count(*)会统计null的行,count(列名)不会统计此列为null的值。count(distinct col )计算该列除Null之外不重复的数量,count(distinct col1,col2)只要有一列全为null,则结果为0。优化方案1、为了更快地计数,创建一个计数器表并让您的应用程序根据它所做的插入和删除来更新。2、SHOW TABLE STATUS 3、查询mysql自己维护的总行数 对于myisam存储引擎的表做不带where条件的count查询性能是很高的,因为myisam存储引擎的表的总行数会被 mysql存储在磁盘上,查询不需要计算。参考:https://blog.csdn.net/weixin_34462016/article/details/113654725https://blog.csdn.net/admin522043032/article/details/121919494https://blog.csdn.net/admin522043032/article/details/121281402
正文sql语句的执行顺序Mysql的执行流程图如下 图片来自网络连接器连接器就是起到连接的作用,主要职责有1、验证请求用户的账户和密码是否正确。2、用于客户端的通信。Mysql的TCP协议是一个半双工通信模式因此在某一固定时刻只能由客户端向服务器请求或者服务器向客户端发送数据,而不能同时进行。单工: 数据传输只允许在一个方向上的传输,单向传输,只能一方来发送数据,另一方来接收数据并发送。例如:遥控器。半双工:数据传输允许两个方向上的传输,但是同一时间内,只可以有一方发送或接受消息。例如:打电话全双工:同时可进行双向传输。例如:websocket、Http2.0。3、如果账号密码验证通过,会在mysql自带的权限表中验证当前用户权限。mysql库中有4个控制权限的表,分别为user表,db表,tables_priv表,columns_priv表。1)user表存放用户账户信息以及全局级别(所有数据库)权限,决定了来自哪些主机的哪些用户可以访问数据库实例。2)db表存放数据库级别的权限,决定了来自哪些主机的哪些用户可以访问此数据库。%同样表示所有的主机可连。3)Tables_priv表:存放表级别的权限,决定了来自哪些主机的哪些用户可以访问数据库的这个表。4)Columns_priv表:存放列级别的权限,决定了来自哪些主机的哪些用户可以访问数据库表的这个字段。验证过程如下:先从user表中的Host,User,Password这3个字段中判断连接的ip、用户名、密码是否存在,存在则通过验证。通过身份认证后,进行权限分配,按照user、db、table、 columns依次判断,如果user表中全局变量都是Y则不再进行下面的判断,否则一步一步判断权限。如果在任何一个过程中权限验证不通过,都会报错。缓存mysql的缓存主要的作用是为了提升查询的效率,缓存以key和value的哈希表形式存储,key是具体的sql语句,value是结果的集合。如果无法命中缓存,就继续走到分析器,如果命中缓存就直接返回给客户端 。不过需要注意的是在mysql的8.0版本以后,缓存被官方删除掉了。之所以删除掉,是因为查询缓存的失效非常频繁,如果在一个写多读少的环境中,缓存会频繁的新增和失效。需要注意 :缓存和哈希自适性索引的区别,自适性哈希是通过哈希表实现的,它是数据库自身创建的不能人为的创建和删除。通过一下sql可以查看。SHOW ENGINE INNODB STATUS ; SHOW ENGINE INNODB STATUS \G; #cmd窗口使用这个自动分行 分析器分析器的主要作用是将客户端发过来的sql语句进行分析,这将包括预处理与解析过程,在这个阶段会解析sql语句的语义,并进行关键词(select、update、delete、where、order by、group by等等)和非关键词进行提取、解析,并组成一个解析树。另外在此过程还会对sql语法进行分析,除此之外还会校验表是否存在,表中的字段值是否存在。下面是一个解析树 优化器能够进入到优化器阶段表示sql是符合mysql的标准语义规则的并且可以执行的,此阶段主要是进行sql语句的优化,会根据执行计划进行最优的选择,匹配合适的索引,选择最佳的执行方案。如MRR(Multi-Range Read 多范围读取)优化,ICP(Index Condition Pushdown 索引下推)优化,是否选择使用索引,选择使用主键索引还是其他索引等。执行器在执行器的阶段,此时会调用存储引擎的API,API会调用存储引擎。下面罗列几个存储引擎。存储引擎是基于表的,而不是数据库。使用下面sql可以查看mysql支持的存储引擎SHOW ENGINES;Sql执行顺序实际上sql语句并不是按照我们写的sql的顺序从左到右依次执行的,它是按照如下顺序执行的。 from 第一步就是选择出from关键词后面跟的表,这也是sql执行的第一步:表示要从数据库中执行哪张表。join on join是表示要关联的表,on是连接的条件。通过from和join on选择出需要执行的数据库表t1和t2产生笛卡尔积,生成t1和t2合并的临时中间表Temp1。on:确定表的绑定关系,通过on产生临时中间表Temp2。where where表示筛选,根据where后面的条件进行过滤,按照指定的字段的值(如果有and连接符会进行联合筛选)从临时中间表Temp2中筛选需要的数据,注意如果在此阶段找不到数据,会直接返回客户端,不会往下进行.这个过程会生成一个临时中间表Temp3。注意在where中不可以使用聚合函数,聚合函数主要是(min、max、count、sum等函数)。group by group by是进行分组,对where条件过滤后的临时表Temp3按照固定的字段进行分组,产生临时中间表Temp4,这个过程只是数据的顺序发生改变,而数据总量不会变化,表中的数据以组的形式存在。having 对临时中间表Temp4进行聚合,然后产生中间表Temp5,在此阶段可以使用select中的别名。select 对分组聚合完的表挑选出需要查询的数据,如果为*会解析为所有数据,此时会产生中间表Temp6。distinct distinct对所有的数据进行去重,此时如果有min、max函数会执行字段函数计算,然后产生临时表Temp7。order by 会根据Temp7进行顺序排列或者逆序排列,然后插入临时中间表Temp8,这个过程比较耗费资源。limit limit对中间表Temp8进行分页,产生临时中间表Temp9,返回给客户端。实际上这个过程也并不是绝对这样的,中间mysql会有部分的优化以达到最佳的优化效果,比如在select筛选出找到的数据集。执行计划下面所有的sql语句是在Mysql 8.0.27版本上执行的。两张表,表结构为CREATE TABLE `tb_salary` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` int NULL DEFAULT NULL COMMENT '用户id', `salary` decimal(10, 2) NOT NULL COMMENT '工资', `salary_time` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '薪资月份', `create_time` datetime NULL DEFAULT NULL, `update_time` datetime NOT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '薪资表' ROW_FORMAT = Dynamic; CREATE TABLE `tb_user` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '名字', `age` int NULL DEFAULT NULL, `position` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '职务', `address` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '地址', `create_time` datetime NULL DEFAULT NULL, `update_time` datetime NULL DEFAULT NULL, `delete_flag` tinyint NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 100 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;explain执行如下sqlEXPLAIN SELECT t2.*,( SELECT 1 FROM tb_user WHERE id = 100000 ) FROM tb_user t1 LEFT JOIN tb_salary t2 ON t1.id = t2.user_id WHERE t2.id IS NOT NULL;执行计划结果如下字段说明id:id列的编号是 select 的序列号,有几个 select 就有几个id,并且id的顺序是按 select 出现的顺序增长的。id列越大执行优先级越高,id相同则从上往下执行,id为NULL最后执行。select: 查询类型(1) SIMPLE(简单SELECT,不使用UNION或子查询等)。(2) PRIMARY(子查询中最外层查询,查询中若包含任何复杂的子部分,最外层的select被标记为PRIMARY)。(3) UNION(UNION中的第二个或后面的SELECT语句)。(4) DEPENDENT UNION(UNION中的第二个或后面的SELECT语句,取决于外面的查询)。(5) UNION RESULT(UNION的结果,union语句中第二个select开始后面所有select)。(6) SUBQUERY(子查询中的第一个SELECT,结果不依赖于外部查询)。(7) DEPENDENT SUBQUERY(子查询中的第一个SELECT,依赖于外部查询)。(8) DERIVED(派生表的SELECT, FROM子句的子查询)。(9) UNCACHEABLE SUBQUERY(一个子查询的结果不能被缓存,必须重新评估外链接的第一行)。派生查询例子#派生表的优化合并,默认是开启的,需要手动关闭。 set session optimizer_switch='derived_merge=off';EXPLAIN SELECT ( SELECT id FROM tb_salary WHERE id = 1 ) FROM ( SELECT * FROM tb_user WHERE id = 95885 ) t1; 其中table中的3指向的是id列的值。table:explain 的一行正在访问哪个表。当 from 子句中有子查询时,table列是 <derivenN> 格式,表示当前查询依赖 id=N 的查询,于是先执行 id=N 的查询。当有 union 时,UNION RESULT 的 table 列的值为<union1,2>,1和2表示参与 union 的 select 行id。partitions:查询是基于分区表的话,会显示查询将访问的分区,mysql5.6版本之后才有。type: 这一列表示关联类型或访问类型,即MySQL决定如何查找表中的行,查找数据行记录的大概范围。 依次从最优到最差分别为:system > const > eq_ref > ref > range > index > ALL 一般来说,得保证查询达到range级别,最好达到ref。(1) const, system:mysql能对查询的某部分进行优化并将其转化成一个常量。用于 primary key 或 唯一索引的所有列与常数比较时,所以表最多有一个匹配行,读取1次,速度比较快。system是 const的特例,表里只有一条元组匹配时为system。EXPLAIN SELECT ( SELECT id FROM tb_salary WHERE id = 1 ) FROM ( SELECT * FROM tb_user WHERE id = 95885 ) t1;(2) eq_ref:使用的索引是唯一索引,对于每个索引键值,表中只有一条记录匹配,简单来说,就是多表连接中使用primary key或者 unique key作为关联条件。EXPLAIN SELECT t2.* FROM tb_user t1 LEFT JOIN tb_salary t2 ON t1.id = t2.user_id WHERE t2.id IS NOT NULL; (3) ref: 与eq_ref 类似,只是不使用唯一索引,而是使用普通索引或者唯一性索引的部分前缀,索引要和某个值相比较,可能会找到多个符合条件的行。EXPLAIN SELECT t2.* FROM tb_user t1 LEFT JOIN tb_salary t2 ON t1.id = t2.user_id WHERE t2.salary=10500.00 其中salary字段上有普通索引。(4) range : 范围扫描通常出现在 in()、 between 、>、<、 >= 、<=等操作中。使用一个索引来检索给定范围的行。EXPLAIN SELECT * FROM tb_user WHERE id <=95908 (5) index : 只遍历索引树就能拿到结果,一般是扫描某个二级索引,这种扫描不会从索引树根节点开始快速查找,而是直接 对二级索引的叶子节点遍历和扫描,速度还是比较慢的,这种查询一般会使用覆盖索引,二级索引一般比较小,所以这 种通常比ALL快。首先对上面的表创建索引-- 创建索引 CREATE INDEX idx_name_age_position ON tb_user(name,age,position); -- 前15个字符创建索引 ALTER TABLE tb_user ADD INDEX address (address(15)); -- 创建聚簇索引 CREATE CLUSTERED INDEX 索引名 ON 表名(字段名); -- 删除索引 DROP INDEX idx_name_age_position ON tb_user; -- 删除索引 ALTER TABLE tb_user DROP INDEX idx_name_age_position; -- 删除主键索引 ALTER TABLE tb_user DROP PRIMARY KEY;EXPLAIN SELECT name,age,position FROM tb_user; (6)All: 即全表扫描,扫描你的聚集索引的所有叶子节点。通常情况下这需要增加索引来进行优化了。EXPLAIN SELECT * FROM tb_user;(7)null : mysql能够在优化阶段分解查询语句,在执行阶段用不着再访问表或索引。EXPLAIN SELECT MAX(id) FROM tb_user;possible_keys:可能用到的索引。key: 实际用到的索引,实际情况中有可能possible_keys有值,而实际执行时候key没有值,这种情况下可能是mysql优化器觉得全表扫描比使用索引查询效率要高,而没有使用索引。如果强制mysql使用索引,则可以使用force index(索引名称)来实现。EXPLAIN SELECT * FROM tb_salary FORCE INDEX ( salary ) WHERE salary = 15000; key_len: 这一列显示了mysql在索引里使用的字节数,通过这个值可以算出具体使用了索引中的哪些列。key_len计算规则如下:字符串,char(n)和varchar(n),5.0.3以后版本中,n均代表字符数,而不是字节数,如果是utf-8,一个数字或字母占1个字节,一 个汉字占3个字节。char(n):如果存汉字长度就是 3n 字节,固定长度。varchar(n):如果存汉字则长度是 3n + 2 字节,加的2字节用来存储字符串长度,因为 varchar是变长字符串。数值类型tinyint:1字节smallint:2字节int:4字节bigint:8字节时间类型date:3字节timestamp:4字节datetime:8字节如果字段允许为 NULL,需要1字节记录是否为 NULL。索引最大长度是768字节,当字符串过长时,mysql会做一个类似左前缀索引的处理,将前半部分的字符提取出来做索引。EXPLAIN SELECT name,age,position FROM tb_user; name为varchar(20) 允许空值(1个字节) 20*3+2+1=63age 为int 允许空值 4+1=5position为varchar(50) 50*3+2+1=153则 63+5+153=221ref: 这一列显示了在key列记录的索引中,表查找值所用到的列或常量,常见的有:const(常量),字段名,null。rows: 预估数据行数,并不一定等于查询的返回结果。filtered:符合某条件的记录数百分比。Extra:Using where——表示MySQL将对存储引擎层提取的结果进行过滤,过滤条件字段无索引, Using where本身其实和是否使用索引无关。Using index——表示使用覆盖索引,查询的字段在覆盖索引中就可以获取到。Using index condition——在5.6版本后加入的新特性(Index Condition Pushdown)后面具体说明。Using filesort——表示没有使用索引的排序。参考 :https://www.cnblogs.com/wyq178/p/11576065.htmlhttps://blog.csdn.net/admin522043032/article/details/121037081
正文一、什么是ForkJoin“分而治之”是一种思想,所谓“分而治之”就是把一个复杂的算法问题按一定的“分解”方法分为规模较小的若干部分,然后逐个解决,分别找出各部分的解,最后把各部分的解合并。而ForkJoin模式就是这种思想,把一个大任务分解成许多个独立的子任务,然后开启多个线程去并行执行这些子任务。对任务一直拆分,直到拆分到最小单位。二、ForkJoin简单使用需求是计算1-1000000的和package com.xiaojie.fork; import java.util.concurrent.ExecutionException; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ForkJoinTask; import java.util.concurrent.RecursiveTask; /** * @description: 使用forkJoin 计算1-1000000的和 * @author xiaojie * @date 2022/1/20 9:56 * @version 1.0 */ public class ForkJoinDemo extends RecursiveTask<Integer> { //RecursiveTask 有返回值的 //RecursiveAction 没有返回值 int start; int end; int max = 5000; //以5000为单位分组 int sum; @Override protected Integer compute() { if (end - start < max) { System.out.println(Thread.currentThread().getName() + "," + "start:" + start + ",end:" + end); for (int i = start; i < end; i++) { sum += i; } } else { //拆分 int middle = (start + end) / 2; ForkJoinDemo left = new ForkJoinDemo(start, middle); left.fork();//拆分 ForkJoinDemo right = new ForkJoinDemo(middle + 1, end); right.fork();//拆分 try { //合并任务 sum = left.get() + right.get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } return sum; } public ForkJoinDemo(int start, int end) { this.start = start; this.end = end; } public static void main(String[] args) throws ExecutionException, InterruptedException { ForkJoinDemo forkJoinDemo = new ForkJoinDemo(1, 1000000); ForkJoinPool forkJoinPool = new ForkJoinPool(); ForkJoinTask<Integer> result = forkJoinPool.submit(forkJoinDemo); System.out.println(result.get()); while (true) { // ForkJoinPool在守护进程模式下使用线程,所以一般程序退出时无需显式关闭这样的池 } } }注意:1、ForkJoinPool在守护进程模式下使用线程,所以一般程序退出时无需显式关闭这样的池,而自动结束任务。2、当任务量很大时才适合使用ForkJoin模式,当任务量不多时,并不适合使用,因为ForkJoin拆分任务时也需要消耗时间和资源(任务量较小时,大部分的时间都用来拆分任务而不划算)。三、ForkJoin原理核心APIForkJoin框架的核心是ForkJoinPool线程池。该线程池使用一个无锁的栈来管理空闲线程,如果一个工作线程暂时取不到可执行的任务,则可能会挂起,而挂起的线程会被压入由ForkJoinPool维护的栈中,等有新任务到来时,再从栈中唤醒这些线程。构造器 public ForkJoinPool(int parallelism, ForkJoinWorkerThreadFactory factory, UncaughtExceptionHandler handler, boolean asyncMode) { this(parallelism, factory, handler, asyncMode, 0, MAX_CAP, 1, null, DEFAULT_KEEPALIVE, TimeUnit.MILLISECONDS); }参数解释int parallelism —— 并行度默认为CPU数量,最小值为1,决定并行执行的线程数量。ForkJoinWorkerThreadFactory factory ——线程创建工厂UncaughtExceptionHandler handler——异常处理类,当执行任务出现异常时,被handler捕获。boolean asyncMode——是否为异步模式,默认值为false,如果为true表示子任务执行遵循FIFO(先进先出)顺序,如果为false表示子任务执行顺序为LIFO(后进先出)顺序,并且子任务可以被合并。ForkJoin框架为每一个独立工作的线程创建了对应的执行任务的工作队列,这个工作队列是使用数组进行双向组合的双向队列。无参构造函数 public ForkJoinPool() { this(Math.min(MAX_CAP, Runtime.getRuntime().availableProcessors()), defaultForkJoinWorkerThreadFactory, null, false, 0, MAX_CAP, 1, null, DEFAULT_KEEPALIVE, TimeUnit.MILLISECONDS); } 这个构造函数的含义是并行度是CPU的核数,线程工厂为defaultForkJoinWorkerThreadFactory,异常处理类为null表示不处理异常,异步模式为false表示执行LIFO(后进先出)可以合并子任务。工作窃取算法ForkJoinPool线程池的任务分为“外部任务”和“内部任务(任务拆分出来的子任务)”,两种任务存放位置不同,外部任务存放在ForkJoinPool的全局队列中,子任务会作为“内部任务”放到内部队列中,ForkJoinPool池中的每个线程都会维护一个内部队列,用于存放这些“内部任务”。由于ForkJoinPool有多个线程,那么就会对应的有多个队列,就会出现任务分步不均的问题,有的队列任务多,一直在执行任务,有的队列为空没有任务,一直空闲。那么就需要一种方式来解决这个问题,答案就是使用工作窃取算法。工作窃取核心思想是,工作线程自己的任务执行完了,就会去查找其他任务队列有没有任务,有的话就去执行其他队列的任务,这样也提高了效率。其实说白了就是我没活了,但是我又闲不住,我就去帮你干活,帮别人干活。那么会存在这样一种情况,自己的任务执行完了,会帮其他线程干活,但是会不会和其他线程同时执行同一个任务呢,就会产生竞争,简化的方案就是线程自己本地的队列采用LIFO(后进先出),窃取其他任务的队列任务采用FIFO(先进先出)策略,就是从队列的两头同时执行。工作窃取的优点1、线程不会因为等待某个子任务执行或者没有任务执行而被阻塞等待,而是会扫描所有的队列窃取任务,直到所有的队列都为空时才会挂起。2、ForkJoin为每个线程维护一个队列,这个对列是一个基于数组的双向对列,可以从首尾两端获取任务,极大的减少竞争的可能性,提高并行的性能。ForKJoin原理ForkJoinPool线程池的任务分为“外部任务”和“内部任务”。“内部任务“”和“外部任务”只是个抽象的概念,不是真的内外之分。“外部任务”存放在ForkJoinPool的全局队列中。ForkJoinPool池中的每个线程都会维护一个任务队列,用于存放“内部任务”,线程切割任务得到的子任务作为“内部任务”放到内部队列中。当工作线程想要获取子任务的执行结果时,会先判断子任务有没有完成,如果没有完成,再判断子任务有没有被其他线程窃取,如果没有被窃取,就有本线程来帮忙完成,如果子任务被窃取了,就去执行本线程“内部队列”的其他任务,或者扫描其他队列并窃取任务。当工作线程完成“内部任务”处于空闲状态时,就会扫描其他任务队列窃取任务,尽可能不会阻塞等待。ForkJoin线程在等待一个任务完成时,要么自己来完成这个任务,要么在其他线程窃取了这个任务的情况下,去执行其他任务,是不会阻塞等待的,从而避免资源浪费,除非所有的任务队列为空。参考《JAVA高并发核心编程(卷2):多线程、锁、JMM、JUC、高并发设计》-尼恩编著
正文package com.xiaojie.test; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; /** * @author xiaojie * @version 1.0 * @description: Calendar代码 * @date 2022/1/20 23:19 */ public class CalendarDemo { public static void main(String[] args) throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); Date date = sdf.parse("2021-12-26 13:14:15"); Calendar calendar = Calendar.getInstance(); calendar.setTime(date); System.out.println("获取当前年份>>>>>>>>>>>>>>" + calendar.get(Calendar.YEAR)); //月份是从0开始的 System.out.println("获取当前月份>>>>>>>>>>>>>>" + (calendar.get(Calendar.MONTH) + 1)); System.out.println("获取当前日期在当前月份第几天>>>>>>>>>>>>>>" + calendar.get(Calendar.DATE)); System.out.println("获取当前日期在当前月份第几天>>>>>>>>>>>>>>" + calendar.get(Calendar.DAY_OF_MONTH)); System.out.println("获取当前日期在当前年份第几天>>>>>>>>>>>>>>" + calendar.get(Calendar.DAY_OF_YEAR)); System.out.println("获取当前日期在当前月份是第几周>>>>>>>>>>>>>>" + calendar.get(Calendar.WEEK_OF_MONTH)); System.out.println("获取当前日期在当前年份是第几周>>>>>>>>>>>>>>" + calendar.get(Calendar.WEEK_OF_YEAR)); //1-7分别是周日、一、二、三、四、五、六 System.out.println("获取当前日期是星期几>>>>>>>>>>>>>>>>>>>>>>" + calendar.get(Calendar.DAY_OF_WEEK)); System.out.println("获取当前日期的时间小时 12小时计时制>>>>>>>>>>>>>>>>>>>>>>" + calendar.get(Calendar.HOUR)); //0表示上午,1-表示下午 System.out.println("获取当前日期的时间为上午还是下午>>>>>>>>>>>>>>>>>>>>>>" + calendar.get(Calendar.AM_PM)); System.out.println("获取当前日期的时间小时 24小时值>>>>>>>>>>>>>>>>>>>>>>" + calendar.get(Calendar.HOUR_OF_DAY)); System.out.println("获取当前日期的时间分钟>>>>>>>>>>>>>>>>>>>>>>" + calendar.get(Calendar.MINUTE)); System.out.println("获取当前日期的时间秒数>>>>>>>>>>>>>>>>>>>>>>" + calendar.get(Calendar.SECOND)); System.out.println("获取当前日期的时间毫秒>>>>>>>>>>>>>>>>>>>>>>" + calendar.get(Calendar.MILLISECOND)); //第二种设置日历方式 Calendar calendar1 = Calendar.getInstance(); calendar1.set(Calendar.YEAR, 2022); calendar1.set(Calendar.MONTH, 0); calendar1.set(Calendar.DATE, 20); // calendar.set(Calendar.HOUR,13); //12小时制 calendar1.set(Calendar.HOUR_OF_DAY, 13);//24小时制 calendar1.set(Calendar.MINUTE, 14); calendar1.set(Calendar.SECOND, 15); System.out.println("获取时间>>>calendar1>>>>>>" + sdf.format(calendar1.getTime())); //第三种 Calendar calendar2 = Calendar.getInstance(); calendar2.set(2022, 0, 21);//时分秒默认补齐 System.out.println("获取日期>>>calendar2>>>>>>>>>" + calendar2.getTime()); //第四种 Calendar calendar3 = Calendar.getInstance(); calendar3.set(2022, 0, 21, 13, 30);//时分秒默认补齐 System.out.println("获取日期>>>calendar3>>>>>>>>>>" + calendar3.getTime()); //第五种 Calendar calendar4 = Calendar.getInstance(); calendar4.set(2022, 0, 21, 13, 14, 15);//时分秒默认补齐 System.out.println("获取日期>>>calendar4>>>>>>>>>>>>" + calendar4.getTime()); } }执行结果如下图 坑一、你会发现12月26日竟然是当前年份的第一周,这个搞得我有点凌乱啊我是百思不能得解啊,终于后来我想明白了,这JAVA是TMD(甜蜜的)外国人写的,你有没有发现西方有个节日,对你没想错,就是圣诞节,我! 擦 !擦 !擦 !哦 麦噶扥 !!!原来西方人过完圣诞节后就开始新的第一周了。I Wish every day is merry, not just Christmas所以如果仅仅是为了计算日期是当前年份的第几周,那么只需要把当前日期减去6之后,就可以实现啦。坑二、这个大部分人应该知道,一周的第一天是周日,而不是星期一所以Calendar.DAY_OF_WEEK的1-7分别代表周日、周一、周二、周三、周四、周五、周六。坑三、月份并不像我们认为的月份是从1开始的,月份在Calendar中是从0开始的,所以聪明的你一定知道我想说啥了。Calendar.MONTH中 0-11依次代表1-12月份,所以写代码时候需要注意了啊0是1月份,1是2月份以此类推。。。。
正文1、AQS是什么 AQS(AbstractQueuedSynchronizer)是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。 AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。 CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。2、LockSupport的使用LockSupport是JUC提供的一个线程阻塞与唤醒的工具类,该工具可以让线程在任意位置阻塞和唤醒。主要方法如下public static void park(Object blocker);//无限期阻塞当前线程,带有blocker对象,用于确定线程阻塞的原因 public static void park();//无限期阻塞线程 public static void parkNanos(long nanos);//阻塞当前线程有阻塞时间限制 public static void parkNanos(Object blocker, long nanos);//阻塞当前线程有阻塞对象,有时间限制 public static void parkUntil(Object blocker, long deadline);//阻塞当前线程,直到某个时间 public static void unpark(Thread thread);//唤醒某个被阻塞的线程,只有线程阻塞了才会唤醒,不阻塞则不执行任何操作。LockSuport简单使用 public static void main(String[] args) { Thread t1 = new Thread(() -> { System.out.println("线程被阻塞了。。。。。。。1"); LockSupport.park(); System.out.println("线程被唤醒了。。。。。。。2"); }, "t1"); t1.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //准备唤醒线程 System.out.println("准备唤醒线程了。。。。。。。。。3"); LockSupport.unpark(t1); }上面执行的结果为1,3,2,当线程被阻塞时只有调用unpark唤醒线程,线程才会继续执行。LockSupport.park()与Thread.sleep()区别Thread.sleep()没法从外部唤醒,只能等待自己醒来或者发生异常。而LockSupport.park()可以通过LockSupport.unpark()方法唤醒。Thread.sleep()声明了InterruptedException中断异常,而LockSupport.park()方法不需要捕获异常与Thread.sleep()相比,LockSupport.park()方法更精确、更灵活的阻塞和唤醒指定线程。Thread.sleep()本身是一个native方法,LockSupport.park()是一个静态方法,但是底层调用了Unsafe类的native()方法。Locksupport.park()允许设置一个Blocker对象,主要用来提供监视工具或者诊断工具确定线程受阻原因。Thread.sleep()和Locksupport.park()都不会释放所持有的锁。LockSupport.park()与Object.wait()区别Object.wait()需要和synchronized关键字配合使用,而LockSuport.park()可以在任何地方执行。Object.wait()同样需要抛出中断异常。LockSupport.park()不需要。如果线程在没有wait()条件下执行notify()会抛出java.lang.IllegalMonitorStateException异常,而LockSupport.unpark()不会抛出任何异常。Object.wait()会释放锁,而LockSupport.park()不会释放锁。3、结合ReentrantLock分析AQS源码本分析基于JDK17,与jdk8代码有一些不同,但思想是一样的。ReentrantLock有以下几个内部类abstract static class Sync extends AbstractQueuedSynchronizer{}//继承了AQSstatic final class NonfairSync extends Sync {}//实现非公平锁的关键static final class FairSync extends Sync {}//实现公平锁的关键非公平加锁操作NonfairSync类图结构如下 执行加锁操作会执行如下方法,一步一步解密加锁的过程。 initialTryLock()方法解读1处的代码通过CAS操作将状态值state由0改为1如果修改成功,将当前线程设置为独占状态,然后返回true。因为lock()方法是(!initialTryLock())所以就直接表示获取锁成功。2处的代码表示将当前线程设置为独占状态。3处的代码判断当前的线程是不是已经持有了锁,如果是同一个就表示重入锁,然后将转状态值state加1,并返回true表示获取锁成功。acquire(1)方法解读 tryAcquire(1)方法解读 这个方法说明如果没有获取到锁,并不会直接将线程阻塞,而是通过CAS再次尝试一下是否可以获取到锁,如果获取到锁就直接返回,表示获取锁成功。不然则需要将线程加入队列等待。这里需要注意用到了模板方法的设计模式。acquire()方法解读final int acquire(Node node, int arg, boolean shared, boolean interruptible, boolean timed, long time) { Thread current = Thread.currentThread(); byte spins = 0, postSpins = 0; // retries upon unpark of first thread boolean interrupted = false, first = false; Node pred = null; // predecessor of node when enqueued /* * Repeatedly: * Check if node now first * if so, ensure head stable, else ensure valid predecessor * if node is first or not yet enqueued, try acquiring * else if node not yet created, create it * else if not yet enqueued, try once to enqueue * else if woken from park, retry (up to postSpins times) * else if WAITING status not set, set and retry * else park and clear WAITING status, and check cancellation */ for (;;) { if (!first && (pred = (node == null) ? null : node.prev) != null && !(first = (head == pred))) { if (pred.status < 0) { cleanQueue(); // predecessor cancelled continue; } else if (pred.prev == null) { Thread.onSpinWait(); // ensure serialization continue; } } if (first || pred == null) { boolean acquired; try { if (shared) acquired = (tryAcquireShared(arg) >= 0); else acquired = tryAcquire(arg); } catch (Throwable ex) { cancelAcquire(node, interrupted, false); throw ex; } if (acquired) { if (first) { node.prev = null; head = node; pred.next = null; node.waiter = null; if (shared) signalNextIfShared(node); if (interrupted) current.interrupt(); } return 1; } } if (node == null) { // allocate; retry before enqueue if (shared) node = new SharedNode(); else node = new ExclusiveNode(); } else if (pred == null) { // try to enqueue node.waiter = current; Node t = tail; node.setPrevRelaxed(t); // avoid unnecessary fence if (t == null) tryInitializeHead(); else if (!casTail(t, node)) node.setPrevRelaxed(null); // back out else t.next = node; } else if (first && spins != 0) { --spins; // reduce unfairness on rewaits Thread.onSpinWait(); } else if (node.status == 0) { node.status = WAITING; // enable signal and recheck } else { long nanos; spins = postSpins = (byte)((postSpins << 1) | 1); if (!timed) LockSupport.park(this); else if ((nanos = time - System.nanoTime()) > 0L) LockSupport.parkNanos(this, nanos); else break; node.clearStatus(); if ((interrupted |= Thread.interrupted()) && interruptible) break; } } return cancelAcquire(node, interrupted, interruptible); }主要分析以下两块代码 4处代码和上面tryAcquire(1)方法解读方法一样,不再赘述。5处代码 通过CAS获取到了锁,证明前面一个线程释放了锁,那么就将前面的节点设置为null方便GC回收,把当前的节点设置为头结点,将当前节点上等待的线程设置为null。6处代码 新建一个节点节点的状态值status=0。7处的代码设置node节点上的等待线程为当前线程,并把新节点设置为尾结点。如果节点上status状态为0就改为1。8处的代码 将当前线程设置为阻塞状态。大致过程下图 释放锁操作tryRelease(1)方法解读 9处代码 如上面加锁过程每加一次锁state值加1,相应的每释放一次锁相应的state的值减110处代码,验证释放锁的是不是独占的线程,如果不是就抛异常,大白话就是谁加的锁还需要谁来释放。11处代码,如果state值减为0,那么就将独占的线程置空,让其他线程来在重新CAS竞争。signalNext(Node h)方法解读 此操作相对简单,就是唤醒当前节点的下一个节点上的线程。但是抛出一个问题,这样唤醒下一个节点上的线程,那听着像是公平锁啊,其实不然。此时如果有一个新的线程不是队列中的线程进来,那么这个新的线程和即将唤醒的线程就会发生竞争。下面看一下公平锁的抢锁代码。 hasQueuedThreads()方法解读 这个方法有点意思,从链表尾部开始遍历,如果状态值大于等于0返回true,可是只要在链表排队的节点status要么等于0要么等于1。这就验证了争抢资源的线程是不是即将被唤醒的线程,来实现公平锁的原理。参考:Java 并发 - 理论基础 | Java 全栈知识体系《JAVA高并发核心编程(卷2):多线程、锁、JMM、JUC、高并发设计》-尼恩编著
正文1、如果报jvm启动失败,是因为启动参数的垃圾回收参数不对,应该是在jdk14之后,已经丢弃了cms垃圾回收器,所以修改响应的垃圾回收器参数即可。2、如果报错如下启动参数 添加 --add-opens=java.base/java.lang=ALL-UNNAMED完整的启动参数为(修改seata-server.bat,这是window环境的启动参数,linux根据需要灵活修改)%JAVACMD% %JAVA_OPTS% -server --add-opens=java.base/java.lang=ALL-UNNAMED -Xmx2048m -Xms2048m -Xmn1024m -Xss512k -XX:SurvivorRatio=10 -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m -XX:MaxDirectMemorySize=1024m -XX:-OmitStackTraceInFastThrow -XX:-UseAdaptiveSizePolicy -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath="%BASEDIR%"/logs/java_heapdump.hprof -XX:+DisableExplicitGC -Xlog:gc:"%BASEDIR%"/logs/seata_gc.log -verbose:gc -Dio.netty.leakDetectionLevel=advanced -Dlogback.color.disable-for-bat=true -classpath %CLASSPATH% -Dapp.name="seata-server" -Dapp.repo="%REPO%" -Dapp.home="%BASEDIR%" -Dbasedir="%BASEDIR%" io.seata.server.Server %CMD_LINE_ARGS%3、idea启动项目时候报如下错误Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled. 2022-01-03 21:58:40.961 ERROR 81432 --- [ main] o.s.boot.SpringApplication : Application run failed org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'globalTransactionScanner' defined in class path resource [io/seata/spring/boot/autoconfigure/SeataAutoConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [io.seata.spring.annotation.GlobalTransactionScanner]: Factory method 'globalTransactionScanner' threw exception; nested exception is java.lang.ExceptionInInitializerError at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:658) ~[spring-beans-5.3.3.jar:5.3.3] at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:638) ~[spring-beans-5.3.3.jar:5.3.3] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1336) ~[spring-beans-5.3.3.jar:5.3.3] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1179) ~[spring-beans-5.3.3.jar:5.3.3] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:571) ~[spring-beans-5.3.3.jar:5.3.3] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:531) ~[spring-beans-5.3.3.jar:5.3.3] at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.3.jar:5.3.3] at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.3.jar:5.3.3] at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.3.jar:5.3.3] at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:213) ~[spring-beans-5.3.3.jar:5.3.3] at org.springframework.context.support.PostProcessorRegistrationDelegate.registerBeanPostProcessors(PostProcessorRegistrationDelegate.java:232) ~[spring-context-5.3.3.jar:5.3.3] at org.springframework.context.support.AbstractApplicationContext.registerBeanPostProcessors(AbstractApplicationContext.java:767) ~[spring-context-5.3.3.jar:5.3.3] at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:572) ~[spring-context-5.3.3.jar:5.3.3] at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:144) ~[spring-boot-2.4.2.jar:2.4.2] at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:767) ~[spring-boot-2.4.2.jar:2.4.2] at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:759) ~[spring-boot-2.4.2.jar:2.4.2] at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:426) ~[spring-boot-2.4.2.jar:2.4.2] at org.springframework.boot.SpringApplication.run(SpringApplication.java:326) ~[spring-boot-2.4.2.jar:2.4.2] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1311) ~[spring-boot-2.4.2.jar:2.4.2] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1300) ~[spring-boot-2.4.2.jar:2.4.2] at com.xiaojie.seata.order.impl.OrderApp.main(OrderApp.java:19) ~[classes/:na] Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [io.seata.spring.annotation.GlobalTransactionScanner]: Factory method 'globalTransactionScanner' threw exception; nested exception is java.lang.ExceptionInInitializerError at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:185) ~[spring-beans-5.3.3.jar:5.3.3] at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:653) ~[spring-beans-5.3.3.jar:5.3.3] ... 20 common frames omitted Caused by: java.lang.ExceptionInInitializerError: null at net.sf.cglib.core.KeyFactory$Generator.generateClass(KeyFactory.java:166) ~[cglib-3.1.jar:na] at net.sf.cglib.core.DefaultGeneratorStrategy.generate(DefaultGeneratorStrategy.java:25) ~[cglib-3.1.jar:na] at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:216) ~[cglib-3.1.jar:na] at net.sf.cglib.core.KeyFactory$Generator.create(KeyFactory.java:144) ~[cglib-3.1.jar:na] at net.sf.cglib.core.KeyFactory.create(KeyFactory.java:116) ~[cglib-3.1.jar:na] at net.sf.cglib.core.KeyFactory.create(KeyFactory.java:108) ~[cglib-3.1.jar:na] at net.sf.cglib.core.KeyFactory.create(KeyFactory.java:104) ~[cglib-3.1.jar:na] at net.sf.cglib.proxy.Enhancer.<clinit>(Enhancer.java:69) ~[cglib-3.1.jar:na] at io.seata.config.ConfigurationCache.proxy(ConfigurationCache.java:88) ~[seata-all-1.4.2.jar:1.4.2] at io.seata.config.ConfigurationFactory.buildConfiguration(ConfigurationFactory.java:136) ~[seata-all-1.4.2.jar:1.4.2] at io.seata.config.ConfigurationFactory.getInstance(ConfigurationFactory.java:94) ~[seata-all-1.4.2.jar:1.4.2] at io.seata.spring.annotation.GlobalTransactionScanner.<init>(GlobalTransactionScanner.java:87) ~[seata-all-1.4.2.jar:1.4.2] at io.seata.spring.annotation.GlobalTransactionScanner.<init>(GlobalTransactionScanner.java:143) ~[seata-all-1.4.2.jar:1.4.2] at io.seata.spring.boot.autoconfigure.SeataAutoConfiguration.globalTransactionScanner(SeataAutoConfiguration.java:55) ~[seata-spring-boot-starter-1.4.2.jar:na] at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na] at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na] at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na] at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na] at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154) ~[spring-beans-5.3.3.jar:5.3.3] ... 21 common frames omitted Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @1aa7ecca at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354) ~[na:na] at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297) ~[na:na] at java.base/java.lang.reflect.Method.checkCanSetAccessible(Method.java:199) ~[na:na] at java.base/java.lang.reflect.Method.setAccessible(Method.java:193) ~[na:na] at net.sf.cglib.core.ReflectUtils$2.run(ReflectUtils.java:56) ~[cglib-3.1.jar:na] at java.base/java.security.AccessController.doPrivileged(AccessController.java:318) ~[na:na] at net.sf.cglib.core.ReflectUtils.<clinit>(ReflectUtils.java:46) ~[cglib-3.1.jar:na] ... 40 common frames omitted在jvm启动参数上添加--add-opens=java.base/java.lang=ALL-UNNAMED 4、启动之后如果报如下错误,不用怀疑,一定是你的config.txt导入的时候配置与application.yml的配置不一致 5、如果启动报一下错误java: java.lang.IllegalAccessError: class lombok.javac.apt.LombokProcessor (in unnamed module @0x68b859c5) cannot access class com.sun.tools.javac.processing.JavacProcessingEnvironment (in module jdk.compiler) because module jdk.compiler does not export com.sun.tools.javac.processing to unnamed module @0x68b859c5只需要将 maven添加版本号即可<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.20</version> <optional>true</optional></dependency>
正文一、什么是CAS 由于JVM的synchronized重量级锁涉及操作系统内核态下互斥锁的使用,因此其线程阻塞和唤醒都涉及进程在用户态和内核态频繁的切换,导致重量级锁开销大,性能低。CAS,Compare And Swap比较并替换。CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(E)新值(N)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。CAS也称为自旋锁,在一个(死)循环[for(;;)]里不断进行CAS操作,直到成功为止(自旋操作),实际上,CAS也是一种乐观锁。Unsafe 提供的CAS方法public final native boolean compareAndSetReference(Object o, long offset, Object expected, Object x); public final native boolean compareAndSetLong(Object o, long offset, long expected, long x); public final native boolean compareAndSetLong(Object o, long offset, long expected, long x); 参数说明o- 需要操作字段所在的对象offset-需要操作字段的偏移量 相对对象头,在64位jvm虚拟机偏移量是12,因为markWord占64位,Class pointer(类对象指针)占32位,所以偏移量是12.expected-期望值,也就是旧值x-更新的值,也就是新值在执行Unsafe的CAS方法时,这些方法值首先将内存位置的值与旧值比较,如果匹配那么CPU会自动将内存位置的值更新为新值,并返回true,如果不匹配,CPU不做任何操作,并返回false。当并发修改的线程少,冲突出现的机会少时,自旋次数也会减少,CAS的性能就会很高,反之冲突越多,自旋的次数越多,CAS的性能就会越低。 二、JUC原子类以AtomicInteger为例public final int get(); //获取当前的值 public final int getAndSet(int newValue); //获取当前的值,然后设置新的值 public final int getAndIncrement() ;//获取当前的值,然后自增 public final int getAndDecrement() ; //获取当前的值,然后自减 public final int getAndAdd(int delta) ; //获取当前的值,并加上预期的值 boolean compareAndSet(int expect, int update);//通过CAS方式设置整数值public class AtomicIntegerDemo extends Thread { private AtomicInteger atomicInteger; AtomicIntegerDemo(AtomicInteger atomicInteger) { this.atomicInteger = atomicInteger; } @Override public void run() { for (int i = 0; i < 100000; i++) { atomicInteger.getAndIncrement(); } } public static void main(String[] args) throws InterruptedException { AtomicInteger atomicInteger = new AtomicInteger(0); AtomicIntegerDemo demo1 = new AtomicIntegerDemo(atomicInteger); AtomicIntegerDemo demo2 = new AtomicIntegerDemo(atomicInteger); AtomicIntegerDemo demo3 = new AtomicIntegerDemo(atomicInteger); demo1.start(); demo2.start(); demo3.start(); demo1.join(); demo2.join(); demo3.join(); System.out.println(atomicInteger.get());//300000 } }由上面执行结果可知AtomicInteger是一个原子的操作,并发操作是线程安全的。AtomicInteger主要通过CAS自旋+volatile的方案实现,既保障了变量操作的线程安全性,又避免了synchronized重量级锁的开销。三、ABA问题 使用CAS操作内存数据时,数据发生过变化也能更新成功,如A——>B——>A,最后一个CAS预期数据A实际已经发生过更改,但也能修改成功,这就产生了ABA的问题。ABA的解决思路一般是使用版本号,每次变更都带上版本号。JDK提供了两个类AtomicStampedReference和AtomicMarkableReference解决ABA的问题。 AtomicStampedReference在CAS的基础上增加了一个Stamp(印戳或者标记),使用这个标识可以判断数据是否发生变化。 public static void main(String[] args) { String str1 = "aaa"; String str2 = "bbb"; //初始化AtomicStampedReference 初始值是aaa,印戳是1 AtomicStampedReference<String> reference = new AtomicStampedReference<String>(str1, 1); //cas比较str1=aaa ,reference.getStamp()=1,就把str2的值更新到新值,印戳加1 reference.compareAndSet(str1, str2, reference.getStamp(), reference.getStamp() + 1); //当前的值为:bbb印戳是:2 System.out.println("当前的值为:" + reference.getReference() + "印戳是:" + reference.getStamp()); boolean flag = reference.weakCompareAndSet(str2, "ccc", 2, reference.getStamp() + 1); //更新后的值为:bbb是否更新成功:false,更新失败,因为印戳4和2比较不对 System.out.println("更新后的值为:" + reference.getReference() + "是否更新成功:" + flag); } AtomicMarkableReference是AtomicStampedReference的简化版,不关心修改过几次,只关心是否修改过,标志属性是boolean类型,其值只记录是够修改过。public static void main(String[] args) { AtomicMarkableReference<Integer> atomicMarkableReference = new AtomicMarkableReference<Integer>(1, true); boolean b = atomicMarkableReference.compareAndSet(1, 2, true, false); //是否更新成功true更新后的值:2更新后的标志 mark:false System.out.println("是否更新成功" + b + "更新后的值:" + atomicMarkableReference.getReference() + "更新后的标志 mark:" + atomicMarkableReference.isMarked()); boolean b1 = atomicMarkableReference.compareAndSet(2, 3, true, false); //是否更新成功false更新后的值:2更新后的标志 mark:false System.out.println("是否更新成功" + b1 + "更新后的值:" + atomicMarkableReference.getReference() + "更新后的标志 mark:" + atomicMarkableReference.isMarked()); }总结:1、操作系统层面的CAS是一条CPU原子指令(cmpxchg指令),由于该指令具有原子性,因此使用CAS不会造成数据不一致的问题。2、使用CAS无锁编程步骤如下,获取字段中的期望值(旧值)与内存地址上的值作比较(没有修改之前的旧值),如果两个值相等,就把新值放在字段的内存地址上,失败则自旋。3、并发线程越少,自旋越少CAS性能越高,反之并发线程越多,自旋的次数越多,CAS的性能就越低。4、jdk中的原子类大都是使用CAS实现的。5、解决ABA问题使用版本号,jdk中有两个类AtomicStampedReference和AtomicMarkableReference解决ABA的问题。参考:《JAVA高并发核心编程(卷2):多线程、锁、JMM、JUC、高并发设计》-尼恩编著
正文 当多个线程访问一个对象时,如果不用考虑这些线程在运行环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。但是现实并不是这样子的,所以JVM实现了锁机制,今天就叭叭叭JAVA中各种各样的锁。1、自旋锁和自适应锁自旋锁:在多线程竞争的状态下共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复阻塞线程并不值得,而是让没有获取到锁的线程自旋(自旋并不会放弃CPU的分片时间)等待当前线程释放锁,如果自旋超过了限定的次数仍然没有成功获取到锁,就应该使用传统的方式去挂起线程了,在JDK定义中,自旋锁默认的自旋次数为10次,用户可以使用参数-XX:PreBlockSpin来更改(jdk1.6之后默认开启自旋锁)。自适应锁:为了解决某些特殊情况,如果自旋刚结束,线程就释放了锁,那么是不是有点不划算。自适应自旋锁是jdk1.6引入,规定自旋的时间不再固定了,而是由前一次在同一个锁上的自旋 时间及锁的拥有者的状态来决定的。如果在同一个锁对象上,自旋等待刚刚成功获取过锁,并且持有锁的线程正在运行中,那么JVM会认为该线程自旋获取到锁的可能性很大,会自动增加等待时间。反之就认为不容易获取到锁,而放弃自旋这种方式。锁消除:锁消除时指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。意思就是:在一段代码中,堆上的所有数据都不会逃逸出去而被其他线程访问到那就可以把他们当作栈上的数据对待,认为他们是线程私有的,不用再加锁。锁粗化: public static void main(String[] args) { StringBuffer buffer = new StringBuffer(); buffer.append("a"); buffer.append("b"); buffer.append("c"); System.out.println("拼接之后的结果是:>>>>>>>>>>>"+buffer); } @Override @IntrinsicCandidate public synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this; }StringBuffer 在拼接字符串时是同步的。但是在一系列的操作中都对同一个对象(StringBuffer )反复加锁和解锁,频繁的进行加锁解锁操作会导致不必要的性能损耗,JVM会将加锁同步的范围扩展到整个操作的外部,只加一次锁。2、轻量级锁和重量级锁 这种锁实现的背后基于这样一种假设,即在真实的情况下我们程序中的大部分同步代码一般都处于无锁竞争状态(即单线程执行环境),在无锁竞争的情况下完全可以避免调用操作系统层面的重量级互斥锁, 取而代之的是在monitorenter和monitorexit中只需要依靠一条CAS原子指令就可以完成锁的获取及释放。轻量级锁是相对于重量级锁而言的。轻量级锁加锁过程 在HotSpot虚拟机的对象头分为两部分,一部分用于存储对象自身的运行时数据,如Hashcode、GC分代年龄、标志位等,这部分长度在32位和64位的虚拟机中分别是32bit和64bit,称为Mark Word。另一部分用于存储指向方法区对象类型数据的指针,如果是数组对象的话,还会有一个额外的部分用于存储数组长度。 对象头信息是与对象自身定义的数据无关的额外存储成本,Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息。mark word中有两个bit存储锁标记位。HotSpot虚拟机对象头Mark Word存储内容标志位状态对象哈希码,分代年龄01无锁指向锁记录的指针00轻量级锁指向重量级锁的指针10膨胀重量级锁空,不需要记录信息11GC标记偏向线程id,偏向时间戳,对象分代年龄01可偏向 在代码进入同步代码块时,如果此对象没有被锁定(标记位为01状态),虚拟机首先在当前线程的栈帧建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前Mark Word的拷贝,然后虚拟机使用CAS操作尝试将对象的Mark Word 更新为指向Lock Record的指针,如果操作成功了,那么这个线程就有了这个对象的锁,并且将Mark Word 的标记位更改为00,表示这个对象处于轻量级锁定状态。如果更新失败了虚拟机会首先检查是否是当前线程拥有了这个对象的锁,如果是就进入同步代码,如果不是,那就说明锁被其他线程占用了。如果有两个以上的线程争夺同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁,锁标记位变为10,后面等待的线程就要进入阻塞状态。轻量级锁解锁过程 解锁过程同样使用CAS操作来进行,使用CAS操作将Mark Word 指向Lock Record 指针释放,如果操作成功,那么整个同步过程就完成了,如果释放失败,说明有其他线程尝试获取该锁,那就在释放锁的同时,唤醒被挂起的线程。3、偏向锁JVM 参数 -XX:-UseBiasedLocking 禁用偏向锁;-XX:+UseBiasedLocking 启用偏向锁。 启用了偏向锁才会执行偏向锁的操作。当锁对象第一次被线程获取时,虚拟机会把对象头中的标记位设置为01,偏向模式。同时使用CAS操作获取到当前线程的线程ID存储到Mark Word 中,如果操作成功,那么持有偏向锁的线程以后每次进入这个锁相关的同步块时,都不需要任何操作,直接进入。如果有多个线程去尝试获取这个锁时,偏向锁就宣告无效,然后会撤销偏向或者恢复到未锁定。然后再膨胀为重量级锁,标记位状态变为10。4、可重入锁和不可重入锁可重入锁就是一个线程获取到锁之后,在另一个代码块还需要该锁,那么不需要重新获取而可以直接使用该锁。大多数的锁都是可重入锁。但是CAS自旋锁不可重入。package com.xiaojie.juc.thread.lock; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * @author xiaojie * @version 1.0 * @description: 测试锁的重入性 * @date 2021/12/30 22:09 */ public class Test01 { public synchronized void a() { System.out.println(Thread.currentThread().getName() + "运行a方法"); b(); } private synchronized void b() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "运行b方法"); } public static void main(String[] args) { Test01 test01 = new Test01(); ExecutorService executorService = Executors.newFixedThreadPool(3); for (int i=0;i<10;i++){ executorService.execute(() -> test01.a()); } } }5、悲观锁和乐观锁悲观锁总是悲观的,总是认为会发生安全问题,所以每次操作都会加锁。比如独占锁、传统数据库中的行锁、表锁、读锁、写锁等。悲观锁存在以下几个缺点:在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延迟,引起性能问题。一个线程占有锁后,其他线程就得阻塞等待。如果优先级高的线程等待一个优先级低的线程,会导致线程优先级导致,可能引发性能风险。乐观锁总是乐观的,总是认为不会发生安全问题。在数据库中可以使用版本号实现乐观锁,JAVA中的CAS和一些原子类都是乐观锁的思想。6、公平锁和非公平锁公平锁:是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。非公平锁:非公平锁不需要按照申请锁的时间顺序来获取锁,而是谁能获取到CPU的时间片谁就先执行。非公平锁的优点是吞吐量比公平锁大,缺点是有可能导致线程优先级反转或者造成过线程饥饿现象(就是有的线程玩命的一直在执行任务,有的线程至死没有执行一个任务)。synchronized中的锁是非公平锁,ReentrantLock默认也是非公平锁,但是可以通过构造函数设置为公平锁。7、共享锁和独占锁共享锁就是同一时刻允许多个线程持有的锁。例如Semaphore(信号量)、ReentrantReadWriteLock的读锁、CountDownLatch倒数闩等。独占锁也叫排它锁、互斥锁、独占锁是指锁在同一时刻只能被一个线程所持有。例如synchronized内置锁和ReentrantLock显示锁,ReentrantReadWriteLock的写锁都是独占锁。package com.xiaojie.juc.thread.lock; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * @description: 读写锁验证共享锁和独占锁 * @author xiaojie * @date 2021/12/30 23:28 * @version 1.0 */ public class ReadAndWrite { static class ReadThred extends Thread { private ReentrantReadWriteLock lock; private String name; public ReadThred(String name, ReentrantReadWriteLock lock) { super(name); this.lock = lock; } @Override public void run() { try { lock.readLock().lock(); System.out.println(Thread.currentThread().getName() + "这是共享锁。。。。。。"); } catch (Exception e) { e.printStackTrace(); } finally { lock.readLock().unlock(); System.out.println(Thread.currentThread().getName() + "释放锁成功。。。。。。"); } } } static class WriteThred extends Thread { private ReentrantReadWriteLock lock; private String name; public WriteThred(String name, ReentrantReadWriteLock lock) { super(name); this.lock = lock; } @Override public void run() { try { lock.writeLock().lock(); Thread.sleep(3000); System.out.println(Thread.currentThread().getName() + "这是独占锁。。。。。。。。"); } catch (Exception e) { e.printStackTrace(); } finally { lock.writeLock().unlock(); System.out.println(Thread.currentThread().getName() + "释放锁。。。。。。。"); } } } public static void main(String[] args) { ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(); ReadThred readThred1 = new ReadThred("read-thread-1", reentrantReadWriteLock); ReadThred readThred2 = new ReadThred("read-thread-1", reentrantReadWriteLock); WriteThred writeThred1 = new WriteThred("write-thread-1", reentrantReadWriteLock); WriteThred writeThred2 = new WriteThred("write-thread-2", reentrantReadWriteLock); readThred1.start(); readThred2.start(); writeThred1.start(); writeThred2.start(); } }8、可中断锁和不可中断锁可中断锁只在抢占锁的过程中可以被中断的锁如ReentrantLock。不可中断锁是不可中断的锁如java内置锁synchronized。总结:参考:《JAVA高并发核心编程(卷2):多线程、锁、JMM、JUC、高并发设计》-尼恩编著《深入理解JAVA虚拟机:JVM高级特性与最佳实践第二版》 Java 全栈知识体系
正文三、四种线程池解析Executors.newSingleThreadExecutor();package com.xiaojie.juc.thread.pool; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * @author xiaojie * @version 1.0 * @description: 单线程线程池 * @date 2021/12/12 22:14 */ public class SingleThreadExecutorDemo { public static void main(String[] args) { ExecutorService executorService = Executors.newSingleThreadExecutor(); for (int i=0;i<5;i++){ int finalI = i; executorService.execute(() -> { try { Thread.sleep(30); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("这是一个单线程线程池的demo,线程名称:"+Thread.currentThread().getName()+">>>>>>>"+ finalI); }); } //关闭线程池 executorService.shutdown(); } }由执行结果可知1、单线程线程池中的任务是按照提交任务的顺序执行的。2、池中唯一的线程存活时间是无限制的。3、当池中的线程正在繁忙时,新提交的任务会进入内部阻塞队列,并且阻塞队列是无界的(LinkedBlockingQueue<Runnable>)。适用场景单线程线程池适用于任务按照提交次序,一个任务一个任务的逐个执行的场景。Executors.newFixedThreadPoolpackage com.xiaojie.juc.thread.pool; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * @author xiaojie * @version 1.0 * @description: 固定长度的线程池 * @date 2021/12/12 22:29 */ public class FixedThreadPoolDemo { public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(3); for (int i = 0; i < 10; i++) { int finalI = i; executorService.execute(new Runnable() { @Override public void run() { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("这是一个固定长度的线程池的demo,线程名称:" + Thread.currentThread().getName() + ">>>>>>>" + finalI); } }); } executorService.shutdown(); } } 有执行结果可知1、并不是按照任务提交顺序执行的。2、如果线程数量没有达到固定数量,每次提交都会创建新的线程,直到达到最大数量3、如果线程洗的大小达到固定数量就会保持不变,如果某个线程因为异常而结束,那么线程池会补充一个新的线程。4、如果接收到新任务没有空闲线程也会进入阻塞队列(LinkedBlockingQueue<Runnable>)。适用场景:需要任务长期执行的场景,固定数量的线程数能够避免频繁的创建和销毁线程,例如CPU密集型的任务,在CPU被工作线程长时间占用的情况下,能确保尽可能减少线程分配。弊端:内部使用无界队列来存放任务,当大量任务超过线程池能处理的最大容量时队列无限增大,使服务器资源迅速耗尽。Executors.newCachedThreadPool()package com.xiaojie.juc.thread.pool; import java.util.concurrent.*; /** * @author xiaojie * @version 1.0 * @description: 缓存功能的线程池 * @date 2021/12/12 20:31 */ public class CachedThreadPoolDemo { public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < 10; i++) { int finalI = i; executorService.execute(new Runnable() { @Override public void run() { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("这是一个缓存功能的线程池的demo,线程名称:" + Thread.currentThread().getName() + ">>>>>>>" + finalI); } }); } executorService.shutdown(); } } 执行结果可知1、当任务提交时,如果线程繁忙,会创建新的线程执行任务。2、对线程池的大小没有限制,底层使用SynchronousQueue<Runnable>队列。3、如果部分线程空闲,线程数量超过了任务数量,就会回收空闲(60秒不执行任务)线程。适用场景需要快速处理突发性强,耗时较短的任务场景,例如Netty的NIO场景,RESTAPI瞬时削峰。可缓存线程池的线程数量不固定,有空闲线程就会自动回收,接收到新任务时判断是否有空闲线程,如果没有就直接创建新的线程。弊端线程池没有最大线程数量限制,如果大量的异步任务同时执行,可能会因创建线程过多而导致资源耗尽。Executors.newScheduledThreadPoolpackage com.xiaojie.juc.thread.pool; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; /** * @author xiaojie * @version 1.0 * @description: 定时,延迟线程池 * @date 2021/12/12 23:08 */ public class ScheduledThreadPoolDemo { public static void main(String[] args) { ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3); for (int i = 0; i < 10; i++) { int finalI = i; scheduledExecutorService.schedule(() -> { System.out.println("这是一个延迟线程池的demo,延迟5秒后执行,线程名称:" + Thread.currentThread().getName() + ">>>>>>>" + finalI); }, 5, TimeUnit.SECONDS); scheduledExecutorService.scheduleAtFixedRate(new Runnable() { @Override public void run() { System.out.println("这个方法延迟1秒后执行,然后每隔1秒后重复执行"); } }, 1, 1, TimeUnit.SECONDS); } // scheduledExecutorService.shutdown(); } }使用 DelayedWorkQueue()队列实现 适用场景周期性的执行任务的场景,例如一些定时任务的实现,Springboot的任务调度。四、自定义线程池Executors创建线程的潜在问题1、创建newFixedThreadPool的潜在问题在于工作队列,使用LinkedBlockingQueue(无界队列),如果任务的提交速度大于任务的处理速度,就会造成大量的任务在阻塞队列中等待,如果阻塞队列很大,很有可能导致OOM(内存溢出)。2、创建newSingleThreadExecutor和newFixedThreadPool线程池一样,同样使用LinkedBlockingQueue(无界队列),如果任务的提交速度大于任务的处理速度,就会造成大量的任务在阻塞队列中等待,如果阻塞队列很大,很有可能导致OOM(内存溢出)。3、newCachedThreadPool线程池的潜在问题在于其核心线程数为0,最大线程数为Integer.MAX_VALUE,使用SynchronousQueue同步队列。如果同时执行大量的任务,就意味会创建大量的线程,可能导致OOM,甚至导致CPU资源耗尽。4、ScheduledThreadPoolExecutor最大线程数也是Integer.MAX_VALUE,和newCachedThreadPool存在同样的问题。自己创建线程池package com.xiaojie.juc.thread.pool; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * @author xiaojie * @version 1.0 * @description: 自定义创建线程池 * @date 2021/12/12 23:56 */ public class MyThreadPool { //定义工作队列 BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(10); public ThreadPoolExecutor threadPoolExecutor(int corePoolSize, int maximumPoolSize, Long keepAliveTime) { return new ThreadPoolExecutor(corePoolSize, maximumPoolSize, 0L, TimeUnit.MILLISECONDS, workQueue); } public static void main(String[] args) { MyThreadPool myThreadPool = new MyThreadPool(); ThreadPoolExecutor executor = myThreadPool.threadPoolExecutor(3, 10, 60L); for (int i = 0; i < 20; i++) { executor.execute(() -> { //最大可以允许20个任务,超过的将进行拒绝策略 System.out.println("通过ThreadPoolExecutor 定义的线程池" + Thread.currentThread().getName()); }); } } }五、如何确定线程池线程数1、由于IO密集型任务的CPU使用率低,导致线程空闲时间很多,因此通常需要开CPU 核心数两倍的线程。当IO线程空闲时,可以启用其他线程继续使用CPU,来提高CPU的利用率。2、如果是CPU密集型,CPU密集型的任务虽然可以并行的执行,但是并行的任务越多,花在线程切换的时间就越多,CPU执行效率就越低,所以一般设置线程数等于CPU的核心数。3、混合型既要满足IO又要满足CPU密集的计算公式最佳线程数=((线程等待时间+线程CPU时间)/线程CPU时间)*CPU核数参考:《JAVA高并发核心编程(卷2):多线程、锁、JMM、JUC、高并发设计》-尼恩编著
正文一、传统日志收集的弊端 我们知道我们大多数是通过日志,然后判断程序哪里报错了,这样针对日志我们才能对症下一剂猛药。如果在集群环境中,成百上千的服务器,如果报错了,我们如何查找日志呢,一个一个日志文件这样排查么?那可就为难死我们了。二、ELK收集系统过程 基于Elasticsearch、Logstash、Kibana可以实现分布式日志收集系统,再加上Kibana的可视化系统,对数据进行分析,嗯真香。 在请求过程中创建AOP,拦截请求,然后在Aop方法中开启异步线程,将消息发送到Kafka(单机或者集群),logstash接收kafka的日志,经过消息过滤,然后发送到ElasticSearch系统,然后经过Kibana可视化界面,对日志进行搜索分析等。三、搭建ELK系统Zookeeper搭建Kafka搭建ElasticSearch搭建Kibana搭建Logstash搭建本文演示基于Docker-compose,所有的均为单机1、搭建docker-compose#下载docker-compose文件 sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose #授权 sudo chmod +x /usr/local/bin/docker-compose2、创建目录mkdir -p /usr/local/docker-compose/elk3、在上面目录创建docker-compose.yml文件 version: '2' services: zookeeper: image: zookeeper:latest container_name: zookeper ports: - "2181:2181" kafka: image: wurstmeister/kafka:latest container_name: kafka volumes: - /etc/localtime:/etc/localtime ports: - "9092:9092" environment: KAFKA_ADVERTISED_HOST_NAME: 192.168.139.160 KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 KAFKA_ADVERTISED_PORT: 9092 KAFKA_LOG_RETENTION_HOURS: 120 KAFKA_MESSAGE_MAX_BYTES: 10000000 KAFKA_REPLICA_FETCH_MAX_BYTES: 10000000 KAFKA_GROUP_MAX_SESSION_TIMEOUT_MS: 60000 KAFKA_NUM_PARTITIONS: 3 KAFKA_DELETE_RETENTION_MS: 1000 elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:7.15.2 restart: always container_name: elasticsearch environment: - discovery.type=single-node #单点启动,实际生产不允许 - "ES_JAVA_OPTS=-Xms512m -Xmx512m" ports: - 9200:9200 kibana: image: docker.elastic.co/kibana/kibana:7.15.2 restart: always container_name: kibana ports: - 5601:5601 environment: - elasticsearch_url=http://192.168.139.160:9200 depends_on: - elasticsearch logstash: image: docker.elastic.co/logstash/logstash:7.15.2 volumes: - /data/logstash/pipeline/:/usr/share/logstash/pipeline/ - /data/logstash/config/logstash.yml:/usr/share/logstash/config/logstash.yml - /data/logstash/config/pipelines.yml:/usr/share/logstash/config/pipelines.yml restart: always container_name: logstash ports: - 9600:9600 depends_on: - elasticsearch4、启动.1. #进入docker-compose所在的目录执行 2. [root@localhost elk]# docker-compose up四、代码切面类package com.xiaojie.elk.aop; import com.alibaba.fastjson.JSONObject; import com.xiaojie.elk.pojo.RequestPojo; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.kafka.core.KafkaTemplate; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.net.InetAddress; import java.net.UnknownHostException; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; /** * @author xiaojie * @version 1.0 * @description: 日志切面类 * @date 2021/12/5 16:51 */ @Aspect @Component public class AopLogAspect { @Value("${server.port}") private String serverPort; @Autowired private KafkaTemplate<String, Object> kafkaTemplate; // 申明一个切点 里面是 execution表达式 @Pointcut("execution(* com.xiaojie.elk.service.*.*(..))") private void serviceAspect() { } @Autowired private LogContainer logContainer; // 请求method前打印内容 @Before(value = "serviceAspect()") public void methodBefore(JoinPoint joinPoint) { ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder .getRequestAttributes(); HttpServletRequest request = requestAttributes.getRequest(); RequestPojo requestPojo = new RequestPojo(); SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");// 设置日期格式 requestPojo.setRequestTime(df.format(new Date())); requestPojo.setUrl(request.getRequestURL().toString()); requestPojo.setMethod(request.getMethod()); requestPojo.setSignature(joinPoint.getSignature().toString()); requestPojo.setArgs(Arrays.toString(joinPoint.getArgs())); // IP地址信息 requestPojo.setAddress(getIpAddr(request) + ":" + serverPort); // 将日志信息投递到kafka中 String log = JSONObject.toJSONString(requestPojo); logContainer.put(log); } // 在方法执行完结后打印返回内容 /* @AfterReturning(returning = "o", pointcut = "serviceAspect()") public void methodAfterReturing(Object o) { ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder .getRequestAttributes(); HttpServletRequest request = requestAttributes.getRequest(); JSONObject respJSONObject = new JSONObject(); JSONObject jsonObject = new JSONObject(); SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");// 设置日期格式 jsonObject.put("response_time", df.format(new Date())); jsonObject.put("response_content", JSONObject.toJSONString(o)); // IP地址信息 jsonObject.put("ip_addres", getIpAddr(request) + ":" + serverPort); respJSONObject.put("response", jsonObject); logContainer.put(respJSONObject.toJSONString()); }*/ /** * 异常通知 * * @param point */ @AfterThrowing(pointcut = "serviceAspect()", throwing = "e") public void serviceAspect(JoinPoint joinPoint, Exception e) { ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder .getRequestAttributes(); HttpServletRequest request = requestAttributes.getRequest(); SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");// 设置日期格式 RequestPojo requestPojo = new RequestPojo(); requestPojo.setRequestTime(df.format(new Date())); requestPojo.setUrl(request.getRequestURL().toString()); requestPojo.setMethod(request.getMethod()); requestPojo.setSignature(joinPoint.getSignature().toString()); requestPojo.setArgs(Arrays.toString(joinPoint.getArgs())); // IP地址信息 requestPojo.setAddress(getIpAddr(request) + ":" + serverPort); requestPojo.setError(e.toString()); // 将日志信息投递到kafka中 String log = JSONObject.toJSONString(requestPojo); logContainer.put(log); } public static String getIpAddr(HttpServletRequest request) { //X-Forwarded-For(XFF)是用来识别通过HTTP代理或负载均衡方式连接到Web服务器的客户端最原始的IP地址的HTTP请求头字段。 String ipAddress = request.getHeader("x-forwarded-for"); if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getHeader("Proxy-Client-IP"); } if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getHeader("WL-Proxy-Client-IP"); } if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getRemoteAddr(); if (ipAddress.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")) { //根据网卡取本机配置的IP InetAddress inet = null; try { inet = InetAddress.getLocalHost(); } catch (UnknownHostException e) { e.printStackTrace(); } ipAddress = inet.getHostAddress(); } } //对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割 if (ipAddress != null && ipAddress.length() > 15) { //"***.***.***.***".length() = 15 if (ipAddress.indexOf(",") > 0) { ipAddress = ipAddress.substring(0, ipAddress.indexOf(",")); } } return ipAddress; } }异步线程package com.xiaojie.elk.aop; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.kafka.core.KafkaTemplate; import org.springframework.stereotype.Component; import java.util.concurrent.BlockingDeque; import java.util.concurrent.LinkedBlockingDeque; /** * @author xiaojie * @version 1.0 * @description: 开启异步线程发送日志 * @date 2021/12/5 16:50 */ @Component public class LogContainer { private static BlockingDeque<String> logDeque = new LinkedBlockingDeque<>(); @Autowired private KafkaTemplate<String, Object> kafkaTemplate; public LogContainer() { // 初始化 new LogThreadKafka().start(); } /** * 存入日志 * * @param log */ public void put(String log) { logDeque.offer(log); } class LogThreadKafka extends Thread { @Override public void run() { while (true) { String log = logDeque.poll(); if (!StringUtils.isEmpty(log)) { // 将消息投递kafka中 kafkaTemplate.send("kafka-log", log); } } } } }五、验证效果 完整代码:spring-boot: Springboot整合redis、消息中间件等相关代码 elk模块
正文ogstash 是一个实时数据收集引擎,可收集各类型数据并对其进行分析,过滤和归纳。按照自己条件分析过滤出符合数据导入到可视化界面。它可以实现多样化的数据源数据全量或增量传输,数据标准格式处理,数据格式化输出等的功能,常用于日志处理。工作流程分为三个阶段: (1)input数据输入阶段,可接收oracle、mysql、postgresql、file等多种数据源; (2)filter数据标准格式化阶段,可过滤、格式化数据,如格式化时间、字符串等; (3)output数据输出阶段,可输出到elasticsearch、mongodb、kafka等接收终端。传统方式1、下载安装包https://artifacts.elastic.co/downloads/logstash/logstash-7.15.2-linux-x86_64.tar.gz2、解压缩,移动重命名[root@localhost ~]# tar -zxvf logstash-7.15.2-linux-x86_64.tar.gz [root@localhost ~]# mv logstash-7.15.2 /usr/local/logstash3、配置注意一下所有的配置文件请设置成utf-8格式,不然启动可能会报错!!!pipelines.yml 配置方式有三种 1、直接写input,output这样,使用config.string字段 - pipeline.id: test pipeline.workers: 1 pipeline.batch.size: 1 config.string: "input { generator {} } filter { sleep { time => 1 } } output { stdout { codec => dots } }" 2、使用配置文件的路径 使用path.config字段 - pipeline.id: another_test queue.type: persisted path.config: "/tmp/logstash/a.config" 3、使用通配符格式 path.config=/tmp/logstash/conf.d/*.conf - pipeline.id: another_test queue.type: persisted path.config: "/tmp/logstash/conf.d/*.conf" #多个路径分开写 - pipeline.id: kafka pipeline.workers: 2 #线程数默认与cpu核数一致 pipeline.batch.size: 1 #批量处理的条数默认125 path.config: "/usr/local/logstash/config/logstash-kafka.conf" - pipeline.id: es queue.type: persisted #队列持久化,防止丢失数据,默认不开启 path.config: "/usr/local/logstash/config/logstash-es.conf" log-es.conf# Sample Logstash configuration for creating a simple # Beats -> Logstash -> Elasticsearch pipeline. input{ file{ # 日志文件路径 path => "/usr/local/es/logs/my-es.log" type => "elasticsearch" start_position => "beginning" #从文件开始处读写 } } #过滤器,正则表达式 filter { #定义数据的格式 grok { match => { "message" => "%{DATA:timestamp}\|%{IP:serverIp}\|%{IP:clientIp}\|%{DATA:logSource}\|%{DATA:userId}\|%{DATA:reqUrl}\|%{DATA:reqUri}\|%{DATA:refer}\|%{DATA:device}\|%{DATA:textDuring}\|%{DATA:duringTime:int}\|\|"} } #定义时间戳的格式 date { match => [ "timestamp", "yyyy-MM-dd-HH:mm:ss" ] locale => "cn" } #定义客户端的IP是哪个字段(上面定义的数据格式) geoip { source => "clientIp" } } output{ elasticsearch{ hosts => ["192.168.139.160:9200","192.168.139.161:9200","192.168.139.162:9200"] # es地址 index => "es-message-%{+YYYY.MM.dd}" #如果es没有设置密码则不需要设置密码 user => "elastic" password => "cGKuMaWGZLBaSSDW7qKX" } stdout{ codec => rubydebug } }logstash-kafka.confinput { kafka { bootstrap_servers => "192.168.139.162:9092" topics => "my-topic-partition" } } filter { #Only matched data are send to output } output { elasticsearch{ hosts => ["192.168.139.160:9200","192.168.139.161:9200","192.168.139.162:9200"] # es地址 index => "kafka-log-%{+YYYY.MM.dd}" user => "elastic" password => "cGKuMaWGZLBaSSDW7qKX" } stdout{ codec => rubydebug } }logstash.yml文件注释说明# Settings file in YAML # # Settings can be specified either in hierarchical form, e.g.: # # pipeline: # batch: # size: 125 # delay: 5 # # Or as flat keys: # # pipeline.batch.size: 125 # pipeline.batch.delay: 5 # # ------------ Node identity ------------ # # Use a descriptive name for the node: #默认机器主机名称 # node.name: test # # If omitted the node name will default to the machine's host name # # ------------ Data path ------------------ # # Which directory should be used by logstash and its plugins # for any persistent needs. Defaults to LOGSTASH_HOME/data #logstash及其插件目录 # path.data: # # ------------ Pipeline Settings -------------- # # The ID of the pipeline. # # pipeline.id: main # # Set the number of workers that will, in parallel, execute the filters+outputs # stage of the pipeline. # # This defaults to the number of the host's CPU cores. #将并行执行管道的过滤器和输出阶段的工作线程数,默认是cpu核数 # pipeline.workers: 2 # # How many events to retrieve from inputs before sending to filters+workers #单个工作线程将从输入中收集的最大事件数 pipeline.batch.size: 125 # # How long to wait in milliseconds while polling for the next event # before dispatching an undersized batch to filters+outputs #轮询下一个事件时等待的时间(毫秒) pipeline.batch.delay: 50 # # Force Logstash to exit during shutdown even if there are still inflight # events in memory. By default, logstash will refuse to quit until all # received events have been pushed to the outputs. # # WARNING: enabling this can lead to data loss during shutdown #设置为 时true,强制 Logstash 在关闭期间退出,即使内存中仍有进行中的事件。 #默认情况下,Logstash 将拒绝退出,直到所有接收到的事件都已推送到输出 # pipeline.unsafe_shutdown: false # # Set the pipeline event ordering. Options are "auto" (the default), "true" or "false". # "auto" will automatically enable ordering if the 'pipeline.workers' setting # is also set to '1'. # "true" will enforce ordering on the pipeline and prevent logstash from starting # if there are multiple workers. # "false" will disable any extra processing necessary for preserving ordering. #排序 # pipeline.ordered: auto # # ------------ Pipeline Configuration Settings -------------- # # Where to fetch the pipeline configuration for the main pipeline # # path.config: # # Pipeline configuration string for the main pipeline # # config.string: # # At startup, test if the configuration is valid and exit (dry run) #检查配置是否正确,默认不检查 # config.test_and_exit: false # # Periodically check if the configuration has changed and reload the pipeline # This can also be triggered manually through the SIGHUP signal #会定期检查配置是否已更改,并在更改时重新加载配置。默认不检查 # config.reload.automatic: false # # How often to check if the pipeline configuration has changed (in seconds) # Note that the unit value (s) is required. Values without a qualifier (e.g. 60) # are treated as nanoseconds. # Setting the interval this way is not recommended and might change in later versions. #Logstash 检查配置文件的更改频率(以秒为单位) # config.reload.interval: 3s # # Show fully compiled configuration as debug log message # NOTE: --log.level must be 'debug' # # config.debug: false # # When enabled, process escaped characters such as \n and \" in strings in the # pipeline configuration files. #带引号的字符串是否转义 # config.support_escapes: false # # ------------ HTTP API Settings ------------- # Define settings related to the HTTP API here. # # The HTTP API is enabled by default. It can be disabled, but features that rely # on it will not work as intended. # http.enabled: true # # By default, the HTTP API is bound to only the host's local loopback interface, # ensuring that it is not accessible to the rest of the network. Because the API # includes neither authentication nor authorization and has not been hardened or # tested for use as a publicly-reachable API, binding to publicly accessible IPs # should be avoided where possible. # # http.host: 127.0.0.1 # # The HTTP API web server will listen on an available port from the given range. # Values can be specified as a single port (e.g., `9600`), or an inclusive range # of ports (e.g., `9600-9700`). #默认9600 # http.port: 9600-9700 # # ------------ Module Settings --------------- # Define modules here. Modules definitions must be defined as an array. # The simple way to see this is to prepend each `name` with a `-`, and keep # all associated variables under the `name` they are associated with, and # above the next, like this: # # modules: # - name: MODULE_NAME # var.PLUGINTYPE1.PLUGINNAME1.KEY1: VALUE # var.PLUGINTYPE1.PLUGINNAME1.KEY2: VALUE # var.PLUGINTYPE2.PLUGINNAME1.KEY1: VALUE # var.PLUGINTYPE3.PLUGINNAME3.KEY1: VALUE # # Module variable names must be in the format of # # var.PLUGIN_TYPE.PLUGIN_NAME.KEY # # modules: # # ------------ Cloud Settings --------------- # Define Elastic Cloud settings here. # Format of cloud.id is a base64 value e.g. dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRub3RhcmVhbCRpZGVudGlmaWVy # and it may have an label prefix e.g. staging:dXMtZ... # This will overwrite 'var.elasticsearch.hosts' and 'var.kibana.host' # cloud.id: <identifier> # # Format of cloud.auth is: <user>:<pass> # This is optional # If supplied this will overwrite 'var.elasticsearch.username' and 'var.elasticsearch.password' # If supplied this will overwrite 'var.kibana.username' and 'var.kibana.password' # cloud.auth: elastic:<password> # # ------------ Queuing Settings -------------- # # Internal queuing model, "memory" for legacy in-memory based queuing and # "persisted" for disk-based acked queueing. Defaults is memory # persisted基于磁盘的 ACKed 队列,会将未消费的消息持久化到磁盘 #memory基于内存,宕机之后,有可能丢失数据,默认是memory # queue.type: memory # # If using queue.type: persisted, the directory path where the data files will be stored. # Default is path.data/queue #启用持久队列时将存储数据文件的目录路径 # path.queue: # # If using queue.type: persisted, the page data files size. The queue data consists of # append-only data files separated into pages. Default is 64mb #启用持久队列时使用的页面数据文件的大小,默认64M # queue.page_capacity: 64mb # # If using queue.type: persisted, the maximum number of unread events in the queue. # Default is 0 (unlimited) #启用持久队列时队列中未读事件的最大数量,0表示没有限制 # queue.max_events: 0 # # If using queue.type: persisted, the total capacity of the queue in number of bytes. # If you would like more unacked events to be buffered in Logstash, you can increase the # capacity using this setting. Please make sure your disk drive has capacity greater than # the size specified here. If both max_bytes and max_events are specified, Logstash will pick # whichever criteria is reached first # Default is 1024mb or 1gb #队列的总容量 # queue.max_bytes: 1024mb # # If using queue.type: persisted, the maximum number of acked events before forcing a checkpoint # Default is 1024, 0 for unlimited #启用持久队列时强制检查点之前 ACKed 事件的最大数量 # queue.checkpoint.acks: 1024 # # If using queue.type: persisted, the maximum number of written events before forcing a checkpoint # Default is 1024, 0 for unlimited #启用持久队列时强制检查点之前的最大写入事件数 # queue.checkpoint.writes: 1024 # # If using queue.type: persisted, the interval in milliseconds when a checkpoint is forced on the head page # Default is 1000, 0 for no periodic checkpoint. # # queue.checkpoint.interval: 1000 # # ------------ Dead-Letter Queue Settings -------------- # Flag to turn on dead-letter queue. #死信队列 # dead_letter_queue.enable: false # If using dead_letter_queue.enable: true, the maximum size of each dead letter queue. Entries # will be dropped if they would increase the size of the dead letter queue beyond this setting. # Default is 1024mb # dead_letter_queue.max_bytes: 1024mb # If using dead_letter_queue.enable: true, the interval in milliseconds where if no further events eligible for the DLQ # have been created, a dead letter queue file will be written. A low value here will mean that more, smaller, queue files # may be written, while a larger value will introduce more latency between items being "written" to the dead letter queue, and # being available to be read by the dead_letter_queue input when items are are written infrequently. # Default is 5000. # # dead_letter_queue.flush_interval: 5000 # If using dead_letter_queue.enable: true, the directory path where the data files will be stored. # Default is path.data/dead_letter_queue # # path.dead_letter_queue: # # ------------ Metrics Settings -------------- # # Bind address for the metrics REST endpoint # # http.host: "127.0.0.1" # # Bind port for the metrics REST endpoint, this option also accept a range # (9600-9700) and logstash will pick up the first available ports. # # http.port: 9600-9700 # # ------------ Debugging Settings -------------- # # Options for log.level: # * fatal # * error # * warn # * info (default) # * debug # * trace # # log.level: info # path.logs: # # ------------ Other Settings -------------- # # Where to find custom plugins # path.plugins: [] # # Flag to output log lines of each pipeline in its separate log file. Each log filename contains the pipeline.name # Default is false #用于启用不同日志文件中每个管道的日志分离 # pipeline.separate_logs: false # # ------------ X-Pack Settings (not applicable for OSS build)-------------- # # X-Pack Monitoring # https://www.elastic.co/guide/en/logstash/current/monitoring-logstash.html #xpack.monitoring.enabled: false #xpack.monitoring.elasticsearch.username: logstash_system #xpack.monitoring.elasticsearch.password: password #xpack.monitoring.elasticsearch.proxy: ["http://proxy:port"] #xpack.monitoring.elasticsearch.hosts: ["https://es1:9200", "https://es2:9200"] # an alternative to hosts + username/password settings is to use cloud_id/cloud_auth #xpack.monitoring.elasticsearch.cloud_id: monitoring_cluster_id:xxxxxxxxxx #xpack.monitoring.elasticsearch.cloud_auth: logstash_system:password # another authentication alternative is to use an Elasticsearch API key #xpack.monitoring.elasticsearch.api_key: "id:api_key" #xpack.monitoring.elasticsearch.ssl.certificate_authority: [ "/path/to/ca.crt" ] #xpack.monitoring.elasticsearch.ssl.truststore.path: path/to/file #xpack.monitoring.elasticsearch.ssl.truststore.password: password #xpack.monitoring.elasticsearch.ssl.keystore.path: /path/to/file #xpack.monitoring.elasticsearch.ssl.keystore.password: password #xpack.monitoring.elasticsearch.ssl.verification_mode: certificate #xpack.monitoring.elasticsearch.sniffing: false #xpack.monitoring.collection.interval: 10s #xpack.monitoring.collection.pipeline.details.enabled: true # # X-Pack Management # https://www.elastic.co/guide/en/logstash/current/logstash-centralized-pipeline-management.html #xpack.management.enabled: false #xpack.management.pipeline.id: ["main", "apache_logs"] #xpack.management.elasticsearch.username: logstash_admin_user #xpack.management.elasticsearch.password: password #xpack.management.elasticsearch.proxy: ["http://proxy:port"] #xpack.management.elasticsearch.hosts: ["https://es1:9200", "https://es2:9200"] # an alternative to hosts + username/password settings is to use cloud_id/cloud_auth #xpack.management.elasticsearch.cloud_id: management_cluster_id:xxxxxxxxxx #xpack.management.elasticsearch.cloud_auth: logstash_admin_user:password # another authentication alternative is to use an Elasticsearch API key #xpack.management.elasticsearch.api_key: "id:api_key" #xpack.management.elasticsearch.ssl.certificate_authority: [ "/path/to/ca.crt" ] #xpack.management.elasticsearch.ssl.truststore.path: /path/to/file #xpack.management.elasticsearch.ssl.truststore.password: password #xpack.management.elasticsearch.ssl.keystore.path: /path/to/file #xpack.management.elasticsearch.ssl.keystore.password: password #xpack.management.elasticsearch.ssl.verification_mode: certificate #xpack.management.elasticsearch.sniffing: false #xpack.management.logstash.poll_interval: 5s # X-Pack GeoIP plugin # https://www.elastic.co/guide/en/logstash/current/plugins-filters-geoip.html#plugins-filters-geoip-manage_update #xpack.geoip.download.endpoint: "https://geoip.elastic.co/v1/database"4、启动1. [root@localhost logstash]# ./bin/logstash 2. #后台启动 3. [root@localhost logstash]# nohup ./bin/logstash &Docker方式1、拉取镜像docker pull docker.elastic.co/logstash/logstash:7.15.22、创建挂载mkdir -p /data/logstash/{pipeline,config} logstash.ymllogstash.yml#开启 http.host: 0.0.0.0pipelines.yml# List of pipelines to be loaded by Logstash # # This document must be a list of dictionaries/hashes, where the keys/values are pipeline settings. # Default values for omitted settings are read from the `logstash.yml` file. # When declaring multiple pipelines, each MUST have its own `pipeline.id`. # # Example of two pipelines: - pipeline.id: kafka pipeline.workers: 2 #线程数默认与cpu核数一致 pipeline.batch.size: 1 #批量处理的条数默认125 path.config: "/usr/share/logstash/pipeline"降配置文件放到pipeline目录(logstash-kafka.conf)3、创建容器docker run -it --name logstash --net=host \ -v /data/logstash/pipeline/:/usr/share/logstash/pipeline/ \ -v /data/logstash/config/logstash.yml:/usr/share/logstash/config/logstash.yml \ -v /data/logstash/config/pipelines.yml:/usr/share/logstash/config/pipelines.yml \ docker.elastic.co/logstash/logstash:7.15.2注意若是es与logstash不在同一台服务器上启动参数一定要加上--net=host,不然其他es节点连接不上!
正文 在最低安全配置中添加密码保护后,需要配置传输层安全 (TLS)。传输层处理集群中节点之间的所有内部通信。如果集群有多个节点,那么您必须在节点之间配置 TLS。如果不启用 TLS,生产模式集群将不会启动。传输层依赖于双向 TLS 来加密和验证节点。正确应用 TLS 可确保恶意节点无法加入集群并与其他节点交换数据。虽然在 HTTP 层实现用户名和密码认证对于保护本地集群很有用,但节点之间的通信安全需要 TLS。在节点之间配置 TLS 是基本的安全设置,以防止未经授权的节点访问您的集群。传统方式集群安装1、生成证书./bin/elasticsearch-certutil caPlease enter the desired output file [elastic-stack-ca.p12]: 此处按回车键Enter password for elastic-stack-ca.p12 : 输入密码 snail(也可以不输入)2、集群中的任意一个节点生成证书和私钥./bin/elasticsearch-certutil cert --ca elastic-stack-ca.p12Enter 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,将该证书复制到每一个节点的config目录下/usr/local/elasticsearch/config4、在每一个节点上存储密码./bin/elasticsearch-keystore add xpack.security.transport.ssl.keystore.secure_password ./bin/elasticsearch-keystore add xpack.security.transport.ssl.truststore.secure_password 5、修改每一个节点elasticsearch.yml 注意不要有多余的空格# ======================== Elasticsearch Configuration ========================= # # NOTE: Elasticsearch comes with reasonable defaults for most settings. # Before you set out to tweak and tune the configuration, make sure you # understand what are you trying to accomplish and the consequences. # # The primary way of configuring a node is via this file. This template lists # the most important settings you may want to configure for a production cluster. # # Please consult the documentation for further information on configuration options: # https://www.elastic.co/guide/en/elasticsearch/reference/index.html # # ---------------------------------- Cluster ----------------------------------- # # Use a descriptive name for your cluster: # cluster.name: my-es # # ------------------------------------ Node ------------------------------------ # # Use a descriptive name for the node: # node.name: node-3 # # Add custom attributes to the node: # #node.attr.rack: r1 # # ----------------------------------- Paths ------------------------------------ # # Path to directory where to store the data (separate multiple locations by comma): # path.data: /usr/local/es/data # # Path to log files: # path.logs: /usr/local/es/logs # # ----------------------------------- Memory ----------------------------------- # # Lock the memory on startup: # #bootstrap.memory_lock: true # # Make sure that the heap size is set to about half the memory available # on the system and that the owner of the process is allowed to use this # limit. # # Elasticsearch performs poorly when the system is swapping the memory. # # ---------------------------------- Network ----------------------------------- # # By default Elasticsearch is only accessible on localhost. Set a different # address here to expose this node on the network: # network.host: 0.0.0.0 # # By default Elasticsearch listens for HTTP traffic on the first free port it # finds starting at 9200. Set a specific HTTP port here: # http.port: 9200 # # For more information, consult the network module documentation. # # --------------------------------- Discovery ---------------------------------- # # Pass an initial list of hosts to perform discovery when this node is started: # The default list of hosts is ["127.0.0.1", "[::1]"] # discovery.seed_hosts: ["192.168.139.160","192.168.139.161", "192.168.139.162"] # # Bootstrap the cluster using an initial set of master-eligible nodes: # cluster.initial_master_nodes: ["node-1", "node-2","node-3"] # # For more information, consult the discovery and cluster formation module documentation. # # ---------------------------------- Various ----------------------------------- # # Require explicit names when deleting indices: # #action.destructive_requires_name: true #设置密码 xpack.security.enabled: true http.cors.allow-headers: Authorization xpack.license.self_generated.type: basic #设置单点模式 #discovery.type: single-node #设置证书 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.p126、授权证书文件chown -R snail_es.es /usr/local/elasticsearch/7、设置密码,请保证每个节点都在运行的状态./bin/elasticsearch-setup-passwords interactive 手动 ./bin/elasticsearch-setup-passwords auto 自动Changed password for user apm_systemPASSWORD apm_system = Yu0vjHZxkCBXuGnTM9VMChanged password for user kibana_systemPASSWORD kibana_system = oNXyGWsWHLC3VllVb4QbChanged password for user kibanaPASSWORD kibana = oNXyGWsWHLC3VllVb4QbChanged password for user logstash_systemPASSWORD logstash_system = XFd1IoqZAgt7scdxwXN2Changed password for user beats_systemPASSWORD beats_system = 1oENHpgMQLeLyiugkmRyChanged password for user remote_monitoring_userPASSWORD remote_monitoring_user = bjGZqG7SxffKciVJRxsXChanged password for user elasticPASSWORD elastic = cGKuMaWGZLBaSSDW7qKXelastic一个内置的超级用户。kibana_systemKibana 用于连接 Elasticsearch 并与之通信的用户。logstash_systemLogstash 在 Elasticsearch 中存储监控信息时使用的用户。beats_systemBeats 在 Elasticsearch 中存储监控信息时使用的用户。apm_systemAPM 服务器在 Elasticsearch 中存储监控信息时使用的用户。remote_monitoring_user在 Elasticsearch 中收集和存储监控信息时使用的用户 Metricbeat。它具有remote_monitoring_agent和 remote_monitoring_collector内置角色。8、结束Docker方式1、创建挂载目录并授权[root@localhost ~]# mkdir -p /data/es/{conf,data,logs,plugins} #授权 [root@localhost ~]# chmod 777 -R /data/2、进入容器生成证书docker exec -it elasticsearch /bin/bash ./bin/elasticsearch-certutil ca #集群中的任意一个节点生成证书和私钥 ./bin/elasticsearch-certutil cert --ca elastic-stack-ca.p123、将证书复制到每个节点的config目录(挂载目录)将证书复制到宿主机 #复制 docker cp elasticsearch:/usr/share/elasticsearch/elastic-certificates.p12 /root4、将证书复制到每一个节点docker容器中docker cp /data/es/conf/elastic-certificates.p12 elasticsearch:/usr/share/elasticsearch/config #存储密码 每一个节点都要执行 ./bin/elasticsearch-keystore add xpack.security.transport.ssl.keystore.secure_password ./bin/elasticsearch-keystore add xpack.security.transport.ssl.truststore.secure_password5、修改配置文件跟上面配置文件一样6、进入容器设置密码docker exec -it elasticsearch /bin/bash ./bin/elasticsearch-setup-passwords interactive 手动 ./bin/elasticsearch-setup-passwords auto 自动7、重新启动容器或者新创建容器docker run --name elasticsearch --privileged=true --net=host \ -v /data/es/conf/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \ -v /data/es/data:/usr/share/elasticsearch/data \ -v /data/es/logs:/usr/share/elasticsearch/logs \ -v /data/es/plugins:/usr/share/elasticsearch/plugins \ -d elasticsearch:7.14.28、结束
正文 Kibana是一个开源分析和可视化平台,旨在与 Elasticsearch 配合使用。您可以使用 Kibana 搜索、查看存储在 Elasticsearch 索引中的数据并与之交互。您可以轻松地执行高级数据分析并在各种图表、表格和地图中可视化您的数据。传统方式1、下载kibanahttps://artifacts.elastic.co/downloads/kibana/kibana-7.15.2-linux-x86_64.tar.gz2、解压缩移动重命名[root@bogon ~]# tar -zxvf kibana-7.15.2-linux-x86_64.tar.gz #移动 [root@bogon ~]# mv kibana-7.15.2-linux-x86_64 /usr/local/kibana 3、修改配置文件kibana.yml# Kibana is served by a back end server. This setting specifies the port to use. #端口号 server.port: 5601 # Specifies the address to which the Kibana server will bind. IP addresses and host names are both valid values. # The default is 'localhost', which usually means remote machines will not be able to connect. # To allow connections from remote users, set this parameter to a non-loopback address. #链接地址 server.host: "192.168.139.161" # Enables you to specify a path to mount Kibana at if you are running behind a proxy. # Use the `server.rewriteBasePath` setting to tell Kibana if it should remove the basePath # from requests it receives, and to prevent a deprecation warning at startup. # This setting cannot end in a slash. #server.basePath: "" # Specifies whether Kibana should rewrite requests that are prefixed with # `server.basePath` or require that they are rewritten by your reverse proxy. # This setting was effectively always `false` before Kibana 6.3 and will # default to `true` starting in Kibana 7.0. #server.rewriteBasePath: false # Specifies the public URL at which Kibana is available for end users. If # `server.basePath` is configured this URL should end with the same basePath. #server.publicBaseUrl: "" # The maximum payload size in bytes for incoming server requests. #server.maxPayload: 1048576 # The Kibana server's name. This is used for display purposes. #server.name: "your-hostname" # The URLs of the Elasticsearch instances to use for all your queries. #es地址 elasticsearch.hosts: ["http://192.168.139.160:9200","http://192.168.139.161:9200","http://192.168.139.162:9200"] # Kibana uses an index in Elasticsearch to store saved searches, visualizations and # dashboards. Kibana creates a new index if the index doesn't already exist. #kibana.index: ".kibana" # The default application to load. #kibana.defaultAppId: "home" # If your Elasticsearch is protected with basic authentication, these settings provide # the username and password that the Kibana server uses to perform maintenance on the Kibana # index at startup. Your Kibana users still need to authenticate with Elasticsearch, which # is proxied through the Kibana server. #如果elasticsearch设置了密码,请打开此处的注释,并填写自己的密码 elasticsearch.username: "kibana_system" elasticsearch.password: "oNXyGWsWHLC3VllVb4Qb" # Kibana can also authenticate to Elasticsearch via "service account tokens". # If may use this token instead of a username/password. # elasticsearch.serviceAccountToken: "my_token" # Enables SSL and paths to the PEM-format SSL certificate and SSL key files, respectively. # These settings enable SSL for outgoing requests from the Kibana server to the browser. #server.ssl.enabled: false #server.ssl.certificate: /path/to/your/server.crt #server.ssl.key: /path/to/your/server.key # Optional settings that provide the paths to the PEM-format SSL certificate and key files. # These files are used to verify the identity of Kibana to Elasticsearch and are required when # xpack.security.http.ssl.client_authentication in Elasticsearch is set to required. #elasticsearch.ssl.certificate: /path/to/your/client.crt #elasticsearch.ssl.key: /path/to/your/client.key # Optional setting that enables you to specify a path to the PEM file for the certificate # authority for your Elasticsearch instance. #elasticsearch.ssl.certificateAuthorities: [ "/path/to/your/CA.pem" ] # To disregard the validity of SSL certificates, change this setting's value to 'none'. #elasticsearch.ssl.verificationMode: full # Time in milliseconds to wait for Elasticsearch to respond to pings. Defaults to the value of # the elasticsearch.requestTimeout setting. #elasticsearch.pingTimeout: 1500 # Time in milliseconds to wait for responses from the back end or Elasticsearch. This value # must be a positive integer. #elasticsearch.requestTimeout: 30000 # List of Kibana client-side headers to send to Elasticsearch. To send *no* client-side # headers, set this value to [] (an empty list). #elasticsearch.requestHeadersWhitelist: [ authorization ] # Header names and values that are sent to Elasticsearch. Any custom headers cannot be overwritten # by client-side headers, regardless of the elasticsearch.requestHeadersWhitelist configuration. #elasticsearch.customHeaders: {} # Time in milliseconds for Elasticsearch to wait for responses from shards. Set to 0 to disable. #elasticsearch.shardTimeout: 30000 # Logs queries sent to Elasticsearch. Requires logging.verbose set to true. #elasticsearch.logQueries: false # Specifies the path where Kibana creates the process ID file. #pid.file: /run/kibana/kibana.pid # Enables you to specify a file where Kibana stores log output. #logging.dest: stdout # Set the value of this setting to true to suppress all logging output. #logging.silent: false # Set the value of this setting to true to suppress all logging output other than error messages. #logging.quiet: false # Set the value of this setting to true to log all events, including system usage information # and all requests. #logging.verbose: false # Set the interval in milliseconds to sample system and process performance # metrics. Minimum is 100ms. Defaults to 5000. #ops.interval: 5000 # Specifies locale to be used for all localizable strings, dates and number formats. # Supported languages are the following: English - en , by default , Chinese - zh-CN . #中文显示 i18n.locale: "zh-CN"4、授权用户chown -R snail_es.es /usr/local/kibana5、启动[snail_es@bogon kibana]$ ./bin/kibana #后台启动 [snail_es@bogon kibana]$ nohup ./bin/kibana &6、 验证浏览器输入 http://192.168.139.161:5601/可以执行es指令如下 #创建索引 PUT /test #查询索引 GET /test; #新增user PUT /test/user/1 { "name":"snail", "sex":0, "age":22 } PUT /test/user/2 { "name":"周依琳", "sex":0, "age":22 } PUT /test/user/3 { "name":"周结论", "sex":0, "age":22 } PUT /test/user/4 { "name":"周蜗牛", "sex":0, "age":22 } PUT /test/user/5 { "name":"牛粪粪", "sex":0, "age":22 } GET /test/user/1 #id查询 GET /test/user/_mget { "ids":["1","2"] } #精确查询 GET test/user/_search { "query": { "term": { "name": "snail" } } } #模糊匹配 GET /test/user/_search { "from": 0, "size": 2, "query": { "match": { "name": "周" } } } Docker方式1、拉取镜像[root@bogon config]# docker pull docker.elastic.co/kibana/kibana:7.15.22、创建挂载目录[root@bogon ~]# mkdir -p /data/kibana/conf3、配置文件kibana.yml# Default Kibana configuration for docker target server.host: "0" server.shutdownTimeout: "5s" elasticsearch.hosts: [ "http://192.168.139.160.147:9200","http://192.168.139.161:9200","http://192.168.139.162:9200" ] monitoring.ui.container.elasticsearch.enabled: true #elasticsearch.username: "elastic" #elasticsearch.password: "123456" #中文 i18n.locale: "zh-CN"4、启动容器docker run -d \ --name=kibana \ --restart=always \ -p 5601:5601 \ -v /data/kibana/conf/kibana.yml:/usr/share/kibana/config/kibana.yml \ docker.elastic.co/kibana/kibana:7.15.25、访问方式同上
正文一、Elasticsearch介绍 Elasticsearch 是一个分布式文档储存中间件,它不会将信息储存为列数据行,而是储存已序列化为 JSON 文档的复杂数据结构。当你在一个集群中有多个节点时,储存的文档分布在整个集群里面,并且立刻可以从任意节点去访问。 当文档被储存时,它将建立索引并且近实时(1s)被搜索。 Elasticsearch 使用一种被称为倒排索引的数据结构,该结构支持快速全文搜索。在倒排索引里列出了所有文档中出现的每一个唯一单词并分别标识了每个单词在哪一个文档中。 索引可以被认为是文档的优化集合,每个文档索引都是字段的集合,这些字段是包含了数据的键值对。默认情况下,Elasticsearch 为每个字段中的所有数据建立倒排索引,并且每个索引字段都有专门的优化数据结构。例如:文本字段在倒排索引里,数值和地理字段被储存在 BKD 树中。正是因为通过使用按字段数据结构组合,才使得 Elasticsearch 拥有如此快速的搜索能力。二、ElasticSearch集群安装本文安装版本为7.15.2,老版本有些参数有些不同;jdk版本为jdk17。传统方式1、安装jdk环境2、解压缩安装包#解压缩文件 tar -zxvf elasticsearch-7.15.2-linux-x86_64.tar.gz -C /usr/local/ #重名名 mv elasticsearch-7.15.2 /usr/local/elasticsearch3、创建用户组由于elasticsearch不能使用root账户启动,所以需要创建账户#创建用户组 groupadd es #创建用户 useradd -g es snail_es #授权 chown -R snail_es.es /usr/local/elasticsearch/4、创建es数据目录存放数据和日志,并授权#创建目录文件并授权 mkdir /usr/local/es chown -R snail_es.es /usr/local/es5、修改配置文件 (各个节点的配置请根据下面的配置文件响应修改)# ======================== Elasticsearch Configuration ========================= # # NOTE: Elasticsearch comes with reasonable defaults for most settings. # Before you set out to tweak and tune the configuration, make sure you # understand what are you trying to accomplish and the consequences. # # The primary way of configuring a node is via this file. This template lists # the most important settings you may want to configure for a production cluster. # # Please consult the documentation for further information on configuration options: # https://www.elastic.co/guide/en/elasticsearch/reference/index.html # # ---------------------------------- Cluster ----------------------------------- # # Use a descriptive name for your cluster: #集群名称,三个节点名字相同 cluster.name: my-es # # ------------------------------------ Node ------------------------------------ # # Use a descriptive name for the node: #每个节点的名字,各不相同 node.name: node-1 # # Add custom attributes to the node: # #node.attr.rack: r1 # # ----------------------------------- Paths ------------------------------------ # # Path to directory where to store the data (separate multiple locations by comma): #数据目录 path.data: /usr/local/es/data # # Path to log files: #日志目录 path.logs: /usr/local/es/logs # # ----------------------------------- Memory ----------------------------------- # # Lock the memory on startup: # #bootstrap.memory_lock: true # # Make sure that the heap size is set to about half the memory available # on the system and that the owner of the process is allowed to use this # limit. # # Elasticsearch performs poorly when the system is swapping the memory. # # ---------------------------------- Network ----------------------------------- # # By default Elasticsearch is only accessible on localhost. Set a different # address here to expose this node on the network: #当前主机ip network.host: 192.168.139.160 # # By default Elasticsearch listens for HTTP traffic on the first free port it # finds starting at 9200. Set a specific HTTP port here: #对外端口号 http.port: 9200 # # For more information, consult the network module documentation. # # --------------------------------- Discovery ---------------------------------- # # Pass an initial list of hosts to perform discovery when this node is started: # The default list of hosts is ["127.0.0.1", "[::1]"] #集群发现,默认端口是9300 discovery.seed_hosts: ["192.168.139.160","192.168.139.161", "192.168.139.162"] #集群节点名称 # Bootstrap the cluster using an initial set of master-eligible nodes: # cluster.initial_master_nodes: ["node-1", "node-2","node-3"] # # For more information, consult the discovery and cluster formation module documentation. # # ---------------------------------- Various ----------------------------------- # # Require explicit names when deleting indices: # #action.destructive_requires_name: true6、修改服务器参数,不然启动时候会报错1、Elasticsearch 使用大量文件描述符或文件句柄。文件描述符用完可能是灾难性的,并且很可能导致数据丢失。 请确保将运行 Elasticsearch 的用户打开文件描述符的数量限制增加到 65536 或更高。 /etc/security/limits.conf 将 nofile 设置为 65535 2、Elasticsearch 对不同类型的操作使用许多线程池。能够在需要时创建新线程很重要。 确保 Elasticsearch 用户可以创建的线程数至少为 4096。 可以通过设置 ulimit -u 4096 以 root 启动 Elasticsearch, 或者通过在 /etc/security/limits.conf 设置 nproc 为 4096 #解决办法 vi /etc/security/limits.conf,添加下面内容: * soft nofile 65536 * hard nofile 131072 * soft nproc 2048 * hard nproc 4096 之后重启服务器生效 3、Elasticsearch 默认使用 mmapfs 目录存储其索引。默认的操作系统对 mmap 计数的限制可能太低,这可能会导致内存不足异常。sysctl -w vm.max_map_count=262144 #解决办法: 在/etc/sysctl.conf文件最后添加一行 vm.max_map_count=262144 执行/sbin/sysctl -p 立即生效7、切换用户进入安装目录启动,分别启动三台节点1. ./bin/elasticsearch 2. #后台启动 3. ./bin/elasticsearch -d8、检测结果,浏览器输入http://192.168.139.160:9200/_cat/nodes?prettyDocker方式安装1、拉取镜像文件[root@bogon ~]# docker pull elasticsearch:7.14.22、创建挂载目录并授权[root@localhost ~]# mkdir -p /data/es/{conf,data,logs,plugins} #授权 [root@localhost ~]# chmod 777 -R /data/3、配置文件,只需要修改相应的node.name: node-1,node.name: node-2,node.name: node-3,# ======================== Elasticsearch Configuration ========================= # # NOTE: Elasticsearch comes with reasonable defaults for most settings. # Before you set out to tweak and tune the configuration, make sure you # understand what are you trying to accomplish and the consequences. # # The primary way of configuring a node is via this file. This template lists # the most important settings you may want to configure for a production cluster. # # Please consult the documentation for further information on configuration options: # https://www.elastic.co/guide/en/elasticsearch/reference/index.html # # ---------------------------------- Cluster ----------------------------------- # # Use a descriptive name for your cluster: #集群名称,三个节点名字相同 cluster.name: my-es # # ------------------------------------ Node ------------------------------------ # # Use a descriptive name for the node: #每个节点的名字,各不相同 node.name: node-1 # # Add custom attributes to the node: # #node.attr.rack: r1 # # ----------------------------------- Paths ------------------------------------ # # Path to directory where to store the data (separate multiple locations by comma): #数据目录 #path.data: /usr/local/es/data # # Path to log files: #日志目录 #path.logs: /usr/local/es/logs # # ----------------------------------- Memory ----------------------------------- # # Lock the memory on startup: # #bootstrap.memory_lock: true # # Make sure that the heap size is set to about half the memory available # on the system and that the owner of the process is allowed to use this # limit. # # Elasticsearch performs poorly when the system is swapping the memory. # # ---------------------------------- Network ----------------------------------- # # By default Elasticsearch is only accessible on localhost. Set a different # address here to expose this node on the network: #当前主机ip network.host: 0.0.0.0 # # By default Elasticsearch listens for HTTP traffic on the first free port it # finds starting at 9200. Set a specific HTTP port here: #对外端口号 http.port: 9200 # # For more information, consult the network module documentation. # # --------------------------------- Discovery ---------------------------------- # # Pass an initial list of hosts to perform discovery when this node is started: # The default list of hosts is ["127.0.0.1", "[::1]"] #集群发现,默认端口是9300 discovery.seed_hosts: ["192.168.139.160","192.168.139.161", "192.168.139.162"] #集群节点名称 # Bootstrap the cluster using an initial set of master-eligible nodes: # cluster.initial_master_nodes: ["node-1", "node-2","node-3"] # # For more information, consult the discovery and cluster formation module documentation. # # ---------------------------------- Various ----------------------------------- # # Require explicit names when deleting indices: # #action.destructive_requires_name: true4、创建docker容器,之前先执行上面第6步docker run --name elasticsearch --privileged=true --net=host \ -v /data/es/conf/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \ -v /data/es/data:/usr/share/elasticsearch/data \ -v /data/es/logs:/usr/share/elasticsearch/logs \ -v /data/es/plugins:/usr/share/elasticsearch/plugins \ -d elasticsearch:7.14.2 5、验证http://192.168.139.160:9200/_cat/nodes?pretty三、配置中文分词器下载分词器,与es版本对应https://github.com/medcl/elasticsearch-analysis-ik/releases 传统方式1、创建ik目录[root@bogon plugins]# mkdir -p /usr/local/elasticsearch/plugins/ik2、将下载的分词解压到ik目录下[root@bogon ik]# unzip /root/elasticsearch-analysis-ik-7.15.2.zip -d /usr/local/elasticsearch/plugins/ik/3、启动elasticsearch验证分词器 Docker方式1、解压到挂载目录下[root@bogon plugins]# unzip /root/elasticsearch-analysis-ik-7.14.2.zip -d /data/es/plugins/ik 2、重启Docker[root@bogon plugins]# docker start elasticsearch3、检验方式与上面一样四、整合SpringBootmaven依赖 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency> <!--swagger依赖 --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-boot-starter</artifactId> <version>3.0.0</version> </dependency> </dependencies>核心代码package com.xiaojie.es.service; import com.xiaojie.es.entity.User; import com.xiaojie.es.mapper.UserMapper; import com.xiaojie.es.util.ElasticSearchUtils; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomUtils; import org.apache.commons.lang3.StringUtils; import org.elasticsearch.common.Strings; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.search.fetch.subphase.FetchSourceContext; import org.elasticsearch.search.sort.SortOrder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.io.IOException; import java.util.List; import java.util.Map; /** * @Description: * @author: yan * @date: 2021.11.30 */ @Service public class UserService { @Autowired private UserMapper userMapper; @Autowired private ElasticSearchUtils elasticSearchUtils; //添加用户 public void add() throws IOException { // elasticSearchUtils.createIndex("user"); for (int i = 0; i < 100; i++) { User user = new User(); String chars = "11月29日在美国休斯敦进行的2021世界乒乓球锦标赛女子双打决赛中中国组合孙颖莎王曼昱以3比0击败日本组合伊藤美诚早田希娜夺得冠军"; user.setName(RandomStringUtils.random(3, chars)); user.setAge(RandomUtils.nextInt(18, 40)); userMapper.add(user); //添加到es elasticSearchUtils.addData(user, "user"); } } /* * * @todo 查询用户 * @author yan * @date 2021/11/30 16:24 * @return java.util.List<java.util.Map<java.lang.String,java.lang.Object>> */ public List<Map<String, Object>> search() throws IOException { //构建查询条件 BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); //精确查询 //boolQueryBuilder.must(QueryBuilders.wildcardQuery("name", "张三")); // 模糊查询 boolQueryBuilder.filter(QueryBuilders.wildcardQuery("name", "王")); // 范围查询 from:相当于闭区间; gt:相当于开区间(>) gte:相当于闭区间 (>=) lt:开区间(<) lte:闭区间 (<=) boolQueryBuilder.filter(QueryBuilders.rangeQuery("age").from(18).to(32)); SearchSourceBuilder query = new SearchSourceBuilder(); query.query(boolQueryBuilder); //需要查询的字段,缺省则查询全部 String fields = ""; //需要高亮显示的字段 String highlightField = "name"; if (StringUtils.isNotBlank(fields)) { //只查询特定字段。如果需要查询所有字段则不设置该项。 query.fetchSource(new FetchSourceContext(true, fields.split(","), Strings.EMPTY_ARRAY)); } //分页参数,相当于pageNum Integer from = 0; //分页参数,相当于pageSize Integer size = 10; //设置分页参数 query.from(from); query.size(size); //设置排序字段和排序方式,注意:字段是text类型需要拼接.keyword //query.sort("age", SortOrder.DESC); query.sort("name" + ".keyword", SortOrder.ASC); return elasticSearchUtils.searchListData("user", query, highlightField); } }完整代码:spring-boot: Springboot整合redis、消息中间件等相关代码 的es模块参考:《Elasticsearch中文文档》 | Elasticsearch 技术论坛https://www.cnblogs.com/wqp001/p/14478900.html
正文一、业务场景 在实际生产过程中,由于网络原因或者Redis故障等等原因会导致Redis与数据库数据不一致的问题,这个时候我们怎么办?或许我们可以写一个定时Job然后定时去跑数据,或者找到那条数据不一致,然后删除Redis中的缓存,但是这些都有缺点,定时任务跑数据,延时性太大了。如果是删除缓存,数据量少还可以,数据量多了呢,写脚本?那脚本执行时候一定会影响Redis性能。Canal这个开源框架可以很友好的解决我们上面这些问题。二、Canal介绍Canal是阿里巴巴的开源项目,其原理是使用数据库的主从复制。当前的 canal 支持源端 MySQL 版本包括 5.1.x , 5.5.x , 5.6.x , 5.7.x , 8.0.x。canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal )canal 解析 binary log 对象(原始为 byte 流)三、搭建Canal传统方式1、搭建mysql数据库2、配置主数据库[mysqld] log-bin=mysql-bin # 开启 binlog binlog-format=ROW # 选择 ROW 模式 server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复。 #授权 canal 链接 MySQL 账号具有作为 MySQL slave 的权限 CREATE USER canal IDENTIFIED BY 'canal'; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%'; FLUSH PRIVILEGES;3、搭建CanalServer#下载canal安装包 [root@localhost ~] wget https://github.com/alibaba/canal/releases/download/canal-1.1.5/canal.deployer-1.1.5.tar.gz4、创建安装目录并解压文件[root@localhost mysql]# mkdir -p /usr/local/canal [root@localhost ~]# tar -zxvf canal.deployer-1.1.5.tar.gz -C /usr/local/canal/5、配置(基于RocketMQ)Rocketmq安装本人使用的是jdk17需要修改启动脚本startup.sh ,jdk8不需要修改,压缩包下载: spring-boot: Springboot整合redis、消息中间件等相关代码 - Gitee.com修改源码maven中的依赖,打包之后修改配合文件即可 <!-- https://mvnrepository.com/artifact/io.netty/netty-all --> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.70.Final</version> </dependency>#启动脚本 #!/bin/bash current_path=`pwd` case "`uname`" in Linux) bin_abs_path=$(readlink -f $(dirname $0)) ;; *) bin_abs_path=`cd $(dirname $0); pwd` ;; esac base=${bin_abs_path}/.. canal_conf=$base/conf/canal.properties canal_local_conf=$base/conf/canal_local.properties logback_configurationFile=$base/conf/logback.xml export LANG=en_US.UTF-8 export BASE=$base if [ -f $base/bin/canal.pid ] ; then echo "found canal.pid , Please run stop.sh first ,then startup.sh" 2>&2 exit 1 fi if [ ! -d $base/logs/canal ] ; then mkdir -p $base/logs/canal fi ## set java path if [ -z "$JAVA" ] ; then JAVA=$(which java) fi ALIBABA_JAVA="/usr/alibaba/java/bin/java" TAOBAO_JAVA="/opt/taobao/java/bin/java" if [ -z "$JAVA" ]; then if [ -f $ALIBABA_JAVA ] ; then JAVA=$ALIBABA_JAVA elif [ -f $TAOBAO_JAVA ] ; then JAVA=$TAOBAO_JAVA else echo "Cannot find a Java JDK. Please set either set JAVA or put java (>=1.5) in your PATH." 2>&2 exit 1 fi fi case "$#" in 0 ) ;; 1 ) var=$* if [ "$var" = "local" ]; then canal_conf=$canal_local_conf else if [ -f $var ] ; then canal_conf=$var else echo "THE PARAMETER IS NOT CORRECT.PLEASE CHECK AGAIN." exit fi fi;; 2 ) var=$1 if [ "$var" = "local" ]; then canal_conf=$canal_local_conf else if [ -f $var ] ; then canal_conf=$var else if [ "$1" = "debug" ]; then DEBUG_PORT=$2 DEBUG_SUSPEND="n" JAVA_DEBUG_OPT="-Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=$DEBUG_PORT,server=y,suspend=$DEBUG_SUSPEND" fi fi fi;; * ) echo "THE PARAMETERS MUST BE TWO OR LESS.PLEASE CHECK AGAIN." exit;; esac str=`file -L $JAVA | grep 64-bit` #if [ -n "$str" ]; then # JAVA_OPTS="-server -Xms2048m -Xmx3072m -Xmn1024m -XX:SurvivorRatio=2 -XX:PermSize=96m -XX:MaxPermSize=256m -Xss256k -XX:-UseAdaptiveSizePolicy -XX:MaxTenuringThreshold=15 -XX:+DisableExplicitGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:+HeapDumpOnOutOfMemoryError" #else # JAVA_OPTS="-server -Xms1024m -Xmx1024m -XX:NewSize=256m -XX:MaxNewSize=256m -XX:MaxPermSize=128m " #fi if [ -n "$str" ]; then JAVA_OPTS="-server -Xms256m -Xmx256m -Xmn128m -XX:SurvivorRatio=2 -Xss256k -XX:-UseAdaptiveSizePolicy -XX:MaxTenuringThreshold=15 -XX:+DisableExplicitGC -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0 " else JAVA_OPTS="-server -Xms256m -Xmx256m -Xmn128m -XX:MaxNewSize=256m " fi JAVA_OPTS=" $JAVA_OPTS -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true -Dfile.encoding=UTF-8" CANAL_OPTS="-DappName=otter-canal -Dlogback.configurationFile=$logback_configurationFile -Dcanal.conf=$canal_conf" if [ -e $canal_conf -a -e $logback_configurationFile ] then for i in $base/lib/*; do CLASSPATH=$i:"$CLASSPATH"; done CLASSPATH="$base/conf:$CLASSPATH"; echo "cd to $bin_abs_path for workaround relative path" cd $bin_abs_path echo LOG CONFIGURATION : $logback_configurationFile echo canal conf : $canal_conf echo CLASSPATH :$CLASSPATH $JAVA $JAVA_OPTS $JAVA_DEBUG_OPT $CANAL_OPTS -classpath .:$CLASSPATH com.alibaba.otter.canal.deployer.CanalLauncher 1>>$base/logs/canal/canal_stdout.log 2>&1 & echo $! > $base/bin/canal.pid echo "cd to $current_path for continue" cd $current_path else echo "canal conf("$canal_conf") OR log configration file($logback_configurationFile) is not exist,please create then first!" ficanal.properties################################################# ######### common argument ############# ################################################# # tcp bind ip canal.ip = # register ip to zookeeper canal.register.ip = canal.port = 11111 canal.metrics.pull.port = 11112 # canal instance user/passwd # canal.user = canal # canal.passwd = E3619321C1A937C46A0D8BD1DAC39F93B27D4458 # canal admin config #canal.admin.manager = 127.0.0.1:8089 canal.admin.port = 11110 canal.admin.user = admin canal.admin.passwd = 4ACFE3202A5FF5CF467898FC58AAB1D615029441 # admin auto register #canal.admin.register.auto = true #canal.admin.register.cluster = #canal.admin.register.name = #集群使用 canal.zkServers = # flush data to zk canal.zookeeper.flush.period = 1000 canal.withoutNetty = false # tcp, kafka, rocketMQ, rabbitMQ #需要修改的地方 canal.serverMode = rocketMQ # flush meta cursor/parse position to file canal.file.data.dir = ${canal.conf.dir} canal.file.flush.period = 1000 ## memory store RingBuffer size, should be Math.pow(2,n) canal.instance.memory.buffer.size = 16384 ## memory store RingBuffer used memory unit size , default 1kb canal.instance.memory.buffer.memunit = 1024 ## meory store gets mode used MEMSIZE or ITEMSIZE canal.instance.memory.batch.mode = MEMSIZE canal.instance.memory.rawEntry = true ## detecing config canal.instance.detecting.enable = false #canal.instance.detecting.sql = insert into retl.xdual values(1,now()) on duplicate key update x=now() canal.instance.detecting.sql = select 1 canal.instance.detecting.interval.time = 3 canal.instance.detecting.retry.threshold = 3 canal.instance.detecting.heartbeatHaEnable = false # support maximum transaction size, more than the size of the transaction will be cut into multiple transactions delivery canal.instance.transaction.size = 1024 # mysql fallback connected to new master should fallback times canal.instance.fallbackIntervalInSeconds = 60 # network config canal.instance.network.receiveBufferSize = 16384 canal.instance.network.sendBufferSize = 16384 canal.instance.network.soTimeout = 30 # binlog filter config canal.instance.filter.druid.ddl = true canal.instance.filter.query.dcl = false canal.instance.filter.query.dml = false canal.instance.filter.query.ddl = false canal.instance.filter.table.error = false canal.instance.filter.rows = false canal.instance.filter.transaction.entry = false canal.instance.filter.dml.insert = false canal.instance.filter.dml.update = false canal.instance.filter.dml.delete = false # binlog format/image check canal.instance.binlog.format = ROW,STATEMENT,MIXED canal.instance.binlog.image = FULL,MINIMAL,NOBLOB # binlog ddl isolation canal.instance.get.ddl.isolation = false # parallel parser config canal.instance.parser.parallel = true ## concurrent thread number, default 60% available processors, suggest not to exceed Runtime.getRuntime().availableProcessors() #canal.instance.parser.parallelThreadSize = 16 ## disruptor ringbuffer size, must be power of 2 canal.instance.parser.parallelBufferSize = 256 # table meta tsdb info canal.instance.tsdb.enable = true canal.instance.tsdb.dir = ${canal.file.data.dir:../conf}/${canal.instance.destination:} canal.instance.tsdb.url = jdbc:h2:${canal.instance.tsdb.dir}/h2;CACHE_SIZE=1000;MODE=MYSQL; canal.instance.tsdb.dbUsername = canal canal.instance.tsdb.dbPassword = canal # dump snapshot interval, default 24 hour canal.instance.tsdb.snapshot.interval = 24 # purge snapshot expire , default 360 hour(15 days) canal.instance.tsdb.snapshot.expire = 360 ################################################# ######### destinations ############# ################################################# #canal实例,对应example文件,如果需要多个请将example也复制多分canal.destinations = example,example1,example2 canal.destinations = example # conf root dir canal.conf.dir = ../conf # auto scan instance dir add/remove and start/stop instance canal.auto.scan = true canal.auto.scan.interval = 5 # set this value to 'true' means that when binlog pos not found, skip to latest. # WARN: pls keep 'false' in production env, or if you know what you want. canal.auto.reset.latest.pos.mode = false canal.instance.tsdb.spring.xml = classpath:spring/tsdb/h2-tsdb.xml #canal.instance.tsdb.spring.xml = classpath:spring/tsdb/mysql-tsdb.xml canal.instance.global.mode = spring canal.instance.global.lazy = false canal.instance.global.manager.address = ${canal.admin.manager} #canal.instance.global.spring.xml = classpath:spring/memory-instance.xml canal.instance.global.spring.xml = classpath:spring/file-instance.xml #canal.instance.global.spring.xml = classpath:spring/default-instance.xml ################################################## ######### MQ Properties ############# ################################################## # aliyun ak/sk , support rds/mq canal.aliyun.accessKey = canal.aliyun.secretKey = canal.aliyun.uid= canal.mq.flatMessage = true canal.mq.canalBatchSize = 50 canal.mq.canalGetTimeout = 100 # Set this value to "cloud", if you want open message trace feature in aliyun. canal.mq.accessChannel = local canal.mq.database.hash = true canal.mq.send.thread.size = 30 canal.mq.build.thread.size = 8 ################################################## ######### Kafka ############# ################################################## kafka.bootstrap.servers = 127.0.0.1:9092 kafka.acks = all kafka.compression.type = none kafka.batch.size = 16384 kafka.linger.ms = 1 kafka.max.request.size = 1048576 kafka.buffer.memory = 33554432 kafka.max.in.flight.requests.per.connection = 1 kafka.retries = 0 kafka.kerberos.enable = false kafka.kerberos.krb5.file = "../conf/kerberos/krb5.conf" kafka.kerberos.jaas.file = "../conf/kerberos/jaas.conf" ################################################## ######### RocketMQ ############# ################################################## rocketmq.producer.group = test rocketmq.enable.message.trace = false rocketmq.customized.trace.topic = rocketmq.namespace = #需要修改的地方,多个值用分号隔开 rocketmq.namesrv.addr = 192.168.6.145:9876;192.168.6.145:9877 rocketmq.retry.times.when.send.failed = 0 rocketmq.vip.channel.enabled = false rocketmq.tag = canal ################################################## ######### RabbitMQ ############# ################################################## rabbitmq.host = rabbitmq.virtual.host = rabbitmq.exchange = rabbitmq.username = rabbitmq.password = rabbitmq.deliveryMode =instance.properties ################################################# ## mysql serverId , v1.0.26+ will autoGen # canal.instance.mysql.slaveId=0 # enable gtid use true/false canal.instance.gtidon=false # position info #修改成自己的数据库连接 canal.instance.master.address=192.168.6.145:3306 canal.instance.master.journal.name= canal.instance.master.position= canal.instance.master.timestamp= canal.instance.master.gtid= # rds oss binlog canal.instance.rds.accesskey= canal.instance.rds.secretkey= canal.instance.rds.instanceId= # table meta tsdb info canal.instance.tsdb.enable=true #canal.instance.tsdb.url=jdbc:mysql://127.0.0.1:3306/canal_tsdb #canal.instance.tsdb.dbUsername=canal #canal.instance.tsdb.dbPassword=canal #canal.instance.standby.address = #canal.instance.standby.journal.name = #canal.instance.standby.position = #canal.instance.standby.timestamp = #canal.instance.standby.gtid= # username/password #数据库连接账号和密码 canal.instance.dbUsername=canal canal.instance.dbPassword=canal canal.instance.connectionCharset = UTF-8 # enable druid Decrypt database password canal.instance.enableDruid=false #canal.instance.pwdPublicKey=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALK4BUxdDltRRE5/zXpVEVPUgunvscYFtEip3pmLlhrWpacX7y7GCMo2/JM6LeHmiiNdH1FWgGCpUfircSwlWKUCAwEAAQ== # table regex #mysql 数据解析关注的表,Perl正则表达式. #多个正则之间以逗号(,)分隔,转义符需要双斜杠(\\) #常见例子: #1. 所有表:.* or .*\\..* #2. canal schema下所有表: canal\\..* #3. canal下的以canal打头的表:canal\\.canal.* #4. canal schema下的一张表:canal\\.test1 #5. 多个规则组合使用:canal\\..*,mysql.test1,mysql.test2 (逗号分隔) canal.instance.filter.regex=.*\\..* # table black regex canal.instance.filter.black.regex=mysql\\.slave_.* # table field filter(format: schema1.tableName1:field1/field2,schema2.tableName2:field1/field2) #canal.instance.filter.field=test1.t_product:id/subject/keywords,test2.t_company:id/name/contact/ch # table field black filter(format: schema1.tableName1:field1/field2,schema2.tableName2:field1/field2) #canal.instance.filter.black.field=test1.t_product:subject/product_image,test2.t_company:id/name/contact/ch # mq config #定义的topic主题 canal.mq.topic=canal-topic # dynamic topic route by schema or table regex #canal.mq.dynamicTopic=mytest1.user,mytest2\\..*,.*\\..* canal.mq.partition=0 # hash partition config #canal.mq.partitionsNum=3 #canal.mq.partitionHash=test.table:id^name,.*\\..* #canal.mq.dynamicTopicPartitionNum=test.*:4,mycanal:6 #################################################6、启动#启动 sh bin/startup.sh #停止 sh bin/stop.shDocker方式1、拉取镜像docker pull canal/canal-server:v1.1.52、创建canal-docker目录存放启动脚本[root@bogon ~]# mkdir /usr/local/canal-docker#!/bin/bash function usage() { echo "Usage:" echo " run.sh [CONFIG]" echo "example 1 :" echo " run.sh -e canal.instance.master.address=127.0.0.1:3306 \\" echo " -e canal.instance.dbUsername=canal \\" echo " -e canal.instance.dbPassword=canal \\" echo " -e canal.instance.connectionCharset=UTF-8 \\" echo " -e canal.instance.tsdb.enable=true \\" echo " -e canal.instance.gtidon=false \\" echo " -e canal.instance.filter.regex=.*\\\\\\..* " echo "example 2 :" echo " run.sh -e canal.admin.manager=127.0.0.1:8089 \\" echo " -e canal.admin.port=11110 \\" echo " -e canal.admin.user=admin \\" echo " -e canal.admin.passwd=4ACFE3202A5FF5CF467898FC58AAB1D615029441" exit } function check_port() { local port=$1 local TL=$(which telnet) if [ -f $TL ]; then data=`echo quit | telnet 127.0.0.1 $port| grep -ic connected` echo $data return fi local NC=$(which nc) if [ -f $NC ]; then data=`nc -z -w 1 127.0.0.1 $port | grep -ic succeeded` echo $data return fi echo "0" return } function getMyIp() { case "`uname`" in Darwin) myip=`echo "show State:/Network/Global/IPv4" | scutil | grep PrimaryInterface | awk '{print $3}' | xargs ifconfig | grep inet | grep -v inet6 | awk '{print $2}'` ;; *) myip=`ip route get 1 | awk '{print $NF;exit}'` ;; esac echo $myip } CONFIG=${@:1} #VOLUMNS="-v $DATA:/home/admin/canal-server/logs" PORTLIST="11110 11111 11112 9100" PORTS="" for PORT in $PORTLIST ; do #exist=`check_port $PORT` exist="0" if [ "$exist" == "0" ]; then PORTS="$PORTS -p $PORT:$PORT" else echo "port $PORT is used , pls check" exit 1 fi done NET_MODE="" case "`uname`" in Darwin) bin_abs_path=`cd $(dirname $0); pwd` ;; Linux) bin_abs_path=$(readlink -f $(dirname $0)) NET_MODE="--net=host" PORTS="" ;; *) bin_abs_path=`cd $(dirname $0); pwd` NET_MODE="--net=host" PORTS="" ;; esac BASE=${bin_abs_path} DATA="$BASE/data" mkdir -p $DATA if [ $# -eq 0 ]; then usage elif [ "$1" == "-h" ] ; then usage elif [ "$1" == "help" ] ; then usage fi MEMORY="-m 4096m" LOCALHOST=`getMyIp` cmd="docker run -d -it -h $LOCALHOST $CONFIG --name=canal-server $VOLUMNS $NET_MODE $PORTS $MEMORY canal/canal-server:v1.1.5" echo $cmd eval $cmd3、创建docker容器 sh run.sh -e canal.auto.scan=false \ -e canal.destinations=test \ -e canal.instance.master.address=127.0.0.1:3306 \ -e canal.instance.dbUsername=canal \ -e canal.instance.dbPassword=canal \ -e canal.instance.connectionCharset=UTF-8 \ -e canal.instance.tsdb.enable=true \ -e canal.instance.gtidon=false \ -e canal.serverMode=rocketMQ \ -e rocketmq.namesrv.addr=192.168.139.156:9876 \ -e rocketmq.producer.group=test \ -e canal.mq.topic=canal-topic 参数说明canal.auto.scan=false #是否开启自动扫描canal.destinations=test #对应配置文件中的canal.destinations=examplecanal.instance.master.address=127.0.0.1:3306 #数据库链接地址canal.instance.dbUsername=canal #数据库用户名canal.instance.dbPassword=canal #数据库密码canal.instance.connectionCharset=UTF-8 #编码格式canal.instance.tsdb.enable=true #切换数据库canal.instance.gtidon=false #没搞明白canal.serverMode=rocketMQ #有tcp, kafka, rocketMQ, rabbitMQrocketmq.namesrv.addr=rocketmq.namesrv.addr=192.168.139.156:9876 #多个地址启动之后,连不上rocketmq,其他的mq没有尝试未知rocketmq.producer.group=test #生产者组canal.mq.topic=canal-topic#对应的topic四、代码演示package com.xiaojie.canal; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.xiaojie.entity.User; import com.xiaojie.utils.RedisUtil; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.rocketmq.spring.annotation.ConsumeMode; import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; import org.apache.rocketmq.spring.annotation.SelectorType; import org.apache.rocketmq.spring.core.RocketMQListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** * @Description:解决redis与数据库数据不一致问题 * @author: yan * @date: 2021.11.25 */ @Component @RocketMQMessageListener(consumerGroup = "canal1", topic = "canal-topic", consumeMode = ConsumeMode.ORDERLY , selectorType = SelectorType.TAG, selectorExpression = "*") @Slf4j public class CanalConsumer implements RocketMQListener<String> { @Autowired private RedisUtil redisUtil; @Override public void onMessage(String message) { try { log.info("接收到的数据是:》》》》》》{}", message); JSONObject jsonObj = JSONObject.parseObject(message); JSONArray jsonArray = (JSONArray) jsonObj.get("data"); if (null != jsonArray && jsonArray.size() > 0) { User user = JSONObject.parseObject(jsonArray.get(0).toString(), User.class); String database = jsonObj.getString("database"); String table = jsonObj.getString("table"); String type = jsonObj.getString("type"); if (StringUtils.isEmpty(type)) { log.info("不用同步数据。。。。。。。。。。。。。。"); } if (type.equals("INSERT") || type.equals("UPDATE")) { log.info("更新数据》》》》》》》》》》》》"); redisUtil.set(database + "_" + table + "_" + user.getId(), JSONObject.toJSONString(user), 24 * 60 * 60); } else { if (redisUtil.hasKey(database + "_" + table + "_" + user.getId())) { log.info("删除数据》》》》》》》》》》》》"); redisUtil.del(database + "_" + table + "_" + user.getId()); } } } } catch (Exception e) { log.info("数据更新异常》》》》》》》》》》》》", e); } } }完整代码:spring-boot: Springboot整合redis、消息中间件等相关代码
正文一、RocketMQ数据存储原理生产者投递消息生产者在投递消息到mq服务器端,会将该消息存放在commitlog日志文件中(顺序写)。Mq后台就会开启一个异步的线程将该commitlogoffset实现分配存放到不同队列中。消费者消费消息:消费者消费消息的时候订阅到队列(consumerqueue),根据queueoffset 获取到该commitlogoffset在根据commitlogoffset 去commitlog日志文件中查找到该消息主体返回给客户端。总结 生产者将消息投递到broker时,会将所有的消息以顺序写的方式追加到Commitlog文件中,MQ开启异步线程将消息分配到相应的队列中(包含commitlogOffset值、msgSize、Tag等信息)。消费者订阅相应的队列,通过consumerQueueOffset的值去获取到commitlogOffset值,然后根据commitlogOffset的值获取到消息体,然后进行消费。commitlog文件每个文件的大小默认1G ,commitlog的文件名fileName,名字长度为20位,左边补零,剩余为起始偏移量;比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1073741824;当这个文件满了,第二个文件名字为00000000001073741824,起始偏移量为1073741824,以此类推,第三个文件名字为00000000002147483648,起始偏移量为2147483648 ,消息存储的时候会顺序写入文件,当文件满了,写入下一个文件。理想状态下一个消费者对应一个队列,如果消费者数量多于队列数量,那么多余的消费者消费不到消息。因此在扩容 Consumer 的实例数量的同时,必须同步扩容主题中的分区(也叫队列)数量,确保 Consumer 的实例数和分区数量是相等的。如果 Consumer 的实例数量超过分区数量,这样的扩容实际上是没有效果的。在集群消费(Clustering)模式下每个消费组都消费主题中一份完整的消息,不同消费组之间消费进度彼此不受影响,也就是说,一条消息被 Consumer Group1 消费过,也会再给Consumer Group2 消费。 消费组中包含多个消费者,同一个组内的消费者是竞争消费的关系,每个消费者负责消费组内的一部分消息。如果一条消息被消费者 Consumer1 消费了,那同组的其他消费者就不会再消费这条消息。RocketMQ和kafka一样,消息消费之后并不会立即删除消息,而是通过删除策略删除消息二、集群原理同步刷盘和异步刷盘 RocketMQ的消息是存储到磁盘上的,这样既能保证断电后恢复,又可以让存储的消息量超出内存的限制。RocketMQ为了提高性能,会尽可能地保证磁盘的顺序写。消息在通过Producer写入RocketMQ的时候,有两种写磁盘方式: 异步刷盘方式:在返回写成功状态时,消息可能只是被写入了内存的PAGECACHE,写操作的返回快,吞吐量大;当内存里的消息量积累到一定程度时,统一触发写磁盘操作,快速写入 优点:性能高 缺点:Master宕机,磁盘损坏的情况下,会丢失少量的消息, 导致MQ的消息状态和生产者/消费者的消息状态不一致 同步刷盘方式:在返回应用写成功状态前,消息已经被写入磁盘。具体流程是,消息写入内存的PAGECACHE后,立刻通知刷盘线程刷盘,然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,给应用返回消息写成功的状态。 优点:可以保持MQ的消息状态和生产者/消费者的消息状态一致 缺点:性能比异步的低配置方式在broker.conf中配置ASYNC_FLUSH 异步刷盘SYNC_FLUSH 同步刷盘flushDiskType=ASYNC_FLUSH同步复制和异步复制 如果一个broker组有Master和Slave,消息需要从Master复制到Slave上,有同步和异步两种复制方式。 同步复制方式:等Master和Slave均写成功后才反馈给客户端写成功状态 优点:如果Master出故障,Slave上有全部的备份数据,容易恢复,消费者仍可以从Slave消费, 消息不丢失 缺点:增大数据写入延迟,降低系统吞吐量,性能比异步复制模式略低,大约低10%左右,发送单个Master的响应时间会略高 异步复制方式:只要Master写成功即可反馈给客户端写成功状态 优点:系统拥有较低的延迟和较高的吞吐量. Master宕机之后,消费者仍可以从Slave消费,此过程对应用透明,不需要人工干预,性能同多个Master模式几乎一样 缺点:如果Master出了故障,有些数据因为没有被写入Slave,而丢失少量消息。配置方式在broker.conf中配置brokerRole参数ASYNC_MASTER 同步SYNC_MASTER 异步SLAVE 从节点集群原理 nameServer:多个Namesrv实例组成集群,但相互独立,没有信息交换。nameserver类似ZK和nacos等注册中心的功能。broker在启动时会将自己的ip和端口号注册到每一个nameserver中,然后与nameserver建立长连接。nameserver每隔30秒会发送一个心跳包,告诉broker自己还存活。而nameServer 定时器每隔10s的时间检测 故障Broker ,如果发生故障Broker 会直接剔除。生产者投递消息时会从nameserver中获取到broker的地址列表,然后进行消息投递。如果生产者在获取到服务列表之后,恰好当前broker宕机,那么生产者默认会有3次重试,如果依然失败,则重新从nameserver获取broker列表,进行消息投递。主从broker如何保证消息消费一致性在/data/rocketmq/store-a/config/consumerOffset.json文件中有如下结构{ "offsetTable":{ "%RETRY%test-group@test-group":{0:0 }, "%RETRY%order-consumer@order-consumer":{0:0 }, "xiaojie-test@test-group":{0:4,1:3,2:4,3:28 } } }在"xiaojie-test@test-group":{0:4,1:3,2:4,3:28}其中 xiaojie-test为topic名称test-group为消费组的名称0:4 表示queueId 为0, consumeroffset为4,也就是说队列id为0的消息消费到偏移量为4的位置。当master节点消费消息时,主节点会将自己commitLog和consumerOffset.json文件异步的同步到salve节点上。当主节点宕机之后,从节点不能支持写操作,但是可以执行读的操作。但此时主节点的consumerOffset.json中consumeroffset值滞后于主节点,当主节点恢复之后,如何消费呢?答案是主节点恢复之后,会首先同步从节点的consumerOffset.json文件,然后再进行消费。三、RocktMQ顺序消费 消息有序指的是可以按照消息的发送顺序来消费(FIFO)。RocketMQ可以严格的保证消息有序,可以分为分区有序或者全局有序。顺序消费的原理解析,在默认的情况下消息发送会采取Round Robin轮询方式把消息发送到不同的queue(分区队列);而消费消息的时候从多个queue上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,则就保证了顺序。当发送和消费参与的queue只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个queue,消息都是有序的。 但正如上图所示,消费者是采用多线程的方式消费的,此时即使投递消息时的队列一致,也不能保证消费的时候就严格按照顺序消费。官网顺序消费demopackage org.apache.rocketmq.example.order2; import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.client.producer.MessageQueueSelector; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.common.message.MessageQueue; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * Producer,发送顺序消息 */ public class Producer { public static void main(String[] args) throws Exception { DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); producer.setNamesrvAddr("127.0.0.1:9876"); producer.start(); String[] tags = new String[]{"TagA", "TagC", "TagD"}; // 订单列表 List<OrderStep> orderList = new Producer().buildOrders(); Date date = new Date(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String dateStr = sdf.format(date); for (int i = 0; i < 10; i++) { // 加个时间前缀 String body = dateStr + " Hello RocketMQ " + orderList.get(i); Message msg = new Message("TopicTest", tags[i % tags.length], "KEY" + i, body.getBytes()); SendResult sendResult = producer.send(msg, new MessageQueueSelector() { @Override public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) { Long id = (Long) arg; //根据订单id选择发送queue long index = id % mqs.size(); return mqs.get((int) index); } }, orderList.get(i).getOrderId());//订单id System.out.println(String.format("SendResult status:%s, queueId:%d, body:%s", sendResult.getSendStatus(), sendResult.getMessageQueue().getQueueId(), body)); } producer.shutdown(); } /** * 订单的步骤 */ private static class OrderStep { private long orderId; private String desc; public long getOrderId() { return orderId; } public void setOrderId(long orderId) { this.orderId = orderId; } public String getDesc() { return desc; } public void setDesc(String desc) { this.desc = desc; } @Override public String toString() { return "OrderStep{" + "orderId=" + orderId + ", desc='" + desc + '\'' + '}'; } } /** * 生成模拟订单数据 */ private List<OrderStep> buildOrders() { List<OrderStep> orderList = new ArrayList<OrderStep>(); OrderStep orderDemo = new OrderStep(); orderDemo.setOrderId(15103111039L); orderDemo.setDesc("创建"); orderList.add(orderDemo); orderDemo = new OrderStep(); orderDemo.setOrderId(15103111065L); orderDemo.setDesc("创建"); orderList.add(orderDemo); orderDemo = new OrderStep(); orderDemo.setOrderId(15103111039L); orderDemo.setDesc("付款"); orderList.add(orderDemo); orderDemo = new OrderStep(); orderDemo.setOrderId(15103117235L); orderDemo.setDesc("创建"); orderList.add(orderDemo); orderDemo = new OrderStep(); orderDemo.setOrderId(15103111065L); orderDemo.setDesc("付款"); orderList.add(orderDemo); orderDemo = new OrderStep(); orderDemo.setOrderId(15103117235L); orderDemo.setDesc("付款"); orderList.add(orderDemo); orderDemo = new OrderStep(); orderDemo.setOrderId(15103111065L); orderDemo.setDesc("完成"); orderList.add(orderDemo); orderDemo = new OrderStep(); orderDemo.setOrderId(15103111039L); orderDemo.setDesc("推送"); orderList.add(orderDemo); orderDemo = new OrderStep(); orderDemo.setOrderId(15103117235L); orderDemo.setDesc("完成"); orderList.add(orderDemo); orderDemo = new OrderStep(); orderDemo.setOrderId(15103111039L); orderDemo.setDesc("完成"); orderList.add(orderDemo); return orderList; } }消费者代码package org.apache.rocketmq.example.order2; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; import org.apache.rocketmq.common.consumer.ConsumeFromWhere; import org.apache.rocketmq.common.message.MessageExt; import java.util.List; import java.util.Random; import java.util.concurrent.TimeUnit; /** * 顺序消息消费,带事务方式(应用可控制Offset什么时候提交) */ public class ConsumerInOrder { public static void main(String[] args) throws Exception { DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3"); consumer.setNamesrvAddr("127.0.0.1:9876"); /** * 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费<br> * 如果非第一次启动,那么按照上次消费的位置继续消费 */ consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); consumer.subscribe("TopicTest", "TagA || TagC || TagD"); consumer.registerMessageListener(new MessageListenerOrderly() { Random random = new Random(); @Override public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) { context.setAutoCommit(true); for (MessageExt msg : msgs) { // 可以看到每个queue有唯一的consume线程来消费, 订单对每个queue(分区)有序 System.out.println("consumeThread=" + Thread.currentThread().getName() + "queueId=" + msg.getQueueId() + ", content:" + new String(msg.getBody())); } try { //模拟业务逻辑处理中... TimeUnit.SECONDS.sleep(random.nextInt(10)); } catch (Exception e) { e.printStackTrace(); } return ConsumeOrderlyStatus.SUCCESS; } }); consumer.start(); System.out.println("Consumer Started."); } }Springboot整合顺序消费 package com.xiaojie.rocket.rocket.producer; import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.spring.core.RocketMQTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.UUID; /** * @author xiaojie * @version 1.0 * @description: springboot顺序生产者 * @date 2021/11/23 22:59 */ @Component @Slf4j public class OrderProducer { @Autowired private RocketMQTemplate rocketMQTemplate; public void orderSend() { String msg = "这是测试顺序发送消息的内容-------insert"; String msg1 = "这是测试顺序发送消息的内容-------update"; String msg2 = "这是测试顺序发送消息的内容-------delete"; String orderId = UUID.randomUUID().toString(); SendResult sendResult1 = rocketMQTemplate.syncSendOrderly("test-orderly", msg, orderId); log.info(">>>>>>>>>>>>>>>result1{}", sendResult1); SendResult sendResult2 = rocketMQTemplate.syncSendOrderly("test-orderly", msg1, orderId); log.info(">>>>>>>>>>>>>>>result2{}", sendResult2); SendResult sendResult3 = rocketMQTemplate.syncSendOrderly("test-orderly", msg2, orderId); log.info(">>>>>>>>>>>>>>>result3{}", sendResult2); } }package com.xiaojie.rocket.rocket.consumer; import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.spring.annotation.ConsumeMode; import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; import org.apache.rocketmq.spring.core.RocketMQListener; import org.springframework.stereotype.Component; import java.util.Random; /** * @author xiaojie * @version 1.0 * @description: 顺序消费者 * @date 2021/11/23 23:18 */ @Component @RocketMQMessageListener(topic = "test-orderly", consumerGroup = "orderly-1", consumeMode = ConsumeMode.ORDERLY) @Slf4j public class OrderConsumer implements RocketMQListener { @Override public void onMessage(Object message) { try { Random r = new Random(100); int i = r.nextInt(500); Thread.sleep(i); } catch (Exception e) { } log.info("消费者监听到消息:<msg:{}>", message); } }参考 :https://blog.csdn.net/guyue35/article/details/105674044
正文一、传统方式1、下载好安装包(Linux - Generic (glibc 2.12) (x86, 64-bit), Compressed TAR Archive)上传到服务器解压缩#执行之后是tar格式文件,再解压 [root@bogon ~]# xz -d mysql-8.0.27-linux-glibc2.12-x86_64.tar.xz #解压 [root@bogon ~]# tar -xvf mysql-8.0.27-linux-glibc2.12-x86_64.tar #移动重命名 [root@bogon ~]# mv mysql-8.0.27-linux-glibc2.12-x86_64 /usr/local/mysql 或者[root@localhost ~]# tar -Jxvf mysql-8.0.27-linux-glibc2.12-x86_64.tar.xz2、在mysql目录下创建data文件存放数据[root@bogon ~]# mkdir -p /usr/local/mysql/data3、创建mysql用户组和用户[root@bogon ~]# groupadd mysql [root@bogon ~]# useradd -g mysql mysql 4、授权[root@bogon ~]# chown -R mysql.mysql /usr/local/mysql/5、创建mariadb文件并授权给 mysql [root@bogon mysql]# mkdir /var/log/mariadb [root@bogon mysql]# touch /var/log/mariadb/mariadb.log [root@bogon mysql]# chown -R mysql:mysql /var/log/mariadb/6、配置mysql的my.cnf如下[mysql] # 设置mysql客户端默认字符集 default-character-set=utf8mb4 [client] port = 3306 socket = /usr/local/mysql/mysql.sock [mysqld] port = 3306 user = mysql socket = /usr/local/mysql/mysql.sock # 设置mysql的安装目录 basedir = /usr/local/mysql # 设置mysql数据库的数据的存放目录 datadir = /usr/local/mysql/data #配置主节点信息 server_id=3306 log-bin = /usr/local/mysql/data/mysql-bin log-bin=mysql-bin #选择row模式 binlog-format=ROW #需要同步的数据库名,如果有多个数据库,可重复此参数,每个数据库一行 binlog-do-db=test #不同步mysql系统数据库 binlog-ignore-db=mysql #配置主节点信息结束 #设置mysql数据库的日志及进程数据的存放目录 log-error =/usr/local/mysql/logs/mysql-error.log pid-file =/usr/local/mysql/mysql.pid # 服务端使用的字符集默认为8比特编码 character-set-server=utf8mb4 lower_case_table_names=1 autocommit =1 sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO ##################以上要修改的######################## skip-external-locking key_buffer_size = 256M max_allowed_packet = 1M table_open_cache = 1024 sort_buffer_size = 4M net_buffer_length = 8K read_buffer_size = 4M read_rnd_buffer_size = 512K myisam_sort_buffer_size = 64M thread_cache_size = 128 #query_cache_size = 128M tmp_table_size = 128M explicit_defaults_for_timestamp = true max_connections = 500 max_connect_errors = 100 open_files_limit = 65535 binlog_format=mixed binlog_expire_logs_seconds =864000 # 创建新表时将使用的默认存储引擎 default_storage_engine = InnoDB innodb_data_file_path = ibdata1:10M:autoextend innodb_buffer_pool_size = 1024M innodb_log_file_size = 256M innodb_log_buffer_size = 8M innodb_flush_log_at_trx_commit = 1 innodb_lock_wait_timeout = 50 transaction-isolation=READ-COMMITTED [mysqldump] quick max_allowed_packet = 16M [myisamchk] key_buffer_size = 256M sort_buffer_size = 4M read_buffer = 2M write_buffer = 2M [mysqlhotcopy] interactive-timeout [mysqld_safe] log-error=/var/log/mariadb/mariadb.log pid-file=/var/run/mariadb/mariadb.pid7、初始化数据库[root@bogon mysql]# bin/mysqld --initialize --user=mysql --basedir=/usr/local/mysql --datadir=/ 记住你的初始密码红框框冒号后面的 iBd5auJXgw+j 8、配置环境变量[root@bogon mysql]# vim /etc/profile export PATH=$PATH:/usr/local/mysql/bin:/usr/local/mysql/lib export PATH #生效 [root@bogon mysql]# source /etc/profile9、添加到服务设置开机启动[root@bogon mysql]# cp support-files/mysql.server /etc/init.d/mysql #授权 [root@bogon mysql]# chmod +x /etc/init.d/mysql #添加到开机启动 [root@bogon mysql]# chkconfig --add mysql [root@bogon mysql]# chkconfig mysql on #检查是否成功 [root@bogon mysql]# chkconfig --list 10、启动 [root@bogon mysql]# service mysql start #相应的命令 {start|stop|restart|reload|force-reload|status} 11、登录数据库1. #输入刚才的密码 2. [root@localhost mysql]# mysql -u root -p 3. Enter password: 12、修改密码设置远程连接#修改密码 ALTER USER 'root'@'localhost' IDENTIFIED with mysql_native_password BY 'root'; flush privileges; #创建任意远程连接 CREATE USER 'root'@'%' IDENTIFIED BY 'root'; flush privileges; #为root账户授权所有数据库所有权限 grant all on *.* to root; flush privileges;注意:log-error =/usr/local/mysql/logs/mysql-error.log这个文件我是手动创建的,创建完成之后,重新授权给mysql用户,如果安装过程出错,请记得看错误日志!!!navicat连接时请关闭防火墙或者开放3306端口。#创建错误日志文件 [root@localhost mysql]# touch /usr/local/mysql/logs/mysql-error.log #重新授权 [root@bogon ~]# chown -R mysql.mysql /usr/local/mysql/二、Docker方式1、拉取镜像[root@localhost mysql]# docker pull mysql2、创建挂载文件[root@localhost mysql]# mkdir -p /data/mysql/conf3、配置文件my.cnf[mysql] # 设置mysql客户端默认字符集 default-character-set=utf8mb4 [client] socket = /data/mysql/mysql.sock [mysqld] pid-file = /var/run/mysqld/mysqld.pid socket = /var/run/mysqld/mysqld.sock datadir = /var/lib/mysql secure-file-priv= NULL #主节点 #配置主节点信息 server_id=1001 log-bin=mysql-bin #选择row模式 binlog-format=ROW #需要同步的数据库名,如果有多个数据库,可重复此参数,每个数据库一行 binlog-do-db=test #不同步mysql系统数据库 binlog-ignore-db=mysql #配置主节点信息结束 # 服务端使用的字符集默认为8比特编码 character-set-server=utf8mb4 lower_case_table_names=1 autocommit =1 sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO ##################以上要修改的######################## skip-external-locking key_buffer_size = 256M max_allowed_packet = 1M table_open_cache = 1024 sort_buffer_size = 4M net_buffer_length = 8K read_buffer_size = 4M read_rnd_buffer_size = 512K myisam_sort_buffer_size = 64M thread_cache_size = 128 #query_cache_size = 128M tmp_table_size = 128M explicit_defaults_for_timestamp = true max_connections = 500 max_connect_errors = 100 open_files_limit = 65535 binlog_format=mixed binlog_expire_logs_seconds =864000 # 创建新表时将使用的默认存储引擎 default_storage_engine = InnoDB innodb_data_file_path = ibdata1:10M:autoextend innodb_buffer_pool_size = 1024M innodb_log_file_size = 256M innodb_log_buffer_size = 8M innodb_flush_log_at_trx_commit = 1 innodb_lock_wait_timeout = 50 transaction-isolation=READ-COMMITTED [mysqldump] quick max_allowed_packet = 16M [myisamchk] key_buffer_size = 256M sort_buffer_size = 4M read_buffer = 2M write_buffer = 2M [mysqlhotcopy] interactive-timeout4、创建Docker容器docker run \ --privileged=true \ --restart=always \ -p 3306:3306 --name mysql \ -v /data/mysql/data:/var/lib/mysql \ -v /data/mysql/conf/my.cnf:/etc/mysql/my.cnf \ -e MYSQL_ROOT_PASSWORD=root \ -d mysql:latest此时已经可以navicat连接了,如果连不上请继续往下。 5、配置远程连接#进入容器 [root@bogon log]# docker container exec -it mysql /bin/bash #配置远程访问 root@4d5306b674f7:/# mysql -uroot -p #然后输入密码root #创建任意远程连接 CREATE USER 'root'@'%' IDENTIFIED BY 'root'; flush privileges; #为root账户授权所有数据库所有权限 grant all on *.* to root; flush privileges;三、主从复制原理简单描述master服务器将数据的改变记录二进制binlog日志,当master上的数据发生改变时,则将其改变写入二进制日志中。slave服务器会定时对master二进制日志询问是否发生改变,如果发生改变,则开始一个I/O线程请求master二进制事件。同时master节点为每个I/O线程启动一个dump线程,用于向其发送二进制事件,并保存至从节点本地的中继日志(relay-log)中,从节点将启动IO线程从中继日志中读取二进制日志,然后SQL线程执行解析后的sql语句。以Docker安装方式为例1、配置主服务器my.cnf的[mysqld]下添加#主节点 server_id = 1001 log-bin = mysql-bin #选择row模式 binlog-format=ROW #需要同步的数据库名,如果有多个数据库,可重复此参数,每个数据库一行 binlog-do-db=test #不同步mysql系统数据库 binlog-ignore-db=mysql查看 show variables like '%server_id%';2、 配置slave的配置文件1. 2. #在/data/mysql下创建salve的配置文件文件 3. [root@bogon mysql]# mkdir -p /data/mysql/slave/conf从节点my.cnf配置文件 [mysql] # 设置mysql客户端默认字符集 default-character-set=utf8mb4 [mysqld] pid-file = /var/run/mysqld/mysqld.pid socket = /var/run/mysqld/mysqld.sock datadir = /var/lib/mysql secure-file-priv= NULL #从节点 server_id = 1002 log-bin = mysql-bin #选择row模式 binlog-format=ROW #需要同步的数据库名,如果有多个数据库,可重复此参数,每个数据库一行 replicate-do-db=test #不同步mysql系统数据库 replicate-ignore-db=mysql #只读 只对非root用户有效 read-only=1 # 服务端使用的字符集默认为8比特编码 character-set-server=utf8mb4 lower_case_table_names=1 autocommit =1 sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO ##################以上要修改的######################## skip-external-locking key_buffer_size = 256M max_allowed_packet = 1M table_open_cache = 1024 sort_buffer_size = 4M net_buffer_length = 8K read_buffer_size = 4M read_rnd_buffer_size = 512K myisam_sort_buffer_size = 64M thread_cache_size = 128 #query_cache_size = 128M tmp_table_size = 128M explicit_defaults_for_timestamp = true max_connections = 500 max_connect_errors = 100 open_files_limit = 65535 binlog_format=mixed binlog_expire_logs_seconds =864000 # 创建新表时将使用的默认存储引擎 default_storage_engine = InnoDB innodb_data_file_path = ibdata1:10M:autoextend innodb_buffer_pool_size = 1024M innodb_log_file_size = 256M innodb_log_buffer_size = 8M innodb_flush_log_at_trx_commit = 1 innodb_lock_wait_timeout = 50 transaction-isolation=READ-COMMITTED [mysqldump] quick max_allowed_packet = 16M [myisamchk] key_buffer_size = 256M sort_buffer_size = 4M read_buffer = 2M write_buffer = 2M [mysqlhotcopy] interactive-timeout3、启动从节点docker run \ --privileged=true \ --restart=always \ -p 33060:3306 --name mysql-slave \ -v /data/mysql/slave/data:/var/lib/mysql \ -v /data/mysql/slave/conf/my.cnf:/etc/mysql/my.cnf \ -e MYSQL_ROOT_PASSWORD=root \ -d mysql:latest 4、开始同步数据在主节点执行sqlshow master status;在从节点上执行如下sqlCHANGE MASTER TO master_host = '192.168.139.159', master_user = 'root', master_password = 'root', master_log_file = 'mysql-bin.000003', master_log_pos = 364;然后从节点执行如下sql#开始同步 start slave; #停止 STOP SLAVE; #查看同步状态 SHOW SLAVE STATUS;到此结束 参考:https://blog.csdn.net/u010565545/article/details/104961184
正文一、XXL-job介绍 传统的定时任务一般可以通过多线程、timetask、线程池、springboot注解、quartz等方式实现。但是一般的定时任务存在一些缺陷,例如集群部署条件下的幂等性问题,跑批问题、消耗CPU等。也许幂等问题可以通过数据库主键或者分布式锁实现,但是如果需要跑批大量的数据几百万几千万的数据时怎么处理呢?XXL-JOB是一个分布式任务调度平台,支持可视化管理界面。分布式任务调度平台XXL-JOB二、原理分析XXL-Job由以下两部分组成调度模块(调度中心):负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。调度系统与任务解耦,提高了系统可用性和稳定性,同时调度系统性能不再受限于任务模块;支持可视化、简单且动态的管理调度信息,包括任务新建,更新,删除,GLUE开发和任务报警等,所有上述操作都会实时生效,同时支持监控调度结果以及执行日志,支持执行器Failover。执行模块(执行器):负责接收调度请求并执行任务逻辑。任务模块专注于任务的执行等操作,开发和维护更加简单和高效;接收“调度中心”的执行请求、终止请求和日志请求等。1、当执行器(定时任务job)启动的时候会将他的IP和端口号信息注册到执行器注册中上(调度中心)。2、当我们现在定时任务模块中启动定时任务的时候,定时任务会通过调度中心获取到执行器的列表,然后通过算法(轮询、一致hash、单个等)执行相应的执行器业务。三、搭建框架1、下载源码 http://gitee.com/xuxueli0323/xxl-job2、生成db# # XXL-JOB v2.3.0 # Copyright (c) 2015-present, xuxueli. CREATE database if NOT EXISTS `xxl_job` default character set utf8mb4 collate utf8mb4_unicode_ci; use `xxl_job`; SET NAMES utf8mb4; CREATE TABLE `xxl_job_info` ( `id` int(11) NOT NULL AUTO_INCREMENT, `job_group` int(11) NOT NULL COMMENT '执行器主键ID', `job_desc` varchar(255) NOT NULL, `add_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, `author` varchar(64) DEFAULT NULL COMMENT '作者', `alarm_email` varchar(255) DEFAULT NULL COMMENT '报警邮件', `schedule_type` varchar(50) NOT NULL DEFAULT 'NONE' COMMENT '调度类型', `schedule_conf` varchar(128) DEFAULT NULL COMMENT '调度配置,值含义取决于调度类型', `misfire_strategy` varchar(50) NOT NULL DEFAULT 'DO_NOTHING' COMMENT '调度过期策略', `executor_route_strategy` varchar(50) DEFAULT NULL COMMENT '执行器路由策略', `executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler', `executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数', `executor_block_strategy` varchar(50) DEFAULT NULL COMMENT '阻塞处理策略', `executor_timeout` int(11) NOT NULL DEFAULT '0' COMMENT '任务执行超时时间,单位秒', `executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数', `glue_type` varchar(50) NOT NULL COMMENT 'GLUE类型', `glue_source` mediumtext COMMENT 'GLUE源代码', `glue_remark` varchar(128) DEFAULT NULL COMMENT 'GLUE备注', `glue_updatetime` datetime DEFAULT NULL COMMENT 'GLUE更新时间', `child_jobid` varchar(255) DEFAULT NULL COMMENT '子任务ID,多个逗号分隔', `trigger_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '调度状态:0-停止,1-运行', `trigger_last_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '上次调度时间', `trigger_next_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '下次调度时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `xxl_job_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `job_group` int(11) NOT NULL COMMENT '执行器主键ID', `job_id` int(11) NOT NULL COMMENT '任务,主键ID', `executor_address` varchar(255) DEFAULT NULL COMMENT '执行器地址,本次执行的地址', `executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler', `executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数', `executor_sharding_param` varchar(20) DEFAULT NULL COMMENT '执行器任务分片参数,格式如 1/2', `executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数', `trigger_time` datetime DEFAULT NULL COMMENT '调度-时间', `trigger_code` int(11) NOT NULL COMMENT '调度-结果', `trigger_msg` text COMMENT '调度-日志', `handle_time` datetime DEFAULT NULL COMMENT '执行-时间', `handle_code` int(11) NOT NULL COMMENT '执行-状态', `handle_msg` text COMMENT '执行-日志', `alarm_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '告警状态:0-默认、1-无需告警、2-告警成功、3-告警失败', PRIMARY KEY (`id`), KEY `I_trigger_time` (`trigger_time`), KEY `I_handle_code` (`handle_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `xxl_job_log_report` ( `id` int(11) NOT NULL AUTO_INCREMENT, `trigger_day` datetime DEFAULT NULL COMMENT '调度-时间', `running_count` int(11) NOT NULL DEFAULT '0' COMMENT '运行中-日志数量', `suc_count` int(11) NOT NULL DEFAULT '0' COMMENT '执行成功-日志数量', `fail_count` int(11) NOT NULL DEFAULT '0' COMMENT '执行失败-日志数量', `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `i_trigger_day` (`trigger_day`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `xxl_job_logglue` ( `id` int(11) NOT NULL AUTO_INCREMENT, `job_id` int(11) NOT NULL COMMENT '任务,主键ID', `glue_type` varchar(50) DEFAULT NULL COMMENT 'GLUE类型', `glue_source` mediumtext COMMENT 'GLUE源代码', `glue_remark` varchar(128) NOT NULL COMMENT 'GLUE备注', `add_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `xxl_job_registry` ( `id` int(11) NOT NULL AUTO_INCREMENT, `registry_group` varchar(50) NOT NULL, `registry_key` varchar(255) NOT NULL, `registry_value` varchar(255) NOT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `i_g_k_v` (`registry_group`,`registry_key`,`registry_value`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `xxl_job_group` ( `id` int(11) NOT NULL AUTO_INCREMENT, `app_name` varchar(64) NOT NULL COMMENT '执行器AppName', `title` varchar(12) NOT NULL COMMENT '执行器名称', `address_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '执行器地址类型:0=自动注册、1=手动录入', `address_list` text COMMENT '执行器地址列表,多地址逗号分隔', `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `xxl_job_user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '账号', `password` varchar(50) NOT NULL COMMENT '密码', `role` tinyint(4) NOT NULL COMMENT '角色:0-普通用户、1-管理员', `permission` varchar(255) DEFAULT NULL COMMENT '权限:执行器ID列表,多个逗号分割', PRIMARY KEY (`id`), UNIQUE KEY `i_username` (`username`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `xxl_job_lock` ( `lock_name` varchar(50) NOT NULL COMMENT '锁名称', PRIMARY KEY (`lock_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; INSERT INTO `xxl_job_group`(`id`, `app_name`, `title`, `address_type`, `address_list`, `update_time`) VALUES (1, 'xxl-job-executor-sample', '示例执行器', 0, NULL, '2018-11-03 22:21:31' ); INSERT INTO `xxl_job_info`(`id`, `job_group`, `job_desc`, `add_time`, `update_time`, `author`, `alarm_email`, `schedule_type`, `schedule_conf`, `misfire_strategy`, `executor_route_strategy`, `executor_handler`, `executor_param`, `executor_block_strategy`, `executor_timeout`, `executor_fail_retry_count`, `glue_type`, `glue_source`, `glue_remark`, `glue_updatetime`, `child_jobid`) VALUES (1, 1, '测试任务1', '2018-11-03 22:21:31', '2018-11-03 22:21:31', 'XXL', '', 'CRON', '0 0 0 * * ? *', 'DO_NOTHING', 'FIRST', 'demoJobHandler', '', 'SERIAL_EXECUTION', 0, 0, 'BEAN', '', 'GLUE代码初始化', '2018-11-03 22:21:31', ''); INSERT INTO `xxl_job_user`(`id`, `username`, `password`, `role`, `permission`) VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', 1, NULL); INSERT INTO `xxl_job_lock` ( `lock_name`) VALUES ( 'schedule_lock'); commit; 3、修改xxl-job-admin项目配置文件 #修改为自己的数据库 spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver4、启动xxl-job-admin项目输入地址127.0.0.1:8080//xxl-job-admin 账号/密码:admin/1234565、新增执行器 通过appname自动注册,一定要和配置文件中的appname一致6、添加任务jobhandler要和代码注解中的一致。然后选择cron启动项目即可四、Docker方式构建1、拉取镜像[root@localhost ~]# docker pull xuxueli/xxl-job-admin:2.3.02、创建数据库注意:我的数据库是为了xxl-job测试用的,实际生产请不要这样1. docker pull mysql 2. docker run -d --name mysql -e MYSQL_ROOT_PASSWORD=root -p 3306:3306 mysql3、创建xxl-job容器并启动 docker run -d -p 8080:8080 \ -e PARAMS="--spring.datasource.url=jdbc:mysql://192.168.6.145:3306/xxl_job? useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai -- spring.datasource.username=root --spring.datasource.password=root" \ -v /tmp:/data/applogs --name xxl-job-admin \ xuxueli/xxl-job-admin:2.3.0 4、自定义镜像创建容器修改配置文件为自己的数据库,然后打包上传到服务器spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver创建文件 mkdir -p /opt/xxl-job 并在 /opt/xxl-job 目录下 创建Dockerfile文件 vi DockerfileFROM openjdk:8-jre-slim ENV PARAMS="" ENV TZ=PRC RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone ADD xxl-job-admin-*.jar /app.jar ###配置容器启动后执行的命令 ENTRYPOINT ["sh","-c","java -jar $JAVA_OPTS /app.jar $PARAMS"]mvn clean packge 打包之后上传至 /opt/xxl-job 目录构建镜像,注意后面的点别丢了docker build -t xxl-job-admin:2.4.0 .启动自定义的镜像docker run --name xxl-job -p 8090:8080 -d xxl-job-admin:2.4.0五、整合Springboot1、添加maven依赖 <dependencies> <!-- xxl-job-core --> <dependency> <groupId>com.xuxueli</groupId> <artifactId>xxl-job-core</artifactId> <version>2.3.0</version> </dependency> </dependencies> 2、配置文件# web port server.port=8083 # no web #spring.main.web-environment=false # log config logging.config=classpath:logback.xml ### xxl-job admin address list, such as "http://address" or "http://address01,http://address02" #填写你自己的xxl-job-admin项目 xxl.job.admin.addresses=http://192.168.6.145:8080/xxl-job-admin ### xxl-job, access token xxl.job.accessToken= ### xxl-job executor appname #与新增的执行器名称一致 xxl.job.executor.appname=xiaojie-job ### xxl-job executor registry-address: default use address to registry , otherwise use ip:port if address is null xxl.job.executor.address= ### xxl-job executor server-info #我写的内网地址 xxl.job.executor.ip=192.168.6.1 xxl.job.executor.port=9997 ### xxl-job executor log-path xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler ### xxl-job executor log-retention-days #日志保留天数 xxl.job.executor.logretentiondays=303、定时器代码 @XxlJob("demoJobHandler") public void demoJobHandler() throws Exception { logger.info(">>>>>>>>>>>>>>>>>>>>>>"+Thread.currentThread().getName()); }六、集群分片当有一个需求,需要给100万个用户发送定时推送时如何处理,此时我们就用分片处理,将xxl-job集群部署,根据分片 发送数据。执行器(定时任务)发送数据分片01-30万分片130万-60万分片260万-100万 @XxlJob("shardingJobHandler") public void shardingJobHandler() throws Exception { // 分片参数 int shardIndex = XxlJobHelper.getShardIndex(); int shardTotal = XxlJobHelper.getShardTotal(); logger.info("总分片数是>>>>>>>>>>>>>>>{},当前分片是>>>>>>>>>>>>>>>>>>>>{}", shardTotal, shardIndex); if (ShardEnum.ONE.getShardNum() == shardIndex) { logger.info("this is first shard >>>>>>>>>>>>>>>>执行数据id:1-100000"); } else if (ShardEnum.TWO.getShardNum() == shardIndex) { logger.info("this is second shard >>>>>>>>>>>>>>>执行数据id:100000-200000"); } else { logger.info("this is second shard >>>>>>>>>>>>>>>执行数据id:200000-300000"); } }完整代码 :spring-boot: Springboot整合redis、消息中间件等相关代码参考: 分布式任务调度平台XXL-JOB
正文#添加用户 create user 'xiaojie'@'%'identified by 'root'; #删除用户 drop user xiaojie; #为用户授权所有数据库的增删改查操作 grant select,insert,update,delete on *.* to xiaojie@'%'; #刷新权限 FLUSH PRIVILEGES; #给用户新增某个表的增删改查权限 grant select, insert, update, delete,create on my_test.tb_user to xiaojie@'%'; FLUSH PRIVILEGES; #给用户设定某个数据库中的所有表的权限 grant select, insert, update, delete,create on my_test.* to xiaojie@'%'; FLUSH PRIVILEGES; #create权限 grant create on *.* to xiaojie@'%' ; FLUSH PRIVILEGES; #修改表权限 grant alter on *.* to xiaojie@'%' ; FLUSH PRIVILEGES; #删除表权限 grant drop on *.* to xiaojie@'%' ; FLUSH PRIVILEGES; #对用户赋予所有数据库的所有权限 grant all on *.* to xiaojie; FLUSH PRIVILEGES; #刷新数据库 grant reload on *.* to xiaojie; FLUSH PRIVILEGES; #给用户设定mysql数据库下所有表的所有权限 grant all on mysql.* to xiaojie; FLUSH PRIVILEGES; #给用户撤销mysql数据库下所有表的所有权限 revoke all on mysql.* from xiaojie; FLUSH PRIVILEGES; #撤销create权限 revoke create on *.* from xiaojie@'%'; FLUSH PRIVILEGES; 查看权限 show grants for xiaojie;
正文一、原理1、生产者投递事务消息到Broker中,设置该消息为半消息,不可以被消费 。2、broker在刷盘成功之后返回ack给生产者。3、生产者执行本地事务4、生产者将本地事务执行结果,告知Broker。5、如果事务执行成功,则将半消息设置成可以消费,然后消费者进行消费,,如果本地事务执行失败,则将半消息删除,进行回滚。6、如果由于网络原因或者其他原因,Broker一直没有收到本地事务执行的结果,则Broker每隔60s主动获取本地事务执行的结果,若果获取到则设置半消息可以消费,反之继续重试。二、代码生产者代码package com.xiaojie.rocket.producer; import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.client.producer.TransactionSendResult; import org.apache.rocketmq.spring.core.RocketMQTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.Message; import org.springframework.messaging.support.MessageBuilder; import org.springframework.stereotype.Component; /** * @author xiaojie * @version 1.0 * @description: 订单生产者 * @date 2021/11/14 23:25 */ @Component @Slf4j public class OrderProducer { @Autowired private RocketMQTemplate rocketMQTemplate; public TransactionSendResult sendSyncMessage(String msg, String destination, String tag) { log.info("【发送消息】:{}........", msg); Message<String> message = MessageBuilder.withPayload(msg).build(); TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(destination, message,null); log.info("【发送状态】:{}", result.getLocalTransactionState()); return result; } }生产者监听package com.xiaojie.rocket.listener; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.xiaojie.rocket.mapper.OrderMapper; import com.xiaojie.rocket.pojo.Order; import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener; import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener; import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.stereotype.Component; import java.io.InputStream; /** * @author xiaojie * @version 1.0 * @description: 回调监听 * @date 2021/11/14 23:59 */ @Slf4j @Component @RocketMQTransactionListener public class ProducerListener implements RocketMQLocalTransactionListener { @Autowired private OrderMapper orderMapper; /** * @description: 执行本地事务 * @author xiaojie * @date 2021/11/15 0:05 * @version 1.0 */ @Override public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) { try { if (message == null) { return null; } String msg = new String((byte[]) message.getPayload()); log.info("发送的消息是message>>>>>>>>>",msg); Order order = JSONObject.parseObject(msg, Order.class); int insert = orderMapper.insert(order); if (insert>0){ //事务执行成功 return RocketMQLocalTransactionState.COMMIT; }else{ return RocketMQLocalTransactionState.ROLLBACK; } } catch (Exception e) { e.printStackTrace(); return RocketMQLocalTransactionState.ROLLBACK; } } /** * @description: 检查本地事务 * @author xiaojie * @date 2021/11/15 0:06 * @version 1.0 */ @Override public RocketMQLocalTransactionState checkLocalTransaction(Message message) { try { if (message == null) { //如果为空,有可能是网络原因,不能删除数据,继续重试 return RocketMQLocalTransactionState.UNKNOWN; } String msg = new String((byte[]) message.getPayload()); log.info("发送的消息是message>>>>>>>>>",msg); Order order = JSONObject.parseObject(msg, Order.class); QueryWrapper queryWrapper=new QueryWrapper(order.getOrderid()); Order dbOrder = orderMapper.selectOne(queryWrapper); if (dbOrder == null) { return RocketMQLocalTransactionState.UNKNOWN; } return RocketMQLocalTransactionState.COMMIT; } catch (Exception e) { e.printStackTrace(); return RocketMQLocalTransactionState.UNKNOWN; } } }消费者package com.xiaojie.rocket.consumer; import com.alibaba.fastjson.JSONObject; import com.xiaojie.rocket.mapper.DispatchMapper; import com.xiaojie.rocket.pojo.Dispatch; import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; import org.apache.rocketmq.spring.core.RocketMQListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** * @author xiaojie * @version 1.0 * @description: 派单消费者 * @date 2021/11/15 0:23 */ @Component @RocketMQMessageListener(consumerGroup = "order-consumer", topic = "order-topic-test") @Slf4j public class DispatchConsumer implements RocketMQListener<String> { @Autowired private DispatchMapper dispatchMapper; @Override public void onMessage(String msg) { log.info(">>>>>>>>>>>>>>>>>>>>>",msg); JSONObject jsonObject = JSONObject.parseObject(msg); String orderId = jsonObject.getString("orderId"); // 计算分配的快递员id Dispatch dispatch=new Dispatch(); dispatch.setOrderid(orderId); //经过一系列的算法得到送餐时间为30分钟 dispatch.setSendtime(30*60L); dispatch.setRiderid(1000012L); dispatch.setUserid(15672L); // 3.插入我们的数据库 int result = dispatchMapper.insert(dispatch); } }完整代码:spring-boot: Springboot整合redis、消息中间件等相关代码
正文本人是在一台虚拟机上搭建的,如果是生产部署请做相应的修改!!!一、安装docker-compose假设你电脑已经安装了docker了1、下载docker-compose[root@bogon ~]# sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/do2、授权[root@bogon ~]# sudo chmod +x /usr/local/bin/docker-compose3、检测是否安装成功[root@bogon bin]# docker-compose --version或者docker-compose -v4、卸载[root@bogon bin]# sudo rm /usr/local/bin/docker-compose二、安装rocketmq配置文件1、创建配置文件目录mkdir -p /data/rocketmq/{logs-nameserver-m,logs-nameserver-s,logs-a,logs-a-s,logs-b,logs-b-s,store-a,store-a-s,store-b,store-b-s,conf}2、broker-a.conf# Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #所属集群名字,同一个集群名字相同 brokerClusterName=rocketmq-cluster #broker名字 brokerName=broker-a #0表示master >0 表示slave brokerId=0 #删除文件的时间点,凌晨4点 deleteWhen=04 #文件保留时间 默认是48小时 fileReservedTime=168 #异步复制Master brokerRole=ASYNC_MASTER #刷盘方式,ASYNC_FLUSH=异步刷盘,SYNC_FLUSH=同步刷盘 flushDiskType=ASYNC_FLUSH #Broker 对外服务的监听端口 listenPort=10911 #nameServer地址,这里nameserver是单台,如果nameserver是多台集群的话,就用分号分割(即namesrvAddr=ip1:port1;ip2:port2;ip3:port3) namesrvAddr=192.168.139.156:9876;192.168.139.156:9877 #每个topic对应队列的数量,默认为4,实际应参考consumer实例的数量,值过小不利于consumer负载均衡 defaultTopicQueueNums=8 #是否允许 Broker 自动创建Topic,生产建议关闭 autoCreateTopicEnable=true #是否允许 Broker 自动创建订阅组,生产建议关闭 autoCreateSubscriptionGroup=true #设置BrokerIP brokerIP1=192.168.139.1563、broker-b.conf# Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #所属集群名字 brokerClusterName=rocketmq-cluster #broker名字,注意此处不同的配置文件填写的不一样 例如:在a.properties 文件中写 broker-a 在b.properties 文件中写 broker-b brokerName=broker-b #0 表示 Master,>0 表示 Slave brokerId=0 #删除文件时间点,默认凌晨 4点 deleteWhen=04 #文件保留时间,默认 48 小时 fileReservedTime=168 #Broker 的角色,ASYNC_MASTER=异步复制Master,SYNC_MASTER=同步双写Master,SLAVE=slave节点 brokerRole=ASYNC_MASTER #刷盘方式,ASYNC_FLUSH=异步刷盘,SYNC_FLUSH=同步刷盘 flushDiskType=SYNC_FLUSH #Broker 对外服务的监听端口 listenPort=11911 #nameServer地址,这里nameserver是单台,如果nameserver是多台集群的话,就用分号分割(即namesrvAddr=ip1:port1;ip2:port2;ip3:port3) namesrvAddr=192.168.139.156:9876;192.168.139.156:9877 #每个topic对应队列的数量,默认为4,实际应参考consumer实例的数量,值过小不利于consumer负载均衡 defaultTopicQueueNums=8 #是否允许 Broker 自动创建Topic,生产建议关闭 autoCreateTopicEnable=true #是否允许 Broker 自动创建订阅组,生产建议关闭 autoCreateSubscriptionGroup=true #设置BrokerIP brokerIP1=192.168.139.1564、broker-a-s.conf# Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #所属集群名字 brokerClusterName=rocketmq-cluster #broker名字,注意此处不同的配置文件填写的不一样 例如:在a.properties 文件中写 broker-a 在b.properties 文件中写 broker-b brokerName=broker-a #0 表示 Master,>0 表示 Slave brokerId=1 #删除文件时间点,默认凌晨 4点 deleteWhen=04 #文件保留时间,默认 48 小时 fileReservedTime=168 #Broker 的角色,ASYNC_MASTER=异步复制Master,SYNC_MASTER=同步双写Master,SLAVE=slave节点 brokerRole=SLAVE #刷盘方式,ASYNC_FLUSH=异步刷盘,SYNC_FLUSH=同步刷盘 flushDiskType=SYNC_FLUSH #Broker 对外服务的监听端口 listenPort=12911 #nameServer地址,这里nameserver是单台,如果nameserver是多台集群的话,就用分号分割(即namesrvAddr=ip1:port1;ip2:port2;ip3:port3) namesrvAddr=192.168.139.156:9876;192.168.139.156:9877 #每个topic对应队列的数量,默认为4,实际应参考consumer实例的数量,值过小不利于consumer负载均衡 defaultTopicQueueNums=8 #是否允许 Broker 自动创建Topic,生产建议关闭 autoCreateTopicEnable=true #是否允许 Broker 自动创建订阅组,生产建议关闭 autoCreateSubscriptionGroup=true #设置BrokerIP brokerIP1=192.168.139.1565、broker-b-s.conf# Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. brokerClusterName=rocketmq-cluster brokerName=broker-b #slave brokerId=1 deleteWhen=04 fileReservedTime=168 brokerRole=SLAVE flushDiskType=ASYNC_FLUSH #Broker 对外服务的监听端口 listenPort=13911 #nameServer地址,这里nameserver是单台,如果nameserver是多台集群的话,就用分号分割(即namesrvAddr=ip1:port1;ip2:port2;ip3:port3) namesrvAddr=192.168.139.156:9876;192.168.139.156:9877 #每个topic对应队列的数量,默认为4,实际应参考consumer实例的数量,值过小不利于consumer负载均衡 defaultTopicQueueNums=8 #是否允许 Broker 自动创建Topic,生产建议关闭 autoCreateTopicEnable=true #是否允许 Broker 自动创建订阅组,生产建议关闭 autoCreateSubscriptionGroup=true #设置BrokerIP brokerIP1=192.168.139.156yml配置文件1、创建目录存放docker-compose.yml[root@bogon ~]# mkdir -p /usr/local/docker-compose/rocketmq2、vi docker-compose.yml 如下内容 version: '2' services: namesrv1: image: foxiswho/rocketmq:4.8.0 container_name: rmqnamesrv1 ports: - 9876:9876 volumes: - /data/rocketmq/logs-nameserver-m:/home/rocketmq/logs environment: JAVA_OPT_EXT: -server -Xms256M -Xmx256M -Xmn128m command: sh mqnamesrv namesrv2: image: foxiswho/rocketmq:4.8.0 container_name: rmqnamesrv2 ports: - 9877:9877 volumes: - /data/rocketmq/logs-nameserver-s:/home/rocketmq/logs environment: JAVA_OPT_EXT: -server -Xms256M -Xmx256M -Xmn128m command: sh mqnamesrv broker-a-m: image: foxiswho/rocketmq:4.8.0 container_name: rmqbroker-a-master ports: - 10909:10909 - 10911:10911 - 10912:10912 volumes: - /data/rocketmq/logs-a:/home/rocketmq/logs - /data/rocketmq/store-a:/home/rocketmq/store - /data/rocketmq/conf/broker-a.conf:/home/rocketmq/rocketmq-4.8.0/conf/broker.conf environment: JAVA_OPT_EXT: -server -Xms256m -Xmx256m -Xmn128m NAMESRV_ADDR: 192.168.139.156:9876;192.168.139.156:9877 command: sh mqbroker -c /home/rocketmq/rocketmq-4.8.0/conf/broker.conf broker-b-m: image: foxiswho/rocketmq:4.8.0 container_name: rmqbroker-b-master ports: - 11909:11909 - 11911:11911 - 11912:11912 volumes: - /data/rocketmq/logs-b:/home/rocketmq/logs - /data/rocketmq/store-b:/home/rocketmq/store - /data/rocketmq/conf/broker-b.conf:/home/rocketmq/rocketmq-4.8.0/conf/broker.conf environment: JAVA_OPT_EXT: -server -Xms256m -Xmx256m -Xmn128m NAMESRV_ADDR: 192.168.139.156:9876;192.168.139.156:9877 command: sh mqbroker -c /home/rocketmq/rocketmq-4.8.0/conf/broker.conf broker-a-s: image: foxiswho/rocketmq:4.8.0 container_name: rmqbroker-a-slave ports: - 12909:12909 - 12911:12911 - 12912:12912 volumes: - /data/rocketmq/logs-a-s:/home/rocketmq/logs - /data/rocketmq/store-a-s:/home/rocketmq/store - /data/rocketmq/conf/broker-a-s.conf:/home/rocketmq/rocketmq-4.8.0/conf/broker.conf environment: JAVA_OPT_EXT: -server -Xms256m -Xmx256m -Xmn128m NAMESRV_ADDR: 192.168.139.156:9876;192.168.139.156:9877 command: sh mqbroker -c /home/rocketmq/rocketmq-4.8.0/conf/broker.conf broker-b-s: image: foxiswho/rocketmq:4.8.0 container_name: rmqbroker-b-slave ports: - 13909:13909 - 13911:13911 - 13912:13912 volumes: - /data/rocketmq/logs-b-s:/home/rocketmq/logs - /data/rocketmq/store-b-s:/home/rocketmq/store - /data/rocketmq/conf/broker-b-s.conf:/home/rocketmq/rocketmq-4.8.0/conf/broker.conf environment: JAVA_OPT_EXT: -server -Xms256m -Xmx256m -Xmn128m NAMESRV_ADDR: 192.168.139.156:9876;192.168.139.156:9877 command: sh mqbroker -c /home/rocketmq/rocketmq-4.8.0/conf/broker.conf depends_on: - namesrv1 - namesrv2注意:yml文件格式一定要写对,建议修改完成之后在线校验一下。一定要对新建的存储目录和配置文件chmod 777授权,否则启动不起来!!!3、启动在docker-compose.yml的目录下启动docker-compose up #后台启动 docker-compose up -d三、安装成功 端口号说明listenPort参数是broker的监听端口号,是remotingServer服务组件使用,作为对Producer和Consumer提供服务的端口号,默认为10911。fastListenPort参数是fastRemotingServer服务组件使用,默认为listenPort - 2,#主要用于slave同步master ,fastListenPort=10909。haListenPort参数是HAService服务组件使用,用于Broker的主从同步,默认为listenPort - 1, haService中使用 haListenPort=10912
正文本人是在一台虚拟机上搭建的,如果是生产部署请做相应的修改!!!1、创建挂载目录[root@bogon ~]# mkdir -p /data/rocketmq/{logs-nameserver-m,logs-nameserver-s,logs-a,logs-a-s,lo2、对这些创建的文件授权(注意是所有的这些创建的文件,包括对应的配置文件!!!) chmod 777 对应的文件名称 3、创建配置文件broker-a.conf# Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #所属集群名字,同一个集群名字相同 brokerClusterName=rocketmq-cluster #broker名字 brokerName=broker-a #0表示master >0 表示slave brokerId=0 #删除文件的时间点,凌晨4点 deleteWhen=04 #文件保留时间 默认是48小时 fileReservedTime=168 #异步复制Master brokerRole=ASYNC_MASTER #刷盘方式,ASYNC_FLUSH=异步刷盘,SYNC_FLUSH=同步刷盘 flushDiskType=ASYNC_FLUSH #Broker 对外服务的监听端口 listenPort=10911 端口号对应docker启动时候的端口号 #nameServer地址,这里nameserver是单台,如果nameserver是多台集群的话,就用分号分割(即namesrvAddr=ip1:port1;ip2:port2;ip3:port3) namesrvAddr=192.168.139.156:9876;192.168.139.156:9877 #每个topic对应队列的数量,默认为4,实际应参考consumer实例的数量,值过小不利于consumer负载均衡 defaultTopicQueueNums=8 #是否允许 Broker 自动创建Topic,生产建议关闭 autoCreateTopicEnable=true #是否允许 Broker 自动创建订阅组,生产建议关闭 autoCreateSubscriptionGroup=true #设置BrokerIP brokerIP1=192.168.139.156broker-b.conf# Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #所属集群名字 brokerClusterName=rocketmq-cluster #broker名字,注意此处不同的配置文件填写的不一样 例如:在a.properties 文件中写 broker-a 在b.properties 文件中写 broker-b brokerName=broker-b #0 表示 Master,>0 表示 Slave brokerId=0 #删除文件时间点,默认凌晨 4点 deleteWhen=04 #文件保留时间,默认 48 小时 fileReservedTime=168 #Broker 的角色,ASYNC_MASTER=异步复制Master,SYNC_MASTER=同步双写Master,SLAVE=slave节点 brokerRole=ASYNC_MASTER #刷盘方式,ASYNC_FLUSH=异步刷盘,SYNC_FLUSH=同步刷盘 flushDiskType=SYNC_FLUSH #Broker 对外服务的监听端口 listenPort=11912 #nameServer地址,这里nameserver是单台,如果nameserver是多台集群的话,就用分号分割(即namesrvAddr=ip1:port1;ip2:port2;ip3:port3) namesrvAddr=192.168.139.156:9876;192.168.139.156:9877 #每个topic对应队列的数量,默认为4,实际应参考consumer实例的数量,值过小不利于consumer负载均衡 defaultTopicQueueNums=8 #是否允许 Broker 自动创建Topic,生产建议关闭 autoCreateTopicEnable=true #是否允许 Broker 自动创建订阅组,生产建议关闭 autoCreateSubscriptionGroup=true #设置BrokerIP brokerIP1=192.168.139.156broker-a-s.conf# Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #所属集群名字 brokerClusterName=rocketmq-cluster #broker名字,注意此处不同的配置文件填写的不一样 例如:在a.properties 文件中写 broker-a 在b.properties 文件中写 broker-b brokerName=broker-a #0 表示 Master,>0 表示 Slave brokerId=1 #删除文件时间点,默认凌晨 4点 deleteWhen=04 #文件保留时间,默认 48 小时 fileReservedTime=168 #Broker 的角色,ASYNC_MASTER=异步复制Master,SYNC_MASTER=同步双写Master,SLAVE=slave节点 brokerRole=SLAVE #刷盘方式,ASYNC_FLUSH=异步刷盘,SYNC_FLUSH=同步刷盘 flushDiskType=SYNC_FLUSH #Broker 对外服务的监听端口 listenPort=11013 #nameServer地址,这里nameserver是单台,如果nameserver是多台集群的话,就用分号分割(即namesrvAddr=ip1:port1;ip2:port2;ip3:port3) namesrvAddr=192.168.139.156:9876;192.168.139.156:9877 #每个topic对应队列的数量,默认为4,实际应参考consumer实例的数量,值过小不利于consumer负载均衡 defaultTopicQueueNums=8 #是否允许 Broker 自动创建Topic,生产建议关闭 autoCreateTopicEnable=true #是否允许 Broker 自动创建订阅组,生产建议关闭 autoCreateSubscriptionGroup=true #设置BrokerIP brokerIP1=192.168.139.156broker-b-s.conf# Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. brokerClusterName=rocketmq-cluster brokerName=broker-b #slave brokerId=1 deleteWhen=04 fileReservedTime=168 brokerRole=SLAVE flushDiskType=ASYNC_FLUSH #Broker 对外服务的监听端口 listenPort=11014 #nameServer地址,这里nameserver是单台,如果nameserver是多台集群的话,就用分号分割(即namesrvAddr=ip1:port1;ip2:port2;ip3:port3) namesrvAddr=192.168.139.156:9876;192.168.139.156:9877 #每个topic对应队列的数量,默认为4,实际应参考consumer实例的数量,值过小不利于consumer负载均衡 defaultTopicQueueNums=8 #是否允许 Broker 自动创建Topic,生产建议关闭 autoCreateTopicEnable=true #是否允许 Broker 自动创建订阅组,生产建议关闭 autoCreateSubscriptionGroup=true #设置BrokerIP brokerIP1=192.168.139.156注意如果拷贝出来的文件格式不对请做相应的修改 解决方法,拷贝的文件格式错乱使用下面命令乎看到文件后面多了^M[root@bogon bin]# vim -b broker-a.conf 去掉^M 再重新启动[root@bogon bin]# sed -i 's/\r//g' broker-a.conf 4、拉取镜像[root@bogon ~]# docker pull foxiswho/rocketmq:4.8.05、安装nameserver#启动 rmqnamesrv-master docker run -d -v /data/rocketmq/logs-nameserver-m:/home/rocketmq/logs \ --name rmqnamesrv-master \ -e "JAVA_OPT_EXT=-Xms256M -Xmx256M -Xmn128m" \ -p 9876:9876 \ foxiswho/rocketmq:4.8.0 \ sh mqnamesrv #启动 rmqnamesrv-slave docker run -d -v /data/rocketmq/logs-nameserver-s:/home/rocketmq/logs\ --name rmqnamesrv-slave \ -e "JAVA_OPT_EXT=-Xms256M -Xmx256M -Xmn128m" \ -p 9877:9877 \ foxiswho/rocketmq:4.8.0 \ sh mqnamesrv6、安装broker启动 mq-a-master docker run -d --name mq-a-master \ -v /data/rocketmq/logs-a:/home/rocketmq/logs \ -v /data/rocketmq/store-a:/home/rocketmq/store \ -v /data/rocketmq/conf/broker-a.conf:/home/rocketmq/rocketmq-4.8.0/conf/broker.conf \ -e "NAMESRV_ADDR=192.168.139.156:9876;192.168.139.156:9877" \ -e "JAVA_OPT_EXT= -server -Xms128M -Xmx128M -Xmn128m" \ -p 10911:10911 \ foxiswho/rocketmq:4.8.0 \ sh mqbroker -c /home/rocketmq/rocketmq-4.8.0/conf/broker.conf #启动 mq-b-master docker run -d --name mq-b-master \ -v /data/rocketmq/logs-b:/home/rocketmq/logs -v /data/rocketmq/store-b:/home/rocketmq/store \ -v /data/rocketmq/conf/broker-b.conf:/home/rocketmq/rocketmq-4.8.0/conf/broker.conf \ -e "NAMESRV_ADDR=192.168.139.156:9876;192.168.139.156:9877" \ -e "JAVA_OPT_EXT= -server -Xms128M -Xmx128M -Xmn128m" \ -p 11912:11912 \ --privileged=true \ foxiswho/rocketmq:4.8.0 \ sh mqbroker -c /home/rocketmq/rocketmq-4.8.0/conf/broker.conf #启动 mq-a-slave docker run -d --name mq-a-slave \ -v /data/rocketmq/logs-a-s:/home/rocketmq/logs -v /data/rocketmq/store-a-s:/home/rocketmq/store \ -v /data/rocketmq/conf/broker-a-s.conf:/home/rocketmq/rocketmq-4.8.0/conf/broker.conf \ -e "NAMESRV_ADDR=192.168.139.156:9876;192.168.139.156:9877" \ -e "JAVA_OPT_EXT= -server -Xms128M -Xmx128M -Xmn128m" \ -p 11013:11013 \ foxiswho/rocketmq:4.8.0 \ sh mqbroker -c /home/rocketmq/rocketmq-4.8.0/conf/broker.conf #启动 mq-b-slave docker run -d --name mq-b-slave \ -v /data/rocketmq/logs-b-s:/home/rocketmq/logs -v /data/rocketmq/store-b-s:/home/rocketmq/store \ -v /data/rocketmq/conf/broker-b-s.conf:/home/rocketmq/rocketmq-4.8.0/conf/broker.conf \ -e "NAMESRV_ADDR=192.168.139.156:9876;192.168.139.156:9877" \ -e "JAVA_OPT_EXT= -server -Xms128M -Xmx128M -Xmn128m" \ -p 11014:11014 \ foxiswho/rocketmq:4.8.0 \ sh mqbroker -c /home/rocketmq/rocketmq-4.8.0/conf/broker.conf 注意:配置文件写的端口要和启动参数中的端口号一致!!!7、验证 搭建成功,有不对的地方请大神不吝赐教。参考:Docker Hub
正文一、RocketMQ介绍 RocketMQ是一款分布式消息中间件,最初是由阿里巴巴消息中间件团队研发并大规模应用于生产系统,满足线上海量消息堆积的需求, 在2016年底捐赠给Apache开源基金会成为孵化项目,经过不到一年时间正式成为了Apache顶级项目;早期阿里曾经基于ActiveMQ研发消息系统, 随着业务消息的规模增大,瓶颈逐渐显现,后来也考虑过Kafka,但因为在低延迟和高可靠性方面没有选择,最后才自主研发了RocketMQ, 各方面的性能都比目前已有的消息队列要好,RocketMQ和Kafka在概念和原理上都非常相似,所以也经常被拿来对比;RocketMQ默认采用长轮询的拉模式, 单机支持千万级别的消息堆积,可以非常好的应用在海量消息系统中。二、名词解释消息模型:RocketMQ主要由 Producer、Broker、Consumer 三部分组成,其中Producer 负责生产消息,Consumer 负责消费消息,Broker 负责存储消息。Broker 在实际部署过程中对应一台服务器,每个 Broker 可以存储多个Topic的消息,每个Topic的消息也可以分片存储于不同的 Broker。Message Queue 用于存储消息的物理地址,每个Topic中的消息地址存储于多个 Message Queue 中。ConsumerGroup 由多个Consumer 实例构成。消息生产者(Producer):负责生产消息,一般由业务系统负责生产消息。一个消息生产者会把业务应用系统里产生的消息发送到broker服务器。RocketMQ提供多种发送方式,同步发送、异步发送、顺序发送、单向发送。同步和异步方式均需要Broker返回确认信息,单向发送不需要。消息消费者(Consumer):负责消费消息,一般是后台系统负责异步消费。一个消息消费者会从Broker服务器拉取消息、并将其提供给应用程序。从用户应用的角度而言提供了两种消费形式:拉取式消费、推动式消费。主题(Topic):表示一类消息的集合,每个主题包含若干条消息,每条消息只能属于一个主题,是RocketMQ进行消息订阅的基本单位。代理服务器(Broker Server):消息中转角色,负责存储消息、转发消息。代理服务器在RocketMQ系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备。代理服务器也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等。名字服务(Name Server):名称服务充当路由消息的提供者。生产者或消费者能够通过名字服务查找各主题相应的Broker IP列表。多个Namesrv实例组成集群,但相互独立,没有信息交换。拉取式消费(Pull Consumer):Consumer消费的一种类型,应用通常主动调用Consumer的拉消息方法从Broker服务器拉消息、主动权由应用控制。一旦获取了批量消息,应用就会启动消费过程。推动式消费(Push Consumer):Consumer消费的一种类型,该模式下Broker收到数据后会主动推送给消费端,该消费模式一般实时性较高。生产者组(Producer Group):同一类Producer的集合,这类Producer发送同一类消息且发送逻辑一致。如果发送的是事务消息且原始生产者在发送之后崩溃,则Broker服务器会联系同一生产者组的其他生产者实例以提交或回溯消费。 消费者组(Consumer Group):同一类Consumer的集合,这类Consumer通常消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面,实现负载均衡和容错的目标变得非常容易。要注意的是,消费者组的消费者实例必须订阅完全相同的Topic。RocketMQ 支持两种消息模式:集群消费(Clustering)和广播消费(Broadcasting)。 集群消费(Clustering):集群消费模式下,相同Consumer Group的每个Consumer实例平均分摊消息。广播消费(Broadcasting):广播消费模式下,相同Consumer Group的每个Consumer实例都接收全量的消息。普通顺序消息(Normal Ordered Message):普通顺序消费模式下,消费者通过同一个消息队列( Topic 分区,称作 Message Queue) 收到的消息是有顺序的,不同消息队列收到的消息则可能是无顺序的。严格顺序消息(Strictly Ordered Message):严格顺序消息模式下,消费者收到的所有消息均是有顺序的。 消息(Message):消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题。RocketMQ中每个消息拥有唯一的Message ID,且可以携带具有业务标识的Key。系统提供了通过Message ID和Key查询消息的功能。 标签(Tag):为消息设置的标志,用于同一主题下区分不同类型的消息。来自同一业务单元的消息,可以根据不同业务目的在同一主题下设置不同标签。标签能够有效地保持代码的清晰度和连贯性,并优化RocketMQ提供的查询系统。消费者可以根据Tag实现对不同子主题的不同消费逻辑,实现更好的扩展性。摘自官网 https://github.com/apache/rocketmq/blob/master/docs/cn/concept.md三、安装安装方式单Master模式:这种方式风险较大,一旦Broker重启或者宕机时,会导致整个服务不可用。多Master模式:一个集群无Slave,全是Master。优点:配置简单,单个Master宕机或重启维护对应用无影响,在磁盘配置为RAID10时,即使机器宕机不可恢复情况下,由于RAID10磁盘非常可靠,消息也不会丢(异步刷盘丢失少量消息,同步刷盘一条不丢),性能最高;缺点:单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅,消息实时性会受到影响。多Master多Slave模式-异步复制:每个Master配置一个Slave,有多对Master-Slave,HA采用异步复制方式,主备有短暂消息延迟(毫秒级)。优点:即使磁盘损坏,消息丢失的非常少,且消息实时性不会受影响,同时Master宕机后,消费者仍然可以从Slave消费,而且此过程对应用透明,不需要人工干预,性能同多Master模式几乎一样;缺点:Master宕机,磁盘损坏情况下会丢失少量消息。多Master多Slave模式-同步双写:每个Master配置一个Slave,有多对Master-Slave,HA(双机集群)采用同步双写方式,即只有主备都写成功,才向应用返回成功。:优点:数据与服务都无单点故障,Master宕机情况下,消息无延迟,服务可用性与数据可用性都非常高;缺点:性能比异步复制模式略低(大约低10%左右),发送单个消息的RT(响应时间)会略高,且目前版本在主节点宕机后,备机不能自动切换为主机。本文以多Master多Slave模式-异步复制这种方式安装准备工作1、由于rocketmq是使用java语言编写,所以首先需要安装jdk环境,本文使用jdk17 安装参考 Linux系统下安装jdk17&jdk8安装jdk172、集群示意图两台服务器互为主从传统方式搭建RocketMQ集群1、下载安装包 版本4.9.2Apache Downloads2、上传到服务器,并解压,如果没有安装unzipan,安装指令[root@localhost ~]# yum install unzip zip3、解压文件#解压 [root@localhost ~]# unzip rocketmq-all-4.9.2-bin-release.zip #移动到/usr/local 并重命名为rocketmq [root@localhost ~]# mv rocketmq-4.9.2 /usr/local/rocketmq4、修改启动脚本 注意:因为我的jdk是17,所以需要修改启动脚本,jdk8不需要修改,修改的地方为垃圾回收器,和启动参数(-Xms256m -Xmx256m -Xmn128m)runbroker.sh #!/bin/sh # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #=========================================================================================== # Java Environment Setting #=========================================================================================== error_exit () { echo "ERROR: $1 !!" exit 1 } [ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=$HOME/jdk/java [ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=/usr/java [ ! -e "$JAVA_HOME/bin/java" ] && error_exit "Please set the JAVA_HOMEvariable in your environment, We need java(x64)!" export JAVA_HOME export JAVA="$JAVA_HOME/bin/java" export BASE_DIR=$(dirname $0)/.. export CLASSPATH=.${JAVA_HOME}/jre/lib/ext:${BASE_DIR}/lib/*:${BASE_DIR}/conf:${CLASSPATH} #export CLASSPATH=${BASE_DIR}/lib/rocketmq-broker-4.5.0.jar:${BASE_DIR}/lib/*:${BASE_DIR}/conf:${CLASSPATH} #=========================================================================================== # JVM Configuration #=========================================================================================== JAVA_OPT="${JAVA_OPT} -server -Xms256m -Xmx256m -Xmn128m" JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0" JAVA_OPT="${JAVA_OPT} -verbose:gc -Xlog:gc:/dev/shm/mq_gc_%p.log -XX:+PrintGCDetails" JAVA_OPT="${JAVA_OPT} -XX:-OmitStackTraceInFastThrow" JAVA_OPT="${JAVA_OPT} -XX:+AlwaysPreTouch" JAVA_OPT="${JAVA_OPT} -XX:MaxDirectMemorySize=15g" JAVA_OPT="${JAVA_OPT} -XX:-UseLargePages -XX:-UseBiasedLocking" #JAVA_OPT="${JAVA_OPT} -Xdebug -Xrunjdwp:transport=dt_socket,address=9555,server=y,suspend=n" JAVA_OPT="${JAVA_OPT} ${JAVA_OPT_EXT}" JAVA_OPT="${JAVA_OPT} -cp ${CLASSPATH}" numactl --interleave=all pwd > /dev/null 2>&1 if [ $? -eq 0 ] then if [ -z "$RMQ_NUMA_NODE" ] ; then numactl --interleave=all $JAVA ${JAVA_OPT} $@ else numactl --cpunodebind=$RMQ_NUMA_NODE --membind=$RMQ_NUMA_NODE $JAVA${JAVA_OPT} $@ fi else $JAVA ${JAVA_OPT} --add-exports=java.base/jdk.internal.ref=ALL-UNNAMED $@ firunserver.sh#!/bin/sh # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #=========================================================================================== # Java Environment Setting #=========================================================================================== error_exit () { echo "ERROR: $1 !!" exit 1 } [ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=$HOME/jdk/java [ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=/usr/java [ ! -e "$JAVA_HOME/bin/java" ] && error_exit "Please set the JAVA_HOMEvariable in your environment, We need java(x64)!" export JAVA_HOME export JAVA="$JAVA_HOME/bin/java" export BASE_DIR=$(dirname $0)/.. export CLASSPATH=.:${BASE_DIR}/conf:${JAVA_HOME}/jre/lib/ext:${BASE_DIR}/lib/* #=========================================================================================== # JVM Configuration #=========================================================================================== JAVA_OPT="${JAVA_OPT} -server -Xms256m -Xmx256m -Xmn128m -XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=160m" # jdk17 可能丢弃了CMS垃圾回收器,需要使用G1收集器 JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0" # JAVA_OPT="${JAVA_OPT} -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+CMSClassUnloadingEnabled -XX:SurvivorRatio=8" JAVA_OPT="${JAVA_OPT} -verbose:gc -Xlog:gc:/dev/shm/rmq_srv_gc.log -XX:+PrintGCDetails" JAVA_OPT="${JAVA_OPT} -XX:-OmitStackTraceInFastThrow" JAVA_OPT="${JAVA_OPT} -XX:-UseLargePages" # JAVA_OPT="${JAVA_OPT} -Djava.ext.dirs=${JAVA_HOME}/jre/lib/ext:${BASE_DIR}/lib" #JAVA_OPT="${JAVA_OPT} -Xdebug -Xrunjdwp:transport=dt_socket,address=9555,server=y,suspend=n" JAVA_OPT="${JAVA_OPT} ${JAVA_OPT_EXT}" JAVA_OPT="${JAVA_OPT} -cp ${CLASSPATH}" $JAVA ${JAVA_OPT} $@tools.sh #!/bin/sh # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #=========================================================================================== # Java Environment Setting #=========================================================================================== error_exit () { echo "ERROR: $1 !!" exit 1 } [ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=$HOME/jdk/java [ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=/usr/java [ ! -e "$JAVA_HOME/bin/java" ] && error_exit "Please set the JAVA_HOME variable in your environment, We need java(x64)!" export JAVA_HOME export JAVA="$JAVA_HOME/bin/java" export BASE_DIR=$(dirname $0)/.. export CLASSPATH=${BASE_DIR}/lib/*:${BASE_DIR}/conf:.:${CLASSPATH} export CLASSPATH=.${JAVA_HOME}/jre/lib/ext:${BASE_DIR}/lib/*:${BASE_DIR}/conf:${CLASSPATH} #export CLASSPATH=.:${BASE_DIR}/conf:${CLASSPATH} #echo "BASE_DIR:$BASE_DIR" #echo "CLASSPATH:$CLASSPATH" #=========================================================================================== # JVM Configuration #=========================================================================================== JAVA_OPT="${JAVA_OPT} -server -Xms256m -Xmx256m -Xmn256m -XX:PermSize=128m-XX:MaxPermSize=128m" JAVA_OPT="${JAVA_OPT} -cp ${CLASSPATH}" $JAVA ${JAVA_OPT} $@ 5、修改服务器1上的配置文件broker-a.properties# Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #所属集群名字,同一个集群名字相同 brokerClusterName=rocketmq-cluster #broker名字 brokerName=broker-a #0表示master >0 表示slave brokerId=0 #删除文件的时间点,凌晨4点 deleteWhen=04 #文件保留时间 默认是48小时 fileReservedTime=168 #异步复制Master brokerRole=ASYNC_MASTER #刷盘方式,ASYNC_FLUSH=异步刷盘,SYNC_FLUSH=同步刷盘 flushDiskType=ASYNC_FLUSH #Broker 对外服务的监听端口 listenPort=10911 #nameServer地址,这里nameserver是单台,如果nameserver是多台集群的话,就用分号分割(即namesrvAddr=ip1:port1;ip2:port2;ip3:port3) namesrvAddr=192.168.6.145:9876;192.168.6.146:9876 #每个topic对应队列的数量,默认为4,实际应参考consumer实例的数量,值过小不利于consumer负载均衡 defaultTopicQueueNums=8 #是否允许 Broker 自动创建Topic,生产建议关闭 autoCreateTopicEnable=true #是否允许 Broker 自动创建订阅组,生产建议关闭 autoCreateSubscriptionGroup=true #设置BrokerIP 服务器1的ip地址 brokerIP1=192.168.6.145 #存储路径 storePathRootDir=/data/rocketmq/store-a #commitLog 存储路径 storePathCommitLog=/data/rocketmq/store-a/commitlog #消费队列存储路径存储路径 storePathConsumerQueue=/data/rocketmq/store-a/consumequeue #消息索引存储路径 storePathIndex=/data/rocketmq/store-a/index #checkpoint 文件存储路径 storeCheckpoint=/data/rocketmq/store-a/checkpoint #abort 文件存储路径 abortFile=/data/rocketmq/store-a/abort #commitLog每个文件的大小默认1G mapedFileSizeCommitLog=1073741824 #ConsumeQueue每个文件默认存30W条,根据业务情况调整 mapedFileSizeConsumeQueue=300000broker-b-s.properties# Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. brokerClusterName=rocketmq-cluster brokerName=broker-b #slave brokerId=1 deleteWhen=04 fileReservedTime=168 brokerRole=SLAVE flushDiskType=ASYNC_FLUSH #Broker 对外服务的监听端口 listenPort=11011 #nameServer地址,这里nameserver是单台,如果nameserver是多台集群的话,就用分号分割(即namesrvAddr=ip1:port1;ip2:port2;ip3:port3) namesrvAddr=192.168.6.145:9876;192.168.6.146:9876 #每个topic对应队列的数量,默认为4,实际应参考consumer实例的数量,值过小不利于consumer负载均衡 defaultTopicQueueNums=8 #是否允许 Broker 自动创建Topic,生产建议关闭 autoCreateTopicEnable=true #是否允许 Broker 自动创建订阅组,生产建议关闭 autoCreateSubscriptionGroup=true #设置BrokerIP brokerIP1=192.168.6.145 #存储路径 storePathRootDir=/data/rocketmq/store-b #commitLog 存储路径 storePathCommitLog=/data/rocketmq/store-b/commitlog #消费队列存储路径存储路径 storePathConsumerQueue=/data/rocketmq/store-b/consumequeue #消息索引存储路径 storePathIndex=/data/rocketmq/store-b/index #checkpoint 文件存储路径 storeCheckpoint=/data/rocketmq/store-b/checkpoint #abort 文件存储路径 abortFile=/data/rocketmq/store-b/abort #commitLog每个文件的大小默认1G mapedFileSizeCommitLog=1073741824 #ConsumeQueue每个文件默认存30W条,根据业务情况调整 mapedFileSizeConsumeQueue=3000006、修改服务器2上的配置文件broker-b.properties# Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #所属集群名字 brokerClusterName=rocketmq-cluster #broker名字,注意此处不同的配置文件填写的不一样 例如:在a.properties 文件中写 broker-a 在b.properties 文件中写 broker-b brokerName=broker-b #0 表示 Master,>0 表示 Slave brokerId=0 #删除文件时间点,默认凌晨 4点 deleteWhen=04 #文件保留时间,默认 48 小时 fileReservedTime=168 #Broker 的角色,ASYNC_MASTER=异步复制Master,SYNC_MASTER=同步双写Master,SLAVE=slave节点 brokerRole=ASYNC_MASTER #刷盘方式,ASYNC_FLUSH=异步刷盘,SYNC_FLUSH=同步刷盘 flushDiskType=SYNC_FLUSH #Broker 对外服务的监听端口 listenPort=10911 #nameServer地址,这里nameserver是单台,如果nameserver是多台集群的话,就用分号分割(即namesrvAddr=ip1:port1;ip2:port2;ip3:port3) namesrvAddr=192.168.6.145:9876;192.168.6.146:9876 #每个topic对应队列的数量,默认为4,实际应参考consumer实例的数量,值过小不利于consumer负载均衡 defaultTopicQueueNums=8 #是否允许 Broker 自动创建Topic,生产建议关闭 autoCreateTopicEnable=true #是否允许 Broker 自动创建订阅组,生产建议关闭 autoCreateSubscriptionGroup=true #设置BrokerIP brokerIP1=192.168.6.146 #存储路径 storePathRootDir=/data/rocketmq/store-b #commitLog 存储路径 storePathCommitLog=/data/rocketmq/store-b/commitlog #消费队列存储路径存储路径 storePathConsumerQueue=/data/rocketmq/store-b/consumequeue #消息索引存储路径 storePathIndex=/data/rocketmq/store-b/index #checkpoint 文件存储路径 storeCheckpoint=/data/rocketmq/store-b/checkpoint #abort 文件存储路径 abortFile=/data/rocketmq/store-b/abort #commitLog每个文件的大小默认1G mapedFileSizeCommitLog=1073741824 #ConsumeQueue每个文件默认存30W条,根据业务情况调整 mapedFileSizeConsumeQueue=300000broker-a-s.properties# Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. #所属集群名字 brokerClusterName=rocketmq-cluster #broker名字,注意此处不同的配置文件填写的不一样 例如:在a.properties 文件中写 broker-a 在b.properties 文件中写 broker-b brokerName=broker-a #0 表示 Master,>0 表示 Slave brokerId=1 #删除文件时间点,默认凌晨 4点 deleteWhen=04 #文件保留时间,默认 48 小时 fileReservedTime=168 #Broker 的角色,ASYNC_MASTER=异步复制Master,SYNC_MASTER=同步双写Master,SLAVE=slave节点 brokerRole=SLAVE #刷盘方式,ASYNC_FLUSH=异步刷盘,SYNC_FLUSH=同步刷盘 flushDiskType=SYNC_FLUSH #Broker 对外服务的监听端口 listenPort=11011 #nameServer地址,这里nameserver是单台,如果nameserver是多台集群的话,就用分号分割(即namesrvAddr=ip1:port1;ip2:port2;ip3:port3) namesrvAddr=192.168.6.145:9876;192.168.6.146:9876 #每个topic对应队列的数量,默认为4,实际应参考consumer实例的数量,值过小不利于consumer负载均衡 defaultTopicQueueNums=8 #是否允许 Broker 自动创建Topic,生产建议关闭 autoCreateTopicEnable=true #是否允许 Broker 自动创建订阅组,生产建议关闭 autoCreateSubscriptionGroup=true #设置BrokerIP brokerIP1=192.168.6.146 #存储路径 storePathRootDir=/data/rocketmq/store-a #commitLog 存储路径 storePathCommitLog=/data/rocketmq/store-a/commitlog #消费队列存储路径存储路径 storePathConsumerQueue=/data/rocketmq/store-a/consumequeue #消息索引存储路径 storePathIndex=/data/rocketmq/store-a/index #checkpoint 文件存储路径 storeCheckpoint=/data/rocketmq/store-a/checkpoint #abort 文件存储路径 abortFile=/data/rocketmq/store-a/abort #commitLog每个文件的大小默认1G mapedFileSizeCommitLog=1073741824 #ConsumeQueue每个文件默认存30W条,根据业务情况调整 mapedFileSizeConsumeQueue=3000007、启动nameserver进入 [root@localhost ~]# cd /usr/local/rocketmq/bin/ 目录 启动 nohup sh mqnamesrv &检验是否安装成功tail -f ~/logs/rocketmqlogs/namesrv.log如果此时报错/usr/local/rocketmq/bin/runserver.sh: line 19: syntax error near unexpected token `$'\r'''usr/local/rocketmq/bin/runserver.sh: line 19: `error_exit ()解决方法,拷贝的文件格式错乱使用下面命令乎看到文件后面多了^M[root@bogon bin]# vim -b runserver.sh 去掉^M 再重新启动[root@bogon bin]# sed -i 's/\r//g' runserver.sh 8、启动broker启动 服务器1上的master[root@bogon rocketmq]# nohup sh bin/mqbroker -c conf/2m-2s-async/broker-a.properties &启动服务器2上的master[root@localhost rocketmq]# nohup sh bin/mqbroker -c conf/2m-2s-async/broker-b.properties & 启动服务器1上的slave[root@bogon rocketmq]# nohup sh bin/mqbroker -c conf/2m-2s-async/broker-b-s.properties &启动服务器2上的slave [root@localhost rocketmq]# nohup sh bin/mqbroker -c conf/2m-2s-async/broker-a-s.properties &9、搭建可视化管理界面maven打包安装不细说mvn clean package -Dmaven.test.skip=truejava -jar target/rocketmq-console-ng-1.0.1.jar下载安装包 Rocketmq可视化工具-Web服务器文档类资源-CSDN下载四、整合Springbootmaven依赖<dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-spring-boot-starter</artifactId> <version>2.2.1</version> </dependency>application.ymlserver: port: 8090 rocketmq: name-server: 192.168.6.145:9876;192.168.6.146:9876 producer: group: rocketmq-producer生产者package com.xiaojie.rocket.rocket.producer; import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.client.producer.SendCallback; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.spring.core.RocketMQTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * @Description: 发送消息:同步消息、异步消息和单向消息。其中前两种消息是可靠的,因为会有发送是否成功的应答 * @author: yan * @date: 2021.11.08 */ @Service @Slf4j public class MqProducer { @Autowired private RocketMQTemplate rocketMQTemplate; /** * @description: 这种方式主要用在不特别关心发送结果的场景,例如日志发送。 * @param: * @return: void * @author xiaojie * @date: 2021/11/9 23:39 */ public void sendMq() { for (int i = 0; i < 10; i++) { rocketMQTemplate.convertAndSend("xiaojie-test", "测试发送消息》》》》》》》》》" + i); } } /** * @description: 这种可靠性同步地发送方式使用的比较广泛,比如:重要的消息通知,短信通知。 * @param: * @return: void * @author xiaojie * @date: 2021/11/10 22:25 */ public void sync() { SendResult sendResult = rocketMQTemplate.syncSend("xiaojie-test", "sync发送消息。。。。。。。。。。"); log.info("发送结果{}", sendResult); } /** * @description: 异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待Broker的响应。 * @param: * @return: void * @author xiaojie * @date: 2021/11/10 22:29 */ public void async() { String msg = "异步发送消息。。。。。。。。。。"; log.info(">msg:<<" + msg); rocketMQTemplate.asyncSend("xiaojie-test", msg, new SendCallback() { @Override public void onSuccess(SendResult var1) { log.info("异步发送成功{}", var1); } @Override public void onException(Throwable var1) { //发送失败可以执行重试 log.info("异步发送失败{}", var1); } }); } }消费者package com.xiaojie.rocket.rocket.consumer; import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; import org.apache.rocketmq.spring.core.RocketMQListener; import org.springframework.stereotype.Component; /** * @Description: * @author: yan * @date: 2021.11.08 */ @RocketMQMessageListener(consumerGroup = "test-group", topic = "xiaojie-test") @Slf4j @Component public class MqConsumer implements RocketMQListener<String> { @Override public void onMessage(String message) { log.info("接收到的数据是:{}", message); } }参考:https://blog.csdn.net/javahongxi/article/details/84931747完整代码参考: spring-boot: Springboot整合redis、消息中间件等相关代码
正文一、Kafka数据存储方式 名词解释Broker:Kafka节点,一个Kafka节点就是一个broker,多个broker可以组成一个Kafka集群Topic:一类消息,消息存放的目录即主题,例如page view日志、click日志等都可以以topic的形式存在,Kafka集群能够同时负责多个topic的分发message: Kafka中最基本的传递对象。Partition:topic物理上的分组,一个topic可以分为多个partition,每个partition是一个有序的队列 每个分区都有一台 server 作为 “leader”,零台或者多台server作为 follwers 。leader server 处理一切对 partition (分区)的读写请求,而follwers只需被动的同步leader上的数据。当leader宕机了,followers 中的一台服务器会自动成为新的 leader。 Replica:副本,为实现备份的功能,保证集群中的某个节点发生故障时,该节点上的 Partition 数据不丢失,且 Kafka 仍然能够继续工作,Kafka 提供了副本机制,一个 Topic 的每个分区都有若干个副本,一个 Leader 和若干个 Follower。Segment:partition物理上由多个segment组成,每个Segment存着message信息Producer : 生产者,生产message发送到topicConsumer : 消费者,订阅topic并消费message, consumer作为一个线程来消费Consumer Group:消费者组,一个Consumer Group包含多个consumerOffset:偏移量,理解为消息partition中的索引即可分区分步示意图创建一个6个分区,3个副本的topic @Bean public NewTopic myTopic() { return new NewTopic("my-topic-partition", 6, (short) 3); }通过ZKtools可知几个分区分步如下。 partition1:{"controller_epoch":4,"leader":1,"version":1,"leader_epoch":0,"isr":[1,3,2]} partition2:{"controller_epoch":4,"leader":2,"version":1,"leader_epoch":0,"isr":[2,1,3]} partition3:{"controller_epoch":4,"leader":3,"version":1,"leader_epoch":0,"isr":[3,2,1]} partition4:{"controller_epoch":4,"leader":1,"version":1,"leader_epoch":0,"isr":[1,2,3]} partition5:{"controller_epoch":4,"leader":2,"version":1,"leader_epoch":0,"isr":[2,3,1]} partition6:{"controller_epoch":4,"leader":3,"version":1,"leader_epoch":0,"isr":[3,1,2]}其中controller_epoch表示的是当前的kafka控制器,leader表示当前分区的leader副本所在的broker的id编号,version表示版本号(当前半本固定位1),leader_epoch表示当前分区的leader纪元,isr表示变更后的isr列表(后面解释什么ISR)。由图可见,每一个Broker都冗余了每个分区的数据。我们称为副本机制。这样有以下优点 提供数据冗余:即使有Broker宕机,系统依然能够继续运转不会丢失数据,因而增加了整体可用性以及数据持久性。提供高伸缩性:支持横向扩展,能够通过添加机器的方式来提升读的性能,进而提高读操作吞吐量。改善数据局部性:允许将数据放入与用户地理位置相近的地方,从而降低系统延时。Kafka数据存放在一个分区中,会将一个大的分区拆分n多个不同小segment文件 ,每个segment文件 存放我们该分区日志消息。在每个segment中会有.index、.log。在默认的情况下,每个segment文件容量最大是为1073741824KB(1024MB),如果超过的情况下依次内推,产生一个新的segment文件,可以通过修改配置文件log.segment.bytes=1073741824修改。00000000000000000000.index-----消息偏移量索引文件00000000000000000000.log-----消息持久化内容 如上图假如第一个分区存放的offset到1000,那么下一个文件的命名从上一个offset位置结束的位置开始。如下图是文件存储的样子总结: 每个分区是由多个segment组成,每个segment由多个index和多个log文件组成,并且是按照一定的顺序存放数据的。命名规则每个segment file也有自己的命名规则,每个名字有20个字符,不够用0填充,每个名字从0开始命名,下一个segment file文件的名字就是,上一个segment file中最后一条消息的索引值。在.index文件中,存储的是key-value格式的,key代表在.log中按顺序开始顺序消费的offset值,value代表该消息的物理消息存放位置。但是在.index中不是对每条消息都做记录,它是每隔一些消息记录一次(稀疏索引),避免占用太多内存。即使消息不在index记录中,在已有的记录中查找,范围也大大缩小了。如何查看Kafka日志和index文件#index ./bin/kafka-run-class.sh kafka.tools.DumpLogSegments --files /kafka/kafka-logs-689fb31d544a/my-topic-partition-1/00000000000000000000.index #log ./bin/kafka-run-class.sh kafka.tools.DumpLogSegments --files /kafka/kafka-logs-689fb31d544a/my-topic-partition-1/00000000000000000000.logIndex文件 Log文件Kafka如何通过offset查找到Message首先根据二分查找法找到对应的segment文件。通过二分查找找到对应的.index索引文件中position的值。通过稀疏索引在log文件中查找对应的message信息。小结:1、topic是逻辑概念,partition是物理概念。2、.log文件存放实际数据,生产者的数据都会追加到.log文件中。3、为防止.log文件过大导致数据定位效率低下,kafka采取了分片(segment)和稀疏索引机制,将partition分为多个segment,分别进行索引。4、.index文件存储大量的索引信息,.log文件存储大量的数据,索引文件中的元数据指向对应数据文件中Message的物理偏移地址。二、Kafka如何确保数据不丢失生产者的ack机制向 Kafka 写数据时,producers 设置 ack 是否提交完成。0:不等待broker返回确认消息,效率高可能丢失数据。 1:leader副本保存成功返回,当leader还没有将数据同步到Follwer宕机,存在丢失数据的可能性。 -1:(all): 所有副本都保存成功返回 设置 “ack = all” 并不能保证所有的副本都写入了消息。注意:默认情况下,当 acks = all 时,只要 ISR 副本同步完成,就会返回消息已经写入。例如,一个 topic 仅仅设置了两个副本,那么只有一个 ISR 副本,那么当设置acks = all时返回写入成功时,剩下了的那个副本数据也可能数据没有写入。消费者的offset commit消费者通过offset commit 来保证数据的不丢失,kafka自己记录了每次消费的offset数值,下次继续消费的时候,会接着上次的offset进行消费。kafka并不像其他消息队列,消费完消息之后,会将数据从队列中删除,而是维护了一个日志文件,通过时间和储存大小进行日志删除策略。默认情况下每隔 5分钟(log.retention.check.interval.ms=300000)会检测一次是否有日志文件需要删除。日志文件会保留log.retention.hours=168小时(7天),当日志文件超过(log.retention.bytes=1073741824)1024MB(与时间保留策略独立)都会进行删除。如果offset没有提交,程序提交之后,会从上次消费的位置继续消费,有可能存在重复消费的情况。Offset Reset 三种模式 earliest(最早):当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,从头开始消费latest(最新的):当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,消费新产生的该分区下的数据none(没有):topic各分区都存在已提交的offset时,从offset后开始消费;只要有一个分区不存在已提交的offset,则抛出异常broker的副本 机制每个broker中的partition我们一般都会设置有replication(副本)的个数,生产者写入的时候首先根据分发策略(有partition按partition,有key按key,都没有轮询)写入到leader中,follower(副本)再跟leader同步数据,这样有了备份,也可以保证消息数据的不丢失。三、Kafka可以支持高吞吐量的原因1、顺序读写:基于磁盘的随机读写确实很慢,但磁盘的顺序读写性能却很高,一些情况下磁盘顺序读写性能甚至要高于内存随机读写。(Kafka的message是不断追加到本地磁盘文件末尾的,而不是随机的写入,这使得Kafka写入吞吐量得到了显著提升 。)2、Page Cache:为了优化读写性能,kafka利用了操作系统本身的page cache,就是利用操作系统自身的内存而不是JVM空间内存,这样做的好处是: a:避免Object消耗:如果是使用java堆,java对象的内存消耗比较大,通常是所存储数据的两倍甚至更多。 b:避免GC问题:随着JVM中数据不断增多,垃圾回收将会变得复杂与缓慢,使用系统缓存就不会存在GC问题。通过操作系统的page cache,kafka的读写操作基本上是基于内存的,读写速度得到了极大的提升。3、零拷贝:(不使用的时候,数据在内核空间和用户空间之间穿梭了两次),使用零拷贝技术后避免了这种拷贝。通过这种 “零拷贝” 的机制,Page Cache 结合 sendfile 方法,Kafka消费端的性能也大幅提升。这也是为什么有时候消费端在不断消费数据时,我们并没有看到磁盘io比较高,此刻正是操作系统缓存在提供数据。4、分区分段+索引:topic 中的数据是按照一个一个的partition即分区存储到不同broker节点的,每个partition对应了操作系统上的一个文件夹,partition实际上又是按照segment分段存储的,这也非常符合分布式系统分区分桶的设计思想。kafka的message消息实际上是分布式存储在一个一个segment中的,每次文件操作也是直接操作的segment。为了进一步的查询优化,kafka又默认为分段后的数据文件建立了索引文件,就是文件系统上的.index文件.这种分区分段+索引的设计,不仅提升了数据读取的效率,同时也提高了数据处理的并行度。5、批量读写:Kafka数据读写也是批量的而不是单条的。在向Kafka写入数据时,可以启用批次写入,这样可以避免在网络上频繁传输单个消息带来的延迟和带宽开销。假设网络带宽为10MB/S,一次性传输10MB的消息比传输1KB的消息10000万次显然要快得多。6、批量压缩:在很多情况下,系统的瓶颈不是CPU或磁盘,而是网络IO,对于需要在广域网上的数据中心之间发送消息的数据流水线尤其如此。进行数据压缩会消耗少量的CPU资源,不过对于kafka而言,网络IO更应该需要考虑。如果每个消息都压缩,但是压缩率相对很低,所以Kafka使用了批量压缩,即将多个消息一起压缩而不是单个消息压缩Kafka允许使用递归的消息集合,批量的消息可以通过压缩的形式传输并且在日志中也可以保持压缩格式,直到被消费者解压缩Kafka支持多种压缩协议,包括Gzip和Snappy压缩协议 Kafka速度的秘诀在于,它把所有的消息都变成一个批量的文件,并且进行合理的批量压缩,减少网络IO损耗,通过mmap提高I/O速度,写入数据的时候由于单个Partion是末尾添加所以速度最优;读取数据的时候配合sendfile直接暴力输出。四、Kafka选举策略 什么是ISR 简单来说,分区中的所有副本统称为 AR (Assigned Replicas)。所有与leader副本保持一定程度同步的副本(包括leader副本在内)组成 ISR (In Sync Replicas)。 ISR 集合是 AR 集合的一个子集。消息会先发送到leader副本,然后follower副本才能从leader中拉取消息进行同步。 同步期间,follow副本相对于leader副本而言会有一定程度的滞后。 “一定程度同步“ 是指可忍受的滞后范围,这个范围可以通过参数进行配置。 于leader副本同步滞后过多的副本(不包括leader副本)将组成 OSR (Out-of-Sync Replied)由此可见,AR = ISR + OSR。 正常情况下,所有的follower副本都应该与leader 副本保持 一定程度的同步,即AR=ISR,OSR集合为空。什么是LEO、LSO、HW、LWLEO:LEO是Log End Offset的缩写,它表示了当前日志文件中下一条待写入消息的offset。上图的LEO分别是8、6、9LSO:Log Stable Offset。这是 Kafka 事务的概念。如果你没有使用到事务,那么这个 值不存在(其实也不是不存在,只是设置成一个无意义的值)。该值控制了事务型消费 者能够看到的消息范围。就是消费者只能消费到事务被提交的消息。HW:分区ISR集合中的每个副本都会维护自身的LEO,而ISR集合中最小的LEO即为分区的HW, 对消费者而言只能消费HW之前的消息,HW之后的消息消费者是消费不到的。LW:Low Watermark的缩写,俗称“低水位”,代表AR集合中最小的logStartOffset值(日志起始位移值)。上图中的LW都是从1开始的。数据更新过程更新记录进入主副本节点处理,为该记录分配Sn(Serial Number),然后将该记录插入prepare list,该list上的记录按照sn有序排列;主副本节点将携带sn的记录发往从节点,从节点同样将该记录插入到prepare list;一旦主节点收到所有从节点的响应,确定该记录已经被正确写入所有的从节点,那就将commit list向前移动,并将这些消息应用到主节点的状态机; 主节点提交后即可给客户端返回响应,同时向所有从节点发送消息,告诉从节点可以提交刚刚写入的记录了。 所有的读需要全部发往主节点,这是因为客户端来读时,主节点有可能尚未将commit消息发送至从,因此,如果读从节点可能会无法获取最新数据。Follwer同步数据首先,Follower 发送 FetchRequest 请求给 Leader。 接着,Leader 会读取底层日志文件中的消 息数据,再更新它内存中的 Follower 副本的 LEO 值,更新为 FetchRequest 请求中的 fetchOffset 值。 最后,尝试更新分区高水位值(HW )。Follower 接收到 FETCH 响应之后,会把 消息写入到底层日志,接着更新 LEO 和 HW 值。Kafaka的复制机制不是完全的同步复制,也不是单纯的异步复制,事实上, 同步复制要求所有能工作的Follower副本都复制完,这条消息才会被确认为成功提交, 这种复制方式影响了性能。而在异步复制的情况下, follower副本异步地从leader副本中复制数据, 数据只要被leader副本写入就被认为已经成功提交。在这种情况下,如果follower副本都没有复制完而落后于leader副本, 如果突然leader副本宕机,则会造成数据丢失。Kafka正是使用这种ISR的方式有效的权衡了数据可靠性与性能之间的关系。分区 Leader故障转移&选举策略Kafka会选择一个 broker 作为 “controller”节点。 controller 节点负责 检测 brokers 级别故障,并负责在 broker 故障的情况下更改这个故障 Broker 中的 partition 的 leadership 。 这种方式可以批量的通知主从关系的变化,使得对于拥有大量partition 的broker ,选举过程的代价更低并且速度更快。 如果 controller 节点挂了,其他存活的 broker 都可能成为新的 controller 节点。Kafka的选举策略大致分一下 几种情况OfflinePartition Leader 选举:每当有分区上线时,就需要执行 Leader 选举。 所谓的分区上线,可能是创建了新分区,也可能是之前的下线分区重新上线。这是最常见的分区 Leader 选举场景。ReassignPartition Leader 选举:当你手动运行 kafka-reassign-partitions 命令,或者是调用 Admin 的 alterPartitionReassignments 方法执行分区副本重分配时, 可能触发此类选举。假设原来的 AR 是[1,2,3],Leader 是 1,当执行副本重分配后,副本集 合 AR 被设置成[4,5,6],显然, Leader 必须要变更,此时会发生 Reassign Partition Leader 选举。PreferredReplicaPartition Leader 选举:当你手动运行 kafka-preferred-replica- election 命令,或自动触发了 Preferred Leader 选举时,该类策略被激活。 所谓的 Preferred Leader,指的是 AR 中的第一个副本。比如 AR 是[3,2,1],那么, Preferred Leader 就是 3。ControlledShutdownPartition Leader 选举:当 Broker 正常关闭时,该 Broker 上 的所有 Leader 副本都会下线,因此,需要为受影响的分区执行相应的 Leader 选举。这 4 类选举策略的大致思想是类似的,即从 AR 中挑选首个在 ISR 中的副本,作为新 Leader。参考:https://blog.csdn.net/sillyzhangye/article/details/86181345https://blog.csdn.net/qq_26838315/article/details/106883256https://www.cnblogs.com/18800105616a/p/13863254.html
正文一、什么是kafka Kafka是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者在网站中的所有动作流数据。 这种动作(网页浏览,搜索和其他用户的行动)是在现代网络上的许多社会功能的一个关键因素。这些数据通常是由于吞吐量的要求而通过处理日志和日志聚合来解决。对于像Hadoop一样的日志数据和离线分析系统,但又要求实时处理的限制,这是一个可行的解决方案。Kafka的目的是通过Hadoop的并行加载机制来统一线上和离线的消息处理,也是为了通过集群来提供实时的消息。——来自百度百科二、卡夫卡安装传统方式下载地址:Apache Download Mirrors安装环境:1、Java8+ ,参考Linux系统下安装jdk17&jdk8安装2、安装ZK,参考搭建ZooKeeper3.7.0集群(传统方式&Docker方式)3、解压文件[root@localhost ~]# tar -zxvf kafka_2.13-3.0.0.tgz4、移动到/usr/local/kafka[root@localhost ~]# mv kafka_2.13-3.0.0 /usr/local/kafka5、修改kafka配置文件[root@localhost config]# vim server.propertiesbroker1broker.id=0 #监听 listeners=PLAINTEXT://192.168.139.155:9092 #zk地址 zookeeper.connect=192.168.139.155:2181, 192.168.139.155:2182, 192.168.139.155:2183broker2broker.id=1 #监听 listeners=PLAINTEXT://192.168.139.156:9092 #zk地址 zookeeper.connect=192.168.139.155:2181, 192.168.139.155:2182, 192.168.139.155:2183broker3broker.id=2 #监听 listeners=PLAINTEXT://192.168.139.157:9094 #zk地址 zookeeper.connect=192.168.139.155:2181, 192.168.139.155:2182, 192.168.139.155:21836、分别启动kafka[root@localhost kafka]# ./bin/kafka-server-start.sh -daemon config/server.properties7、在其中一台创建topic[root@localhost kafka]# ./bin/kafka-topics.sh --bootstrap-server 192.168.139.155:9092 --create --topic test-topic --partitions 3 --replication-factor 3 通过zk的可视化工具可知,分区已经创建完成。 8、测试发送消息[root@localhost kafka]# ./bin/kafka-console-producer.sh --topic test-topic --bootstrap-server 192.168.139.155:9092消费消息 在另一台broker上接收消息[root@localhost kafka]# ./bin/kafka-console-consumer.sh --topic test-topic --from-beginning --bootstrap-server 192.168.139.156:9092docker方式安装1、拉取镜像[root@localhost ~]# docker pull wurstmeister/kafka2、安装Broker1docker run -d --name kafka1 \ -p 9092:9092 \ -e KAFKA_BROKER_ID=1 \ -e KAFKA_ZOOKEEPER_CONNECT=192.168.139.155:2181,192.168.139.155:2182,192.168.139.155:2183 \ -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://192.168.139.155:9092 \ -e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 -t wurstmeister/kafkaBroker2docker run -d --name kafka2 \ -p 9093:9093 \ -e KAFKA_BROKER_ID=2 \ -e KAFKA_ZOOKEEPER_CONNECT=192.168.139.155:2181,192.168.139.155:2182,192.168.139.155:2183 \ -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://192.168.139.155:9093 \ -e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9093 -t wurstmeister/kafkaBroker3docker run -d --name kafka3 \ -p 9094:9094 \ -e KAFKA_BROKER_ID=3 \ -e KAFKA_ZOOKEEPER_CONNECT=192.168.139.155:2181,192.168.139.155:2182,192.168.139.155:2183 \ -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://192.168.139.155:9094 \ -e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9094 -t wurstmeister/kafka3、测试进入容器[root@bogon ~]# docker container exec -it 2d4be3823f16 /bin/bash 进入/opt/kafka_2.13-2.7.1/bin目录创建topicbash-5.1# ./kafka-topics.sh --bootstrap-server 192.168.139.155:9092 --create --topic my-topic --partitions 3 --replication-factor 3创建消息bash-5.1# ./kafka-console-producer.sh --topic my-topic --bootstrap-server 192.168.139.155:9092 消费者 ,进入另一个容器进行消费bash-5.1# ./kafka-console-consumer.sh --topic my-topic --from-beginning --bootstrap-server 192.168.139.155:9093三、整合SpringBoot一般模式消费默认情况下是自动提交offset值。可通过consumer下的属性配置enable-auto-commit: false 生产者 public void sendMSg(){ System.out.println(">>>>>>>>>>>>>>>>>"); for (int i=0;i<5;i++){ kafkaTemplate.send("xiaojie-topic","test message>>>>>>>>>>>>>>>>>>>>>>"+i); } }消费者 @KafkaListener(groupId = "xiaojie_group",topics = {"xiaojie-topic"}) public void onMessage(ConsumerRecord<?, ?> record) { log.info("消费主题>>>>>>{},消费分区>>>>>>>>{},消费偏移量>>>>>{},消息内容>>>>>{}", record.topic(), record.partition(), record.offset(), record.value()); }生产者回调模式生产者回调函数,可以确认消息是否成功发送到broker,发送失败,进行重试或者人工补偿措施,确保消息投递到broker。有以下两种方式方式1:public void sendMsgCallback(String callbackMessage){ kafkaTemplate.send("callback-topic","xiaojie_key",callbackMessage).addCallback(success -> { //当消息发送成功的回调函数 // 消息发送到的topic String topic = success.getRecordMetadata().topic(); // 消息发送到的分区 int partition = success.getRecordMetadata().partition(); // 消息在分区内的offset long offset = success.getRecordMetadata().offset(); System.out.println("发送消息成功>>>>>>>>>>>>>>>>>>>>>>>>>" + topic + "-" + partition + "-" + offset); }, failure -> { //消息发送失败的回调函数 System.out.println("消息发送失败,可以进行人工补偿"); }); }方式2public void sendMsgCallback1(String callbackMessage){ kafkaTemplate.send("callback-topic","xiaojie_key",callbackMessage).addCallback(new ListenableFutureCallback<SendResult<String, String>>() { @Override public void onFailure(Throwable ex) { //发送失败 System.out.println("发送失败。。。。。。。。。。。"); } @Override public void onSuccess(SendResult<String, String> result) { //分区信息 Integer partition = result.getRecordMetadata().partition(); //主题 String topic=result.getProducerRecord().topic(); String key=result.getProducerRecord().key(); //发送成功 System.out.println("发送成功。。。。。。。。。。。分区为:"+partition+",主题topic:"+topic+",key:"+key); } }); }Kafka事务应用场景最简单的需求是producer发的多条消息组成一个事务这些消息需要对consumer同时可见或者同时不可见 。producer可能会给多个topic,多个partition发消息,这些消息也需要能放在一个事务里面,这就形成了一个典型的分布式事务。kafka的应用场景经常是应用先消费一个topic,然后做处理再发到另一个topic,这个consume-transform-produce过程需要放到一个事务里面,比如在消息处理或者发送的过程中如果失败了,消费位点也不能提交。producer或者producer所在的应用可能会挂掉,新的producer启动以后需要知道怎么处理之前未完成的事务 。流式处理的拓扑可能会比较深,如果下游只有等上游消息事务提交以后才能读到,可能会导致rt非常长吞吐量也随之下降很多,所以需要实现read committed和read uncommitted两种事务隔离级别在spring.kafka.producer.transaction-id-prefix: tx #开启事务管理注意:此时重试retries不能为0,acks=-1或者all /** * @description: kafak事务提交 本地事务不需要事务管理器 * @param: * @return: void * @author xiaojie * @date: 2021/10/14 21:35 */ public void sendTx(){ kafkaTemplate.executeInTransaction(kafkaOperations -> { String msg="这是一条测试事务的数据......"; kafkaOperations.send("tx-topic",msg); int i=1/0; //报错之后,由于事务存在,消息并不会发送到broker return null; }); } 消费者批量消费消息消费者批量消费消息,如果此时开启批量消费模式,那么同样的topic,消费者将会进行批量消费,不再进行逐条消费。 消费者手动确认Kafak并不会像rabbitmq那样,消息消费之后,会将消息从队列中删除,Kafka通常根据时间决定数据可以保留多久。默认使用log.retention.hours参数配置时间,默认值是168小时,也就是一周。除此之外,还有其他两个参数,log.retention.minutes和log.retention.ms,这三个参数作用是一样的,都是决定消息多久以会被删除,不过还是推荐使用log.retention.ms,如果指定了不止一个参数,Kafka会优先使用最小值的那个参数。卡夫卡是以offset的位置进行消费,如果不进行,确认那么消费者下次消费的时候,还会从上次消费的位置进行消费。修改消费者自动提交为false:enable-auto-commit: false配置工厂类/* RECORD,当每一条记录被消费者监听器(ListenerConsumer)处理之后提交 BATCH,当每一批记录被消费者监听器(ListenerConsumer)处理之后提交 TIME, 每隔多长时间提交,超过该时间会自动提交 COUNT, 每次提交的数量,超过该数量自动提交 COUNT_TIME, 满足时间和数量的任何一个条件提交 MANUAL_IMMEDIATE MANUAL */ @Bean("manualListenerContainerFactory") public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<String, String>> manualListenerContainerFactory( ConsumerFactory<String, String> consumerFactory) { ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>(); factory.setConsumerFactory(consumerFactory); factory.getContainerProperties().setPollTimeout(1500); factory.setBatchListener(true); //设置批量为true,那么消费端就要一批量的形式接收信息 //配置手动提交offset factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL); return factory; }消费者 /* * * @param message * @param ack * @手动提交ack * containerFactory 手动提交消息ack * errorHandler 消费端异常处理器 * @author xiaojie * @date 2021/10/14 * @return void */ @KafkaListener(containerFactory = "manualListenerContainerFactory", topics = "xiaojie-topic", errorHandler = "consumerAwareListenerErrorHandler" ) public void onMessageManual(List<ConsumerRecord<?, ?>> record, Acknowledgment ack) { for (int i=0;i<record.size();i++){ System.out.println(record.get(i).value()); } ack.acknowledge();//直接提交offset } 指定消费 /** * @description: id:消费者ID; * groupId:消费组ID; * topics:监听的topic,可监听多个; topics不能和topicPartitions同时使用 * topicPartitions:可配置更加详细的监听信息,可指定topic、parition、offset监听。 * @param: * @param: record * @return: void * @author xiaojie * @date: 2021/10/14 21:50 */ @KafkaListener(groupId = "xiaojie_group",topicPartitions = { @TopicPartition(topic = "test-topic", partitions = {"1"}), @TopicPartition(topic = "xiaojie-test-topic", partitions = {"1"}, partitionOffsets = @PartitionOffset(partition = "2", initialOffset = "15")) }) public void onMessage1(ConsumerRecord<?, ?> record) { //指定消费某个topic,的某个分区,指定消费位置 //执行消费xiaojie-test-topic的1号分区,和xiaojie-test-topic的1和2号分区,并且2号分区从15开始消费 log.info("消费主题>>>>>>:{},消费分区>>>>>>>>:{},消费偏移量>>>>>:{},消息内容>>>>>:{}", record.topic(), record.partition(), record.offset(), record.value()); } 指定自定义分区器我们知道,kafka中每个topic被划分为多个分区,那么生产者将消息发送到topic时,具体追加到哪个分区呢?这就是所谓的分区策略,Kafka 为我们提供了默认的分区策略,同时它也支持自定义分区策略。其路由机制为:1、若发送消息时指定了分区(即自定义分区策略),则直接将消息append到指定分区;2、若发送消息时未指定 patition,但指定了 key(kafka允许为每条消息设置一个key),则对key值进行hash计算,根据计算结果路由到指定分区,这种情况下可以保证同一个 Key 的所有消息都进入到相同的分区;这种方式可以解决消息顺序消费3、patition 和 key 都未指定,则使用kafka默认的分区策略,轮询选出一个 patition;package com.xiaojie.config; import org.apache.kafka.clients.producer.Partitioner; import org.apache.kafka.common.Cluster; import org.springframework.stereotype.Component; import java.util.Map; /** * @Description:自定义分区器 我们知道,kafka中每个topic被划分为多个分区,那么生产者将消息发送到topic时,具体追加到哪个分区呢?这就是所谓的分区策略,Kafka 为我们提供了默认的分区策略,同时它也支持自定义分区策略。其路由机制为: * 若发送消息时指定了分区(即自定义分区策略),则直接将消息append到指定分区; * 若发送消息时未指定 patition,但指定了 key(kafka允许为每条消息设置一个key),则对key值进行hash计算,根据计算结果路由到指定分区, * 这种情况下可以保证同一个 Key 的所有消息都进入到相同的分区;这种方式可以解决消息顺序消费 * patition 和 key 都未指定,则使用kafka默认的分区策略,轮询选出一个 patition; * @author: xiaojie * @date: 2021.10.14 */ @Component public class CustomizePartitioner implements Partitioner { @Override public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) { //计算分区器 System.out.println("key>>>>>>>>>>>>>"+key); if ("weixin".equals(key)&&"test-topic".equals(topic)){ return 1; } return 0; } @Override public void close() { } @Override public void configure(Map<String, ?> configs) { } } 消费端异常处理package com.xiaojie.config; import org.springframework.context.annotation.Bean; import org.springframework.kafka.listener.ConsumerAwareListenerErrorHandler; import org.springframework.stereotype.Component; /** * @author xiaojie * @version 1.0 * @description:通过异常处理器,我们可以处理consumer在消费时发生的异常。 * 将这个异常处理器的BeanName放到@KafkaListener注解的errorHandler属性里面 * @date 2021/10/14 21:56 */ @Component public class MyErrorHandler { @Bean ConsumerAwareListenerErrorHandler consumerAwareListenerErrorHandler(){ return (message, e, consumer) -> { System.out.println("消息消费异常"+message.getPayload()); System.out.println("异常信息>>>>>>>>>>>>>>>>>"+e); return null; }; } }使用方法 /* * * @param message * @param ack * @手动提交ack * containerFactory 手动提交消息ack * errorHandler 消费端异常处理器 * @author xiaojie * @date 2021/10/14 * @return void */ @KafkaListener(containerFactory = "manualListenerContainerFactory", topics = "xiaojie-topic", errorHandler = "consumerAwareListenerErrorHandler" ) public void onMessageManual(List<ConsumerRecord<?, ?>> record, Acknowledgment ack) { for (int i=0;i<record.size();i++){ System.out.println(record.get(i).value()); } ack.acknowledge();//直接提交offset } 消息过滤器 @Bean("filterFactory") public ConcurrentKafkaListenerContainerFactory filterFactory(ConsumerFactory<String, String> consumerFactory) { ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory(); factory.setConsumerFactory(consumerFactory); factory.setAckDiscarded(true); factory.setRecordFilterStrategy(consumerRecord -> { String value = (String) consumerRecord.value(); if (value.contains("hello")) { //返回false消息没有被过滤继续消费 return false; } System.out.println("...................."); //返回true 消息被过滤掉了 return true; }); return factory; } 使用方法 /** * @description: 消费者过滤器 * @param: * @param: record * @return: void * @author xiaojie * @date: 2021/10/16 1:04 */ @KafkaListener(topics = "filter-topic",containerFactory = "filterFactory") public void filterOnmessage(ConsumerRecord<?,?> record){ log.info("消费到的消息是:》》》》》》》》》》》{}",record.value()); }完整代码请参考,kafka部分:spring-boot: Springboot整合redis、消息中间件等相关代码 参考:SpringBoot集成kafka全面实战_Felix-CSDN博客
正文一、分布式事务请参考之前的文章二、思路原理 当派单系统派单成功之后,订单系统报错,此时将会产生分布式事务的问题,派单数据生成,但此时订单数据异常事务回滚,就发生了分布式事务问题。此时解决分布式事务,生成一个订单的消费者,专门去消费生成订单异常时的一个程序,我们称之为补单系统。三、代码订单派单package com.xiaojie.service.impl; import com.alibaba.fastjson.JSONObject; import com.xiaojie.entity.Order; import com.xiaojie.mapper.OrderMapper; import com.xiaojie.service.OrderService; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.connection.CorrelationData; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.UUID; /** * @author xiaojie * @version 1.0 * @description: * @date 2021/10/11 22:09 */ @Service @Slf4j public class OrderServiceImpl implements OrderService, RabbitTemplate.ConfirmCallback { //定义交换机 private static final String XIAOJIE_ORDER_EXCHANGE = "xiaojie_order_exchange"; @Autowired private OrderMapper orderMapper; @Autowired private RabbitTemplate rabbitTemplate; @Override @Transactional public String saveOrder() { Order order = new Order(); String orderId = UUID.randomUUID().toString(); order.setOrderId(orderId); order.setOrderName("小谷姐姐麻辣烫"); order.setPayMoney(35.68); order.setStatus(1);//假设订单支付完成 int result = orderMapper.addOrder(order); if (result < 0) { return "下单失败"; } //发送派单 String orderJson = JSONObject.toJSONString(order); sendDispatchMsg(orderJson); //模拟报错 int i = 1 / 0; return orderId; } @Async public void sendDispatchMsg(String jsonMSg) { // 设置生产者消息确认机制 this.rabbitTemplate.setMandatory(true); this.rabbitTemplate.setConfirmCallback(this); CorrelationData correlationData = new CorrelationData(); correlationData.setId(jsonMSg); //将订单数据发送 rabbitTemplate.convertAndSend(XIAOJIE_ORDER_EXCHANGE, "", jsonMSg, correlationData); } @Override public void confirm(CorrelationData correlationData, boolean ack, String s) { if (ack) { log.info(">>>>>>>>消息发送成功:correlationData:{},ack:{},s:{}", correlationData, ack, s); } else { log.info(">>>>>>>消息发送失败{}", ack); } } }补单系统消费端package com.xiaojie.consumer; import com.alibaba.fastjson.JSONObject; import com.rabbitmq.client.Channel; import com.xiaojie.entity.Order; import com.xiaojie.mapper.OrderMapper; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.IOException; /** * @author xiaojie * @version 1.0 * @description: 补单消费者 * @date 2021/10/11 22:37 */ @Component public class OrderConsumer { @Autowired private OrderMapper orderMapper; @RabbitListener(queues = {"xiaojie_order_queue"}) /** * @description: 补单消费者,补偿分布式事务解决框架 数据最终一致性 * @param: * @param: message * @param: channel * @return: void * @author xiaojie * @date: 2021/10/11 22:41 */ public void compensateOrder(Message message, Channel channel) throws IOException { // 1.获取消息 String msg = new String(message.getBody()); // 2.获取order对象 Order orderEntity = JSONObject.parseObject(msg, Order.class); //根据订单号查询订单是否存在 Order dbOrder = orderMapper.getOrder(orderEntity.getOrderId()); if (dbOrder != null) { // 手动ack丢弃消息 channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); return; } //订单没有生成,开始补单 int result = orderMapper.addOrder(orderEntity); if (result > 0) { // 手动ack 删除该消息 channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); } } }派单消费者package com.xiaojie.consumer; import com.alibaba.fastjson.JSONObject; import com.rabbitmq.client.Channel; import com.xiaojie.entity.Dispatch; import com.xiaojie.mapper.DispatchMapper; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.IOException; /** * @author xiaojie * @version 1.0 * @description: 派单消费者 * @date 2021/10/11 22:58 */ @Component public class DispatchConsumer { @Autowired private DispatchMapper dispatchMapper; @RabbitListener(queues = "dispatch_order_queue") public void dispatchConsumer(Message message, Channel channel) throws IOException { // 1.获取消息 String msg = new String(message.getBody()); // 2.转换json JSONObject jsonObject = JSONObject.parseObject(msg); String orderId = jsonObject.getString("orderId"); // 计算分配的快递员id Dispatch dispatch=new Dispatch(); dispatch.setOrderId(orderId); //经过一系列的算法得到送餐时间为30分钟 dispatch.setSendTime(30*60L); dispatch.setRiderId(1000012L); dispatch.setUserId(15672L); // 3.插入我们的数据库 int result = dispatchMapper.saveDispatch(dispatch); if (result > 0) { // 手动ack 删除该消息 channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); } } }完整代码参考:项目中mq-transaction子模块spring-boot: Springboot整合redis、消息中间件等相关代码
正文一、消息幂等性在编程中一个幂等操作的特点是其任意多次执行所产生的结果与一次执行的产生的结果相同,在mq中由于网络故障或客户端延迟消费mq自动重试过程中可能会导致消息的重复消费,那我们如何保证消息的幂等问题呢?也可以理解为如何保证消息不被重复消费呢,不重复消费也就解决了幂等问题。二、解决方案1、生成全局id,存入redis或者数据库,在消费者消费消息之前,查询一下该消息是否有消费过。2、如果该消息已经消费过,则告诉mq消息已经消费,将该消息丢弃(手动ack)。3、如果没有消费过,将该消息进行消费并将消费记录写进redis或者数据库中。注:还有一种方式,数据库操作可以设置唯一键(消息id),防止重复数据的插入,这样插入只会报错而不会插入重复数据,本人没有测试。三、代码简单描述一下需求,如果订单完成之后,需要为用户累加积分,又需要保证积分不会重复累加。那么再mq消费消息之前,先去数据库查询该消息是否已经消费,如果已经消费那么直接丢弃消息。如果是Redis存放数据key=全局id,value=积分值,在消费消息之前,通过全局id去redis查询是否有该数据,如果有直接丢弃。该方法本人没有测试,只是说说自己的思路。有不对的希望大佬们不吝赐教。生产者package com.xiaojie.score.producer; import com.alibaba.fastjson.JSONObject; import com.xiaojie.score.entity.Score; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.connection.CorrelationData; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import java.util.UUID; /** * @author xiaojie * @version 1.0 * @description:发送积分消息的生产者 * @date 2021/10/10 22:18 */ @Component @Slf4j public class ScoreProducer implements RabbitTemplate.ConfirmCallback { @Autowired private RabbitTemplate rabbitTemplate; //定义交换机 private static final String SCORE_EXCHANGE = "xiaojie_score_exchaneg"; //定义路由键 private static final String SCORE_ROUTINNGKEY = "score.add"; /** * @description: 订单完成 * @param: * @return: java.lang.String * @author xiaojie * @date: 2021/10/10 22:30 */ public String completeOrder() { String orderId = UUID.randomUUID().toString(); System.out.println("订单已完成"); //发送积分通知 Score score = new Score(); score.setScore(100); score.setOrderId(orderId); String jsonMSg = JSONObject.toJSONString(score); sendScoreMsg(jsonMSg, orderId); return orderId; } /** * @description: 发送积分消息 * @param: * @param: message * @param: orderId * @return: void * @author xiaojie * @date: 2021/10/10 22:22 */ @Async public void sendScoreMsg(String jsonMSg, String orderId) { this.rabbitTemplate.setConfirmCallback(this); rabbitTemplate.convertAndSend(SCORE_EXCHANGE, SCORE_ROUTINNGKEY, jsonMSg, message -> { //设置消息的id为唯一 message.getMessageProperties().setMessageId(orderId); return message; }); } @Override public void confirm(CorrelationData correlationData, boolean ack, String s) { if (ack) { log.info(">>>>>>>>消息发送成功:correlationData:{},ack:{},s:{}", correlationData, ack, s); } else { log.info(">>>>>>>消息发送失败{}", ack); } } }消费者package com.xiaojie.score.consumer; import com.alibaba.fastjson.JSONObject; import com.rabbitmq.client.Channel; import com.xiaojie.score.entity.Score; import com.xiaojie.score.mapper.ScoreMapper; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.handler.annotation.Headers; import org.springframework.stereotype.Component; import java.io.IOException; import java.util.Map; /** * @author xiaojie * @version 1.0 * @description: 积分的消费者 * @date 2021/10/10 22:37 */ @Component @Slf4j public class ScoreConsumer { @Autowired private ScoreMapper scoreMapper; @RabbitListener(queues = {"xiaojie_score_queue"}) public void onMessage(Message message, @Headers Map<String, Object> headers, Channel channel) throws IOException { String orderId = message.getMessageProperties().getMessageId(); if (StringUtils.isBlank(orderId)) { return; } log.info(">>>>>>>>消息id是:{}", orderId); String msg = new String(message.getBody()); Score score = JSONObject.parseObject(msg, Score.class); if (score == null) { return; } //执行前去数据库查询,是否存在该数据,存在说明已经消费成功,不存在就去添加数据,添加成功丢弃消息 Score dbScore = scoreMapper.selectByOrderId(orderId); if (dbScore != null) { //证明已经消费消息,告诉mq已经消费,丢弃消息 channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); return; } Integer result = scoreMapper.save(score); if (result > 0) { //积分已经累加,删除消息 channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); return; } else { log.info("消费失败,采取相应的人工补偿"); } } }完整代码:spring-boot: Springboot整合redis、消息中间件等相关代码
正文一、死信队列DLX(dead-letter-exchange),死信队列也是一般的队列,当消息变成死信时,消息会投递到死信队列中,经过死信队列进行消费的一种形式,对应的交换机叫死信交换机DLX。二、产生原因1、当消息投递到mq后,没有消费者去消费,而消息过期后会进入死信队列。package com.xiaojie.springboot.config; import org.springframework.amqp.core.*; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.Map; /** * @author xiaojie * @version 1.0 * @description:死信队列配置 * @date 2021/10/8 21:07 */ @Component public class DLXConfig { //定义队列 private static final String MY_DIRECT_QUEUE = "snail_direct_queue"; //定义队列 private static final String MY_DIRECT_DLX_QUEUE = "xiaojie_direct_dlx_queue"; //定义死信交换机 private static final String MY_DIRECT_DLX_EXCHANGE = "xiaojie_direct_dlx_exchange"; //定义交换机 private static final String MY_DIRECT_EXCHANGE = "snail_direct_exchange"; //死信路由键 private static final String DIRECT_DLX_ROUTING_KEY = "msg.dlx"; //绑定死信队列 @Bean public Queue dlxQueue() { return new Queue(MY_DIRECT_DLX_QUEUE); } //绑定死信交换机 @Bean public DirectExchange dlxExchange() { return new DirectExchange(MY_DIRECT_DLX_EXCHANGE); } @Bean public Queue snailQueue() { Map<String, Object> args = new HashMap<>(2); // 绑定我们的死信交换机 args.put("x-dead-letter-exchange", MY_DIRECT_DLX_EXCHANGE); // 绑定我们的路由key args.put("x-dead-letter-routing-key", DIRECT_DLX_ROUTING_KEY); return new Queue(MY_DIRECT_QUEUE, true, false, false, args); } @Bean public DirectExchange snailExchange() { return new DirectExchange(MY_DIRECT_EXCHANGE); } //绑定队列到交换机 @Bean public Binding snailBindingExchange(Queue snailQueue, DirectExchange snailExchange) { return BindingBuilder.bind(snailQueue).to(snailExchange).with("msg.send"); } //绑定死信队列到死信交换机 @Bean public Binding dlxBindingExchange(Queue dlxQueue, DirectExchange dlxExchange) { return BindingBuilder.bind(dlxQueue).to(dlxExchange).with(DIRECT_DLX_ROUTING_KEY); } }生产者产生消息后,并没与消费者去消费,等待消息过期后,自动进入死信队列public class DLXProvider { //定义交换机 private static final String MY_DIRECT_EXCHANGE = "snail_direct_exchange"; //普通队列路由键 private static final String DIRECT_ROUTING_KEY = "msg.send"; @Autowired private RabbitTemplate rabbitTemplate; public String sendDlxMsg(){ String msg="我是模拟死信队列的消息。。。。。"; rabbitTemplate.convertAndSend(MY_DIRECT_EXCHANGE, DIRECT_ROUTING_KEY, msg, (message) -> { //设置有效时间,如果消息不被消费,进入死信队列 message.getMessageProperties().setExpiration("10000"); return message; }); return "success"; } }2、当队列满了之后 @Bean public Queue snailQueue() { Map<String, Object> args = new HashMap<>(2); // 绑定我们的死信交换机 args.put("x-dead-letter-exchange", MY_DIRECT_DLX_EXCHANGE); // 绑定我们的路由key args.put("x-dead-letter-routing-key", DIRECT_DLX_ROUTING_KEY); // args.put("x-message-ttl", 5000); //为队列设置过期时间 // x-max-length:队列最大容纳消息条数,大于该值,mq拒绝接受消息,消息进入死信队列 args.put("x-max-length", 5); return new Queue(MY_DIRECT_QUEUE, true, false, false, args); }注意:如果在添加了这一条(队列长度)发生异常时,请删除掉交换机和队列后,重新启动程序,重新进行绑定。3、消费者拒绝消费消息(消费端发生异常,mq无法收到消费端的ack)package com.xiaojie.springboot.consumer; import com.rabbitmq.client.Channel; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.support.AmqpHeaders; import org.springframework.messaging.handler.annotation.Headers; import org.springframework.stereotype.Component; import java.util.Map; /** * @Description: 消费snail消息的消费者 * @author: xiaojie * @date: 2021.10.09 */ @Component @Slf4j public class SnailConsumer { @RabbitListener(queues = "snail_direct_queue") public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception { // 获取消息Id String messageId = message.getMessageProperties().getMessageId(); String msg = new String(message.getBody(), "UTF-8"); log.info("获取到的消息>>>>>>>{},消息id>>>>>>{}", msg, messageId); try { int result = 1 / 0; System.out.println("result" + result); // // 手动ack Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG); // 手动签收 channel.basicAck(deliveryTag, false); } catch (Exception e) { //拒绝消费消息(丢失消息) 给死信队列 channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false); } } }三、解决订单超时 代码实现绑定订单死信队列package com.xiaojie.springboot.config; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.DirectExchange; import org.springframework.amqp.core.Queue; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.Map; /** * @author xiaojie * @version 1.0 * @description:解决订单超时未支付问题,绑定订单死信队列 * @date 2021/10/8 23:12 */ @Component public class OrderDlxConfig { @Value(value="${xiaojie.order.queue}") private String orderQueue; //订单队列 @Value(value="${xiaojie.order.exchange}") private String orderExchange;//订单队列 @Value(value="${xiaojie.dlx.queue}") private String orderDeadQueue;//订单死信队列 @Value(value="${xiaojie.dlx.exchange}") private String orderDeadExChange;//订单死信交换机 @Value(value="${xiaojie.order.routingKey}") private String orderRoutingKey;//订单路由键 @Value(value="${xiaojie.dlx.routingKey}") private String orderDeadRoutingKey;//死信队列路由键 @Bean public Queue orderQueue(){ Map<String, Object> args = new HashMap<>(2); // 绑定我们的死信交换机 args.put("x-dead-letter-exchange", orderDeadExChange); // 绑定我们的路由key args.put("x-dead-letter-routing-key", orderDeadRoutingKey); return new Queue(orderQueue, true, false, false, args); } @Bean public Queue orderDeadQueue(){ return new Queue(orderDeadQueue); } //绑定交换机 @Bean public DirectExchange orderExchange(){ return new DirectExchange(orderExchange); } @Bean public DirectExchange orderDeadExchange(){ return new DirectExchange(orderDeadExChange); } //绑定路由键 @Bean public Binding orderBindingExchange(Queue orderQueue, DirectExchange orderExchange) { return BindingBuilder.bind(orderQueue).to(orderExchange).with(orderRoutingKey); } //绑定死信队列到死信交换机 @Bean public Binding deadBindingExchange(Queue orderDeadQueue, DirectExchange orderDeadExchange) { return BindingBuilder.bind(orderDeadQueue).to(orderDeadExchange).with(orderDeadRoutingKey); } }创建订单完成之后,发送消息package com.xiaojie.springboot.service.impl; import com.alibaba.fastjson.JSONObject; import com.xiaojie.springboot.entity.Order; import com.xiaojie.springboot.mapper.OrderMapper; import com.xiaojie.springboot.service.OrderService; import org.springframework.amqp.AmqpException; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessagePostProcessor; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import java.util.UUID; /** * @author xiaojie * @version 1.0 * @description: 订单实现类 * @date 2021/10/8 22:16 */ @Service public class OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; @Autowired private RabbitTemplate rabbitTemplate; @Value(value = "${xiaojie.order.exchange}") private String orderExchange; @Value(value = "${xiaojie.order.routingKey}") private String orderRoutingKey; @Override public String saveOrder(Order order) { String orderId = UUID.randomUUID().toString(); order.setOrderId(orderId); order.setOrderName("test"); order.setPayMoney(3000D); Integer result = orderMapper.addOrder(order); if (result > 0) { String msg = JSONObject.toJSONString(order); //发送mq sendMsg(msg, orderId); return "success"; } return "fail"; } /** * @description: 发送mq消息 * @param: * @param: msg * @param: orderId * @return: void * @author xiaojie * @date: 2021/10/8 22:33 */ @Async //异步线程发送 ,此处需要单独创建一个类去创建该方法,不然该异步线程可能不会生效 public void sendMsg(String msg, String orderId) { rabbitTemplate.convertAndSend(orderExchange, orderRoutingKey, msg, new MessagePostProcessor() { @Override public Message postProcessMessage(Message message) throws AmqpException { //设置过期时间30s message.getMessageProperties().setExpiration("30000"); // message.getMessageProperties().setMessageId(orderId); return message; } }); } @Override public Order getByOrderId(String orderId) { return orderMapper.getOrder(orderId); } @Override public Integer updateOrderStatus(String orderId) { return orderMapper.updateOrder(orderId); } }死信队列消费者package com.xiaojie.springboot.service; import com.alibaba.fastjson.JSONObject; import com.rabbitmq.client.Channel; import com.xiaojie.springboot.entity.Order; import com.xiaojie.springboot.myenum.OrderStatus; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.core.ExchangeTypes; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.*; import org.springframework.amqp.support.AmqpHeaders; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.handler.annotation.Headers; import org.springframework.messaging.handler.annotation.Payload; import org.springframework.stereotype.Component; import java.io.IOException; import java.util.Map; /** * @author xiaojie * @version 1.0 * @description: 死信队列解决订单超时问题 * @date 2021/10/8 22:43 */ @Component @RabbitListener(bindings = @QueueBinding( value = @Queue("xiaojie_order_dlx_queue"), exchange = @Exchange(value = "xiaojie_order_dlx_exchange", type = ExchangeTypes.DIRECT), key = "order.dlx")) @Slf4j public class Consumer { @Autowired private OrderService orderService; /* * @param msg * @param headers * @param channel * @死信队列消费消息,如果订单状态是未支付,则修改订单状态 * @author xiaojie * @date 2021/10/9 13:49 * @return void */ @RabbitHandler public void handlerMsg(@Payload String msg, @Headers Map<String, Object> headers, Channel channel) throws IOException { log.info("接收到的消息是direct:{}" + msg); try { Order orderEntity = JSONObject.parseObject(msg, Order.class); if (orderEntity == null) { return; } // 根据订单号码查询该笔订单是否存在 Order order = orderService.getByOrderId(orderEntity.getOrderId()); if (order == null) { return; } //判读订单状态 if (OrderStatus.UNPAY.getStatus() == order.getStatus()) { //未支付,修改订单状态 orderService.updateOrderStatus(orderEntity.getOrderId()); //库存+1 System.out.println("库存+1"); } //delivery tag可以从消息头里边get出来 Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG); //手动应答,消费者成功消费完消息之后通知mq,从队列移除消息,需要配置文件指明。第二个参数为是否批量处理 channel.basicAck(deliveryTag, false); } catch (IOException e) { e.printStackTrace(); //补偿机制 } } }完整代码:spring-boot: Springboot整合redis、消息中间件等相关代码
正文一、RabitMQ如何确认消息不丢失1、从生产者角度来考虑产生原因:我们的生产者发送消息之后可能由于网络故障等各种原因导致我们的消息并没有发送到MQ之中,但是这个时候我们生产端又不知道我们的消息没有发出去,这就会造成消息的丢失。解决方法:为了解决这个问题,RabbitMQ引入了事务机制和发送方确认机制(confirm)。事务机制开启之后,相当于同步执行,必然会降低系统的性能,因此一般我们不采用这种方式。确实机制,是当mq收到生产者发送的消息时,会返回一个ack告知生产者,收到了这条消息,如果没有收到,那就采取重试机制后者其他方式补偿。事务方式package com.xiaojie.rabbitmq.tx; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.xiaojie.rabbitmq.MyConnection; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeoutException; /** * @Description: mq事务模式保证消息可靠性 * @author: xiaojie * @date: 2021.09.28 */ public class TxProvider { //定义队列 private static final String QUEUE_NAME = "myqueue"; static Channel channel = null; static Connection connection = null; public static void main(String[] args) { try { System.out.println("生产者启动成功.."); // 1.创建连接 connection = MyConnection.getConnection(); // 2.创建通道 channel = connection.createChannel(); channel.queueDeclare(QUEUE_NAME, false, false, false, null); String msg = "测试事务机制保证消息发送可靠性。。。。"; channel.txSelect(); //开启事务 channel.basicPublish("", QUEUE_NAME, null, msg.getBytes(StandardCharsets.UTF_8)); //发生异常时,mq中并没有新的消息入队列 //int i=1/0; //没有发生异常,提交事务 channel.txCommit(); System.out.println("生产者发送消息成功:" + msg); } catch (Exception e) { e.printStackTrace(); //发生异常则回滚事务 try { if (channel != null) { channel.txRollback(); } } catch (IOException ioException) { ioException.printStackTrace(); } } finally { try { if (channel != null) { channel.close(); } if (connection != null) { connection.close(); } } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } } }消费者package com.xiaojie.rabbitmq.tx; import com.rabbitmq.client.*; import com.xiaojie.rabbitmq.MyConnection; import java.io.IOException; import java.util.concurrent.TimeoutException; /** * @Description: 事务模拟消费者 * 如果不改为手动应达模式,那么事务开启对消费者没有影响 * @author: xiaojie * @date: 2021.09.28 */ public class Txconsumer { private static final String QUEUE_NAME = "myqueue"; public static void main(String[] args) throws IOException, TimeoutException { // 1.创建我们的连接 Connection connection = MyConnection.getConnection(); // 2.创建我们通道 Channel channel = connection.createChannel(); channel.txSelect();//开启事务 DefaultConsumer defaultConsumer = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String msg = new String(body, "UTF-8"); //设置手动应答 channel.basicAck(envelope.getDeliveryTag(), true); System.out.println("消费消息msg:" + msg+"手动应答:"+envelope.getDeliveryTag()); channel.txCommit(); //消费者开启事务,必须要提交事务之后,消息才会从队列中移除,否则不移除。 } }; // 3.创建我们的监听的消息 false 关闭自动确认模式 channel.basicConsume(QUEUE_NAME, false, defaultConsumer); } }Confirm模式package com.xiaojie.rabbitmq.confirm; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.xiaojie.rabbitmq.MyConnection; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeoutException; /** * @author xiaojie * @version 1.0 * @description: confirm模式保证消息可靠性 * @date 2021/9/28 */ public class Provider { private static final String QUEUE_NAME = "myqueue"; static Connection connection = null; static Channel channel = null; static String msg = "confirm模式保证消息可靠性。。。。"; public static void main(String[] args) throws IOException, TimeoutException, InterruptedException { try { //创建连接 connection = MyConnection.getConnection(); //创建通道 channel = connection.createChannel(); channel.queueDeclare(QUEUE_NAME, false, false, false, null); channel.confirmSelect();//开启确认模式,确认消息已经被mq持久化到硬盘上 channel.basicQos(1);//每次发送一条消息,并且收到消费者的ack之后才会发送第二条 //如果异常进行重试,超过重试次数放弃执行 // int i=1/0; //发送消息 channel.basicPublish("", QUEUE_NAME, null, msg.getBytes(StandardCharsets.UTF_8)); if (channel.waitForConfirms()) { //确认消息已经持久化到硬盘 System.out.println("消息发送成功。。。。。。"); } } catch (Exception e) { for (int i = 0; i < 3; i++) { //发送消息 System.out.println("重试次数" + (i + 1)); channel.basicPublish("", QUEUE_NAME, null, msg.getBytes(StandardCharsets.UTF_8)); if (i >= 3) { //实际生产时候,可以放到表中记录失败的消息,然后采取补偿措施 break; } } } finally { //关闭通道,关闭连接 if (channel != null) { channel.close(); } if (connection != null) { connection.close(); } } } }2、从mq角度考虑产生原因:消息到达mq之后,mq宕机了,然后消息又没有进行持久化,这时消息就会丢失。如果队列满了,或者mq拒绝接受消息时都会导致消息丢失(死信队列)。解决方法:开启mq的持久化机制,消息队列,交换机、消息都要开启持久化。当然也存在特殊情况,消息在还没有持久化到硬盘的时候宕机了,这种小概率事件的发生。也可以采用集群模式,尽可能保证消息的可靠性。(假如机房炸了,这种情况实在也是没有办法)。3、从消费者角度考虑产生原因:在RabbitMQ将消息发出后,消费端还没接收到消息之前,发生网络故障,消费端与RabbitMQ断开连接,此时消息会丢失;在RabbitMQ将消息发出后,消费端还没接收到消息之前,消费端挂了,此时消息会丢失;消费端正确接收到消息,但在处理消息的过程中发生异常或宕机了,消息也会丢失。解决方法:开启手动应答模式,只要mq没有收到消费者已经消费掉消息的ack,那消息就不会从队列中移除。后面整合Springboot时,在代码中标明。二、RabbitMQ持久化机制手动开启持久化 如图,在手动创建队列,交换机时,开启持久化模式,默认情况下是持久化方式的。2、代码方式//队列持久化 channel.queueDeclare(EMAIL_QUEUE_FANOUT, true, false, false, null); //生产者绑定交换机 参数1 交换机的名称。2,交换机的类型,3,true标识持久化 channel.exchangeDeclare(EXCHANGE, "direct",true);Springboot模式下默认绑定是持久化绑定(springboot2.4.2版本) 三、SpringBoot整合RabbitMQ配置文件package com.xiaojie.springboot.config; import org.springframework.amqp.core.*; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; /** * @author xiaojie * @version 1.0 * @description: 配置rabbitmq * @date 2021/9/25 22:16 */ @Component public class RabbitMqConfig { //定义队列 private static final String MY_FANOUT_QUEUE = "xiaojie_fanout_queue"; //定义队列 private static final String MY_DIRECT_QUEUE = "xiaojie_direct_queue"; //定义队列 private static final String MY_TOPIC_QUEUE = "xiaojie_topic_queue"; //定义fanout交换机 private static final String MY_FANOUT_EXCHANGE = "xiaojie_fanout_exchange"; //定义direct交换机 private static final String MY_DIRECT_EXCHANGE = "xiaojie_direct_exchange"; //定义topics交换机 private static final String MY_TOPICS_EXCHANGE = "xiaojie_topics_exchange"; //创建队列 默认开启持久化 @Bean public Queue fanoutQueue() { return new Queue(MY_FANOUT_QUEUE); } //创建队列 @Bean public Queue directQueue() { return new Queue(MY_DIRECT_QUEUE); } //创建队列 @Bean public Queue topicQueue() { return new Queue(MY_TOPIC_QUEUE); } //创建fanout交换机 默认开启持久化 @Bean public FanoutExchange fanoutExchange() { return new FanoutExchange(MY_FANOUT_EXCHANGE); } //创建direct交换机 @Bean public DirectExchange directExchange() { return new DirectExchange(MY_DIRECT_EXCHANGE); } //创建direct交换机 @Bean public TopicExchange topicExchange() { return new TopicExchange(MY_TOPICS_EXCHANGE); } //绑定fanout交换机 @Bean public Binding fanoutBindingExchange(Queue fanoutQueue, FanoutExchange fanoutExchange) { return BindingBuilder.bind(fanoutQueue).to(fanoutExchange); } //绑定direct交换机 @Bean public Binding directBindingExchange(Queue directQueue, DirectExchange directExchange) { return BindingBuilder.bind(directQueue).to(directExchange).with("msg.send"); } //绑定topic交换机 @Bean public Binding topicBindingExchange(Queue topicQueue, TopicExchange topicExchange) { return BindingBuilder.bind(topicQueue).to(topicExchange).with("msg.#"); } //多个队列绑定到同一个交换机 // @Bean // public Binding topicBindingExchange1(Queue directQueue, TopicExchange topicExchange) { // return BindingBuilder.bind(directQueue).to(topicExchange).with("msg.send"); // } }生产者回调package com.xiaojie.springboot.callback; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.core.ReturnedMessage; import org.springframework.amqp.rabbit.connection.CorrelationData; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; /** * @Description: 生产者发送消息之后,接受服务器回调 * @author: xiaojie * @date: 2021.09.29 */ @Component @Slf4j public class ConfirmCallBackListener implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback { @Autowired private RabbitTemplate rabbitTemplate; @PostConstruct public void init() { //指定 ConfirmCallback rabbitTemplate.setConfirmCallback(this); rabbitTemplate.setReturnsCallback(this); } @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { log.info("correlation>>>>>>>{},ack>>>>>>>>>{},cause>>>>>>>>{}", correlationData, ack, cause); if (ack) { //确认收到消息 } else { //收到消息失败,可以开启重试机制,或者将失败的存起来,进行补偿 } } /* * * @param returnedMessage * 消息是否从Exchange路由到Queue, 只有消息从Exchange路由到Queue失败才会回调这个方法 * @author xiaojie * @date 2021/9/29 13:53 * @return void */ @Override public void returnedMessage(ReturnedMessage returnedMessage) { log.info("被退回信息是》》》》》》{}", returnedMessage.getMessage()); log.info("replyCode》》》》》》{}", returnedMessage.getReplyCode()); log.info("replyText》》》》》》{}", returnedMessage.getReplyText()); log.info("exchange》》》》》》{}", returnedMessage.getExchange()); log.info("routingKey>>>>>>>{}", returnedMessage.getRoutingKey()); } }生产者package com.xiaojie.springboot.provider; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.AmqpException; import org.springframework.amqp.core.AmqpTemplate; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.ReturnedMessage; import org.springframework.amqp.rabbit.connection.CorrelationData; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; /** * @author xiaojie * @version 1.0 * @description:消息生产者 publisher-confirm-type: correlated * #NONE值是禁用发布确认模式,是默认值 * #CORRELATED值是发布消息成功到交换器后会触发回调方法 * #SIMPLE值经测试有两种效果,其一效果和CORRELATED值一样会触发回调方法, * 其二在发布消息成功后使用rabbitTemplate调用waitForConfirms或waitForConfirmsOrDie方法等待broker节点返回发送结果, * 根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDie方法如果返回false则会关闭channel,将无法发送消息到broker * @date 2021/9/25 22:47 */ @Component @Slf4j public class MsgProvider { //定义队列 private static final String MY_FANOUT_QUEUE = "xiaojie_fanout_queue"; private static final String MY_DIRECT_QUEUE = "xiaojie_direct_queue"; private static final String DIRECT_ROUTING_KEY = "msg.send"; private static final String MY_TOPIC_QUEUE = "xiaojie_topic_queue"; private static final String TOPIC_ROUTING_KEY = "msg.send"; //定义fanout交换机 private static final String MY_FANOUT_EXCHANGE = "xiaojie_fanout_exchange"; //定义direct交换机 private static final String MY_DIRECT_EXCHANGE = "xiaojie_direct_exchange"; //定义topics交换机 private static final String MY_TOPICS_EXCHANGE = "xiaojie_topics_exchange"; @Autowired private AmqpTemplate amqpTemplate; @Autowired private RabbitTemplate rabbitTemplate; public String sendMSg(Integer type, String msg) { try { if (1 == type) { //发送到fanout rabbitTemplate.convertAndSend(MY_FANOUT_QUEUE, "fanout" + msg); } else if (2 == type) { //第一个参数交换机,第二个参数,路由键,第三个参数,消息 rabbitTemplate.convertAndSend(MY_DIRECT_EXCHANGE, DIRECT_ROUTING_KEY, "direct" + msg); } else if (3 == type) { rabbitTemplate.convertAndSend(MY_TOPICS_EXCHANGE, TOPIC_ROUTING_KEY, "topic" + msg); } return "success"; } catch (AmqpException e) { e.printStackTrace(); } return "error"; } }消费者Fanoutpackage com.xiaojie.springboot.consumer; import com.rabbitmq.client.Channel; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.core.ExchangeTypes; import org.springframework.amqp.rabbit.annotation.*; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.support.AmqpHeaders; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.handler.annotation.Headers; import org.springframework.messaging.handler.annotation.Payload; import org.springframework.stereotype.Component; import java.io.IOException; import java.util.Map; /** * @author xiaojie * @version 1.0 * @description: fanout消费者 * @date 2021/9/25 22:57 */ @Component @RabbitListener(bindings = @QueueBinding( value = @Queue("xiaojie_fanout_queue"), exchange = @Exchange(value = "xiaojie_fanout_exchange", type = ExchangeTypes.FANOUT), key = "")) @Slf4j public class FanoutMsgConsumer { @Autowired private RabbitTemplate rabbitTemplate; /** * @description: 接收到信息 * @param: * @param: msg * @return: void * @author xiaojie * @date: 2021/9/25 23:02 */ @RabbitHandler public void handlerMsg(@Payload String msg, @Headers Map<String, Object> headers, Channel channel) throws IOException { log.info("接收到的消息是fanout:{}" + msg); //delivery tag可以从消息头里边get出来 Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG); // int i=1/0; 模拟重试机制,如果重试则代码不能try(有点类似事务),并且自动应答模式下,重试次数结束之后,自动应答消息出队列。 //手动应答,第二个参数为是否批量处理 channel.basicAck(deliveryTag, false); boolean redelivered = (boolean) headers.get(AmqpHeaders.REDELIVERED); //第二个参数为是否批量,第三个参数为是否重新进入队列,如果为true,则重新进入队列 // channel.basicNack(deliveryTag, false, !redelivered); } }完整代码:spring-boot: Springboot整合redis、消息中间件等相关代码
正文一、什么是消息中间件消息队列已经逐渐成为企业IT系统内部通信的核心手段。它具有低耦合、可靠投递、广播、流量控制、最终一致性等一系列功能,成为异步RPC的主要手段之一。当今市面上有很多主流的消息中间件,如老牌的ActiveMQ、RabbitMQ,炙手可热的Kafka,阿里巴巴自主开发RocketMQ等。RabitMQ是使用Erlang编写的一个开源的消息队列,本身支持很多的协议:AMQP,XMPP, SMTP,STOMP,也正是如此,使的它变的非常重量级,更适合于企业级的开发。同时实现了Broker架构,核心思想是生产者不会将消息直接发送给队列,消息在发送给客户端时先在中心队列排队。对路由(Routing),负载均衡(Load balance)、数据持久化都有很好的支持。多用于进行企业级的ESB(企业服务总线)整合。消息中间件的组成Broker:消息服务器,作为server提供消息核心服务。Producer:消息生产者,业务的发起方,负责生产消息传输给broker。Consumer:消息消费者,业务的处理方,负责从broker获取消息并进行业务逻辑处理。Topic:主题,发布订阅模式下的消息统一汇集地,不同生产者向topic发送消息,由MQ服务器分发到不同的订阅者,实现消息的广播。Queue:队列,PTP模式下,特定生产者向特定queue发送消息,消费者订阅特定的queue完成指定消息的接收。Message:消息体,根据不同通信协议定义的固定格式进行编码的数据包,来封装业务数据,实现消息的传输。二、应用场景解耦 如上图,未使用MQ的情况下,如果B、C、D三个系统,有需求改变,那么A系统都会响应的更改,使用MQ之后,我们只需要把数据发送的MQ中,B、C、D自己根据需求去mq订阅响应的内容即可,从而达到系统耦合的结果。异步 如果某些需求场景不需要立即返回数据结果,那么就可以采用MQ的形式,对消息异步的处理,这样可以提高系统的响应速度。削峰填谷如果请求超过了服务器承受的最大值,那么就可能会击垮服务器,这时候,把请求通过mq队列缓存起来,来限制请求的峰值,从而达到保护服务器的目的。三、消息中间件选型四、RabitMQ环境搭建单机模式#拉取镜像 docker pull rabbitmq:3.9-management #创建容器并启动 [root@bogon ~]# docker run -d -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3.9-management 登录 http://192.168.139.154:15672/#/ 默认账号密码是guest,guest注意:如果安装过程出现这个错误,重启一下docker就可以解决了systemctl restart docker。iptables failed: iptables --wait -t nat -A DOCKER -p tcp -d 0/0 --dport 15672 -j DNAT --to-destination 172.17.0.2:15672 ! -i docker0: iptables: No chain/target/match by that name集群模式安装容器[root@bogon ~]# docker run -d --hostname rabbit1 --name rabbitmq1 -p 15672:15672 -p 5672:5672 -e RABBITMQ_ERLANG_COOKIE='rabbitmq_cookie' rabbitmq:3.9-management [root@bogon ~]# docker run -d --hostname rabbit2 --name rabbitmq2 -p 5673:5672 --link rabbitmq1:rabbit1 -e RABBITMQ_ERLANG_COOKIE='rabbitmq_cookie' rabbitmq:3.9-management [root@bogon ~]# docker run -d --hostname rabbit3 --name rabbitmq3 -p 5674:5672 --link rabbitmq1:rabbit1 --link rabbitmq2:rabbit2 -e RABBITMQ_ERLANG_COOKIE='rabbitmq_cookie' rabbitmq:3.9-management添加节点#节点1 [root@bogon ~]# docker exec -it rabbitmq1 bash root@rabbit1:/# rabbitmqctl stop_app RABBITMQ_ERLANG_COOKIE env variable support is deprecated and will be REMOVED in a future version. Use the $HOME/.erlang.cookie file or the --erlang-cookie switch instead. Stopping rabbit application on node rabbit@rabbit1 ... root@rabbit1:/# rabbitmqctl reset RABBITMQ_ERLANG_COOKIE env variable support is deprecated and will be REMOVED in a future version. Use the $HOME/.erlang.cookie file or the --erlang-cookie switch instead. Resetting node rabbit@rabbit1 ... root@rabbit1:/# rabbitmqctl start_app RABBITMQ_ERLANG_COOKIE env variable support is deprecated and will be REMOVED in a future version. Use the $HOME/.erlang.cookie file or the --erlang-cookie switch instead. Starting node rabbit@rabbit1 ... root@rabbit1:/# #节点2 [root@bogon ~]# docker exec -it rabbitmq2 bash root@rabbit2:/# rabbitmqctl stop_app RABBITMQ_ERLANG_COOKIE env variable support is deprecated and will be REMOVED in a future version. Use the $HOME/.erlang.cookie file or the --erlang-cookie switch instead. Stopping rabbit application on node rabbit@rabbit2 ... root@rabbit2:/# rabbitmqctl reset RABBITMQ_ERLANG_COOKIE env variable support is deprecated and will be REMOVED in a future version. Use the $HOME/.erlang.cookie file or the --erlang-cookie switch instead. Resetting node rabbit@rabbit2 ... root@rabbit2:/# rabbitmqctl join_cluster --ram rabbit@rabbit1 root@rabbit2:/# rabbitmqctl start_app#节点3 [root@bogon ~]# docker exec -it rabbitmq3 bash root@rabbit3:/# rabbitmqctl stop_app RABBITMQ_ERLANG_COOKIE env variable support is deprecated and will be REMOVED in a future version. Use the $HOME/.erlang.cookie file or the --erlang-cookie switch instead. Stopping rabbit application on node rabbit@rabbit3 ... root@rabbit3:/# rabbitmqctl reset RABBITMQ_ERLANG_COOKIE env variable support is deprecated and will be REMOVED in a future version. Use the $HOME/.erlang.cookie file or the --erlang-cookie switch instead. Resetting node rabbit@rabbit3 ... root@rabbit3:/# rabbitmqctl join_cluster --ram rabbit@rabbit1 RABBITMQ_ERLANG_COOKIE env variable support is deprecated and will be REMOVED in a future version. Use the $HOME/.erlang.cookie file or the --erlang-cookie switch instead. Clustering node rabbit@rabbit3 with rabbit@rabbit1 13:45:31.790 [warn] Feature flags: the previous instance of this node must have failed to write the `feature_flags` file at `/var/lib/rabbitmq/mnesia/rabbit@rabbit3-feature_flags`: 13:45:31.790 [warn] Feature flags: - list of previously disabled feature flags now marked as such: [:maintenance_mode_status] 13:45:32.000 [error] Failed to create a tracked connection table for node :rabbit@rabbit3: {:node_not_running, :rabbit@rabbit3} 13:45:32.001 [error] Failed to create a per-vhost tracked connection table for node :rabbit@rabbit3: {:node_not_running, :rabbit@rabbit3} 13:45:32.001 [error] Failed to create a per-user tracked connection table for node :rabbit@rabbit3: {:node_not_running, :rabbit@rabbit3} root@rabbit3:/# rabbitmqctl start_app RABBITMQ_ERLANG_COOKIE env variable support is deprecated and will be REMOVED in a future version. Use the $HOME/.erlang.cookie file or the --erlang-cookie switch instead. Starting node rabbit@rabbit3 ...此时安装完成的为普通集群模式Exchange 的元数据信息在所有节点上是一致的,而 Queue(存放消息的队列)的完整数据则只会存在于创建它的那个节点上。其他节点只知道这个 queue 的 metadata 信息和一个指向 queue 的 owner node 的指针;RabbitMQ 集群会始终同步四种类型的内部元数据(类似索引):队列元数据:队列名称和它的属性;交换器元数据:交换器名称、类型和属性;绑定元数据:一张简单的表格展示了如何将消息路由到队列;vhost元数据:为 vhost 内的队列、交换器和绑定提供命名空间和安全属性;因此,当用户访问其中任何一个 RabbitMQ 节点时,通过 rabbitmqctl 查询到的元数据信息都是相同的。无法实现高可用性,当创建 queue 的节点故障后,其他节点是无法取到消息实体的。如果做了消息持久化,那么得等创建 queue 的节点恢复后,才可以被消费。如果没有持久化的话,就会产生消息丢失的现象。 配置镜像集群模式概念:把队列做成镜像队列,让各队列存在于多个节点中,属于 RabbitMQ 的高可用性方案。镜像模式和普通模式的不同在于,queue和 message 会在集群各节点之间同步,而不是在 consumer 获取数据时临时拉取。特点:(1)实现了高可用性。部分节点挂掉后,不会影响 rabbitmq 的使用。(2)降低了系统性能。镜像队列数量过多,大量的消息同步也会加大网络带宽开销。(3)适合对可用性要求较高的业务场景。name:随便取,策略名称Pattern:^ 匹配符,只有一个^代表匹配所有Definition:ha-mode=all 为匹配类型,分为3种模式:all(表示所有的queue)或者使用命令:#rabbitmqctl set_policy ha-all "^" '{"ha-mode":"all"}'添加一个队列 可见队列已经同步到其他节点上。
正文一、Redis分布式锁原理分布式锁需要满足以下几点要求在分布式系统环境下,一个方法在同一时间只能被一个机器的的一个线程执行 即独享(相互排斥);高可用的获取锁与释放锁 ;高性能的获取锁与释放锁;具备可重入特性;具备锁失效机制,防止死锁;具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。目前市面上普遍使用的分布式锁实现的方式主要有三种一种是基于数据库,一种是基于Zookeeper(点击链接可查看基于Zk实现的分布式锁),还有一种就是现在说的基于Redis实现的分布式锁。本文只说单机状态下Redis的分布式锁。如果在主从复制或者集群模式下(如果你能允许下面这些小概率的事件发生,同样也可以用在集群或者主从复制的情况下),如果主节点数据还没有同步到从节点的时候,突然宕机了,然后从节点变为主节点,获取到了锁,就违背了分布式锁的相互排斥原则。那么Redis实现分布式锁的原理是什么?Redis实现分布式锁基于SetNx命令,因为在Redis中key是保证是唯一的。所以当多个线程同时的创建setNx时,只要谁能够创建成功谁就能够获取到锁。SetNx命令 每次SetNx检查该 key是否已经存在,如果已经存在的话不会执行任何操作,返回为0。 如果不存在的话直接新增该key,新增成功返回1。二、Redis实现分布式锁 基于Jedis实现工具类package com.xiaojie.utils; import org.springframework.stereotype.Component; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; /** * @ClassRedisPoolUtil * @Description jedispool连接工具 * @AuthorAdministrator * @Date {Date}{Time} * @Version 1.0 **/ @Component public class RedisPoolUtil { private static String IP = "192.168.139.154"; //Redis的端口号 private static int PORT = 6379; //可用连接实例的最大数目,默认值为8; //如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽)。 private static int MAX_ACTIVE = 100; //控制一个pool最多有多少个状态为idle(空闲的)的jedis实例,默认值也是8。 private static int MAX_IDLE = 20; //等待可用连接的最大时间,单位毫秒,默认值为-1,表示永不超时。如果超过等待时间,则直接抛出JedisConnectionException; private static int MAX_WAIT = 3000; private static int TIMEOUT = 3000; //在borrow一个jedis实例时,是否提前进行validate操作;如果为true,则得到的jedis实例均是可用的; private static boolean TEST_ON_BORROW = true; //在return给pool时,是否提前进行validate操作; private static boolean TEST_ON_RETURN = true; private static JedisPool jedisPool = null; /** * redis过期时间,以秒为单位 */ public final static int EXRP_HOUR = 60 * 60; //一小时 public final static int EXRP_DAY = 60 * 60 * 24; //一天 public final static int EXRP_MONTH = 60 * 60 * 24 * 30; //一个月 /** * 初始化Redis连接池 */ private static void initialPool() { try { JedisPoolConfig config = new JedisPoolConfig(); config.setMaxTotal(MAX_ACTIVE); config.setMaxIdle(MAX_IDLE); config.setMaxWaitMillis(MAX_WAIT); config.setTestOnBorrow(TEST_ON_BORROW); jedisPool = new JedisPool(config, IP, PORT, TIMEOUT, "xiaojie"); } catch (Exception e) { //logger.error("First create JedisPool error : "+e); e.getMessage(); } } /** * 在多线程环境同步初始化 */ private static synchronized void poolInit() { if (jedisPool == null) { initialPool(); } } /** * 同步获取Jedis实例 * * @return Jedis */ public synchronized static Jedis getJedis() { if (jedisPool == null) { poolInit(); } Jedis jedis = null; try { if (jedisPool != null) { jedis = jedisPool.getResource(); } } catch (Exception e) { e.getMessage(); // logger.error("Get jedis error : "+e); } return jedis; } /** * 释放jedis资源 * * @param jedis */ public static void returnResource(final Jedis jedis) { if (jedis != null && jedisPool != null) { jedisPool.returnResource(jedis); } } public static Long sadd(String key, String... members) { Jedis jedis = null; Long res = null; try { jedis = getJedis(); res = jedis.sadd(key, members); } catch (Exception e) { //logger.error("sadd error : "+e); e.getMessage(); } return res; } }获取锁package com.xiaojie.lock; import com.xiaojie.utils.RedisPoolUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import redis.clients.jedis.Jedis; import java.util.UUID; /** * @author xiaojie * @version 1.0 * @description: 基于redis实现分布式锁 * @date 2021/9/19 21:17 */ @Component public class RedisDistributeLock { @Autowired private RedisPoolUtil redisPoolUtil; /** * @description: * @param: key 键值 * @param: notLockTimeOut 为获取锁的等待时间 * @param: timeOut 键值过期时间 * @return: java.lang.String * @author xiaojie * @date: 2021/9/19 21:15 */ public String getLock(String key, Integer notLockTimeOut, Long timeOut) { Jedis jedis = redisPoolUtil.getJedis(); //计算锁的超时时间 Long endTime = System.currentTimeMillis() + notLockTimeOut; //当前时间比锁的超时时间小,证明获取锁时间没有超时,继续获取 while (System.currentTimeMillis() < endTime) { String lockValue = UUID.randomUUID().toString(); //如果设置成功返回1 获取到锁,如果返回0 获取锁失败,继续获取 if (1 == jedis.setnx(key, lockValue)) { //设置键值过期时间,防止死锁 jedis.expire(key, timeOut); return lockValue; } } //关闭连接 try { if (jedis != null) { jedis.close(); } } catch (Exception e) { e.printStackTrace(); } return null; } /** * @description: 释放锁 * @param: key * @return: boolean * @author xiaojie * @date: 2021/9/19 21:25 */ public boolean unLock(String key, String lockValue) { //获取Redis连接 Jedis jedis = redisPoolUtil.getJedis(); try { // 判断获取锁的时候保证自己删除自己 if (lockValue.equals(jedis.get(key))) { return jedis.del(key) > 0 ? true : false; } } catch (Exception e) { e.printStackTrace(); } finally { if (jedis != null) { jedis.close(); } } return false; } } 基于RedisTemplate实现package com.xiaojie.lock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.core.script.RedisScript; import org.springframework.stereotype.Component; import java.util.Collections; /** * @author xiaojie * @version 1.0 * @description: 基于RedisTemplate实现分布式锁 * @date 2021/9/19 21:57 */ @Component public class RedisTemplateDistributeLock { private static final Long SUCCESS = 1L; @Autowired private RedisTemplate redisTemplate; /** * @description: 获取锁 * @param: lockKey 键值 * @param: value 唯一的值 区别那个获取到的锁 * @param: expireTime 过期时间 * @return: boolean * @author xiaojie * @date: 2021/9/19 22:15 */ public boolean getLock(String lockKey, String value, Long expireTime) { try { //lua脚本 String script = "if redis.call('setNx',KEYS[1],ARGV[1]) == 1 then if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('expire',KEYS[1],ARGV[2]) else return 0 end else return 0 end"; Long result = (Long) redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Collections.singletonList(lockKey), value, expireTime); return SUCCESS.equals(result); } catch (Exception e) { e.printStackTrace(); } return false; } /** * @description: 释放锁 * @param: * @param: lockKey * @param: value * @return: boolean * @author xiaojie * @date: 2021/9/19 22:18 */ public boolean releaseLock(String lockKey, String value) { try { //lua脚本 String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class); Long result = (Long) redisTemplate.execute(redisScript, Collections.singletonList(lockKey), value); return SUCCESS.equals(result); } catch (Exception e) { e.printStackTrace(); } return false; } }三、测试分布式锁在分布式定时任务的测试package com.xiaojie.schedule; import com.xiaojie.entity.User; import com.xiaojie.lock.RedisDistributeLock; import com.xiaojie.lock.RedisTemplateDistributeLock; import com.xiaojie.mapper.UserMapper; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.util.List; import java.util.UUID; /** * @ClassMyTask * @Description 模拟定时任务 * @AuthorAdministrator * @Date {Date}{Time} * @Version 1.0 **/ @Component public class MyTask { @Autowired private UserMapper userMapper; @Autowired private RedisDistributeLock redisDistributeLock; private static final String REDISKEY = "xiaojie_redis_lock"; private static final Integer NOLOCK_TIMEOUT=5; private static final Long TIME_OUT=5L; private static final String value= UUID.randomUUID().toString(); int i; @Scheduled(cron = "0/5 * * * * ? ") public void scheduledTask(){ String lockValue = redisDistributeLock.getLock(REDISKEY, NOLOCK_TIMEOUT, TIME_OUT); if(StringUtils.isBlank(lockValue)){ System.out.println("获取锁失败"); return; } List<User> users = userMapper.selectAll(); for (User user:users){ user.setNum(user.getNum()+1); int updateNum = userMapper.updateNum(user.getNum(),user.getId()); System.out.println(updateNum); } i++; System.out.println("执行了"+i+"次"); //释放锁 redisDistributeLock.unLock(REDISKEY,lockValue); } }在不加锁的情况下,开启两台机器 可见一个机器上运行了5次一次运行7次理论上应该是7+5=12,但是数据库结果却是8。加上锁之后一个执行了7次一个执行了2次 7+2=9,正好符合我们测试需要的结果。完整代码:spring-boot: Springboot整合redis、消息中间件等相关代码参考:springboot的RedisTemplate实现分布式锁_long2010110的专栏-CSDN博客_redistemplate实现分布式锁
正文一、布隆过滤器布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量(数组)和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。布隆过滤器的原理 布隆过滤器就是由一个二进制的数据和一些hash算法维护,如果xiaojie经过hash算法之后,落在下标为0,3,5的位置,那么对应的二进制数组的位置改为1。那么问题来了,如果我有一个值xiaoli经过hash算法之后也落在了0,3,5的位置,那么此时就会产生hash冲突,这就是为什么布隆过滤器会产生误判的原因。所以如果需要避免这种情况,数组就要尽可能的大,然后避免这种碰撞。布隆过滤器并不会存真实的数据,所以对于保密性数据很友好。应用场景对URL的去重,比如在爬虫获取数据时候。反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱(同理,垃圾短信)缓存穿透,将所有可能存在的数据缓存放到布隆过滤器中,当恶意访问时,直接避免不必要的IO读取数据库空值的操作。二、代码测试类代码package com.xiaojie.test; import com.google.common.hash.BloomFilter; import com.google.common.hash.Funnels; import java.nio.charset.Charset; import java.util.ArrayList; /** * 布隆过滤器 */ public class BlongTest { private static Integer size = 2<<20; public static void main(String[] args) { BloomFilter<String> integerBloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), size, 0.03); for (int i = 0; i < size; i++) { integerBloomFilter.put(i+""); } ArrayList<Integer> errorList = new ArrayList<>(); for (int j = size; j < size + 10000; j++) { // 使用该pai判断key在布隆过滤器中是否存在 返回true 存在 false 表示不存在 if (integerBloomFilter.mightContain(j+"")) { //误判的数据添加到集合 errorList.add(j); } } System.out.println("误判数据的个数:" + errorList.size()); } }Redis缓存穿透 //提前将数据存入布隆过滤器 @Override public void preBlongData() { List<User> users = userMapper.selectAll(); for (User user:users){ namesBloomFilter.put(user.getName()); } } //查询之前判断 public User getUserByName(String name) { //判断布隆过滤器是否含有该数据 if(!namesBloomFilter.mightContain(name)){ //如果不存在该数据直接返回,而不进行数据库查询 return null; } JSONObject obj= (JSONObject) redisUtil.get(USERKEY + ":" + name); if (null==obj){ System.out.println("缓存中没有该值,查询数据库"); User resultUser = userMapper.selectByName(name); if (null!= resultUser) { redisUtil.set(USERKEY+":"+resultUser.getName(), JSONObject.toJSON(resultUser),60); return resultUser; } } User user = JSONObject.toJavaObject(obj,User.class); return user; }完整代码:spring-boot: Springboot整合redis、消息中间件等相关代码
正文Redis的空间半径查询Geo是Redis自3.2版本之后新增的,这个查询可以满足空间距离范围的查询。下面直接上代码吧。有效的经度从-180度到180度。有效的纬度从-85.05112878度到85.05112878度。主要命令1、geoadd:添加地理位置的坐标。2、geopos:获取地理位置的坐标。3、geodist:计算两个位置之间的距离。4、georadius:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。5、georadiusbymember:根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合。6、geohash:返回一个或多个位置对象的 geohash 值。package com.xiaojie.test; import redis.clients.jedis.GeoCoordinate; import redis.clients.jedis.GeoRadiusResponse; import redis.clients.jedis.GeoUnit; import redis.clients.jedis.Jedis; import redis.clients.jedis.params.GeoRadiusParam; import java.util.List; /** * @Description:Redis * @author: xiaojie * @date: 2021.09.14 */ public class RedisGeoTest { /* * @param null * @geoadd:添加地理位置的坐标。 geopos:获取地理位置的坐标。 geodist:计算两个位置之间的距离。 georadius:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。 georadiusbymember:根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合。 geohash:返回一个或多个位置对象的 geohash 值。 * 有效的经度从-180度到180度。longitude *有效的纬度从-85.05112878度到85.05112878度。latitude * @author xiaojie * @date 2021/9/14 * @return */ public static void main(String[] args) { Jedis jedis = new Jedis("192.168.6.137", 6379); jedis.geoadd("dist", 116.25, 39.54, "bj"); jedis.geoadd("dist", 116.45, 38.34, "tj"); jedis.geoadd("dist", 114.30, 37.27, "sjz"); //获取经纬度 List<GeoCoordinate> geopos = jedis.geopos("dist", "tj", "bj"); for (int i = 0; i < geopos.size(); i++) { System.out.println("经度是:" + geopos.get(i).getLongitude() + "纬度是:" + geopos.get(i).getLatitude()); } //计算两地之间的距离 默认单位是米 System.out.println("北京和天津的距离是:" + jedis.geodist("dist", "bj", "tj", GeoUnit.KM) + "km"); //根据给定的坐标来获取指定范围内的地理位置集合,按照距离正序排列 List<GeoRadiusResponse> dist = jedis.georadius("dist", 115.88, 37.30, 150, GeoUnit.KM, GeoRadiusParam.geoRadiusParam().withDist().sortAscending()); for (int i = 0; i < dist.size(); i++) { System.out.println("距离给定地点的位置是:" + dist.get(i).getMemberByString() + "相距:" + dist.get(i).getDistance() + "km"); } //查询距北京150km范围内的地点 List<GeoRadiusResponse> bj150 = jedis.georadiusByMember("dist", "bj", 150, GeoUnit.KM, GeoRadiusParam.geoRadiusParam().withDist().sortAscending()); for (int i = 0; i < bj150.size(); i++) { System.out.println("距离北京150km内位置是:" + bj150.get(i).getMemberByString() + "相距:" + bj150.get(i).getDistance() + "km"); } } }完整代码:spring-boot: Springboot整合redis、消息中间件等相关代码
正文一、Redis集群原理Redis 集群没有使用一致性hash, 而是引入了 哈希槽的概念。Redis 集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽。集群的每个节点负责一部分hash槽,举个例子,比如当前集群有3个节点,那么:节点 A 包含 0 到 5500号哈希槽。节点 B 包含5501 到 11000 号哈希槽。节点 C 包含11001 到 16384号哈希槽。注意:一个键值并不是对应一个哈希槽,一个哈希槽可以有很多键值。理论上一个集群可以存放很多很多键值。CRC16算法static const uint16_t crc16tab[256]= { 0x0000,0x1021,0x2042,0x3063,0x4084,0x50a5,0x60c6,0x70e7, 0x8108,0x9129,0xa14a,0xb16b,0xc18c,0xd1ad,0xe1ce,0xf1ef, 0x1231,0x0210,0x3273,0x2252,0x52b5,0x4294,0x72f7,0x62d6, 0x9339,0x8318,0xb37b,0xa35a,0xd3bd,0xc39c,0xf3ff,0xe3de, 0x2462,0x3443,0x0420,0x1401,0x64e6,0x74c7,0x44a4,0x5485, 0xa56a,0xb54b,0x8528,0x9509,0xe5ee,0xf5cf,0xc5ac,0xd58d, 0x3653,0x2672,0x1611,0x0630,0x76d7,0x66f6,0x5695,0x46b4, 0xb75b,0xa77a,0x9719,0x8738,0xf7df,0xe7fe,0xd79d,0xc7bc, 0x48c4,0x58e5,0x6886,0x78a7,0x0840,0x1861,0x2802,0x3823, 0xc9cc,0xd9ed,0xe98e,0xf9af,0x8948,0x9969,0xa90a,0xb92b, 0x5af5,0x4ad4,0x7ab7,0x6a96,0x1a71,0x0a50,0x3a33,0x2a12, 0xdbfd,0xcbdc,0xfbbf,0xeb9e,0x9b79,0x8b58,0xbb3b,0xab1a, 0x6ca6,0x7c87,0x4ce4,0x5cc5,0x2c22,0x3c03,0x0c60,0x1c41, 0xedae,0xfd8f,0xcdec,0xddcd,0xad2a,0xbd0b,0x8d68,0x9d49, 0x7e97,0x6eb6,0x5ed5,0x4ef4,0x3e13,0x2e32,0x1e51,0x0e70, 0xff9f,0xefbe,0xdfdd,0xcffc,0xbf1b,0xaf3a,0x9f59,0x8f78, 0x9188,0x81a9,0xb1ca,0xa1eb,0xd10c,0xc12d,0xf14e,0xe16f, 0x1080,0x00a1,0x30c2,0x20e3,0x5004,0x4025,0x7046,0x6067, 0x83b9,0x9398,0xa3fb,0xb3da,0xc33d,0xd31c,0xe37f,0xf35e, 0x02b1,0x1290,0x22f3,0x32d2,0x4235,0x5214,0x6277,0x7256, 0xb5ea,0xa5cb,0x95a8,0x8589,0xf56e,0xe54f,0xd52c,0xc50d, 0x34e2,0x24c3,0x14a0,0x0481,0x7466,0x6447,0x5424,0x4405, 0xa7db,0xb7fa,0x8799,0x97b8,0xe75f,0xf77e,0xc71d,0xd73c, 0x26d3,0x36f2,0x0691,0x16b0,0x6657,0x7676,0x4615,0x5634, 0xd94c,0xc96d,0xf90e,0xe92f,0x99c8,0x89e9,0xb98a,0xa9ab, 0x5844,0x4865,0x7806,0x6827,0x18c0,0x08e1,0x3882,0x28a3, 0xcb7d,0xdb5c,0xeb3f,0xfb1e,0x8bf9,0x9bd8,0xabbb,0xbb9a, 0x4a75,0x5a54,0x6a37,0x7a16,0x0af1,0x1ad0,0x2ab3,0x3a92, 0xfd2e,0xed0f,0xdd6c,0xcd4d,0xbdaa,0xad8b,0x9de8,0x8dc9, 0x7c26,0x6c07,0x5c64,0x4c45,0x3ca2,0x2c83,0x1ce0,0x0cc1, 0xef1f,0xff3e,0xcf5d,0xdf7c,0xaf9b,0xbfba,0x8fd9,0x9ff8, 0x6e17,0x7e36,0x4e55,0x5e74,0x2e93,0x3eb2,0x0ed1,0x1ef0 }; uint16_t crc16(const char *buf, int len) { int counter; uint16_t crc = 0; for (counter = 0; counter < len; counter++) crc = (crc<<8) ^ crc16tab[((crc>>8) ^ *buf++)&0x00FF]; return crc; }集群间通信方式 所有的集群节点都通过TCP连接和一个二进制协议(集群连接,cluster bus)建立通信。 每一个节点都通过集群连接(cluster bus)与集群上的其余每个节点连接起来。节点之间使用一个 gossip流言协议来传播集群的信息。主节点宕机节点的失效有两种状态PFAIL和FAIL,当集群中A节发送给B节点的一个活跃的 ping 包(active ping)(一个发送后要等待其回复的 ping 包)已经等待了超过 NODE_TIMEOUT 时间,那么A认为这个节点具有不可达性,标记为PFAIL,如果其他节点也把B节点标记为PFAIL状态(A节点通过gossip 字段收集到集群中大部分主节点标识的节点 B 的状态信息为PFAIL),那么A就会把B标记为FAIL,并告诉其他可达的节点,B节点FAIL了。从节点选举为主节点从节点的选举和提升都是由从节点处理的,主节点会投票要提升哪个从节点。一个从节点的选举是在主节点被至少一个具有成为主节点必备条件的从节点标记为 FAIL 的状态的时候发生的。当以下条件满足时,一个从节点可以发起选举:该从节点的主节点处于 FAIL 状态。这个主节点负责的哈希槽数目不为零。从节点和主节点之间的重复连接(replication link)断线不超过一段给定的时间,这是为了确保从节点的数据是可靠的。一个从节点想要被推选出来,那么第一步应该是提高它的 currentEpoch(可以理解为事件版本号) 计数,并且向主节点们请求投票。从节点通过广播一个 FAILOVER_AUTH_REQUEST 数据包给集群里的每个主节点来请求选票。然后等待回复(最多等 NODE_TIMEOUT 这么长时间)。一旦一个主节点给这个从节点投票,会回复一个 FAILOVER_AUTH_ACK,并且在 NODE_TIMEOUT * 2 这段时间内不能再给同个主节点的其他从节点投票。一旦某个从节点收到了大多数主节点的回应,那么它就赢得了选举。否则,如果无法在 NODE_TIMEOUT 时间内访问到大多数主节点,那么当前选举会被中断并在 NODE_TIMEOUT * 4 这段时间后由另一个从节点尝试发起选举。从节点并不是在主节点一进入 FAIL 状态就马上尝试发起选举,而是有一点点延迟。主节点接收到来自于从节点、要求以 FAILOVER_AUTH_REQUEST 请求的形式投票的请求。 要授予一个投票,必须要满足以下条件:在一个给定的时段(epoch)里,一个主节点只能投一次票,并且拒绝给以前时段投票:每个主节点都有一个 lastVoteEpoch 域,一旦认证请求数据包(auth request packet)里的 currentEpoch 小于 lastVoteEpoch,那么主节点就会拒绝再次投票。当一个主节点积极响应一个投票请求,那么 lastVoteEpoch 会相应地进行更新。一个主节点投票给某个从节点当且仅当该从节点的主节点被标记为 FAIL。 如果认证请求里的 currentEpoch 小于主节点里的 currentEpoch 的话,那么该请求会被忽视掉。因此,主节点的回应总是带着和认证请求一致的 currentEpoch。如果同一个从节点在增加 currentEpoch 后再次请求投票,那么保证一个来自于主节点的、旧的延迟回复不会被新一轮选举接受。二、RedisCluster安装传统方式1、创建文件[root@localhost local]# mkdir redis-cluster [root@localhost redis-cluster]# mkdir 7000 [root@localhost redis-cluster]# mkdir 7001 [root@localhost redis-cluster]# mkdir 7002 [root@localhost redis-cluster]# mkdir 7003 [root@localhost redis-cluster]# mkdir 7004 [root@localhost redis-cluster]# mkdir 7005 #降配置文件复制到相应的目录下 [root@localhost redis-cluster]# cp /usr/local/redis-6.2.4/redis.conf /usr/local/redis-cluster/7000/ -r [root@localhost redis-cluster]# cp /usr/local/redis-6.2.4/redis.conf /usr/local/redis-cluster/7001/ -r [root@localhost redis-cluster]# cp /usr/local/redis-6.2.4/redis.conf /usr/local/redis-cluster/7002/ -r [root@localhost redis-cluster]# cp /usr/local/redis-6.2.4/redis.conf /usr/local/redis-cluster/7003/ -r [root@localhost redis-cluster]# cp /usr/local/redis-6.2.4/redis.conf /usr/local/redis-cluster/7004/ -r [root@localhost redis-cluster]# cp /usr/local/redis-6.2.4/redis.conf /usr/local/redis-cluster/7005/ -r2、修改配置文件#注释掉 bind 127.0.0.1 -::1 #后台启动 daemonize yes # 允许外部访问 protected-mode no #修改端口号,从7000到7005 port 7000 #指定启动的pid文件 7000-7005 pidfile /var/run/redis_7000.pid #日志路径 引号不要丢 7000-7005 logfile "usr/local/redis-cluster/7000/redis.log" #修改rdb文件,为后面的扩容使用,因为是同一台虚拟机,不在同一个虚拟机可以不用改 7000-7005 dbfilename dump_7000.rdb #修改密码 requirepass xiaojie #集群的密码,不然节点切换的时候需要输入密码 masterauth xiaojie #开启集群 cluster-enabled yes #自动生成文件 7000-7005 cluster-config-file nodes-7000.conf 3、启动集群启动脚本 授权 chmod +x startall.sh /usr/local/redis/bin/redis-server /usr/local/redis-cluster/7000/redis.conf /usr/local/redis/bin/redis-server /usr/local/redis-cluster/7001/redis.conf /usr/local/redis/bin/redis-server /usr/local/redis-cluster/7002/redis.conf /usr/local/redis/bin/redis-server /usr/local/redis-cluster/7003/redis.conf /usr/local/redis/bin/redis-server /usr/local/redis-cluster/7004/redis.conf /usr/local/redis/bin/redis-server /usr/local/redis-cluster/7005/redis.conf4、创建集群#无密码 cluster-replicas 1 主从节点1:1分配 ./redis-cli --cluster create 192.168.139.154:7000 192.168.139.154:7001 192.168.139.154:7002 192.168.139.154:7003 192.168.139.154:7004 192.168.139.154:7005 --cluster-replicas 1 #有密码 ./redis-cli --cluster create 192.168.139.154:7000 192.168.139.154:7001 192.168.139.154:7002 192.168.139.154:7003 192.168.139.154:7004 192.168.139.154:7005 --cluster-replicas 1 -a xiaojie 输入yes接收卡槽分配。 看到这个图说明 集群安装成功了。cluster nodes 指令查看集群节点测试 [root@localhost bin]# ./redis-cli -p 7000 -c -a 'xiaojie' OK搭建完成。Docker方式1、拉取镜像[root@localhost bin]# docker pull redis2、创建文件[root@localhost local]# mkdir docker-redis-cluster 3、创建redis-cluster.tmpl文件 #端口 port ${PORT} #集群密码 masterauth xiaojie #redis密码 requirepass xiaojie #开启集群 cluster-enabled yes #配置文件 cluster-config-file nodes.conf cluster-node-timeout 5000 #集群通讯的ip如果在外网访问,需要填写服务器的公网ip cluster-announce-ip 192.168.6.137 ##集群节点的汇报port,防止nat cluster-announce-port ${PORT} #集群节点的汇报bus-port,防止nat cluster-announce-bus-port 1${PORT} #开启aof appendonly yes4、创建data和conf文件 [root@localhost docker-redis-cluster]# for port in `seq 9000 9005`; do mkdir -p ./${port}/conf && PORT=${port} envsubst < ./redis-cluster.tmpl > ./${port}/conf/redis.conf && mkdir -p ./${port}/data; done 5、创建Redis容器[root@localhost docker-redis-cluster]# for port in `seq 9000 9005`; do docker run -d --net=host -v /usr/local/docker-redis-cluster/${port}/conf/redis.conf:/etc/redis/redis.conf -v /usr/local/docker-redis-cluster/${port}/data:/data --restart always --name=redis-${port} redis redis-server /etc/redis/redis.conf; done6、进入任一容器创建集群#进入容器 [root@localhost docker-redis-cluster]# docker exec -it 9e36f2ab4ae7 bash #创建集群 root@localhost:/data# redis-cli --cluster create 192.168.6.137:9000 192.168.6.137:9001 192.168.6.137:9002 192.168.6.137:9003 192.168.6.137:9004 192.168.6.137:9005 --cluster-replicas 1 -a xiaojie7、测试root@localhost:/data# redis-cli -p 9000 -c -a 'xiaojie' Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe. 192.168.6.137:9002> set docker docker -> Redirected to slot [9730] located at 192.168.6.137:9001 OK 192.168.6.137:9001> get docker "docker" 192.168.6.137:9001> 三、集群扩容集群扩容就是在原来的基础上新增集群节点,而不需要重新启动服务器。简单明白的说,就是新增一个服务器,然后从其他节点上分给它一些卡槽和数据(如果卡槽上有数据则连带数据一起分给新增的节点)。添加两个配置文件,在下面源码地址上有新增主节点#启动新增的连个节点 [root@localhost bin]# ./redis-server /usr/local/redis-cluster/7006/redis.conf [root@localhost bin]# ./redis-server /usr/local/redis-cluster/7007/redis.conf #新增主节点 [root@localhost bin]# /usr/local/redis/bin/redis-cli --cluster add-node 192.168.139.154:7006 192.168.139.154:7000 -a xiaojie #没有密码则不用-a xiaojie 可以看到新增的节点是没有卡槽的。下面我们为新增的主节点分配从节点[root@localhost bin]# ./redis-cli --cluster add-node 192.168.139.154:7007 192.168.139.154:7000 --cluster-slave --cluster-master-id 9493cb0e7ee7b35da0c9078e9a99e5b577de9f1a -a xiaojie #master-id 为7006节点对应的id可以看到我们为7006新增从节点 输入cluster nodes 检查集群状态,如上图,还没有分配卡槽下面分配卡槽#这个客户端可以连接任何一个节点,不限于7000 [root@localhost bin]# ./redis-cli --cluster reshard 192.168.139.154:7000 -a xiaojie 你想移动多少卡槽? 16384/4=4096 所以我们想要移动4096个卡槽到7006节点上,然后就会从其他的三个主节(从节点上是没有卡槽的)点上分给7006。所以输入4096 输入对应的id,这里我们选择all 再接着选择yes等待分配完卡槽就好啦。右上图可见,已经给我们分配完卡槽了。 四、集群缩容集群缩容,正好和扩容相反,就是在不重启服务器的情况 下移除服务节点。 我们选择将7006卡槽全部给7001,注意:移除节点时,一定要确保,所有的卡槽已经移除到别的主节点上。[root@localhost bin]# ./redis-cli --cluster reshard 192.168.139.154:7000 --cluster-from 9493cb0e7ee7b35da0c9078e9a99e5b577de9f1a -a xiaojie --cluster-to d5ac3163a51a0b7e70e777d8ff137a4ba901d611 --cluster-slots -a xiaojie五、SpringBoot整合Redis Clusterspring: jackson: time-zone: GMT+8 date-format: yyyy-MM-dd HH:mm:ss datasource: name: my-test url: jdbc:mysql://127.0.0.1:3306/my_test?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai username: root password: root type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver redis: cluster: nodes: #我连接的是Docker安装的集群 - 192.168.6.137:9000 - 192.168.6.137:9001 - 192.168.6.137:9002 - 192.168.6.137:9003 - 192.168.6.137:9004 - 192.168.6.137:9005 password: xiaojie connect-timeout: 5000 # password: xiaojie #这个密码一定要加,然后才能保证能连接到redis服务器,如果没有配置密码则不需要 # connect-timeout: 5000 # database: 0 # sentinel: # master: mymaster # nodes: 192.168.6.137:26379,192.168.6.137:26380,192.168.6.137:26381 # password: xiaojie #这个密码是哨兵的密码,如果在sentinel.conf中没有配置requirepass则不需要完整代码和集群配置文件:spring-boot: Springboot整合redis、消息中间件等相关代码 参考:REDIS cluster-spec -- Redis中文资料站 -- Redis中国用户组(CRUG)docker部署redis集群 - 桥头堡洗脚城 - 博客园
正文一、Redis主从复制 主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者为主节点(master),后者称为从节点(slave);数据的复制只能由主节点到从节点。主从复制保证了数据的备份,高可用(配合哨兵机制)、负载均衡(读写分离分担master压力)。Redis主从数据同步有两种方式,(full resync)全量复制和增量复制(partial resync)full resync(全量复制)Redis通过psync命令进行全量复制的过程如下:从节点判断无法进行部分复制,向主节点发送全量复制的请求;或从节点发送部分复制的请求,但主节点判断无法进行部分复制,就进行全量复制。主节点收到全量复制的命令后,执行bgsave,在后台生成RDB文件,并使用一个缓冲区(称为复制缓冲区)记录从现在开始执行的所有写命令主节点的bgsave执行完成后,将RDB文件发送给从节点;从节点首先清除自己的旧数据,然后载入接收的RDB文件,将数据库状态更新至主节点执行bgsave时的数据库状态主节点将前述复制缓冲区中的所有写命令发送给从节点,从节点执行这些写命令,将数据库状态更新至主节点的最新状态如果从节点开启了AOF,则会触发bgrewriteaof的执行,从而保证AOF文件更新至主节点的最新状态partial resync(增量复制)由于网络原因导致主从服务器断开连接,当主从重新连接之后,不需要全量复制,只需要进行增量复制。因为主从服务器都会维持一个offset(偏移量),当连接恢复之后,对比两者的偏移量,把不同的数据同步过来。在命令传播阶段,主节点除了将写命令发送给从节点,还会发送一份给积压缓冲区,作为写命令的备份;除了存储写命令,积压缓冲区中还存储了其中的每个字节对应的复制偏移量(offset)。由于复制积压缓冲区定长且是先进先出(队列),所以它保存的是主节点最近执行的写命令;时间较早的写命令会被挤出缓冲区。由于该缓冲区长度固定且有限,因此可以备份的写命令也有限,当主从节点offset的差距过大超过缓冲区长度时,将无法执行部分复制,只能执行全量复制。反过来说,为了提高网络中断时部分复制执行的概率,可以根据需要增大复制积压缓冲区的大小(通过配置repl-backlog-size 1M 默认是1M);如果offset偏移量之后的数据,仍然都在复制积压缓冲区里,则执行部分复制;如果offset偏移量之后的数据已不在复制积压缓冲区中(数据已被挤出),则执行全量复制。每个Redis节点(无论主从),在启动时都会自动生成一个随机ID(每次启动都不一样),由40个随机的十六进制字符组成;runid用来唯一识别一个Redis节点。通过info Server命令,可以查看节点的runid。主从节点初次复制时,主节点将自己的runid发送给从节点,从节点将这个runid保存起来;当断线重连时,从节点会将这个runid发送给主节点;主节点根据runid判断能否进行部分复制:如果从节点保存的runid与主节点现在的runid相同,说明主从节点之前同步过,主节点会继续尝试使用部分复制(到底能不能部分复制还要看offset和复制积压缓冲区的情况);如果从节点保存的runid与主节点现在的runid不同,说明从节点在断线前同步的Redis节点并不是当前的主节点,只能进行全量复制。二、Redis主从复制配置配置方式有两种,这种方式不推荐,如果从服务器过多,数据同步效率很差采用树状结构 配置主从复制由于我是在一台虚拟机上模拟所以需要修改响应的端口号为6379,6380,6381 修改响应的 pidfile /var/run/redis_6379.pid,pidfile /var/run/redis_6380.pid,pidfile /var/run/redis_6381.pid在6380配置文件添加指向主节点slaveof 192.168.139.154 6379 #密码 masterauth xiaojie在6381上修改配置文件如下slaveof 192.168.139.154 6380 masterauth xiaojie 启动redis实例#由于是同一台虚拟机,启动时候需要指定端口,不然默认是6379 [root@bogon bin]# ./redis-cli -p 6379 [root@bogon bin]# ./redis-cli -p 6380 [root@bogon bin]# ./redis-cli -p 6381输入指令127.0.0.1:6379> info replication 此时我们Redis主从复制已经搭建好了,在6379写入数据,其他两个节点能同步数据,从节点从2.6版本之后默认只能读不能写,这个可以配置replica-read-only yes(yes只能读不能写)。这个时候如果主节点宕机之后,从节点不能写,然后服务就不能用了,于是引入哨兵机制。 三、哨兵机制原理Redis 的 Sentinel 系统用于管理多个 Redis 服务器(instance), 该系统执行以下三个任务:监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器。Redis Sentinel 是一个分布式系统, 你可以在一个架构中运行多个 Sentinel 进程(progress), 这些进程使用流言协议(gossip protocols)来接收关于主服务器是否下线的信息, 并使用投票协议(agreement protocols)来决定是否执行自动故障迁移, 以及选择哪个从服务器作为新的主服务器。每个 Sentinel 都需要定期执行的任务每个 Sentinel 以每秒钟一次的频率向它所知的主服务器、从服务器以及其他 Sentinel 实例发送一个 PING 命令。如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 那么这个实例会被 Sentinel 标记为主观下线(subjectively down,简称 SDOWN )。 一个有效回复可以是: +PONG 、 -LOADING 或者 -MASTERDOWN 。如果一个主服务器被标记为主观下线, 那么正在监视这个主服务器的所有 Sentinel 要以每秒一次的频率确认主服务器的确进入了主观下线状态。如果一个主服务器被标记为主观下线, 并且有足够数量的 Sentinel (至少要达到配置文件指定的数量)在指定的时间范围内同意这一判断, 那么这个主服务器被标记为客观下线(objectively down, 简称 ODOWN)。在一般情况下, 每个 Sentinel 会以每 10 秒一次的频率向它已知的所有主服务器和从服务器发送 INFO 命令。 当一个主服务器被 Sentinel 标记为客观下线时, Sentinel 向下线主服务器的所有从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。当没有足够数量的 Sentinel 同意主服务器已经下线, 主服务器的客观下线状态就会被移除。 当主服务器重新向 Sentinel 的 PING 命令返回有效回复时, 主服务器的主观下线状态就会被移除。自动发现 Sentinel 和从服务器一个 Sentinel 可以与其他多个 Sentinel 进行连接, 各个 Sentinel 之间可以互相检查对方的可用性, 并进行信息交换。你无须为运行的每个 Sentinel 分别设置其他 Sentinel 的地址, 因为 Sentinel 可以通过发布与订阅功能来自动发现正在监视相同主服务器的其他 Sentinel , 这一功能是通过向频道 sentinel:hello 发送信息来实现的。与此类似, 你也不必手动列出主服务器属下的所有从服务器, 因为 Sentinel 可以通过(info)询问主服务器来获得所有从服务器的信息。每个 Sentinel 会以每两秒一次的频率, 通过发布与订阅功能, 向被它监视的所有主服务器和从服务器的 sentinel:hello 频道发送一条信息, 信息中包含了 Sentinel 的 IP 地址、端口号和运行 ID (runid)。每个 Sentinel 都订阅了被它监视的所有主服务器和从服务器的 sentinel:hello 频道, 查找之前未出现过的 sentinel (looking for unknown sentinels)。 当一个 Sentinel 发现一个新的 Sentinel 时, 它会将新的 Sentinel 添加到一个列表中, 这个列表保存了 Sentinel 已知的, 监视同一个主服务器的所有其他 Sentinel 。Sentinel 发送的信息中还包括完整的主服务器当前配置(configuration)。 如果一个 Sentinel 包含的主服务器配置比另一个 Sentinel 发送的配置要旧, 那么这个 Sentinel 会立即升级到新配置上。在将一个新 Sentinel 添加到监视主服务器的列表上面之前, Sentinel 会先检查列表中是否已经包含了和要添加的 Sentinel 拥有相同运行 ID 或者相同地址(包括 IP 地址和端口号)的 Sentinel , 如果是的话, Sentinel 会先移除列表中已有的那些拥有相同运行 ID 或者相同地址的 Sentinel(自己发布的消息) , 然后再添加新 Sentinel 。Sentinel 选主规则在失效主服务器属下的从服务器当中, 那些被标记为主观下线、已断线、或者最后一次回复PING 命令的时间大于五秒钟的从服务器都会被淘汰。在失效主服务器属下的从服务器当中, 那些与失效主服务器连接断开的时长超过 down-after 选项指定的时长十倍的从服务器都会被淘汰。在经历了以上两轮淘汰之后剩下来的从服务器中, 我们选出复制偏移量(replication offset)最大的那个从服务器作为新的主服务器; 如果复制偏移量不可用, 或者从服务器的复制偏移量相同, 那么带有最小运行 ID(runid) 的那个从服务器成为新的主服务器。四、哨兵机制配置复制redis解压文件中的sentinel.conf文件到安装目录的bin下面[root@bogon bin]# cp /usr/local/redis-6.2.5/sentinel.conf /usr/local/redis6379/bin/ [root@bogon bin]# cp /usr/local/redis-6.2.5/sentinel.conf /usr/local/redis6380/bin/ [root@bogon bin]# cp /usr/local/redis-6.2.5/sentinel.conf /usr/local/redis6381/bin/ [root@bogon bin]# 修改sentinel.conf#后台启动 daemonize yes #修改端口 port 26379-26381 #指定pid pidfile /var/run/redis-sentinel-6379.pid #指定监听的主节点 2是至少有2个哨兵认为宕机的个数 sentinel monitor mymaster 192.168.139.154 6379 2 #指定主节点密码 sentinel auth-pass mymaster xiaojie #指定哨兵的密码 requirepass xiaojie #打开这个注释 protected-mode no 启动哨兵./redis-sentinel sentinel.conf 然后手动宕机6379服务节点。启动6379的节点再检查info replication 此时6380节点被选举为master,6379原来的master成为了slave。五、哨兵机制整合SpringBoot来 上才艺,不对 !上代码pom文件<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.xiaojie</groupId> <artifactId>springboot-redis</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <parent> <artifactId>spring-boot-starter-parent</artifactId> <groupId>org.springframework.boot</groupId> <version>2.4.2</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.4</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.12</version> </dependency> <!--序列化--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.47</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.10.4</version> </dependency> <!--redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <version>2.2.7.RELEASE</version> </dependency> <!--lombok--> <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.20</version> <scope>provided</scope> </dependency> </dependencies> </project>配置文件spring: jackson: time-zone: GMT+8 date-format: yyyy-MM-dd HH:mm:ss datasource: name: iot-home url: jdbc:mysql://127.0.0.1:3306/my_test?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai username: root password: root type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver redis: password: xiaojie #这个密码一定要加,然后才能保证能连接到redis服务器,如果没有配置密码则不需要 connect-timeout: 5000 database: 0 sentinel: master: mymaster nodes: 192.168.6.137:26379,192.168.6.137:26380,192.168.6.137:26381 password: xiaojie #这个密码是哨兵的密码,如果在sentinel.conf中没有配置requirepass则不需要核心代码 package com.xiaojie.config; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.net.UnknownHostException; /* * * @param null * @redis配置 * @author xiaojie * @date 2021/9/8 * @return */ @Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); StringRedisSerializer stringRedisSerializer=new StringRedisSerializer(); redisTemplate.setKeySerializer(stringRedisSerializer); redisTemplate.setHashKeySerializer(stringRedisSerializer); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer=new Jackson2JsonRedisSerializer(Object.class); ObjectMapper objectMapper=new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance , ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); redisTemplate.afterPropertiesSet(); return redisTemplate; } } public User getUserByName(String name) { JSONObject obj= (JSONObject) redisUtil.get(USERKEY + ":" + name); if (null==obj){ System.out.println("缓存中没有该值,查询数据库"); User resultUser = userMapper.selectByName(name); if (null!= resultUser) { redisUtil.set(USERKEY+":"+resultUser.getName(), JSONObject.toJSON(resultUser),30); return resultUser; } } User user = JSONObject.toJavaObject(obj,User.class); return user; }测试:宕机主节点,然后还能继续写入缓存,搭建完成。 完整代码 :spring-boot: Springboot整合redis、消息中间件等相关代码参考:深入学习Redis(3):主从复制 - 编程迷思 - 博客园REDIS sentinel-old -- Redis中国用户组(CRUG)
正文一、Redis的淘汰策略由于Redis的数据存放在内存中,假如Redis一直往内存(内存又称主。它是CPU能直接寻址的存存储空间,由半导体器件制成。特点是存取速率快)中存值,总有一天,你的内存会被占满,这将是一个悲剧,所以Redis设置了淘汰策略。Redis6.2.5有8种淘汰策略volatile-lru(Least Recently Used(最近最少使用):在设置了过期时间的键空间中,优先移除最近未使用的key。allkeys-lru(Least Recently Used(最近最少使用):在所有的key中,优先移除最近未使用的key。(推荐)volatile-lfu(Least Frequently Used(使用频率最低)):在设置了过期时间的键空间中,优先移除最近使用频率最低的key。allkeys-lfu(Least Frequently Used(使用频率最低)):在所有的key中,优先移除最近使用频率最低的key。volatile-random:在设置了过期时间的键空间中,随机删除key。allkeys-random:在所有的键空间中,随机删除key。volatile-ttl:删除过期时间最近的key(离过期时间最短的key)。noeviction:不删除任何key,只返回写操作错误。(默认这种模式)1、2、3、4、7的淘汰算法是随机算法。注意:对于有的键值,在以上的淘汰策略中都无法淘汰,那么Redis将在需要更多的内存。这些命令通常用于创建新键、添加数据或修改现有键。例如:SET、INCR、HSET、LPUSH、SUNIONSTORE、SORT(由于STORE参数)和EXEC(如果事务包含任何需要内存的命令)。设置Redis淘汰策略如果在noeviction这种模式下不用设置阈值。如果在其他的模式下,建议设置较低的maxmemory ,如果超过了这个阈值,只是写的操作会报错,例如set、lpush、hmset这些写的的指令会报错,但是get指令可以正常使用。只需要修改配置文件redis.conf中修改阈值:maxmemory 200M修改淘汰策略方式:maxmemory-policy allkeys-lru二、Redis的过期策略定时过期在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除。就是我们通常使用的expire(key,time)方法。这种方式的优点是,当键值过期时,内存能够及时的被释放,但是缺点是,如果同时有大量的键值同时过期(缓存雪崩),需要更多的CPU内存去删除这些键值,删除这些key会占用很多的CPU时间,很严重的影响性能。惰性过期key过期的时候不删除,每次通过key获取值的时候去检查是否过期,若过期,则删除,返回null(用的时候再检查删除)。这种方式的优点是删除操作只发生在通过key取值的时候发生,而且只删除当前key,所以对CPU时间的占用是比较少的,但这种的缺点是,如果有些键值一直不使用,那么就会一直占用内存。定期过期每隔一段时间执行一次删除过期key操作。这种方式的优点是通过限制删除操作的时长和频率,来减少删除操作对CPU时间的占用,既不会像定时删除占用CPU也不会像惰性删除那样占内存。active-expire-effort 1(取值范围1-10,值越大,占用CPU越多)。Redis使用的过期策略是定:定期过期+惰性过期三、过期回调(监听键值失效)notify-keyspace-events Ex 打开这个注释开启键值失效的配置,添加监听类package com.xiaojie.listener; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.listener.RedisMessageListenerContainer; @Configuration public class RedisListenerConfig { @Bean RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(connectionFactory); return container; } }package com.xiaojie.listener; import org.springframework.data.redis.connection.Message; import org.springframework.data.redis.listener.KeyExpirationEventMessageListener; import org.springframework.data.redis.listener.RedisMessageListenerContainer; import org.springframework.stereotype.Component; @Component public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener { public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) { super(listenerContainer); } /** * 使用该方法监听 当我们都key失效的时候执行该方法 * @param message * @param pattern */ @Override public void onMessage(Message message, byte[] pattern) { String expiraKey = message.toString(); System.out.println("我已经监测到键值失效啦键值是"+expiraKey+"你可以执行你的业务了"); } }测试1. 127.0.0.1:6379> set xiaojie xiaojie EX 10 2. OK
正文一、Redis事务传统数据库的特性Atomicity(原子性):一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被恢复(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。 Isolation(隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。Redis的事务经过MULTI指令开启事务,然后一系列的指令(EXEC、DISCARD、MULTI 和 WATCH这几个命令除外)进入队列,然后经过EXEC提交事务。Redis事务是一次性、顺序性、排他性的执行一个队列中的一系列命令。语法错误时,事务并不会提交(这样看事务是原子性)127.0.0.1:6379> multi #开启事务 OK 127.0.0.1:6379(TX)> set a a QUEUED 127.0.0.1:6379(TX)> set b b QUEUED 127.0.0.1:6379(TX)> zset c c #语法错误 (error) ERR unknown command `zset`, with args beginning with: `c`, `c`, 127.0.0.1:6379(TX)> exec #提交事务,所有的指令并不会执行,即使正确的指令也不会执行 (error) EXECABORT Transaction discarded becaus e of previous errors. 127.0.0.1:6379> get a #值没有改变 "1" 运行错误,这种错误在实际执行之前Redis是无法发现的,如果事务里的一条命令出现了运行错误,事务里其他的命令依然会继续执行。(这么看是不支持原子性的)。我偏向这么理解单个Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚(不支持事务回滚),也不会影响后面指令的正常执行。127.0.0.1:6379> multi OK 127.0.0.1:6379(TX)> set a 1 QUEUED 127.0.0.1:6379(TX)> sadd b 2 QUEUED 127.0.0.1:6379(TX)> set c 3 QUEUED 127.0.0.1:6379(TX)> exec 1) OK 2) (error) WRONGTYPE Operation against a key holding the wrong kind of value 3) OK 127.0.0.1:6379> get c "3" DISCARD取消事务,也就是清空事务队列里的指令。127.0.0.1:6379> multi OK 127.0.0.1:6379(TX)> set a a QUEUED 127.0.0.1:6379(TX)> set b b QUEUED 127.0.0.1:6379(TX)> discard #取消事务,清空事务队列 OK 127.0.0.1:6379> set a a OKWATCH 命令用于在事务开始之前监视任意数量的键: 当调用EXEC命令执行事务时, 如果任意一个被监视的键已经被其他客户端修改了, 那么整个事务不再执行, 直接返回失败127.0.0.1:6379> watch a #初始值是100 OK 127.0.0.1:6379> multi OK 127.0.0.1:6379(TX)> set a 200 QUEUED #在这个时候,我用redis客户端,或者重新连接一个客户端,将a值修改之后 127.0.0.1:6379(TX)> get a QUEUED 127.0.0.1:6379(TX)> exec #事务执行失败 (nil) 127.0.0.1:6379> 二、Redis存放二进制对象StringRedisTemplate 这个类是存放String类型的。RedisTemplate 这个类存放的是二进制类型。 实体对象实现序列化,启动类加上@EnableCaching @Cacheable(cacheNames = "users", key = "'getListUsers'") @RequestMapping("/getListUsers") public List<User> getListUsers() { List<User> all = userMapper.findAll(); return all; }三、Redis持久化 RDB方式 默认开启 文件名称是 dump.rdbRedis6.0版本save 3600 1 在3600秒(一个小时)之后,如果至少有1个key发生变化,则dump内存快照。save 300 100 在300秒(5分钟)之后,如果至少有100个key发生变化,则dump内存快照。 save 60 10000 在60秒之后,如果至少有10000个key发生变化,则dump内存快照。AOF开启方式 修改配置文件 appendonly yes 文件名称appendonly.aof# appendfsync always 每次有数据修改发生时都会写入AOF文件,能够保证数据不丢失,但是效率非常低。appendfsync everysec (官方推荐)每秒钟同步一次,可能会丢失1s内的数据,但是效率非常高# appendfsync no 效率高但是不会持久化数据两者优缺点RDB存在哪些优势呢?一旦采用该方式,那么你的整个Redis数据库将只包含一个文件,这对于文件备份而言是非常完美的。比如,你可能打算每个小时归档一次最近24小时的数据,同时还要每天归档一次最近30天的数据。通过这样的备份策略,一旦系统出现灾难性故障,我们可以非常容易的进行恢复。对于灾难恢复而言,RDB是非常不错的选择。因为我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。性能最大化。对于Redis的服务进程而言,在开始持久化时,它唯一需要做的只是fork出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行IO操作了。相比于AOF机制,如果数据集很大,RDB的启动效率会更高。RDB又存在哪些劣势呢?如果你想保证数据的高可用性,即最大限度的避免数据丢失,那么RDB将不是一个很好的选择。因为系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。AOF的优势有哪些呢?该机制可以带来更高的数据安全性,即数据持久性。Redis中提供了3中同步策略,即每秒同步、每修改同步和不同步。事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会被立即记录到磁盘中。可以预见,这种方式在效率上是最低的。至于无同步,无需多言,我想大家都能正确的理解它。由于该机制对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。然而如果我们本次操作只是写入了一半数据就出现了系统崩溃问题,不用担心,在Redis下一次启动之前,我们可以通过redis-check-aof工具来帮助我们解决数据一致性的问题。如果日志过大,Redis可以自动启用rewrite机制。即Redis以append模式不断的将修改数据写入到老的磁盘文件中,同时Redis还会创建一个新的文件用于记录此期间有哪些修改命令被执行。因此在进行rewrite切换时可以更好的保证数据安全性。AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。事实上,我们也可以通过该文件完成数据的重建。AOF的劣势有哪些呢?对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。根据同步策略的不同,AOF在运行效率上往往会慢于RDB。总之,每秒同步策略的效率是比较高的,同步禁用策略的效率和RDB一样高效。总结:RDB快照是紧凑压缩的二进制文件相对于aof文件要小,存储效率高。内部存储的时redis在某个时间点的数据快照,非常适合数据备份,全量复制等场景。数据恢复的速度比AOF快得多。缺点是存在丢失数据的风险,bgsave指令每次运行要执行fork操作创建子进程,要牺牲一些性能。AOF数据存储相对安全,即使丢失数据也只是丢失1秒的数据,数据量大时,会使用rewrite机制,恢复数据,恢复数据期间会生成一个新的文件来生成新的操作记录,数据恢复完之后,也会将新生成的操作记录恢复到内存中去。缺点是文件相对较大,恢复效率较低。参考 :https://www.cnblogs.com/chenliangcl/p/7240350.html
正文一、什么是RedisRedis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。从2010年3月15日起,Redis的开发工作由VMware主持。从2013年5月开始,Redis的开发由Pivotal赞助。——百度百科二、Redis线程模型Redis的底层采用Nio中的多路IO复用的机制,能够非常好的支持这样的并发,从而保证线程安全问题。Redis单线程,也就是底层采用一个线程维护多个不同的客户端io操作。但是Nio在不同的操作系统上实现的方式有所不同,在我们windows操作系统使用select实现轮训时间复杂度是为o(n),而且还存在空轮训的情况,效率非常低, 其次是默认对我们轮训的数据有一定限制,所以支持上万的tcp连接是非常难。所以在linux操作系统采用epoll实现事件驱动回调,不会存在空轮训的情况,只对活跃的 socket连接实现主动回调这样在性能上有大大的提升,所以时间复杂度是为o(1)。注意:windows操作系统是没有epoll,只有linux系统才有epoll。redis单线程模型中最为核心的就是文件事件处理器。 这块理解的不是很深,仅供参考三、Redis应用场景Token令牌的生成 在分布式场景中可以将Token令牌存放到redis中。短信验证码Code。 Redis可以设置过期时间,过期后自动删除,或者验证过验证码之后,删除redis的键值。缓存查询数据。减少数据库的操作,增加程序执行效率等等。计数器 由于Redis是原子性的,所以可以用做技术器redisTemplate.opsForValue().increment("key", 1);分布式锁 Redisson发布订阅(很少使用)四、Redis优缺点 优点1、Redis具有缓存功能,缓存就是直接操作内存,内存的读写速度极快。数据库按存储方式可分为:硬盘数据库和内存数据库。Redis 将数据储存在内存里面,读写数据的时候都不会受到硬盘 I/O 速度的限制,所以速度极快。2、Redis采用单线程,避免了不必要的上下文切换和竞争。不用考虑各种锁的问题,不存在加锁释放锁操作,不会因死锁而导致的性能消耗。3、官方数据表示Redis读的速度是110000次/s,写的速度是81000次/s 。 缺点1、只能使用CPU一个核。2、如果删除的键过大(比如Set类型中有上百万个对象),会导致服务端阻塞好几秒。3、QPS难再提高。五、Redis安装传统方式1、上传安装包到linux2、进入/usr/local创建安装目录[root@bogon local]# mkdir redis3、 解压文件到指定目录tar -zxvf redis-6.2.5.tar.gz -C /usr/local/4、进入解压目录安装root@bogon redis-6.2.5]# make install PREFIX=/usr/local/redis5、配置后台启动[root@bogon bin]# cp ../../redis-6.2.5/redis.conf /usr/local/redis/bin/ 编辑配置文件vim redis.conf 修改为yes daemonize yes 6、配置密码&远程访问#配置文件注释掉这个 requirepass xiaojie #开启远程ip访问 注释掉bind 127.0.0.1 protected-mode no ###允许外界访问7、启动[root@bogon bin]# ./redis-server redis.confDocker方式https://blog.csdn.net/weixin_39555954/article/details/117200004六、Redis指令127.0.0.1:6379> set a 10 (error) NOAUTH Authentication required. 127.0.0.1:6379> auth xiaojie #密码登录 OK 127.0.0.1:6379> set a 10 #设置值 OK 127.0.0.1:6379> get a #取值 "10" 127.0.0.1:6379> lpush name xiaoming xiaohong xiaoli xiaolan #list结构 (integer) 4 127.0.0.1:6379> lrange name 0 10 #取值 0起始位置,10结束为止,类似分页 1) "xiaolan" 2) "xiaoli" 3) "xiaohong" 4) "xiaoming" 127.0.0.1:6379> hmset music jazz mercy #hash结构数据 OK 127.0.0.1:6379> hmget music jazz 1) "mercy" 127.0.0.1:6379> sadd nba Lakers Heat #set结构 (integer) 2 127.0.0.1:6379> smembers nba #取值 1) "Heat" 2) "Lakers" 127.0.0.1:6379> zadd zaddlist 1 redis #zset有序集合 (integer) 1 127.0.0.1:6379> zadd zaddlist 2 java (integer) 1 127.0.0.1:6379> zrange zaddlist 0 3 #取值 根据最大最小排序score排序 1) "redis" 2) "java" 127.0.0.1:6379> 七、Redis数据结构String类型:String是redis组基本的数据类型一个键最大能存储512MB。Hash类型、List类型、Set类型 、Sorted-Sets。package com.xiaojie.util; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; @Component public class RedisUtil { @Autowired private RedisTemplate redisTemplate; /** * 指定缓存失效时间 * @param key 键 * @param time 时间(秒) * @return */ public boolean expire(String key, long time) { try { if (time > 0) { redisTemplate.expire(key, time, TimeUnit.SECONDS); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 根据key 获取过期时间 * @param key 键 不能为null * @return 时间(秒) 返回0代表为永久有效 */ public long getExpire(String key) { return redisTemplate.getExpire(key, TimeUnit.SECONDS); } /** * 判断key是否存在 * @param key 键 * @return true 存在 false不存在 */ public boolean hasKey(String key) { try { return redisTemplate.hasKey(key); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 删除缓存 * @param key 可以传一个值 或多个 */ @SuppressWarnings("unchecked") public void del(String... key) { if (key != null && key.length > 0) { if (key.length == 1) { redisTemplate.delete(key[0]); } else { redisTemplate.delete(CollectionUtils.arrayToList(key)); } } } /****************** common end ****************/ /****************** String start ****************/ /** * 普通缓存获取 * @param key 键 * @return 值 */ public Object get(String key) { return key == null ? null : redisTemplate.opsForValue().get(key); } /** * 普通缓存放入 * @param key 键 * @param value 值 * @return true成功 false失败 */ public boolean set(String key, Object value) { try { redisTemplate.opsForValue().set(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 普通缓存放入并设置时间 * @param key 键 * @param value 值 * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期 * @return true成功 false 失败 */ public boolean set(String key, Object value, long time) { try { if (time > 0) { redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); } else { set(key, value); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 递增 * @param key 键 * @param delta 要增加几(大于0) * @return */ public long incr(String key, long delta) { if (delta < 0) { throw new RuntimeException("递增因子必须大于0"); } return redisTemplate.opsForValue().increment(key, delta); } /** * 递减 * @param key 键 * @param delta 要减少几(小于0) * @return */ public long decr(String key, long delta) { if (delta < 0) { throw new RuntimeException("递减因子必须大于0"); } return redisTemplate.opsForValue().increment(key, -delta); } /****************** String end ****************/ /****************** Map start ****************/ /** * HashGet * @param key 键 不能为null * @param item 项 不能为null * @return 值 */ public Object hget(String key, String item) { return redisTemplate.opsForHash().get(key, item); } /** * 获取hashKey对应的所有键值 * @param key 键 * @return 对应的多个键值 */ public Map<Object, Object> hmget(String key) { return redisTemplate.opsForHash().entries(key); } /** * HashSet * @param key 键 * @param map 对应多个键值 * @return true 成功 false 失败 */ public boolean hmset(String key, Map<String, Object> map) { try { redisTemplate.opsForHash().putAll(key, map); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * HashSet 并设置时间 * @param key 键 * @param map 对应多个键值 * @param time 时间(秒) * @return true成功 false失败 */ public boolean hmset(String key, Map<String, Object> map, long time) { try { redisTemplate.opsForHash().putAll(key, map); if (time > 0) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 向一张hash表中放入数据,如果不存在将创建 * @param key 键 * @param item 项 * @param value 值 * @return true 成功 false失败 */ public boolean hset(String key, String item, Object value) { try { redisTemplate.opsForHash().put(key, item, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 向一张hash表中放入数据,如果不存在将创建 * @param key 键 * @param item 项 * @param value 值 * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间 * @return true 成功 false失败 */ public boolean hset(String key, String item, Object value, long time) { try { redisTemplate.opsForHash().put(key, item, value); if (time > 0) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 删除hash表中的值 * @param key 键 不能为null * @param item 项 可以使多个 不能为null */ public void hdel(String key, Object... item) { redisTemplate.opsForHash().delete(key, item); } /** * 判断hash表中是否有该项的值 * @param key 键 不能为null * @param item 项 不能为null * @return true 存在 false不存在 */ public boolean hHasKey(String key, String item) { return redisTemplate.opsForHash().hasKey(key, item); } /** * hash递增 如果不存在,就会创建一个 并把新增后的值返回 * @param key 键 * @param item 项 * @param by 要增加几(大于0) * @return */ public double hincr(String key, String item, long by) { return redisTemplate.opsForHash().increment(key, item, by); } /** * hash递减 * @param key 键 * @param item 项 * @param by 要减少记(小于0) * @return */ public double hdecr(String key, String item, long by) { return redisTemplate.opsForHash().increment(key, item, -by); } /****************** Map end ****************/ /****************** Set start ****************/ /** * 根据key获取Set中的所有值 * @param key 键 * @return */ public Set<Object> sGet(String key) { try { return redisTemplate.opsForSet().members(key); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 根据value从一个set中查询,是否存在 * @param key 键 * @param value 值 * @return true 存在 false不存在 */ public boolean sHasKey(String key, Object value) { try { return redisTemplate.opsForSet().isMember(key, value); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将数据放入set缓存 * @param key 键 * @param values 值 可以是多个 * @return 成功个数 */ public long sSet(String key, Object... values) { try { return redisTemplate.opsForSet().add(key, values); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 将set数据放入缓存 * @param key 键 * @param time 时间(秒) * @param values 值 可以是多个 * @return 成功个数 */ public long sSetAndTime(String key, long time, Object... values) { try { Long count = redisTemplate.opsForSet().add(key, values); if (time > 0) expire(key, time); return count; } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 获取set缓存的长度 * @param key 键 * @return */ public long sGetSetSize(String key) { try { return redisTemplate.opsForSet().size(key); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 移除值为value的 * @param key 键 * @param values 值 可以是多个 * @return 移除的个数 */ public long setRemove(String key, Object... values) { try { Long count = redisTemplate.opsForSet().remove(key, values); return count; } catch (Exception e) { e.printStackTrace(); return 0; } } /****************** Set end ****************/ /****************** List start ****************/ /** * 获取list缓存的内容 * @param key 键 * @param start 开始 * @param end 结束 0 到 -1代表所有值 * @return */ public List<Object> lGet(String key, long start, long end) { try { return redisTemplate.opsForList().range(key, start, end); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 获取list缓存的长度 * @param key 键 * @return */ public long lGetListSize(String key) { try { return redisTemplate.opsForList().size(key); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 通过索引 获取list中的值 * @param key 键 * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推 * @return */ public Object lGetIndex(String key, long index) { try { return redisTemplate.opsForList().index(key, index); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 将list放入缓存 * @param key 键 * @param value 值 * @return */ public boolean lSet(String key, Object value) { try { redisTemplate.opsForList().rightPush(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将list放入缓存 * @param key 键 * @param value 值 * @param time 时间(秒) * @return */ public boolean lSet(String key, Object value, long time) { try { redisTemplate.opsForList().rightPush(key, value); if (time > 0) expire(key, time); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将list放入缓存 * @param key 键 * @param value 值 * @return */ public boolean lSet(String key, List<Object> value) { try { redisTemplate.opsForList().rightPushAll(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将list放入缓存 * @param key 键 * @param value 值 * @param time 时间(秒) * @return */ public boolean lSet(String key, List<Object> value, long time) { try { redisTemplate.opsForList().rightPushAll(key, value); if (time > 0) expire(key, time); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 根据索引修改list中的某条数据 * @param key 键 * @param index 索引 * @param value 值 * @return */ public boolean lUpdateIndex(String key, long index, Object value) { try { redisTemplate.opsForList().set(key, index, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 移除N个值为value * @param key 键 * @param count 移除多少个 * @param value 值 * @return 移除的个数 */ public long lRemove(String key, long count, Object value) { try { Long remove = redisTemplate.opsForList().remove(key, count, value); return remove; } catch (Exception e) { e.printStackTrace(); return 0; } } }代码来自 https://blog.csdn.net/stonezry/article/details/106076303
正文一、Zookeeper中角色zookeeper服务器集群存在三种节点型Leader(领导者):各个节点之间的老大,是集群中的核心。没有leader集群将不能工作。所有的写请求最终都会转交给领导者Leader执行;与跟随者(Follower)和观察者(Observer)进行心跳连接;数据同步到Follower和Observer。Follower(追随者):跟随者自己不会执行写的操作,而是会将写的操作转交给Leader执行。负责处理Leader发来的请求和数据,当Leader宕机之后,会进行投票和选举(从剩余的Follwer),从而选举新的Leader。观察者(observer):Observer同Follwer一样,也不能执行写操作,他的功能跟Follwer差不多。observer为用于提高读取吞吐量,减少选举的时候而生,因此Observer不能参与投票和选举。二、Observer集群搭建配置在这个基础上修改搭建ZooKeeper3.7.0集群(传统方式&Docker方式)传统方式dataDir =/usr/local/zookeeper/data dataLogDir=/usr/local/zookeeper/logs #ip对应的是你的ip server.1=192.168.6.137:2888:3888 server.2=192.168.6.138:2888:3888 server.3=192.168.6.139:2888:3888 server.4=192.168.6.139:2888:3888:observer server.5=192.168.6.139:2888:3888:observerDocker方式#节点1 docker run -d -p 2181:2181 --name zookeeper_node01 --privileged --restart always --network zoonet --ip 172.18.0.2 \ -v /data/zookeeper/cluster/zk1/data:/data \ -v /data/zookeeper/cluster/zk1/datalog:/datalog \ -v /data/zookeeper/cluster/zk1/logs:/logs \ -e ZOO_MY_ID=1 \ -e "ZOO_SERVERS=server.1=172.18.0.2:2888:3888;2181 server.2=172.18.0.3:2888:3888;2181 server.3=172.18.0.4:2888:3888;2181 server.4=172.18.0.5:2888:3888:observer;2181 server.5=172.18.0.6:2888:3888:observer;2181" zookeeper #节点2 docker run -d -p 2182:2181 --name zookeeper_node02 --privileged --restart always --network zoonet --ip 172.18.0.3 \ -v /data/zookeeper/cluster/zk2/data:/data \ -v /data/zookeeper/cluster/zk2/datalog:/datalog \ -v /data/zookeeper/cluster/zk2/logs:/logs \ -e ZOO_MY_ID=2 \ -e "ZOO_SERVERS=server.1=172.18.0.2:2888:3888;2181 server.2=172.18.0.3:2888:3888;2181 server.3=172.18.0.4:2888:3888;2181 server.4=172.18.0.5:2888:3888:observer;2181 server.5=172.18.0.6:2888:3888:observer;2181" zookeeper #节点3 docker run -d -p 2183:2181 --name zookeeper_node03 --privileged --restart always --network zoonet --ip 172.18.0.4 \ -v /data/zookeeper/cluster/zk3/data:/data \ -v /data/zookeeper/cluster/zk3/datalog:/datalog \ -v /data/zookeeper/cluster/zk3/logs:/logs \ -e ZOO_MY_ID=3 \ -e "ZOO_SERVERS=server.1=172.18.0.2:2888:3888;2181 server.2=172.18.0.3:2888:3888;2181 server.3=172.18.0.4:2888:3888;2181 server.4=172.18.0.5:2888:3888:observer;2181 server.5=172.18.0.6:2888:3888:observer;2181" zookeeper #节点4 docker run -d -p 2184:2181 --name zookeeper_node04 --privileged --restart always --network zoonet --ip 172.18.0.5 \ -v /data/zookeeper/cluster/zk4/data:/data \ -v /data/zookeeper/cluster/zk4/datalog:/datalog \ -v /data/zookeeper/cluster/zk4/logs:/logs \ -e ZOO_MY_ID=4 \ -e PEER_TYPE=observer \ -e "ZOO_SERVERS=server.1=172.18.0.2:2888:3888;2181 server.2=172.18.0.3:2888:3888;2181 server.3=172.18.0.4:2888:3888;2181 server.4=172.18.0.5:2888:3888:observer;2181 server.5=172.18.0.6:2888:3888:observer;2181" zookeeper #节点5 docker run -d -p 2185:2181 --name zookeeper_node05 --privileged --restart always --network zoonet --ip 172.18.0.6 \ -v /data/zookeeper/cluster/zk5/data:/data \ -v /data/zookeeper/cluster/zk5/datalog:/datalog \ -v /data/zookeeper/cluster/zk5/logs:/logs \ -e ZOO_MY_ID=5 \ -e PEER_TYPE=observer \ -e "ZOO_SERVERS=server.1=172.18.0.2:2888:3888;2181 server.2=172.18.0.3:2888:3888;2181 server.3=172.18.0.4:2888:3888;2181 server.4=172.18.0.5:2888:3888:observer;2181 server.5=172.18.0.6:2888:3888:observer;2181" zookeeper 可看到新增的节点已经为Observer角色了。三、ZAB协议Zookeeper Atomic Broadcast (ZAB) :ZooKeeper并没有完全采用Paxos算法,而是使用了一种称为zookeeper原子消息广播协议的协议作为其数据一致性的核心算法。所有事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器称为Leader服务器,而余下的其他服务器则成为Follower服务器。Leader服务器负责将一个客户端事务请求转换成一个事务Proposal(提议),并将该Proposal分发给集群中所有的Follower服务器。之后Leader服务器需要等待所有Follower服务器的反馈,一旦超过半数的Follower服务器进行了正确反馈后,那么Leader就会再次向所有的Follower服务器分发Commit消息,要求其将前一个Proposal进行提交。Zookeeper 的核心是原子广播机制,这个机制保证了各个 server 之间的同步。实现这个机制的协议叫做 Zab 协议。Zab 协议有两种模式,它们分别是恢复模式和广播模式。恢复模式当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数server 完成了和 leader 的状态同步以后,退出恢复模式进入广播模式。状态同步保证了 leader 和 server 具有相同的系统状态。广播模式一旦 leader 已经和多数的 follower 进行了状态同步后,它就可以开始广播消息了,即进入广播状态。这时候当一个 server 加入 ZooKeeper 服务中,它会在恢复模式下启动,自觉的发现 leader,并和 leader 进行状态同步。待到同步结束,它也退出恢复模式,进入广播模式。ZooKeeper 服务一直维持在 Broadcast 状态,直到 leader 崩溃了或者 leader 失去了大部分的 followers 支持。四、Zookeeper数据同步1、leader 接受到消息请求后,将消息赋予给一个全局唯一的64位自增id,叫:zxid。2、leader 为每个follower 准备了一个FIFO队列(通过TCP协议来实现,以实现了全局有序这个特点)将带有zxid的消息作为一个提案(proposal)分发给所有的follower。3、当follower接受到proposal,先把proposal写到磁盘,写入成功以后再向leader恢复一个ack4、当leader 接受到合法数量(超过半数节点)的 ack,leader 就会向这些follower发送commit命令,同时会在本地执行该消息5、当follower接受到消息的commit命令以后,就会提交该消息。参考:https://blog.csdn.net/qq_39938758/article/details/105754198
正文一、传统方式安装1、下载安装包https://dlcdn.apache.org/zookeeper/zookeeper-3.7.0/apache-zookeeper-3.7.0-bin.tar.gz2、将下载好的tar.gz包上传服务器,解压缩tar -zxvf apache-zookeeper-3.7.0-bin.tar.gz -C /usr/local/ 3、进入/usr/local/目录更换名字[root@localhost local]# mv apache-zookeeper-3.7.0-bin zookeeper4、进入zookeeper文件,创建data和logs文件夹[root@localhost local]# cd zookeeper/ [root@localhost zookeeper]# mkdir data [root@localhost zookeeper]# mkdir logs [root@localhost zookeeper]# 5、更换zoo_sample.cfg名字为zoo.cfg[root@localhost zookeeper]# cd conf/ [root@localhost conf]# mv zoo_sample.cfg zoo.cfg [root@localhost conf]#6、编辑zoo.cfg文件添加如下内容(vim编辑 :wq保存退出)dataDir =/usr/local/zookeeper/data dataLogDir=/usr/local/zookeeper/logs #ip对应的是你的ip server.1=192.168.6.137:2888:3888 server.2=192.168.6.138:2888:3888 server.3=192.168.6.139:2888:3888 7、修改data文件[root@localhost conf]# cd ../data/ #在这个文件里输入1 [root@localhost data]# vim myid [root@localhost data]# cat myid 1 [root@localhost data]# 8、将此虚拟机赋值两份,另外两台机器分别修改myid为2和3。9、分别启动三台1. [root@localhost data]# cd ../bin/ 2. [root@localhost bin]# ./zkServer.sh start二、Docker安装Zookeeper1、搜索镜像文件[root@localhost bin]# docker search zookeeper2、下载镜像(apach)[root@localhost bin]# docker pull zookeeper3、创建挂载文件mkdir -p /data/zookeeper/{conf,data,logs}4、启动docker run --name zookeeper -d -p 2181:2181 -v/data/zookeeper/conf/zoo.cfg:/conf/zoo.cfg -v/data/zookeeper/data:/data -v/data/zookeeper/logs:/logs zoozookeeper5、docker -ps检查容器是否启动成功 三、Docker安装Zookeeper集群就Docker而言,桥接网络使用软件桥接器,该软件桥接器允许连接到同一桥接网络的容器进行通信,同时提供与未连接到该桥接网络的容器的隔离。Docker桥驱动程序会自动在主机中安装规则,以便不同桥接网络上的容器无法直接相互通信。就是说我们建立三个节点的话,zk之间是无法获取到彼此的节点的。1、搭建Docker桥接网络[root@localhost ~]# docker network create --driver bridge --subnet=172.18.0.0/16 --gateway=172.18.0.1 zoonet 1e88b597e707a19fba03a4368e553289de3e410dd587b8faef7780aebd9149b1[root@localhost ~]# docker network ls NETWORK ID NAME DRIVER SCOPE 3ae0cb2e611a bridge bridge local b8fccf7a64a2 host host local 6fed61369952 none null local 1e88b597e707 zoonet bridge local 2、创建挂载目录mkdir /data/zookeeper/cluster/zk1/{data,datalog,logs} -p mkdir /data/zookeeper/cluster/zk2/{data,datalog,logs} -p mkdir /data/zookeeper/cluster/zk3/{data,datalog,logs} -p3、启动节点节点1docker run -d -p 2181:2181 --name zookeeper_node01 --privileged --restart always --network zoonet --ip 172.18.0.2 \ -v /data/zookeeper/cluster/zk1/data:/data \ -v /data/zookeeper/cluster/zk1/datalog:/datalog \ -v /data/zookeeper/cluster/zk1/logs:/logs \ -e ZOO_MY_ID=1 \ -e "ZOO_SERVERS=server.1=172.18.0.2:2888:3888;2181 server.2=172.18.0.3:2888:3888;2181 server.3=172.18.0.4:2888:3888;2181" zookeeper节点2docker run -d -p 2182:2181 --name zookeeper_node02 --privileged --restart always --network zoonet --ip 172.18.0.3 \ -v /data/zookeeper/cluster/zk2/data:/data \ -v /data/zookeeper/cluster/zk2/datalog:/datalog \ -v /data/zookeeper/cluster/zk2/logs:/logs \ -e ZOO_MY_ID=2 \ -e "ZOO_SERVERS=server.1=172.18.0.2:2888:3888;2181 server.2=172.18.0.3:2888:3888;2181 server.3=172.18.0.4:2888:3888;2181" zookeeper 节点3docker run -d -p 2183:2181 --name zookeeper_node03 --privileged --restart always --network zoonet --ip 172.18.0.4 \ -v /data/zookeeper/cluster/zk3/data:/data \ -v /data/zookeeper/cluster/zk3/datalog:/datalog \ -v /data/zookeeper/cluster/zk3/logs:/logs \ -e ZOO_MY_ID=3 \ -e "ZOO_SERVERS=server.1=172.18.0.2:2888:3888;2181 server.2=172.18.0.3:2888:3888;2181 server.3=172.18.0.4:2888:3888;2181" zookeeper 如下图 4、查看docker 日志docker logs -f 1912ae817d335、停止删除所有镜像docker stop $(docker ps -a -q) docker rm $(docker ps -a -q) #批量停止容器 docker stop $(docker ps -a | grep "xxx" | awk '{print $1}')6、进入容器检验[root@bogon conf]# docker container exec -it a819ee7874b4 /bin/bash root@a819ee7874b4:/apache-zookeeper-3.7.0-bin# cd bin/ root@a819ee7874b4:/apache-zookeeper-3.7.0-bin/bin# ./zkServer.sh status ZooKeeper JMX enabled by default Using config: /conf/zoo.cfg Client port found: 2181. Client address: localhost. Client SSL: false. Mode: leader root@a819ee7874b4:/apache-zookeeper-3.7.0-bin/bin# 搭建成功四、Zookeeper选举启动选举在集群初始化阶段,当有一台服务器Server1启动时,其单独无法进行和完成Leader选举,当第二台服务器Server2启动时,两台机器此时可以相互通信,每台机器都试图找到Leader,于是进入选举过程。 选举过程如下: (1)每个Server发出一个投票,由于是初始情况,Server1和server2都会将自己作为Leader服务器来进行投票。每台服务器会往其他服务器发送投票信息,这个投票信息包括了SID和ZXID,其中SID就是该台机器的唯一标识(myid);ZXID是事务id,该ID是64位的,分为高32位和低32位。 (2)由于是初次投票,此时的ZXID相同,所以比较的就是SID,SID越大,获得的Leader的可能越大(为了严谨,本文针对任何情况都只说可能,不说绝对)。 (3)两台服务器发出自己的投票信息后,再根据自己收到的其他服务器的投票信息决定自己的投票信息是否变更,第一台服务器SID为1,第二台服务器SID为2,所以Server2的投票变更为2,即有两票,由于一共三台服务器,此时Server2已经处于半数以上,所以决定出来的Leader为Server2;(半数投票)即使Server3启动,由于Leader已经决定出来,所以不需要在进行投票,Server3只需要与Leader建立连接并进行状态同步即可。宕机选举假如此时有5台服务器,并且已经选举出Server3作为Leader,突然Leader(Server3)宕机,那么此时其他四台服务器要进行重新选举,它们便会进入LOOKING状态。 (1)在运行期间,它们的ZXID可能不会相同,于是再新一轮的Leader选举中,不仅仅需要比较SID(myid),还要比较ZXID,ZXID越大(在zk每次提交事务时,zxid相应的增加,所以认为zxid越大,数据越新),选举成Leader的可能越大。 (2)在初次选举中我们可以得出一个结论,便是SID(myid)位于中间,选举出Leader的可能性最大。但在运行时Leader突然宕机,再次进行选举时,这种结论已经不适用了,有可能选举出的Leader是Server1,也有可能是Server2,或者是Server4、Server5。 (3)值得注意的一点是,刚刚说了ZXID越大,选举出Leader的可能越大,前面说过ZXID分为高32位和低32位,ZXID中的低32位相比较的话,低32位越小的一方得到的Leader的可能性越大。2、每台机器发出投票后,也会接受到其他机器的选举,每台机器会根据一定的规则来处理收到的其他机器的投票信息,与自己进行对比。这个规则是整个Leader选举算法的核心所在。其中术语描述如下:vote_sid:接收到的投票中所推举Leader服务器的SID(myid)。vote_zxid:接收到的投票中所推举Leader服务器的ZXID。self_sid:当前服务器自己的SID(myid)。self_zxid:当前服务器自己的ZXID。 每次对收到的投票的处理,都是对(vote_sid,vote_zxid)和(self_sid,self_zxid)对比的过程。规则一:如果vote_zxid大于self_zxid,就认可当前收到的投票,并再次将该投票发送出去。规则二:如果vote_zxid小于self_zxid,那么坚持自己的投票,不做任何变更。规则三:如果vote_zxid等于self_zxid,那么就对比两者的SID,如果vote_sid大于self_sid,那么就认可当前收到的投票,并再次将该投票发送出去。规则四:如果vote_zxid等于self_zxid,并且vote_sid小于self_sid,那么坚持自己的投票,不做任何变更。参考:基于Docker进行Zookeeper集群的安装 - wellDoneGaben - 博客园zookeeper-选举机制 - 秋刀 - 博客园
正文一、入口package com.xiaojie.spring; import com.mysql.cj.jdbc.MysqlDataSource; import org.springframework.context.annotation.*; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import javax.sql.DataSource; /* * * @param null * 配置类 * @author xiaojie * @date 2021/8/24 * @return */ @Configuration @ComponentScan("com.xiaojie") @EnableTransactionManagement public class Config { /* * * 加载数据源 * @author xiaojie * @date 2021/8/24 * @return javax.sql.DataSource */ @Bean DataSource dataSource(){ MysqlDataSource mysqlDataSource = new MysqlDataSource(); mysqlDataSource.setURL("jdbc:mysql://127.0.0.1:3306/order?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai"); mysqlDataSource.setUser("root"); mysqlDataSource.setPassword("root"); // mysqlDataSource.setDatabaseName("order"); return mysqlDataSource; } /* * 加载事务管理器 * @author xiaojie * @date 2021/8/24 * @return org.springframework.transaction.PlatformTransactionManager */ @Bean PlatformTransactionManager platformTransactionManager(){ return new DataSourceTransactionManager(dataSource()); } /* *数据库连接 * @author xiaojie * @date 2021/8/24 * @return org.springframework.jdbc.core.JdbcTemplate */ @Bean JdbcTemplate jdbcTemplate(){ return new JdbcTemplate(dataSource()); } }@EnableTransactionManagement 为事务的入口二、流程图三 、源码AutoProxyRegistrar //默认是代理模式 if (mode == AdviceMode.PROXY) { //注册InfrastructureAdvisorAutoProxyCreator.class类到ioc容器 AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry); if ((Boolean) proxyTargetClass) { AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry); return; } }AopConfigUtilsprivate static BeanDefinition registerOrEscalateApcAsRequired( Class<?> cls, BeanDefinitionRegistry registry, @Nullable Object source) { Assert.notNull(registry, "BeanDefinitionRegistry must not be null"); if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { BeanDefinition apcDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); if (!cls.getName().equals(apcDefinition.getBeanClassName())) { int currentPriority = findPriorityForClass(apcDefinition.getBeanClassName()); int requiredPriority = findPriorityForClass(cls); if (currentPriority < requiredPriority) { apcDefinition.setBeanClassName(cls.getName()); } } return null; } RootBeanDefinition beanDefinition = new RootBeanDefinition(cls); beanDefinition.setSource(source); beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE); beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); //AUTO_PROXY_CREATOR_BEAN_NAME=org.springframework.aop.config.internalAutoProxyCreator registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition); return beanDefinition; }上述注册完之后会将InfrastructureAdvisorAutoProxyCreator.class类注入到IOC中beanId:org.springframework.aop.config.internalAutoProxyCreatorClass:InfrastructureAdvisorAutoProxyCreator.class 由类图InfrastructureAdvisorAutoProxyCreator.class继承了BeanPostProcessor同样会执行前置增强或者后置增强。ProxyTransactionManagementConfiguration.class//将TransactionInterceptor 类注入到IOC @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public TransactionInterceptor transactionInterceptor(TransactionAttributeSource transactionAttributeSource) { TransactionInterceptor interceptor = new TransactionInterceptor(); interceptor.setTransactionAttributeSource(transactionAttributeSource); if (this.txManager != null) { interceptor.setTransactionManager(this.txManager); } return interceptor; }再看BeanPostProcessor后置增强方法 然后就会发现跟AOP的代码是一样的JdkDynamicAopProxy下invoke()public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object oldProxy = null; boolean setProxyContext = false; .........省略 // Get the interception chain for this method. List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); // Check whether we have any advice. If we don't, we can fallback on direct // reflective invocation of the target, and avoid creating a MethodInvocation. if (chain.isEmpty()) { // We can skip creating a MethodInvocation: just invoke the target directly // Note that the final invoker must be an InvokerInterceptor so we know it does // nothing but a reflective operation on the target, and no hot swapping or fancy proxying. Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse); } else { // We need to create a method invocation... MethodInvocation invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain); // Proceed to the joinpoint through the interceptor chain. //由此进入proceed()方法 retVal = invocation.proceed(); } .........省略 }ReflectiveMethodInvocationpublic Object proceed() throws Throwable { // We start with an index of -1 and increment early. if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) { return invokeJoinpoint(); } Object interceptorOrInterceptionAdvice = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex); if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) { // Evaluate dynamic method matcher here: static part will already have // been evaluated and found to match. InterceptorAndDynamicMethodMatcher dm = (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice; Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass()); if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) { return dm.interceptor.invoke(this); } else { // Dynamic matching failed. // Skip this interceptor and invoke the next in the chain. return proceed(); } } else { // It's an interceptor, so we just invoke it: The pointcut will have // been evaluated statically before this object was constructed. //此处用到了责任链设计模式+递归思想实现所有的通知增强 return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this); } }MethodInterceptorObject invoke(@Nonnull MethodInvocation invocation) throws Throwable; TransactionInterceptor类下的invoke()方法 protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, TransactionAspectSupport.InvocationCallback invocation) throws Throwable { ......省略 TransactionAspectSupport.TransactionInfo txInfo = this.createTransactionIfNecessary(ptm, txAttr, joinpointIdentification); Object retVal; try { //执行目标方法 retVal = invocation.proceedWithInvocation(); } catch (Throwable var20) { //发生异常 则执行事务回滚 this.completeTransactionAfterThrowing(txInfo, var20); throw var20; } finally { //将当前事务归还ThreadLocal<TransactionAspectSupport.TransactionInfo> this.cleanupTransactionInfo(txInfo); } if (retVal != null && vavrPresent && TransactionAspectSupport.VavrDelegate.isVavrTry(retVal)) { TransactionStatus status = txInfo.getTransactionStatus(); if (status != null && txAttr != null) { retVal = TransactionAspectSupport.VavrDelegate.evaluateTryFailure(retVal, txAttr, status); } } //提交事务 this.commitTransactionAfterReturning(txInfo); return retVal; .........省略 }Spring 版本为5.3.9
2022年10月
2022年05月