暂时未有相关云产品技术能力~
之前介绍了RabbitMQ以及如何在SpringBoot项目中整合使用RabbitMQ,看过的朋友都说写的比较详细,希望再总结一下目前比较流行的MQTT。所以接下来,就来介绍什么MQTT?它在IoT中有着怎样的作用?如何在项目中使用MQTT?一、MQTT介绍1.1 什么是MQTT?MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于发布/订阅(publish/subscribe)模式的“轻量级”通讯协议,该协议构建于TCP/IP协议上,由IBM在1999年发布。MQTT最大优点在于用极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务。MQTT具有协议简洁、轻巧、可扩展性强、低开销、低带宽占用等优点,已经有PHP,JAVA,Python,C,C#,Go等多个语言版本,基本可以使用在任何平台上。在物联网、小型设备、移动应用等方面有较广泛的应用,特别适合用来当做物联网的通信协议。1.2 MQTT特点MQTT是一个基于客户端-服务器的消息发布/订阅传输协议。MQTT协议是轻量、简单、开放和易于实现的,这些特点使它适用范围非常广泛。在很多情况下,包括受限的环境中,如:机器与机器(M2M)通信和物联网(IoT)。MQTT协议是为硬件性能有限,且工作在低带宽、不可靠的网络的远程传感器和控制设备通讯而设计的协议,它具有以下主要的几项特性:1.使用发布/订阅消息模式,提供多对多的消息发布,解除应用程序耦合;2.对负载内容屏蔽的消息传输;3.使用TCP/IP 提供网络连接;4.支持三种消息发布服务质量(QoS):QoS 0(最多一次):消息发布完全依赖底层 TCP/IP 网络。会发生消息丢失或重复。这个级别可用于如下情况,环境传感器数据,丢失一次数据无所谓,因为不久后还会有第二次发送。QoS 1(至少一次):确保消息到达,但消息重复可能会发生。QoS 2(只有一次):确保消息到达一次。这个级别可用于如下情况,在计费系统中,消息重复或丢失会导致不正确的结果。5.传输数据小,开销很小(固定长度的头部是 2 字节),协议交换最小化,以降低网络流量;(用极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务。)1.3 MQTT应用场景MQTT作为一种低开销、低带宽占用的即时通讯协议,使其在物联网、小型设备、移动应用等方面有着广泛的应用。MQTT服务只负责消息的接收和传递,应用系统连接到MQTT服务器后,可以实现采集数据接收、解析、业务处理、存储入库、数据展示等功能。常见的应用场景主要有以下几个方面:(1)消息推送: 如PC端的推送公告,比如安卓的推送服务,还有一些即时通信软件如微信、易信等也是采用的推送技术。(2)智能点餐: 通过MQTT消息队列产品,消费者可在餐桌上扫码点餐,并与商家后端系统连接实现自助下单、支付。(3)信息更新: 实现商场超市等场所的电子标签、公共场所的多媒体屏幕的显示更新管理。(4)扫码出站: 最常见的停车场扫码缴费,自动起竿;地铁闸口扫码进出站。二、MQTT的角色组成2.1 MQTT的客户端和服务端2.1.1 服务端(Broker)EMQX就是一个MQTT的Broker,emqx只是基于erlang语言开发的软件而已,其它的MQ还有ActiveMQ、RabbitMQ、HiveMQ等等。EMQX服务端:https://www.emqx.io/zh/downloads?os=Windows2.1.2 客户端(发布/订阅)EMQX客户端:https://mqttx.app/zh这个是用来测试验证的客户端,实际项目是通过代码来实现我们消息的生产者和消费者。2.2 MQTT中的几个概念相比RabbitMQ等消息队列,MQTT要相对简单一些,只有Broker、Topic、发布者、订阅者等几部分构成。接下来我们先简单整理下MQTT日常使用中最常见的几个概念:1.Topic主题:MQTT消息的主要传播途径, 我们向主题发布消息, 订阅主题, 从主题中读取消息并进行.业务逻辑处理, 主题是消息的通道2.生产者:MQTT消息的发送者, 他们向主题发送消息3.消费者:MQTT消息的接收者, 他们订阅自己需要的主题, 并从中获取消息4.broker服务:消息转发器, 消息是通过它来承载的, EMQX就是我们的broker, 在使用中我们不用关心它的具体实现其实, MQTT的使用流程就是: 生产者给broker的某个topic发消息->broker通过topic进行消息的传递->订阅该主题的消费者拿到消息并进行相应的业务逻辑三、EMQX的安装和使用下面以Windows为例,演示Windows下如何安装和使用EXQX。step 1:下载EMQ安装包,配置EMQ环境EMQX服务端:https://www.emqx.io/zh/downloads?os=Windowsstep 2:下载压缩包解压,cmd进入bin文件夹step 3:启动EMQX服务在命令行输入:emqx start 启动服务,打卡浏览器输入:http://localhost:18083/ 进入登录页面。默认用户名密码 admin/public 。登录成功后,会进入emqx的后台管理页面,如下图所示:四、使用SpringBoot整合MQTT协议前面介绍了MQTT协议以及如何安装和启动MQTT服务。接下来演示如何在SpringBoot项目中整合MQTT实现消息的订阅和发布。4.1 创建工程首先,创建spring-boot-starter-mqtt父工程,在父工程下分别创建消息的提供者spring-boot-starter-mqtt-provider 模块和消息的消费者spring-boot-starter-mqtt-consumer模块。4.2 实现生产者接下来,修改生产者模块spring-boot-starter-mqtt-provider 相关的代码,实现消息发布的功能模块。4.2.1 导入依赖包修改pom.xml 文件,添加MQTT相关依赖,具体示例代码如下所示:<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.integration</groupId> <artifactId>spring-integration-mqtt</artifactId> <version>5.3.2.RELEASE</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.4</version> </dependency> </dependencies>4.2.2 修改配置文件修改application.yml配置文件,增加MQTT相关配置。示例代码如下所示:spring: application: name: provider #MQTT配置信息 mqtt: #MQTT服务地址,端口号默认11883,如果有多个,用逗号隔开 url: tcp://127.0.0.1:11883 #用户名 username: admin #密码 password: public #客户端id(不能重复) client: id: provider-id #MQTT默认的消息推送主题,实际可在调用接口是指定 default: topic: topic server: port: 8080 4.2.3 消息生产者客户端配置创建MqttProviderConfig配置类,读取application.yml中的相关配置,并初始化创建MQTT的连接。示例代码如下所示:import lombok.extern.slf4j.Slf4j; import org.eclipse.paho.client.mqttv3.*; import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import javax.annotation.PostConstruct; @Configuration @Slf4j public class MqttProviderConfig { @Value("${spring.mqtt.username}") private String username; @Value("${spring.mqtt.password}") private String password; @Value("${spring.mqtt.url}") private String hostUrl; @Value("${spring.mqtt.client.id}") private String clientId; @Value("${spring.mqtt.default.topic}") private String defaultTopic; /** * 客户端对象 */ private MqttClient client; /** * 在bean初始化后连接到服务器 */ @PostConstruct public void init(){ connect(); } /** * 客户端连接服务端 */ public void connect(){ try{ //创建MQTT客户端对象 client = new MqttClient(hostUrl,clientId,new MemoryPersistence()); //连接设置 MqttConnectOptions options = new MqttConnectOptions(); //是否清空session,设置false表示服务器会保留客户端的连接记录(订阅主题,qos),客户端重连之后能获取到服务器在客户端断开连接期间推送的消息 //设置为true表示每次连接服务器都是以新的身份 options.setCleanSession(true); //设置连接用户名 options.setUserName(username); //设置连接密码 options.setPassword(password.toCharArray()); //设置超时时间,单位为秒 options.setConnectionTimeout(100); //设置心跳时间 单位为秒,表示服务器每隔 1.5*20秒的时间向客户端发送心跳判断客户端是否在线 options.setKeepAliveInterval(20); //设置遗嘱消息的话题,若客户端和服务器之间的连接意外断开,服务器将发布客户端的遗嘱信息 options.setWill("willTopic",(clientId + "与服务器断开连接").getBytes(),0,false); //设置回调 client.setCallback(new MqttProviderCallBack()); client.connect(options); } catch(MqttException e){ e.printStackTrace(); } } public void publish(int qos,boolean retained,String topic,String message){ MqttMessage mqttMessage = new MqttMessage(); mqttMessage.setQos(qos); mqttMessage.setRetained(retained); mqttMessage.setPayload(message.getBytes()); //主题的目的地,用于发布/订阅信息 MqttTopic mqttTopic = client.getTopic(topic); //提供一种机制来跟踪消息的传递进度 //用于在以非阻塞方式(在后台运行)执行发布是跟踪消息的传递进度 MqttDeliveryToken token; try { //将指定消息发布到主题,但不等待消息传递完成,返回的token可用于跟踪消息的传递状态 //一旦此方法干净地返回,消息就已被客户端接受发布,当连接可用,将在后台完成消息传递。 token = mqttTopic.publish(mqttMessage); token.waitForCompletion(); } catch (MqttException e) { e.printStackTrace(); } } }4.2.4 生产者客户端消息回调创建MqttProviderCallBack类并继承MqttCallback,实现相关消息回调事件,示例代码如下图所示:import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; import org.eclipse.paho.client.mqttv3.MqttCallback; import org.eclipse.paho.client.mqttv3.MqttMessage; public class MqttConsumerCallBack implements MqttCallback{ /** * 客户端断开连接的回调 */ @Override public void connectionLost(Throwable throwable) { System.out.println("与服务器断开连接,可重连"); } /** * 消息到达的回调 */ @Override public void messageArrived(String topic, MqttMessage message) throws Exception { System.out.println(String.format("接收消息主题 : %s",topic)); System.out.println(String.format("接收消息Qos : %d",message.getQos())); System.out.println(String.format("接收消息内容 : %s",new String(message.getPayload()))); System.out.println(String.format("接收消息retained : %b",message.isRetained())); } /** * 消息发布成功的回调 */ @Override public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { System.out.println(String.format("接收消息成功")); } }4.2.5 创建Controller控制器实现消息发布功能创建SendController控制器类,实现消息的发送功能,示例代码如下所示:@Controller public class SendController { @Autowired private MqttProviderConfig providerClient; @RequestMapping("/sendMessage") @ResponseBody public String sendMessage(int qos,boolean retained,String topic,String message){ try { providerClient.publish(qos, retained, topic, message); return "发送成功"; } catch (Exception e) { e.printStackTrace(); return "发送失败"; } } }4.3 实现消费者前面完成了生成者消息发布的模块,接下来修改消费者模块spring-boot-starter-mqtt-consumer实现消息订阅、处理的功能。4.3.1 导入依赖包修改pom.xml 文件,添加MQTT相关依赖,具体示例代码如下所示:<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.integration</groupId> <artifactId>spring-integration-mqtt</artifactId> <version>5.3.2.RELEASE</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.4</version> </dependency> </dependencies>4.3.2 修改配置文件修改application.yml配置文件,增加MQTT相关配置。示例代码如下所示:spring: application: name: consumer #MQTT配置信息 mqtt: #MQTT服务端地址,端口默认为11883,如果有多个,用逗号隔开 url: tcp://127.0.0.1:11883 #用户名 username: admin #密码 password: public #客户端id(不能重复) client: id: consumer-id #MQTT默认的消息推送主题,实际可在调用接口时指定 default: topic: topic server: port: 80854.3.3 消费者客户端配置创建消费者客户端配置类MqttConsumerConfig,读取application.yml中的相关配置,并初始化创建MQTT的连接。示例代码如下所示:import javax.annotation.PostConstruct; import org.eclipse.paho.client.mqttv3.MqttClient; import org.eclipse.paho.client.mqttv3.MqttConnectOptions; import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; @Configuration public class MqttConsumerConfig { @Value("${spring.mqtt.username}") private String username; @Value("${spring.mqtt.password}") private String password; @Value("${spring.mqtt.url}") private String hostUrl; @Value("${spring.mqtt.client.id}") private String clientId; @Value("${spring.mqtt.default.topic}") private String defaultTopic; /** * 客户端对象 */ private MqttClient client; /** * 在bean初始化后连接到服务器 */ @PostConstruct public void init(){ connect(); } /** * 客户端连接服务端 */ public void connect(){ try { //创建MQTT客户端对象 client = new MqttClient(hostUrl,clientId,new MemoryPersistence()); //连接设置 MqttConnectOptions options = new MqttConnectOptions(); //是否清空session,设置为false表示服务器会保留客户端的连接记录,客户端重连之后能获取到服务器在客户端断开连接期间推送的消息 //设置为true表示每次连接到服务端都是以新的身份 options.setCleanSession(true); //设置连接用户名 options.setUserName(username); //设置连接密码 options.setPassword(password.toCharArray()); //设置超时时间,单位为秒 options.setConnectionTimeout(100); //设置心跳时间 单位为秒,表示服务器每隔1.5*20秒的时间向客户端发送心跳判断客户端是否在线 options.setKeepAliveInterval(20); //设置遗嘱消息的话题,若客户端和服务器之间的连接意外断开,服务器将发布客户端的遗嘱信息 options.setWill("willTopic",(clientId + "与服务器断开连接").getBytes(),0,false); //设置回调 client.setCallback(new MqttConsumerCallBack()); client.connect(options); //订阅主题 //消息等级,和主题数组一一对应,服务端将按照指定等级给订阅了主题的客户端推送消息 int[] qos = {1,1}; //主题 String[] topics = {"topic1","topic2"}; //订阅主题 client.subscribe(topics,qos); } catch (MqttException e) { e.printStackTrace(); } } /** * 断开连接 */ public void disConnect(){ try { client.disconnect(); } catch (MqttException e) { e.printStackTrace(); } } /** * 订阅主题 */ public void subscribe(String topic,int qos){ try { client.subscribe(topic,qos); } catch (MqttException e) { e.printStackTrace(); } } }4.3.4 消费者客户端消息回调创建MqttConsumerCallBack类并继承MqttCallback,实现相关消息回调事件,示例代码如下图所示:import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; import org.eclipse.paho.client.mqttv3.MqttCallback; import org.eclipse.paho.client.mqttv3.MqttMessage; public class MqttConsumerCallBack implements MqttCallback{ /** * 客户端断开连接的回调 */ @Override public void connectionLost(Throwable throwable) { System.out.println("与服务器断开连接,可重连"); } /** * 消息到达的回调 */ @Override public void messageArrived(String topic, MqttMessage message) throws Exception { System.out.println(String.format("接收消息主题 : %s",topic)); System.out.println(String.format("接收消息Qos : %d",message.getQos())); System.out.println(String.format("接收消息内容 : %s",new String(message.getPayload()))); System.out.println(String.format("接收消息retained : %b",message.isRetained())); } /** * 消息发布成功的回调 */ @Override public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { System.out.println(String.format("接收消息成功")); } }4.3.5 创建Controller控制器,实现MQTT连接的建立和断开接下来,创建Controller控制器MqttController,并实现MQTT连接的建立和断开等方法。示例代码如下所示:import com.weiz.mqtt.config.MqttConsumerConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; @Controller public class MqttController { @Autowired private MqttConsumerConfig client; @Value("${spring.mqtt.client.id}") private String clientId; @RequestMapping("/connect") @ResponseBody public String connect(){ client.connect(); return clientId + "连接到服务器"; } @RequestMapping("/disConnect") @ResponseBody public String disConnect(){ client.disConnect(); return clientId + "与服务器断开连接"; } }4.4 测试验证首先,分别启动生产者spring-boot-starter-mqtt-provider 和消费者spring-boot-starter-mqtt-consumer两个项目,打开浏览器,输入地址http://localhost:18083/,在EMQX管理界面可以看到连接上来的两个客户端。如下图所示:接下来,调用生产者的消息发布接口验证消息发布是否成功。使用Pomstman调用消息发送接口:http://localhost:8080/sendMessage ,如下图所示:通过上图可以发现,生产者模块已经把消息发送成功。接下来查看消费者模块,验证消息是否处理成功。如下图所示:通过日志输出可以发现,消费者已经成功接收到生产者发送的消息,说明我们成功实现在Spring Boot项目中整合MQTT实现了消息的发布和订阅的功能。最后以上就是如何在Spring Boot中使用MQTT的详细内容,更多关于在Spring Boot中MQTT的使用大家可以去自己研究学习。比如:如何利用qos机制保证数据不会丢失?消息的队列和排序?集群模式下的应用?等等。
前面为大家讲述了 Spring Boot的整合Redis、RabbitMQ、Elasticsearch等各种框架组件;随着移动互联网的发展,服务端消息数据推送已经是一个非常重要、非常普遍的基础功能。今天就和大家聊聊在SpringBoot轻松整合WebSocket,实现Web在线聊天室,希望能对大家有所帮助。一、WebSocket简介1.1 什么是WebSocket?WebSocket协议是基于TCP的一种网络协议,它实现了浏览器与服务器全双工(Full-duplex)通信。它允许服务端主动向客户端推送数据,这使得客户端和服务器之间的数据交换变得更加简单高效。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。WebSocket 在握手之后便直接基于 TCP 进行消息通信,只是 TCP的基础上的一层非常轻的封装,它只是将TCP的字节流转换成消息流(文本或二进制),至于怎么解析这些消息的内容完全依赖于应用本身。1.2 为什么需要 WebSocket?我们知道HTTP 协议有一个缺陷:通信只能由客户端发起,服务器端无法向某个客户端推送数据。然而,在某些场景下,数据推送是非常必要的功能,为了实现推送技术,所用的技术都是轮询,即:客户端在特定的的时间间隔(如每 1 秒),由浏览器对服务器发出 HTTP 请求,然后由服务器返回最新的数据给客户端的浏览器。例如,在外卖场景下,当骑手位置更新时,服务器端向客户端推送骑手位置数据。如果使用HTTP协议,那么就只能轮询。轮询模式具有很明显的缺点,即浏览器需要不断地向服务器发出请求,然而 HTTP 请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源,同样,数据时效性较低,存在一定的数据延迟。在这种情况下,WebSocket 出现了,使用 WebSocket 协议可以实现由服务端主动向客户端推送消息,同时也可以实现客户端向服务器端发送消息。这样能更好得节省服务器资源和带宽;并且能够更实时地进行通讯。随着HTML 5 的流行, WebSocket已经成为国际标准,目前主流的浏览器都已经支持。1.3 WebSocket的优点较少的控制开销。在连接建立后,服务端和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有 2 至 10 字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的 4 字节的掩码。这相对于 HTTP 协议每次都要携带完整的头部信息,此项开销显著减少了。更强的实时性。由于WebSocket协议是全双工的,所以服务器可以随时主动向客户端推送数据。相对于 HTTP 请求必须等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet 等类似的长轮询比较,WebSocket也能在短时间内更高效的传递数据。保持连接状态。与 HTTP 不同的是, Websocket 需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息,而 HTTP 请求需要在每个请求都携带状态信息(如Token等)。更好的二进制支持。 Websocket 定义了二进制帧,相对 HTTP,可以更轻松地处理二进制数据。Websocket 定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持Gzip压缩等。更好的压缩效果。相对于 HTTP 压缩, Websocket 在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。1.4 WebSocket的应用场景随着移动互联网的发展,WebSocket的使用越来越广泛。基本上只要是时效性要求高的业务场景都可以使用WebSocket,例如:协同编辑基于位置的应用体育实况更新股票基金报价实时更新多玩家游戏音视频聊天视频会议在线教育社交订阅除此之外,还有系统消息通知、用户上下线提醒、客户端数据同步,实时数据更新,多屏幕同步,用户在线状态,消息扫描二维码登录/二维码支付,弹幕、各类信息提醒,在线选座,实时监控大屏等等; 二、WebSocket的事件我们知道HTTP协议使用http和https的统一资源标志符。WebSocket与HTTP类似,使用的是 ws 或 wss(类似于 HTTPS),其中 wss 表示在 TLS 之上的Websocket。例如:ws://example.com/wsapi wss://secure.example.com/WebSocket 使用和 HTTP 相同的 TCP 端口,可以绕过大多数防火墙的限制。默认情况下, WebSocket 协议使用80 端口;运行在 TLS 之上时,默认使用 443 端口。WebSocket 只是在 Socket 协议的基础上,非常轻的一层封装。在WebSocket API中定义了open、close、error、message等几个基本事件,这就使得WebSocket使用起来非常简单。 下面是在WebSocket API定义的事件:事件 事件处理程序描述openSokcket onopen连接建立时触发messageSokcket onmessage客户端接收服务端数据时触发errorSokcket onerror通讯发生错误时触发closeSokcket onclose连接关闭时触发三、Spring Boot整合WebSocket实现聊天室Spring Boot 提供了 Websocket 组件 spring-boot-starter-websocket,用来支持在 Spring Boot环境下对Websocket 的使用。下面我们就以多人在线聊天室为例,演示 Spring Boot 是如何整合Websocket 实现服务端消息推送的。3.1 创建前端页面首先,创建spring boot项目:spring-boot-starter-websocket。接下来,我们利用前端框架 Bootstrap 构建前台交互页面,创建index.html页面并集成Bootstrap框架,最后在 js 中实现WebSocket通讯,完整页面代码如下所示:<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>Chat Room</title> <script type="text/javascript"> var urlPrefix ='ws://localhost:8080/chat/'; var ws = null; // 加入 function join() { var username = document.getElementById('uid').value; var url = urlPrefix + username; ws = new WebSocket(url); ws.onmessage = function(event){ var ta = document.getElementById('responseText'); ta.value += event.data+"\r\n"; }; ws.onopen = function(event){ var ta = document.getElementById('responseText'); ta.value += "建立 websocket 连接... \r\n"; }; ws.onclose = function(event){ var ta = document.getElementById('responseText'); ta.value += "用户["+username+"] 已经离开聊天室! \r\n"; ta.value += "关闭 websocket 连接. \r\n"; }; } // 退出 function exit(){ if(ws) { ws.close(); } } // 发送消息 function send(){ var message = document.getElementById('message').value; if(!window.WebSocket){return;} if(ws.readyState == WebSocket.OPEN){ ws.send(message); }else{ alert("WebSocket 连接没有建立成功!"); } } </script> </head> <body> <form onSubmit="return false;"> <h3>BBS聊天室</h3> <textarea id="responseText" style="width: 1024px;height: 300px;"></textarea> <br/> <br /> <label>昵称 : </label><input type="text" id="uid" /> &nbsp; <input type="button" value="加入聊天室" onClick="join()" /> &nbsp; <input type="button" value="离开聊天室" onClick="exit()" /> <br /> <br /> <label>消息 : </label><input type="text" id="message" /> &nbsp; <input type="button" value="发送消息" onClick="send()" /> </form> </body> </html>上面的示例中,js中定义了WebSocket通讯相关的代码,如:ws.onopen、ws.onmessage、ws.onclose等事件。3.2 创建后端服务接下来,我们开始创建后台WebSocket服务,实现WebSocket后台通讯服务。step 1 :引入相关依赖首先,修改项目的pom.xml文件,主要添加 Web 和 Websocket 组件。具体代码如下所示:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>step2:消息接收首先创建ChatServerEndpoint类,并使用@ServerEndpoint注解创建WebSocket EndPoint实现客户端连接、消息的接收、等事件。具体示例代码如下所示:@RestController @ServerEndpoint("/chat/{username}") public class ChatServerEndpoint { private static final Logger logger = LoggerFactory.getLogger(ChatRoomServerEndpoint.class); @OnOpen public void openSession(@PathParam("username") String username, Session session) { ONLINE_USER_SESSIONS.put(username, session); String message = "欢迎用户[" + username + "] 来到聊天室!"; logger.info("用户登录:"+message); sendMessageAll(message); } @OnMessage public void onMessage(@PathParam("username") String username, String message) { logger.info("发送消息:"+message); sendMessageAll("用户[" + username + "] : " + message); } @OnClose public void onClose(@PathParam("username") String username, Session session) { //当前的Session 移除 ONLINE_USER_SESSIONS.remove(username); //并且通知其他人当前用户已经离开聊天室了 sendMessageAll("用户[" + username + "] 已经离开聊天室了!"); try { session.close(); } catch (IOException e) { logger.error("onClose error",e); } } @OnError public void onError(Session session, Throwable throwable) { try { session.close(); } catch (IOException e) { logger.error("onError excepiton",e); } logger.info("Throwable msg "+throwable.getMessage()); } }上面的示例中,我们使用 @ServerEndpoint("/chat/{username}") 注解监听此地址的 WebSocket 信息,客户端也是通过此地址向服务端接收和发送消息。同时使用@OnOpen注解实现客户端连接事件,@OnMessage注解实现消息发送事件,@OnClose注解实现客户端连接关闭事件,@OnError注解实现消息错误事件。step3:消息发送我们先创建一个 WebSocketUtils 工具类,用来存储聊天室在线的用户信息,以及向客户端发送消息的功能。具体代码如下所示:public final class WebSocketUtils { private static final Logger logger = LoggerFactory.getLogger(WebSocketUtils.class); // 存储 websocket session public static final Map<String, Session> ONLINE_USER_SESSIONS = new ConcurrentHashMap<>(); /** * @param session 用户 session * @param message 发送内容 */ public static void sendMessage(Session session, String message) { if (session == null) { return; } final RemoteEndpoint.Basic basic = session.getBasicRemote(); if (basic == null) { return; } try { basic.sendText(message); } catch (IOException e) { logger.error("sendMessage IOException ",e); } } /** * 推送消息到其他客户端 * @param message */ public static void sendMessageAll(String message) { ONLINE_USER_SESSIONS.forEach((sessionId, session) -> sendMessage(session, message)); } }step4:开启 WebSocket 功能修改项目启动类,需要添加 @EnableWebSocket 开启 WebSocket 功能。具体示例代码如下所示:@EnableWebSocket @SpringBootApplication public class WebSocketApplication { public static void main(String[] args) { SpringApplication.run(WebSocketApplication.class, args); } @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }以上,我们WebSocket服务端内容就实现完毕了。接下来我们验证整个聊天室功能是否正常?3.3 验证测试前面,我们已经把整个WebSocket聊天室的前后台功能介绍完了。接下来我们验证整个聊天室功能是否正常?首先,启动项目,在浏览器中分别输入地址:http://localhost:8080/ 打开三个聊天室页面。如下图所示:然后,分别在三个聊天室页面中,输入三个昵称并加入聊天室,与服务端成功建立WebSocket连接,即可在聊天室发送消息。点击页面上的离开聊天室,此页面与服务端建立的WebSocket连接就会断开。其他连接不受影响。最后以上,我们就把Spring Boot整合WebSocket,实现BBS聊天室的功能介绍完了。WebSocket能够以非常简单的方式,实现客户端与服务器端的双向通讯。在实际项目开发过程中使用越来越广泛,希望大家能熟悉掌握。
近段时间一直在总结分布式系统架构常见的算法。前面我们介绍过布隆过滤器算法。接下来介绍一个非常重要、也非常实用的算法:一致性哈希算法。通过介绍一致性哈希算法的原理并给出了一种实现和实际运用的案例,带大家真正理解一致性哈希算法。一、背景在具体介绍一致性哈希算法之前,先问一个问题:为什么需要一致性哈希算法?下面我们通过一个案例来回答这个问题。假设有这么一种场景:我们有三台缓存服务器分别为:node0、node1、node2,有3000万个缓存数据需要存储在这三台服务器组成的集群中,希望可以将这些数据均匀的缓存到三台机器上,你会想到什么方案呢?我们可能首先想到的方案是:取模算法hash(key)%N,即:对缓存数据的key进行hash运算后取模,N是机器的数量;运算后的结果映射对应集群中的节点。具体如下图所示:如上图所示,首先对key进行hash计算后的结果对3取模,得到的结果一定是0、1或者2;然后映射对应的服务器node0、node1、node2,最后直接找对应的服务器存取数据即可。通过取模算法将每个数据请求都均匀的分散到了三个不同的服务器节点上,看起来很完美!但是,在分布式集群系统的负载均衡实现上,这种模型在集群扩容和收缩时却有一定的局限性:因为在生产环境中根据业务量的大小,调整服务器数量是常有的事,而服务器数量N发生变化后hash(key)%N计算的结果也会随之变化!导致整个集群的缓存数据必须重新计算调整,进而导致大量缓存在同一时间失效,造成缓存的雪崩,最终导致整个缓存系统的不可用,这是不能接受的。为了解决优化上述情况,一致性哈希算法应运而生。二、一致性哈希简介有些朋友一听到算法就头大,其实大可不必,一致性哈希算法听起来高大上,其实非常简单。接下来开始介绍什么是一致性哈希算法,它解决了什么问题。2.1 什么是一致性哈希?一致性哈希(Consistent Hash)算法是1997年提出,是一种特殊的哈希算法,目的是解决分布式系统的数据分区问题:当分布式集群移除或者添加一个服务器时,必须尽可能小地改变已存在的服务请求与处理请求服务器之间的映射关系。2.2 一致性哈希主要解决问题我们知道,传统的按服务器节点数量取模在集群扩容和收缩时存在一定的局限性。而一致性哈希算法正好解决了简单哈希算法在分布式集群中存在的动态伸缩的问题。降低节点上下线的过程中带来的数据迁移成本,同时节点数量的变化与分片原则对于应用系统来说是无感的,使上层应用更专注于领域内逻辑的编写,使得整个系统架构能够动态伸缩,更加灵活方便。2.3 一致性哈希的使用场景一致性哈希算法是分布式系统中的重要算法,使用场景也非常广泛。主要是是负载均衡、缓存数据分区等场景。一致性哈希应该是实现负载均衡的首选算法,它的实现比较灵活,既可以在客户端实现,也可以在中间件上实现,比如日常使用较多的缓存中间件memcached 使用的路由算法用的就是一致性哈希算法。此外,其它的应用场景还有很多:RPC框架Dubbo用来选择服务提供者分布式关系数据库分库分表:数据与节点的映射关系LVS负载均衡调度器三、一致性哈希的原理2.1 算法原理前面介绍的取模算法虽然使用简单,但缺陷也很明显,如果服务器中保存有服务请求对应的数据,那么如果重新计算请求的哈希值,会造成缓存的雪崩的问题。这种情况在分布式系统中是非常糟糕的。一个设计良好的分布式系统应该具有良好的单调性,即服务器的添加与移除不会造成大量的哈希重定位,而一致性哈希恰好可以解决这个问题 。其实,一致性哈希算法本质上也是一种取模算法。只不过前面介绍的取模算法是按服务器数量取模,而一致性哈希算法是对固定值2^32取模,这就使得一致性算法具备良好的单调性:不管集群中有多少个节点,只要key值固定,那所请求的服务器节点也同样是固定的。其算法的工作原理如下:一致性哈希算法将整个哈希值空间映射成一个虚拟的圆环,整个哈希空间的取值范围为0~2^32-1;计算各服务器节点的哈希值,并映射到哈希环上;将服务发来的数据请求使用哈希算法算出对应的哈希值;将计算的哈希值映射到哈希环上,同时沿圆环顺时针方向查找,遇到的第一台服务器就是所对应的处理请求服务器。当增加或者删除一台服务器时,受影响的数据仅仅是新添加或删除的服务器到其环空间中前一台的服务器(也就是顺着逆时针方向遇到的第一台服务器)之间的数据,其他都不会受到影响。综上所述,一致性哈希算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性 。2.2 深入剖析说了那么多,可能你还是云里雾里的,那么接下来我们详细剖析一致性哈希的实现原理。2.2.1 哈希环首先,一致性哈希算法将整个哈希值空间映射成一个虚拟的圆环。整个哈希空间的取值范围为0~2^32-1,按顺时针方向开始从0~2^32-1排列,最后的节点2^32-1在0开始位置重合,形成一个虚拟的圆环。如下图所示:2.2.2 服务器映射到哈希环接下来,将服务器节点映射到哈希环上对应的位置。我们可以对服务器IP地址进行哈希计算,哈希计算后的结果对2^32取模,结果一定是一个0到2^32-1之间的整数。最后将这个整数映射在哈希环上,整数的值就代表了一个服务器节点的在哈希环上的位置。即:hash(服务器ip)% 2^32。下面我们依次将node0、node1、node2三个缓存服务器映射到哈希环上,如下图所示:2.2.3 对象key映射到服务器当服务器接收到数据请求时,首先需要计算请求Key的哈希值;然后将计算的哈希值映射到哈希环上的具体位置;接下来,从这个位置沿着哈希环顺时针查找,遇到的第一个节点就是key对应的节点;最后,将请求发送到具体的服务器节点执行数据操作。假设我们有“key-01:张三”、“key-02:李四”、“key-03:王五”三条缓存数据。经过哈希算法计算后,映射到哈希环上的位置如下图所示:如上图所示,通过哈希计算后,key-01顺时针寻找将找到node0,key-02顺时针寻找将找到node1,key-03顺时针寻找将找到node2。最后,请求找到的服务器节点执行具体的业务操作。以上便是一致性哈希算法的工作原理。四、服务器扩容&缩容前面介绍了一致性哈希算法的工作原理,那么,一致性哈希算法如何避免服务器动态伸缩的问题的呢?4.1 服务器缩容服务器缩容就是减少集群中服务器节点的数量或是集群中某个节点故障。假设,集群中的某个节点故障,原本映射到该节点的请求,会找到哈希环中的下一个节点,数据也同样被重新分配至下一个节点,其它节点的数据和请求不受任何影响。这样就确保节点发生故障时,集群能保持正常稳定。如下图所示:如上图所示:节点node2发生故障时,数据key-01和key-02不会受到影响,只有key-03的请求被重定位到node0。在一致性哈希算法中,如果某个节点宕机不可用了,那么受影响的数据仅仅是会寻址到此节点和前一节点之间的数据。其他哈希环上的数据不会受到影响。4.2 服务器扩容服务器扩容就是集群中需要增加一个新的数据节点,假设,由于需要缓存的数据量太大,必须对集群进行扩容增加一个新的数据节点。此时,只需要计算新节点的哈希值并将新的节点加入到哈希环中,然后将哈希环中从上一个节点到新节点的数据映射到新的数据节点即可。其他节点数据不受影响,具体如下图所示:如上图所示,加入新的node3节点后,key-01、key-02不受影响,只有key-03的寻址被重定位到新节点node3,受影响的数据仅仅是会寻址到新节点和前一节点之间的数据。通过一致性哈希算法,集群扩容或缩容时,只需要重新定位哈希环空间内的一小部分数据。其他数据保持不变。当节点数越多的时候,使用哈希算法时,需要迁移的数据就越多,使用一致哈希时,需要迁移的数据就越少。所以,一致哈希算法具有较好的容错性和可扩展性。五、数据倾斜与虚拟节点5.1 什么是数据倾斜?前面说了一致性哈希算法的原理以及扩容缩容的问题。但是,由于哈希计算的随机性,导致一致性哈希算法存在一个致命问题:数据倾斜,,也就是说大多数访问请求都会集中少量几个节点的情况。特别是节点太少情况下,容易因为节点分布不均匀造成数据访问的冷热不均。这就失去了集群和负载均衡的意义。如下图所示:如上图所示,key-1、key-2、key-3可能被映射到同一个节点node0上。导致node0负载过大,而node1和node2却很空闲的情况。这有可能导致个别服务器数据和请求压力过大和崩溃,进而引起集群的崩溃。5.2 如何解决数据倾斜?为了解决数据倾斜的问题,一致性哈希算法引入了虚拟节点机制,即对每一个物理服务节点映射多个虚拟节点,将这些虚拟节点计算哈希值并映射到哈希环上,当请求找到某个虚拟节点后,将被重新映射到具体的物理节点。虚拟节点越多,哈希环上的节点就越多,数据分布就越均匀,从而避免了数据倾斜的问题。说起来可能比较复杂,一句话概括起来就是:原有的节点、数据定位的哈希算法不变,只是多了一步虚拟节点到实际节点的映射。具体如下图所示:如上图所示,我们可以在服务器ip或主机名的后面增加编号来实现,将全部的虚拟节点加入到哈希环中,增加了节点后,数据在哈希环上的分布就相对均匀了。当有访问请求寻址到node0-1这个虚拟节点时,将被重新映射到物理节点node0。六、一致性Hash算法实现前面介绍了一致性哈希算法的原理、动态伸缩以及数据倾斜的问题后,下面我们根据上面的讲述,使用Java实现一个简单的一致性哈希算法。6.1 数据节点首先定义一个节点类,实现数据节点的功能,具体代码如下:public class Node { private static final int VIRTUAL_NODE_NO_PER_NODE = 200; private final String ip; private final List<Integer> virtualNodeHashes = new ArrayList<>(VIRTUAL_NODE_NO_PER_NODE); private final Map<Object, Object> cacheMap = new HashMap<>(); public Node(String ip) { Objects.requireNonNull(ip); this.ip = ip; initVirtualNodes(); } private void initVirtualNodes() { String virtualNodeKey; for (int i = 1; i <= VIRTUAL_NODE_NO_PER_NODE; i++) { virtualNodeKey = ip + "#" + i; virtualNodeHashes.add(HashUtils.hashcode(virtualNodeKey)); } } public void addCacheItem(Object key, Object value) { cacheMap.put(key, value); } public Object getCacheItem(Object key) { return cacheMap.get(key); } public void removeCacheItem(Object key) { cacheMap.remove(key); } public List<Integer> getVirtualNodeHashes() { return virtualNodeHashes; } public String getIp() { return ip; } } 6.2 实现一致性哈希算法接下来实现核心功能:一致性哈希算法,主要使用java的TreeMap类,实现哈希环和哈希查找的功能。具体代码如下所示:public class ConsistentHash { private final TreeMap<Integer, Node> hashRing = new TreeMap<>(); public List<Node> nodeList = new ArrayList<>(); /** * 增加节点 * 每增加一个节点,就会在闭环上增加给定虚拟节点 * 例如虚拟节点数是2,则每调用此方法一次,增加两个虚拟节点,这两个节点指向同一Node * @param ip */ public void addNode(String ip) { Objects.requireNonNull(ip); Node node = new Node(ip); nodeList.add(node); for (Integer virtualNodeHash : node.getVirtualNodeHashes()) { hashRing.put(virtualNodeHash, node); System.out.println("虚拟节点[" + node + "] hash:" + virtualNodeHash + ",被添加"); } } /** * 移除节点 * @param node */ public void removeNode(Node node){ nodeList.remove(node); } /** * 获取缓存数据 * 先找到对应的虚拟节点,然后映射到物理节点 * @param key * @return */ public Object get(Object key) { Node node = findMatchNode(key); System.out.println("获取到节点:" + node.getIp()); return node.getCacheItem(key); } /** * 添加缓存 * 先找到hash环上的节点,然后在对应的节点上添加数据缓存 * @param key * @param value */ public void put(Object key, Object value) { Node node = findMatchNode(key); node.addCacheItem(key, value); } /** * 删除缓存数据 */ public void evict(Object key) { findMatchNode(key).removeCacheItem(key); } /** * 获得一个最近的顺时针节点 * @param key 为给定键取Hash,取得顺时针方向上最近的一个虚拟节点对应的实际节点 * * @return 节点对象 * @return */ private Node findMatchNode(Object key) { Map.Entry<Integer, Node> entry = hashRing.ceilingEntry(HashUtils.hashcode(key)); if (entry == null) { entry = hashRing.firstEntry(); } return entry.getValue(); } }如上所示,通过TreeMap的ceilingEntry() 方法,实现顺时针查找下一个的服务器节点的功能。6.3 哈希计算方法哈希计算方法比较常见,网上也有很多计算hash 值的函数。示例代码如下:public class HashUtils { /** * FNV1_32_HASH * * @param obj * object * @return hashcode */ public static int hashcode(Object obj) { final int p = 16777619; int hash = (int) 2166136261L; String str = obj.toString(); for (int i = 0; i < str.length(); i++) hash = (hash ^ str.charAt(i)) * p; hash += hash << 13; hash ^= hash >> 7; hash += hash << 3; hash ^= hash >> 17; hash += hash << 5; if (hash < 0) hash = Math.abs(hash); //System.out.println("hash computer:" + hash); return hash; } } 6.4 验证测试一致性哈希算法实现后,接下来添加一个测试类,验证此算法时候正常。示例代码如下:public class ConsistentHashTest { public static final int NODE_SIZE = 10; public static final int STRING_COUNT = 100 * 100; private static ConsistentHash consistentHash = new ConsistentHash(); private static List<String> sList = new ArrayList<>(); public static void main(String[] args) { // 增加节点 for (int i = 0; i < NODE_SIZE; i++) { String ip = new StringBuilder("10.2.1.").append(i) .toString(); consistentHash.addNode(ip); } // 生成需要缓存的数据; for (int i = 0; i < STRING_COUNT; i++) { sList.add(RandomStringUtils.randomAlphanumeric(10)); } // 将数据放入到缓存中。 for (String s : sList) { consistentHash.put(s, s); } for(int i = 0 ; i < 10 ; i ++) { int index = RandomUtils.nextInt(0, STRING_COUNT); String key = sList.get(index); String cache = (String) consistentHash.get(key); System.out.println("Random:"+index+",key:" + key + ",consistentHash get value:" + cache +",value is:" + key.equals(cache)); } // 输出节点及数据分布情况 for (Node node : consistentHash.nodeList){ System.out.println(node); } // 新增一个数据节点 consistentHash.addNode("10.2.1.110"); for(int i = 0 ; i < 10 ; i ++) { int index = RandomUtils.nextInt(0, STRING_COUNT); String key = sList.get(index); String cache = (String) consistentHash.get(key); System.out.println("Random:"+index+",key:" + key + ",consistentHash get value:" + cache +",value is:" + key.equals(cache)); } // 输出节点及数据分布情况 for (Node node : consistentHash.nodeList){ System.out.println(node); } } }运行此测试,输出结果如下所示:最后以上,我们就把一致性哈希算法的实现原理,应用场景、解决了哪些问题都介绍完了,并用java简单实现了一个一致性哈希算法。相信看完之后,大家对一致性哈希算法应该不会那么陌生害怕了吧。
MongoDB 如今是最流行的 NoSQL 数据库,被广泛应用于各行各业中,很多创业公司数据库选型就直接使用了 MongoDB。MongoDB一经推出就受到了广大社区的热爱,可以说是对程序员最友好的一种数据库,下面我们来了解一下它的特性。一、MongoDB简介1.1 什么是MongoDBMongoDB 是一个基于分布式文件存储的数据库。由 C++ 语言编写。旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。它可以应用于各种规模的企业、各个行业以及各类应用程序的开源的非关系型数据库。MongoDB的数据结构非常灵活,它可以随着应用程序的发展而灵活地更新。与此同时,它也为开发人员提供了许多传统数据库的功能:二级索引、完整的查询系统及数据一致性等。可以说是最像关系型数据库的非关系型数据库。MongoDB能够使企业更加具有灵活性和可扩展性,无论是创业公司、互联网企业或者是传统企业都可以通过MongoDB 来创建新的应用。MongoDB具备高可扩展性、高性能和高可用性等非关系型数据库的特性,可以从单服务器部署扩展到大型、复杂的多数据中心架构。利用内存计算的优势, MongoDB 能够提供高性能的数据读写操作。 MongoDB的本地复制和自动故障转移功能使应用程序具有企业级的可靠性和操作灵活性。1.2 MongoDB的特点MongoDB 是一个面向文档存储的数据库,操作起来比较简单和容易。相比其它的数据库,MongoDB具有如下特点:1、易扩展性,MongoDB使用分片技术对数据进行扩展,MongoDB能自动分片、自动转移分片里面的数据块,去掉了关系型数据库的关系型特性,数据之间没有关系。让每一个服务器里面存储的数据都是一样大小。这样就非常容易扩展。2、高性能,Mongo非常适合实时的插入,保留了关系型数据库即时查询的能力,并具备网站实时数据存储所需的复制及高度伸缩性。3、高伸缩性,Mongo非常适合由数十或数百台服务器组成的数据库,Mongo的路线图中已经包含对MapReduce引擎的内置支持。4、存储动态性,相较于传统的数据库当要增加一个属性值的时,对表的改动比较大,mongodb的面向文档的形式可以使其属性值轻意的增加和删除。而原来的关系型数据库要实现这个需要有很多的属性表来支持。5、速度与持久性,MongoDB通过驱动调用写入时,可以立即得到返回得到成功的结果(即使是报错),这样让写入的速度更加快,当然会有一定的不安全性,完全依赖网络。1.3 MongoDB 相关概念我们常说 MongoDB是最像关系数据库的非关系型数据库,在学习 MongoDB 之前我们先了解一些MongoDB的相关概念,并用关系数据库和 MongoDB 作一下对比,方便更清晰地认识它。SQL 术语MongoDB 术语说明DataBaseDataBase数据库TableCollection数据库表/集合RowDocument数据记录行/文档ColumnField数据字段/域indexindex索引Table joinsMongoDB 不支持primary keyprimary key 主键,MongoDB 自动将 _id字段设置为主键如上表所示:MongoDB 和关系数据库一样有库的概念,一个MongoDB 可以有多个数据库, MongoDB 中的集合就相当于我们关系数据库中的表,文档就相当于关系数据库中的数据行,域就相当于关系数据库中的列, MongoDB也支持各种索引有唯一主键,但不支持表连接查询。二、MongoDB安装MonggoDB支持以下Windows,Linux等平台。同时也提供了C、C++、C# / .NET、Erlang、Java、Ruby、Go等语言的驱动客户端。MonggoDB官网下载地址为:https://www.mongodb.com/download-center#community。下面我们以Centos 系统为例,演示MongoDB的安装。注意,在安装前需要安装libcurl、openssl等依赖包。命令如下:sudo yum install libcurl openssl。2.1 下载step1:这里使用wget在线下载安装,也可以通过上面的下载地址,手动下载安装包。# 1.下载 wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-rhel70-5.0.13.tgz # 2.解压 tar -zxvf mongodb-linux-x86_64-rhel70-5.0.13.tgz # 3.将解压包拷贝到指定目录 mv mongodb-linux-x86_64-rhel70-5.0.13 /usr/local/mongodb 2.2 创建数据库目录默认情况下 MongoDB 启动后会初始化以下两个目录:数据存储目录:/var/lib/mongodb日志文件目录:/var/log/mongodb所以,在启动前先创建这两个目录,命令如下:sudo mkdir -p /var/lib/mongo sudo mkdir -p /var/log/mongodb sudo chown 777 /var/lib/mongo # 设置权限 sudo chown 777 /var/log/mongodb # 设置权限2.3 创建配置文件MongoDB的bin目录下创建一个mongodb.conf 文件,增加如下配置:#touch mongodb.conf port=27017 #端口 bind_ip=0.0.0.0 #默认是127.0.0.1 dbpath=/var/lib/mongo/ #数据库存放 logpath=/var/log/mongodb/mongod.log #日志文件 fork=true #设置后台运行 #auth=true #开启认证MongoDB默认没有配置文件,需要我们手动创建配置文件。建议使用自定义配置文件,而不是默认配置。bind_ip 设置为0.0.0.0,否则Mongo服务只能本地连接,远程服务器会连接不上。2.4 启动MongoDB服务 接下来启动 MongoDB 服务,命令如下:cd /usr/local/mongodb/bin mongod --config mongodb.conf打开 /var/log/mongodb/mongod.log 文件看到以下信息,说明启动成功。如果要停止 mongodb 可以使用以下命令:mongod --dbpath /var/lib/mongo --logpath /var/log/mongodb/mongod.log --shutdown三、MongoDB的基本操作接下来,我们使用MongoDB后台客户端操作数据库。MongoDB Shell 是 MongoDB 自带的交互式 Javascript shell,用来对 MongoDB 进行操作和管理的交互式环境。3.1 客户端连接在MongoDB安装目录的下的 bin 目录下的mongo命令文件。使用./mongo 命令进入 MongoDB 后台后,它默认会链接到 test 数据库:3.2 基本操作MongoDB可以说是最像关系数据库的非关系数据库。一些命令和Mysql 比较类似。比如show databases查看数据库,use database 切换数据库等。# 查询数据库 show databases # 切换数据库, use test # 查询当前数据库下面的集合 show collections # 创建集合 db.createCollection("集合名称") # 删除集合 db.集合名称.drop() # 删除数据库 db.dropDatabase() //首先要通过use切换到当前的数据库MongoDB没有创建数据库的命令,提供了use 命令切换数据库,如果数据库不存在,则切换后,创建完机会后会自动创建数据库。如果你要创建一个新的数据库,使用use 命令切换到新数据库,然后创建collection 即可。四、增删改查接下来,我们介绍如何对MongoDB 的集合中数据进行增删改查等操作。MongoDB的数据结构和 JSON 基本一样。所有存储在集合中的数据都是 BSON 格式存储(一种类似 JSON 的二进制形式的存储格式,是 Binary JSON 的简称)。4.1 新增(insert)插入数据之前,需要创建collocation,这里使用db.createCollection("userinfo")命令创建了userinfo集合。#新增一条数据 db.userinfo.insert({name:"张三",age:25,gander:"男",address:'海淀区'}) #新增多条数据 db.userinfo.insert([{name:"张三",age:25,gander:"男",address:'海淀区'},{name:"李四",age:16,gander:"女",address:'昌平区'}])4.2 删除(delete)在 MongoDB 中,remove 和 deleteOne 以及 deleteMany 都用于删除文档记录。但是,remove 函数返回的删除的结果的 WriteResult,而 delete 函数返回的是 bson 格式。其中 remove 是根据参数 justOne 来判断是删除所有匹配的文档记录还是仅仅删除一条匹配的文档记录,默认是删除所有的匹配的记录。deleteOne 函数仅仅删除一条匹配的文档记录,而 deleteMany 函数是删除所有的匹配的文档记录。# 全部删除 db.userinfo.deleteMany({}) # 删除age为25的一条数据 db.inventory.deleteOne( { age:25} ) # 删除年龄为16岁的全部数据 db.userinfo.deleteMany({age:16}) # remove 移除 db.userinfo.remove({'name':'王五'})根据MongoDB的官方说明,remove() 方法已经过时了,现在官方推荐使用 deleteOne() 和 deleteMany() 方法。4.3 修改(update)MongoDB提供了 update() 方法来更新集合中的数据。update() 方法使用比较复杂,语法格式如下所示:db.collection.update( <query>, <update>, { upsert: <boolean>, multi: <boolean>, writeConcern: <document> } )参数说明:query : update的查询条件,类似sql update语句后where查询条件。update : update的对象和一些更新的操作(如$,$inc...)等,也可以理解为sql update查询内set 部分。upsert : 可选,这个参数的意思是,如果不存在update的记录是否插入,true为插入,默认是false 不插入。multi : 可选,mongodb 默认是false,只更新找到的第一条记录,如果这个参数为true,就把按条件查出来多条记录全部更新。writeConcern :可选,抛出异常的级别。下面,我们通过 update() 方法来更新一条数据:db.userinfo.update({'name':'王五'},{$set:{'address':'房山区'}})上面的示例中,通过update()方法将姓名(name)为‘王五’的用户的住址信息(address)改为‘房山区’。4.4 查询(find)MongoDB 使用 find() 方法查询显示文档数据。语法格式如下:db.collection.find(query, projection)。query 指定查询条件,类似sql select语句后的where条件,projection 为指定返回的键。默认返回文档中所有键值。# 查询全部 db.userinfo.find() # pretty() 方法以Json格式化显示所有文档。 db.userinfo.find().pretty() # 查询一条数据 db.userinfo.findOne() # 限制返回条数 db.userinfo.find().limit(1)4.5 运算符我们在查询数据的时候,经常会在查询条件中遇到条件判断的情况。如:查询年龄大于18岁的所有人员。同样,MongoDB中也提供类类似的条件运算符,具体有如下几个:(>) 大于 - $gt(<) 小于 - $lt(>=) 大于等于 - $gte(<= ) 小于等于 - $lte# 查询年龄大于20的全部人员 db.userinfo.find({age:{$gt:20}})MongoDB同样也有运算符$in,查询是否在某个集合中,类似sql 中的in关键字。使用方式如下:db.userinfo.find({age:{$in:[16,20]}})4.6 排序&分页MongoDB提供了sort() 方法对数据进行排序,通过参数指定排序的字段,并使用 1 和 -1 来指定排序的方式,其中 1 为升序排列,而 -1 是用于降序排列。语法格式为:db.collection.find().sort({key:1})。# 按年龄升序 db.userinfo.find({}).sort({age:1}) # 按年龄降序 db.userinfo.find({}).sort({age:-1})MongoDB提供了skip()方法来跳过指定数量的数据,skip方法同样接受一个数字参数作为跳过的记录条数。语法格式为:db.collection.find().limit(NUMBER).skip(NUMBER)。#分页查询 跳过20条查询10条 db.c1.find({}).sort({age:1}).skip(20).limit(10)如上所示,通过skip() 和limit() 方法,即可实现数据分页查询的功能。五、Spring Boot 整合MongoDBSpring Boot提供了MongoDB的组件:spring-boot-starter-data-mongodb ,它是 Spring Data 的一个子模块。熟悉Spring Boot的朋友应该知道,Redis、Elasticsearch、JPA等数据操作组件都在Spring Data下。所以,在Spring Boot中操作mongodb和操作其他的数据库基本是一样的。spring-boot-starter-data-mongodb 核心功能是映射 POJO 到 Mongo的DBCollection 中的文档,并且提供 Repository 风格数据访问层。spring-bootstarter-data-mongodb 除了继承 Spring Data 的通用功能外,针对 MongoDB 的特性开发了很多定制的功能,让我们使用 Spring Boot 操作 MongoDB 更加简便。Spring Boot 操作 MongoDB 有两种比较流行的使用方法,一种是将 MongoTemplate 直接注入到 Dao 中使用,一种是继承 MongoRepository, MongoRepository 内置了很多方法可直接使用。下面我们分别来介绍它们的使用。5.1 MongoTemplateMongoTemplate 提供了非常多的操作 MongoDB 方法,MongoTemplate 实现了MongoOperations 接口,此接口定义了众多的操作方法如 find、 findAndModify、findOne、 insert、 remove、 save、 update and updateMulti 等。并提供了Query、 Criteria and Update 等流式 API。5.1.1添加依赖首先创建Spring Boot项目spring-boot-starter-mongodb,在 pom 包里面添加 spring-boot-starter-data-mongodb 包引用,示例代码如下:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency>5.1.2 添加MongoDB连接配置修改application.properties配置文件,添加Mongo连接配置,具体如下:spring.data.mongodb.uri=mongodb://192.168.78.101:27017/mongotestmongodb默认没有账号密码,IP+端口+数据库就可以连接成功。如果mongodb配置了有账号密码,那连接字符串则需要增加相应的账号密码:spring.data.mongodb.uri=mongodb://username:password@192.168.78.101:27017/mongotest此外,如果MongoDB采用集群部署的方式,可以使用集群的配置方式,具体如下:spring.data.mongodb.uri=mongodb://user:pwd@ip1:port1,ip2:port2/database5.1.3 创建数据实体public class UserEntity implements Serializable { private static final long serialVersionUID = -3258839839160856613L; private Long id; private String name; private Integer age; private String gander; private String address; ... 省略getter和setter方法 }5.1.4 增删改查操作首先,创建一个测试类:MongoTemplateTest,将 MongoTemplate 注入到测试类中。@SpringBootTest public class MongoTemplateTest { @Autowired private MongoTemplate mongoTemplate; }然后,实现了UserEntity对象的增、删、改、查功能。 @Test public void testSaveUser() { UserEntity user=new UserEntity(); user.setId(1L); user.setName("test1"); user.setAddress("通州区"); user.setAge(29); user.setGander("男"); mongoTemplate.save(user); } @Test public void updateUser(){ Query query=new Query(Criteria.where("id").is(1)); Update update= new Update().set("age", 20).set("address", "大兴区"); //更更新查询返回结果集的第⼀一条 UpdateResult result =mongoTemplate.updateFirst(query,update,UserEntity.class); if(result!=null) System.out.println(result.getMatchedCount()); } @Test public void findUserByUserName(){ Query query=new Query(Criteria.where("name").is("test1")); UserEntity user = mongoTemplate.findOne(query , UserEntity.class); System.out.println(user); } @Test public void deleteUserById(){ Query query=new Query(Criteria.where("id").is(1L)); mongoTemplate.remove(query, UserEntity.class); }5.2 MongoRepository熟悉Spring Data的同学应该对Repository比较熟悉。MongoRepository 继承于 PagingAndSortingRepository,而PagingAndSortingRepository则是继承于CrudRepository,这两个接口是所有Repository接口的父接口。所以MongoRepository 和前面 JPA、 Elasticsearch 的使用比较类似,都是 Spring Data 家族的产品,最终使用方法也就和 JPA、 ElasticSearch 的使用方式类似。下面就来演示下MongoRepository的用法。首先创建一个UserRepository接口,继承 MongoRepository,这样就可直接使用 MongoRepository 的全部内置方法。具体代码如下:public interface UserRepository extends MongoRepository<UserEntity, Long> { UserEntity findByUserName(String userName); }然后,创建一个测试类:MongoRepositoryTest,将 UserRepository 注入到测试类中。使用 UserRepository 进行增、删、改、查功能测试。具体代码如下:@SpringBootTest public class MongoRepositoryTest { @Autowired private UserRepository userRepository; @Test public void testSaveUser() { UserEntity user=new UserEntity(); user.setId(1L); user.setName("test1"); user.setAddress("通州区"); user.setAge(29); user.setGander("男"); userRepository.save(user); } @Test public void updateUser(){ UserEntity user=new UserEntity(); user.setId(1L); user.setName("test1"); user.setAddress("通州区"); user.setAge(29); user.setGander("男"); userRepository.save(user); } @Test public void findUserByUserName(){ UserEntity user = userRepository.findByUserName("test1"); System.out.println(user); } @Test public void deleteUserById(){ userRepository.deleteById(1L); } }细心的同学已经发现了, MongoRepository 的使用方式和 Spring Boot JPA 的用法非常相似,其实 spring-boot-starter-data-mongodb 和 spring-boot-starter-data-jpa 都来自于 Spring Data,它们的实现原理基本一致,因此使用 Repository操作MongoDB 完全可以参考JPA 用法。最后以上,我们就把MongoDB的安装和使用 以及 如何在Spring Boot 项目中整合使用MongoDB介绍完了。MongoDB 如今是最流行的 NoSQL 数据库之一,被广泛应用于各行各业中,作为程序员必须熟练掌握。
前面我们介绍了什么是分布式存储系统,介绍了什么是MinIO,最后如何使用MinIO构建分布式文件系统。那么怎么在实际的项目中使用MinIO呢?接下来就手把手教你如何在SpringBoot中轻松整合MinIO 。一、SpringBoot整合MinIO下面开始在SpringBoot中轻松整合MinIO 。首先创建一个Spring Boot项目,添加MinIO依赖。1.1 添加MinIO依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.20</version> </dependency> <dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> <version>8.2.2</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.11</version> </dependency>1.2 配置MinIO修改applicationtion.yml文件,增加MinIO服务的地址,账号密码等相关配置,具体如下:minio: endpoint: http://192.168.78.101:9001 accessKey: admin secretKey: 12345678 bucketName: weiz-test上面的示例中,bucketName指的就是之前创建的MinIO桶Bucket。1.3 配置类创建MinIO配置对应的配置类MinioConfig,并注入MinIO客户端。具体代码如下:/** * @author weiz */ @Data @Configuration public class MinioConfig { /** * 访问地址 */ @Value("${minio.endpoint}") private String endpoint; /** * accessKey类似于用户ID,用于唯一标识你的账户 */ @Value("${minio.accessKey}") private String accessKey; /** * secretKey是你账户的密码 */ @Value("${minio.secretKey}") private String secretKey; /** * 默认存储桶 */ @Value("${minio.bucketName}") private String bucketName; @Bean public MinioClient minioClient() { MinioClient minioClient = MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); return minioClient; } }1.4 创建MinIO操作类封装一个MinIO相关操作的通用工具类MinioUtils,负责创建Bucket、上传、下载数据到MinIO服务。具体代码如下:/** * MinIO工具类 * */ @Slf4j @Component @RequiredArgsConstructor public class MinioUtils { private final MinioClient minioClient; /****************************** Operate Bucket Start ******************************/ /** * 启动SpringBoot容器的时候初始化Bucket * 如果没有Bucket则创建 * * @param bucketName */ @SneakyThrows(Exception.class) private void createBucket(String bucketName) { if (!bucketExists(bucketName)) { minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); } } /** * 判断Bucket是否存在,true:存在,false:不存在 * * @param bucketName * @return */ @SneakyThrows(Exception.class) public boolean bucketExists(String bucketName) { return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()); } /** * 获得Bucket的策略 * * @param bucketName * @return */ @SneakyThrows(Exception.class) public String getBucketPolicy(String bucketName) { return minioClient.getBucketPolicy(GetBucketPolicyArgs .builder() .bucket(bucketName) .build()); } /** * 获得所有Bucket列表 * * @return */ @SneakyThrows(Exception.class) public List<Bucket> getAllBuckets() { return minioClient.listBuckets(); } /** * 根据bucketName获取其相关信息 * * @param bucketName * @return */ @SneakyThrows(Exception.class) public Optional<Bucket> getBucket(String bucketName) { return getAllBuckets().stream().filter(b -> b.name().equals(bucketName)).findFirst(); } /** * 根据bucketName删除Bucket,true:删除成功; false:删除失败,文件或已不存在 * * @param bucketName * @throws Exception */ @SneakyThrows(Exception.class) public void removeBucket(String bucketName) { minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build()); } /****************************** Operate Bucket End ******************************/ /****************************** Operate Files Start ******************************/ /** * 判断文件是否存在 * * @param bucketName * @param objectName * @return */ public boolean isObjectExist(String bucketName, String objectName) { boolean exist = true; try { minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build()); } catch (Exception e) { log.error("[Minio工具类]>>>> 判断文件是否存在, 异常:", e); exist = false; } return exist; } /** * 判断文件夹是否存在 * * @param bucketName * @param objectName * @return */ public boolean isFolderExist(String bucketName, String objectName) { boolean exist = false; try { Iterable<Result<Item>> results = minioClient.listObjects( ListObjectsArgs.builder().bucket(bucketName).prefix(objectName).recursive(false).build()); for (Result<Item> result : results) { Item item = result.get(); if (item.isDir() && objectName.equals(item.objectName())) { exist = true; } } } catch (Exception e) { log.error("[Minio工具类]>>>> 判断文件夹是否存在,异常:", e); exist = false; } return exist; } /** * 根据文件前置查询文件 * * @param bucketName 存储桶 * @param prefix 前缀 * @param recursive 是否使用递归查询 * @return MinioItem 列表 */ @SneakyThrows(Exception.class) public List<Item> getAllObjectsByPrefix(String bucketName, String prefix, boolean recursive) { List<Item> list = new ArrayList<>(); Iterable<Result<Item>> objectsIterator = minioClient.listObjects( ListObjectsArgs.builder().bucket(bucketName).prefix(prefix).recursive(recursive).build()); if (objectsIterator != null) { for (Result<Item> o : objectsIterator) { Item item = o.get(); list.add(item); } } return list; } /** * 获取文件流 * * @param bucketName 存储桶 * @param objectName 文件名 * @return 二进制流 */ @SneakyThrows(Exception.class) public InputStream getObject(String bucketName, String objectName) { return minioClient.getObject( GetObjectArgs.builder() .bucket(bucketName) .object(objectName) .build()); } /** * 断点下载 * * @param bucketName 存储桶 * @param objectName 文件名称 * @param offset 起始字节的位置 * @param length 要读取的长度 * @return 二进制流 */ @SneakyThrows(Exception.class) public InputStream getObject(String bucketName, String objectName, long offset, long length) { return minioClient.getObject( GetObjectArgs.builder() .bucket(bucketName) .object(objectName) .offset(offset) .length(length) .build()); } /** * 获取路径下文件列表 * * @param bucketName 存储桶 * @param prefix 文件名称 * @param recursive 是否递归查找,false:模拟文件夹结构查找 * @return 二进制流 */ public Iterable<Result<Item>> listObjects(String bucketName, String prefix, boolean recursive) { return minioClient.listObjects( ListObjectsArgs.builder() .bucket(bucketName) .prefix(prefix) .recursive(recursive) .build()); } /** * 使用MultipartFile进行文件上传 * * @param bucketName 存储桶 * @param file 文件名 * @param objectName 对象名 * @param contentType 类型 * @return */ @SneakyThrows(Exception.class) public ObjectWriteResponse uploadFile(String bucketName, MultipartFile file, String objectName, String contentType) { InputStream inputStream = file.getInputStream(); return minioClient.putObject( PutObjectArgs.builder() .bucket(bucketName) .object(objectName) .contentType(contentType) .stream(inputStream, inputStream.available(), -1) .build()); } /** * 图片上传 * @param bucketName * @param imageBase64 * @param imageName * @return */ public ObjectWriteResponse uploadImage(String bucketName, String imageBase64, String imageName) { if (!StringUtils.isEmpty(imageBase64)) { InputStream in = base64ToInputStream(imageBase64); String newName = System.currentTimeMillis() + "_" + imageName + ".jpg"; String year = String.valueOf(new Date().getYear()); String month = String.valueOf(new Date().getMonth()); return uploadFile(bucketName, year + "/" + month + "/" + newName, in); } return null; } public static InputStream base64ToInputStream(String base64) { ByteArrayInputStream stream = null; try { byte[] bytes = new BASE64Decoder().decodeBuffer(base64.trim()); stream = new ByteArrayInputStream(bytes); } catch (Exception e) { e.printStackTrace(); } return stream; } /** * 上传本地文件 * * @param bucketName 存储桶 * @param objectName 对象名称 * @param fileName 本地文件路径 * @return */ @SneakyThrows(Exception.class) public ObjectWriteResponse uploadFile(String bucketName, String objectName, String fileName) { return minioClient.uploadObject( UploadObjectArgs.builder() .bucket(bucketName) .object(objectName) .filename(fileName) .build()); } /** * 通过流上传文件 * * @param bucketName 存储桶 * @param objectName 文件对象 * @param inputStream 文件流 * @return */ @SneakyThrows(Exception.class) public ObjectWriteResponse uploadFile(String bucketName, String objectName, InputStream inputStream) { return minioClient.putObject( PutObjectArgs.builder() .bucket(bucketName) .object(objectName) .stream(inputStream, inputStream.available(), -1) .build()); } /** * 创建文件夹或目录 * * @param bucketName 存储桶 * @param objectName 目录路径 * @return */ @SneakyThrows(Exception.class) public ObjectWriteResponse createDir(String bucketName, String objectName) { return minioClient.putObject( PutObjectArgs.builder() .bucket(bucketName) .object(objectName) .stream(new ByteArrayInputStream(new byte[]{}), 0, -1) .build()); } /** * 获取文件信息, 如果抛出异常则说明文件不存在 * * @param bucketName 存储桶 * @param objectName 文件名称 * @return */ @SneakyThrows(Exception.class) public String getFileStatusInfo(String bucketName, String objectName) { return minioClient.statObject( StatObjectArgs.builder() .bucket(bucketName) .object(objectName) .build()).toString(); } /** * 拷贝文件 * * @param bucketName 存储桶 * @param objectName 文件名 * @param srcBucketName 目标存储桶 * @param srcObjectName 目标文件名 */ @SneakyThrows(Exception.class) public ObjectWriteResponse copyFile(String bucketName, String objectName, String srcBucketName, String srcObjectName) { return minioClient.copyObject( CopyObjectArgs.builder() .source(CopySource.builder().bucket(bucketName).object(objectName).build()) .bucket(srcBucketName) .object(srcObjectName) .build()); } /** * 删除文件 * * @param bucketName 存储桶 * @param objectName 文件名称 */ @SneakyThrows(Exception.class) public void removeFile(String bucketName, String objectName) { minioClient.removeObject( RemoveObjectArgs.builder() .bucket(bucketName) .object(objectName) .build()); } /** * 批量删除文件 * * @param bucketName 存储桶 * @param keys 需要删除的文件列表 * @return */ public void removeFiles(String bucketName, List<String> keys) { List<DeleteObject> objects = new LinkedList<>(); keys.forEach(s -> { objects.add(new DeleteObject(s)); try { removeFile(bucketName, s); } catch (Exception e) { log.error("[Minio工具类]>>>> 批量删除文件,异常:", e); } }); } /** * 获取文件外链 * * @param bucketName 存储桶 * @param objectName 文件名 * @param expires 过期时间 <=7 秒 (外链有效时间(单位:秒)) * @return url */ @SneakyThrows(Exception.class) public String getPresignedObjectUrl(String bucketName, String objectName, Integer expires) { GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder().expiry(expires).bucket(bucketName).object(objectName).build(); return minioClient.getPresignedObjectUrl(args); } /** * 获得文件外链 * * @param bucketName * @param objectName * @return url */ @SneakyThrows(Exception.class) public String getPresignedObjectUrl(String bucketName, String objectName) { GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder() .bucket(bucketName) .object(objectName) .method(Method.GET).build(); return minioClient.getPresignedObjectUrl(args); } /** * 将URLDecoder编码转成UTF8 * * @param str * @return * @throws UnsupportedEncodingException */ public String getUtf8ByURLDecoder(String str) throws UnsupportedEncodingException { String url = str.replaceAll("%(?![0-9a-fA-F]{2})", "%25"); return URLDecoder.decode(url, "UTF-8"); } }1.5 创建Controller创建测试控制器OSSController,示例代码如下:@Slf4j @RestController @RequestMapping("/oss") public class OSSController { @Autowired private MinioUtils minioUtils; @Autowired private MinioConfig minioConfig; /** * 文件上传 * * @param file */ @PostMapping("/upload") public String upload(@RequestParam("file") MultipartFile file) { try { //文件名 String fileName = file.getOriginalFilename(); String newFileName = System.currentTimeMillis() + "." + StringUtils.substringAfterLast(fileName, "."); //类型 String contentType = file.getContentType(); minioUtils.uploadFile(minioConfig.getBucketName(), file, newFileName, contentType); return "上传成功"; } catch (Exception e) { log.error("上传失败"); return "上传失败"; } } /** * 删除 * * @param fileName */ @DeleteMapping("/") public void delete(@RequestParam("fileName") String fileName) { minioUtils.removeFile(minioConfig.getBucketName(), fileName); } /** * 获取文件信息 * * @param fileName * @return */ @GetMapping("/info") public String getFileStatusInfo(@RequestParam("fileName") String fileName) { return minioUtils.getFileStatusInfo(minioConfig.getBucketName(), fileName); } /** * 获取文件外链 * * @param fileName * @return */ @GetMapping("/url") public String getPresignedObjectUrl(@RequestParam("fileName") String fileName) { return minioUtils.getPresignedObjectUrl(minioConfig.getBucketName(), fileName); } /** * 文件下载 * * @param fileName * @param response */ @GetMapping("/download") public void download(@RequestParam("fileName") String fileName, HttpServletResponse response) { try { InputStream fileInputStream = minioUtils.getObject(minioConfig.getBucketName(), fileName); response.setHeader("Content-Disposition", "attachment;filename=" + fileName); response.setContentType("application/force-download"); response.setCharacterEncoding("UTF-8"); IOUtils.copy(fileInputStream, response.getOutputStream()); } catch (Exception e) { log.error("下载失败"); } } }二、测试验证上面我们已经把MinIO整合到了Spring Boot项目中了,接下来,我们使用Postman 验证下文件的上传下载是否正常。1)文件上传使用Postman调用http://localhost:8080/oss/upload 接口,选择某个文件测试上传功能,如下图所示:2)文件下载在浏览器中,调用http://localhost:8080/oss/download?fileName=1665744927595.jpg 接口,验证文件下载接口,如下图所示:当然,也可以直接访问minio的地址:http://192.168.78.101:9000/weiz-test/1665744927595.jpg。验证文件是否上传成功。最后以上,我们就把如何在Spring Boot项目中整合MinIO 介绍完了。MinIO是目前非常流行的分布式对象存储系统(OSS),作为程序员还是有必要熟悉的。
随着文件数据的越来越多,传统的文件存储方式通过tomcat或nginx虚拟化的静态资源文件在单一的服务器节点内已经无法满足系统需求,也不利于文件的管理和维护,这就需要一个系统来管理多台计算机节点上的文件数据,这就是分布式文件系统。一、什么是分布式文件系统?1.1 什么是分布式文件系统分布式文件系统(Distributed File System,DFS)是指文件系统管理的物理存储资源不一定直接连接在本地节点上,而是通过计算机网络与节点(可简单的理解为一台计算机)相连;或是若干不同的逻辑磁盘分区或卷标组合在一起而形成的完整的有层次的文件系统。DFS为分布在网络上任意位置的资源提供一个逻辑上的树形文件系统结构,从而使用户访问分布在网络上的共享文件更加简便。1.3 分布式文件系统的优势可扩展:分布式存储系统可以扩展到数百甚至数千个这样的集群大小,并且系统的整体性能可以线性增长。高可用性:在分布式文件系统中,高可用性包含两层,一是整个文件系统的可用性,二是数据的完整和一致性低成本:分布式存储系统的自动容错和自动负载平衡允许在成本较低服务器上构建分布式存储系统。此外,线性可扩展性还能够增加和降低服务器的成本。弹性存储: 可以根据业务需要灵活地增加或缩减数据存储以及增删存储池中的资源,而不需要中断系统运行1.4 分布式文件系统的应用场景分布式文件系统广发适用于互联网、金融等海量非结构化数据的存储需求:电商网站:海量商品图片视频平台:视频、图片文件存储网盘应用:文件存储社交网站:海量视频、图片二、分布式文件系统与传统文件系统对比传统的网络存储系统采用集中的服务器存放所有数据,到一定程度服务器会成为系统性能的瓶颈,也是可靠性和安全性的焦点,不能满足大规模存储应用的需要。分布式文件系统是将文件分散的存储在多台服务器上,采用可扩展的系统结构,利用多台服务器分担负荷,利用位置服务器定位存储信息。这不但提高了系统的可靠性、可用性和存取效率,还易于扩展,避免单点故障。分布式文件系统一般文件系统存储方式数据分散的存储在多台服务器上集中存放所有数据,在一台服务器上器上特点分布式网络存储系统采用可扩展的系统结构,利用多台存储服务器分担存储负荷,利用位置服务器定位存储信息,它不但提高了系统的可靠性、可用性和存取效率,还易于扩展。传统的网络存储系统采用集中的存储服务器存放所有数据,存储服务器成为系统性能的瓶颈,也是可靠性和安全性的焦点,不能满足大规模存储应用的需要。使用分布式文件系统可以解决如下几点问题:海量文件数据存储文件数据高可用(冗余备份)读写性能和负载均衡以上三点都是传统文件系统无法达到的,这也是我们为什么要使用分布式文件系统的原因。目前,可用于文件存储的网络服务选择有很多,其中最常用的分布式文件系统有:DFS、FastDfs、MinIO、Ceph等。接下来我们就来详细介绍MinIO并通过MinIO搭建分布式存储系统。三、MinIO简介3.1 什么是MinIO?MinIO 是在 GNU Affero 通用公共许可证 v3.0 下发布的高性能对象存储。它与 Amazon S3 云存储服务 API 兼容。使用 MinIO 为机器学习、分析和应用程序数据工作负载构建高性能基础架构。官方文档:https//docs.min.io/中文文档:http://docs.minio.org.cn/docs/GitHub 地址:https://github.com/minio/minio3.2 MinIO的特点数据保护——分布式 MinIO采用 纠删码来防范多个节点宕机和位衰减 bit rot。分布式 MinIO至少需要 4 个硬盘,使用分布式 MinIO自动引入了纠删码功能。高可用——单机MinIO服务存在单点故障风险,相反,如果是一个有 N 块硬盘的分布式 MinIO,只要有 N/2 硬盘在线,你的数据就是安全的。不过你需要至少有 N/2+1 个硬盘来创建新的对象。一致性——MinIO在分布式和单机模式下,所有读写操作都严格遵守 read-after-write 一致性模型。3.3 MinIO的优点部署简单,一个二进制文件(minio)即是一切,还可以支持各种平台;支持海量存储,可以按 zone 扩展,支持单个对象最大 5TB;低冗余且磁盘损坏高容忍,标准且最高的数据冗余系数为 2(即存储一个 1M 的数据对象,实际占用磁盘空间为 2M)。但在任意 n/2 块 disk 损坏的情况下依然可以读出数据(n 为一个纠删码集合中的 disk 数量)。并且这种损坏恢复是基于单个对象的,而不是基于整个存储卷的;读写性能优异,MinIO号称是目前速度最快的对象存储服务器。在标准硬件上,对象存储的读/写速度最高可以高达183 GB/s和171 GB/s。3.4 MinIO 基础概念S3——Simple Storage Service,简单存储服务,这个概念是 Amazon 在 2006 年推出的,对象存储就是从那个时候诞生的。S3 提供了一个简单 Web 服务接口,可用于随时在 Web 上的任何位置存储和检索任何数量的数据;Object——存储到 MinIO 的基本对象,如文件、字节流等各种类型的数据;Bucket——用来存储 Object 的逻辑空间。每个 Bucket 之间的数据是相互隔离的;Drive——部署 MinIO时设置的磁盘,MinIO 中所有的对象数据都会存储在 Drive 里;Set——一组 Drive 的集合,分布式部署根据集群规模自动划分一个或多个 Set ,每个 Set 中的 Drive 分布在不同位置。一个对象存储在一个 Set 上一个集群划分为多个 Set一个 Set 包含的 Drive 数量是固定的,默认由系统根据集群规模自动计算得出一个 SET 中的 Drive 尽可能分布在不同的节点上Set /Drive 的关系Set /Drive 这两个概念是 MinIO 里面最重要的两个概念,一个对象最终是存储在 Set 上面的。Set 是另外一个概念,Set 是一组 Drive 的集合,图中,所有蓝色、橙色背景的 Drive(硬盘)的就组成了一个 Set。3.5 什么是纠删码(Erasure Code)?前面我们介绍MinIO的时候提到过:Minio 采用纠删码来防范多个节点宕机或是故障,保证数据安全。那究竟什么是纠删码呢?纠删码(Erasure Code)简称 EC,它是一种恢复丢失和损坏数据的算法,也是一种编码技术。通过将数据分割成片段,把冗余数据块扩展、编码,并将其存储在不同的位置,比如磁盘、存储节点或者其它地理位置,实现数据的备份与安全。其实,简单来说就是:纠删码可通过将 n 份原始数据,增加 m 份校验数据,并能通过 n+m 份中的任意 n 份原始数据,还原为原始数据。即如果有任意小于等于 m 份的校验数据失效,仍然能通过剩下的数据还原出来。目前,纠删码技术在分布式存储系统中的应用主要有三类:阵列纠删码(Array Code: RAID5、RAID6 等)、RS(Reed-Solomon)里德-所罗门类纠删码和LDPC(LowDensity Parity Check Code)低密度奇偶校验纠删码。Minio 采用 Reed-Solomon code 将对象拆分成 N/2 数据和 N/2 奇偶校验块。在同一集群内,MinIO 自己会自动生成若干纠删组(Set),用于分布存放桶数据。一个纠删组中的一定数量的磁盘发生的故障(故障磁盘的数量小于等于校验盘的数量),通过纠删码校验算法可以恢复出正确的数据。四、MinIO安装部署4.1MinIO部署方式MinIO支持多种部署方式:单主机单硬盘模式、单主机多硬盘模式、多主机多硬盘模式(也就是分布式)。下面介绍下这三种方式。4.1.1 单主机,单硬盘模式如上图所示,此模式下MinIO 只在一台服务器上搭建服务,且数据都存在单块磁盘上,该模式存在单点风险,主要用作开发、测试等使用4.1.2 单主机,多硬盘模式如上图所示,该模式下MinIO 在一台服务器上搭建服务,但数据分散在多块(大于 4 块)磁盘上,提供了数据上的安全保障。4.1.3 多主机、多硬盘模式(分布式)如上图所示,此模式是 MinIO 服务最常用的架构,通过共享一个 access_key 和 secret_key,在多台服务器上搭建服务,且数据分散在多块(大于 4 块,无上限)磁盘上,提供了较为强大的数据冗余机制(Reed-Solomon 纠删码)。4.2MinIO 分布式部署4.2.1 环境准备由于是MinIO分布式部署,准备了2台Linux虚拟机,Centos 7.5的操作系统。同时每台服务器额外增加了2个磁盘。Nginx则是用于集群的负载均衡,也可以使用etcd。节点IP磁盘minio node1192.168.78.101/mnt/disk1,/mnt/disk2minio node2192.168.78.102/mnt/disk1,/mnt/disk2nginx192.168.78.101/home/nginx【温馨提示】磁盘大小必须>1G,这里我添加的是 4*1G 的盘。MinIO官网下载地址:https://min.io/download#/linux4.2.2 搭建MinIO集群1)创建安装目录首先,在每台服务器上创建minio的目录。mkdir -p /home/minio/{run,conf} && mkdir -p /etc/minio2)下载MinIO接下来进入到我们刚刚创建的minio目录,下载MinIO程序,具体命令如下所示:cd /home/minio/run wget https://dl.min.io/server/minio/release/linux-amd64/minio chmod +x miniominio的程序很简单,下载后就一个可执行文件。两台服务器都要执行如下操作,当然也可以一台服务器上面执行,然后拷贝到另一台服务器。3)配置服务启动脚本Minio 默认9000端口,在配置文件中加入–address “127.0.0.1:9029” 可更改端口。同时还有一些启动参数如下所示:MINIO_ACCESS_KEY:用户名,长度最小是 5 个字符;MINIO_SECRET_KEY:密码,密码不能设置过于简单,不然 minio 会启动失败,长度最小是 8 个字符;–config-dir:指定集群配置文件目录;–address:api 的端口,默认是9000--console-address :web 后台端口,默认随机;编写启动脚本(/home/minio/run/minio-run.sh)#!/bin/bash export MINIO_ACCESS_KEY=admin export MINIO_SECRET_KEY=12345678 /home/minio/run/minio server --config-dir /home/minio/conf \ --address "192.168.78.102:9000" --console-address ":50000" \ http://192.168.78.102/mnt/disk1 http://192.168.78.102/mnt/disk2 \ http://192.168.78.101/mnt/disk1 http://192.168.78.101/mnt/disk2 \ 如上示例代码所示,我们的minio服务绑定主机192.168.1.102和端口9000,后台端口50000,配置MinIO服务的登录账号密码为:admin\12345678。此启动脚本同样需要复制到另外一台服务器。【温馨提示】脚本复制时 \ 后不要有空格,还有就是上面的目录是对应的一块磁盘,而非简单的在/mnt 目录下创建四个目录,要不然会报如下错误,看提示以为是 root 权限问题。part of root disk, will not be used (*errors.errorString)4)启动Minio集群MinIO配置完成后,在两台测试服务器上都执行该脚本,即以分布式的方式启动MINIO服务。sh /home/minio/run/minio-run.sh集群启动成功后,接下来分别访问节点上的MinIO后台管理页面,两个节点都可以访问http://192.168.78.101:50000/,http://192.168.78.102:50000/ 。账号密码:admin/12345678以上,说明MinIO集群启动成功。4.2.3 使用 nginx 负载均衡上面我们部署好了MinIO集群,我们知道每个集群上的节点都可以单独访问,虽然每个节点的数据都是一致的,但这样显然不合理。接下来我们通过使用 nginx 进行负载均衡。具体的的配置如下:upstream minio_server { server 192.168.78.101:9000; server 192.168.78.102:9000; } upstream minio_console { server 192.168.78.101:50000; server 192.168.78.102:50000; } server{ listen 9001; server_name 192.168.78.101; ignore_invalid_headers off; client_max_body_size 0; proxy_buffering off; location / { proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_connect_timeout 300; proxy_http_version 1.1; chunked_transfer_encoding off; proxy_ignore_client_abort on; proxy_pass http://minio_server; } } server{ listen 50001; server_name 192.168.78.101; ignore_invalid_headers off; client_max_body_size 0; proxy_buffering off; location / { proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_connect_timeout 300; proxy_http_version 1.1; chunked_transfer_encoding off; proxy_ignore_client_abort on; proxy_pass http://minio_console; } }这里就不介绍如何安装Nginx了。不了解的同学可以查看我之前关于Nginx的系列文章。接下来,保存配置并重启Nginx服务,然后在浏览器中访问:http://192.168.78.101:50001/ 验证MinIO集群是否可以访问。最后以上,我们就把分布式存储系统介绍完了,并且介绍了目前最流行的分布式对象存储MinIO。接下来还会介绍如何在项目中整合MinIO服务。
之前我们介绍Redis入门系列课程的时候,讲了Redis的缓存雪崩、穿透、击穿。在文章里我们说了解决缓存穿透的办法之一,就是使用布隆过滤器,但是由于并没有详细介绍什么是布隆过滤器,所以就有很多小伙伴问我——到底什么是布隆过滤器?那么接下来就来给大家介绍什么是布隆过滤器以及他的实现原理。一、什么是布隆过滤器?布隆过滤器(Bloom Filter)是非常经典的以空间换时间的算法。布隆过滤器由布隆在 1970 年提出。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。其实说白了,布隆过滤器就是一种节省空间的概率数据结构,通过使用很数组和一些列随机映射函数。用于判断一个元素是否在一个集合中,0代表不存在某个数据,1代表存在某个数据。二、布隆过滤器的优缺点2.1优点相比于其它的数据结构,布隆过滤器在空间和时间方面都有巨大的优势。布隆过滤器存储空间和插入/查询时间都是常数(即hash函数的个数);Hash 函数相互之间没有关系,方便由硬件并行实现;布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势;布隆过滤器可以表示全集,其它任何数据结构都不能;2.2缺点布隆过滤器的缺点和优点一样明显:误算率(False Positive)是其中之一。随着存入的元素数量增加,误算率随之增加(误判补救方法是:再建立一个小的白名单,存储那些可能被误判的信息)。但是如果元素数量太少,则使用散列表足矣。一般情况下不能从布隆过滤器中删除元素。我们很容易想到把位列阵变成整数数组,每插入一个元素相应的计数器加 1, 这样删除元素时将计数器减掉就可以了。然而要保证安全的删除元素并非如此简单。首先我们必须保证删除的元素的确在布隆过滤器里面. 这一点单凭这个过滤器是无法保证的。另外计数器回绕也会造成问题。三、布隆过滤器的使用场景利用布隆过滤器减少磁盘 IO 或者网络请求,因为一旦一个值必定不存在的话,就可以直接结束查询,比如以下场景:大数据去重,比如判断一个数字是否存在于包含大量数字的数字集中(数字集很大,5 亿以上!);网页爬虫对 URL 的去重,避免爬取相同的 URL 地址;反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱;缓存击穿,将已存在的缓存放到布隆过滤器中,当黑客频繁访问不存在的缓存时迅速返回避免缓存及数据库挂掉;四、布隆过滤器实现原理4.1数据结构布隆过滤器是一个基于数组和哈希函数散列元素的结构,很像HashMap的哈希桶。它可以用于检测一个元素是否在集合中。它的优点是空间效率和查询时间比一般算法要好很多,缺点是有一定概率的误判性,如HashMap出现哈希碰撞。4.2实现原理(1)存入过程布隆过滤器的核心就是一个二进制数据的集合和hash计算函数。当一个元素加入布隆过滤器中的时会进行如下操作:1.使用布隆过滤器中的哈希函数对元素值进行计算,返回对应的哈希值(一般有几个哈希函数得到几个哈希值);2.根据返回的hash值映射到对应的二进制集合的下标;3.将下标对应的二进制数据改成1;如上图所示,三个Hash函数计算key值“test”的hash值分别为2、5、9;那么就会把集合中2、5、9下标对应的数据改成1。(2)判断是否存在当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行如下操作:1.对给定元素再次进行相同的哈希计算;2.根据返回的hash值判断位数组中对应的元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,则说明该元素不在布隆过滤器中。从上图可以看到,元素“test”通过哈希函数计算,得到下标为 2、5、9 这 3个数据。虽然前两个点都为 1,但是很明显第 3 个点得到的数据为0,说明元素不在集合中。五、布隆过滤器实现目前市面上有很多实现布隆过滤器的方式,比如Google的GUAVA实现,还有Redis的插件RedisBloom等。接下来,我们简单实现一个布隆过滤器算法。需要注意的是,我这里的示例是为了演示布隆过滤器的实现原理的简单实现,实际上完善的布隆过滤器的算法还是比较复杂的,包括误判率,哈希计算方式等。1. 构建集合根据之前介绍的布隆过滤器的实现原理,布隆过滤器的实现主要包括可以存放二进制元素的 BitSet 以及多样性的哈希计算函数。下面通过示例演示布隆过滤器的实现。public class MyBloomFilter { /** * 位数组的大小 */ private static final int DEFAULT_SIZE = 2 << 24; /** * 位数组。数组中的元素只能是 0 或者 1 */ private BitSet bits = new BitSet(DEFAULT_SIZE); /** * 通过这个数组可以创建 3 个不同的哈希函数 */ private static final int[] SEEDS = new int[]{3, 13, 46}; /** * 存放包含 hash 函数的类的数组 */ private SimpleHash[] func = new SimpleHash[SEEDS.length]; /** * 初始化多个包含 hash 函数的类的数组,每个类中的 hash 函数都不一样 */ public MyBloomFilter() { // 初始化多个不同的 Hash 函数 for (int i = 0; i < SEEDS.length; i++) { func[i] = new SimpleHash(DEFAULT_SIZE, SEEDS[i]); } } }所有的元素存放都经过多样的哈希计算存放到 BitSet 中,这样可以尽可能的分散元素,减少误判性。2. 哈希函数这里只是提供了一种哈希计算的方式,实际可以实现多种不同的hash计算方式,每一个哈希计算都是一次扰动处理。一个元素的存放可以经过多次哈希,尽量让元素值做到散列,从而避免hash碰撞。 /** * 静态内部类。用于 hash 操作! */ public static class SimpleHash { private int cap; private int seed; public SimpleHash(int cap, int seed) { this.cap = cap; this.seed = seed; } /** * 计算 hash 值 */ public int hash(Object value) { int h; return (value == null) ? 0 : Math.abs(seed * (cap - 1) & ((h = value.hashCode()) ^ (h >>> 16))); } }3. 添加元素添加元素就是当某个元素不在集合中时,我们使用布隆过滤器中的哈希函数对元素值进行计算得到哈希值,然后根据返回的哈希值,将集合数组中把对应下标的值置为 1。具体代码如下: /** * 添加元素到位数组 */ public void add(Object value) { for (SimpleHash f : func) { bits.set(f.hash(value), true); } }4. 比对元素比对元素就是判断某个元素是否存在。我们对该元素进行哈希计算,然后通过哈希值获取集合中的数据,最后把这些哈希值 进行&& 计算,从而确定该元素是否存在。具体代码如下: /** * 判断指定元素是否存在于位数组 */ public boolean contains(Object value) { boolean ret = true; for (SimpleHash f : func) { ret = ret && bits.get(f.hash(value)); } return ret; }这里使用多个集合中的bit位置记录同一个元素的状态,确保结果更加准确,避免hash碰撞。5. 验证测试接下来我们创建一个测试类,验证布隆过滤器是否生效。示例代码如下:public class MyBloomFilterTest { public static void main(String[] args) { String value1 = "com:weiz:user:logininfo"; String value2 = "https://www.cnblogs.com/zhangweizhong"; String value3 = "200110221"; MyBloomFilter filter = new MyBloomFilter(); filter.add(value1); filter.add(value2); System.out.println("key:" + value1 +"是否存在:"+ filter.contains(value1)); System.out.println("key:" + value2 +"是否存在:"+ filter.contains(value2)); System.out.println("key:" + value3 +"是否存在:"+ filter.contains(value3)); } }运行上面的测试代码,验证布隆过滤器算法是否正常,具体结果如下图所示:通过上面的输出结果可以看到,value1和value2已经添加到布隆过滤器,返回结果为true,而value3未加入到布隆过滤器,所以返回false。说明布隆过滤器起到了数据过滤的作用。五、常见面试题1.布隆过滤器的使用场景?(1)解决Redis缓存穿透(2)在爬虫时,对爬虫网址进行过滤,已经存在布隆中的网址,不在爬取。(3)垃圾邮件过滤,对每一个发送邮件的地址进行判断是否在布隆的黑名单中,如果在就判断为垃圾邮件。2.布隆过滤器的实现原理和方式?参照上面讲的布隆过滤器原理。3.如何提高布隆过滤器的准确性?使用更大的集合和同时用多个不同的hash函数计算方式。4.你了解哪些类型的布隆过滤器实现?(1)Google 开源的 Guava 中自带的布隆过滤器;(2)Redis 中的布隆过滤器插件RedisBloom;最后以上,我们就把布隆过滤器的原理介绍完了,布隆过滤器的原理还是比较简单的,但是要实现真正的布隆过滤器算法,还需要考虑很多其他的问题:如误判率等。感兴趣的朋友可以深入研究。
1、什么是 Redis?Redis 是完全开源免费的,遵守 BSD 协议,是一个高性能的 key-value 数据库。Redis是一个内存中的键值数据库,通常称为数据结构服务器。它和其他键值数据库之间的主要区别之一是Redis存储和操作高级数据类型的能力。这些数据类型是大多数开发人员熟悉的基本数据结构(列表,映射,集合和排序集)。Redis的卓越性能,简单性和数据结构的原子操作有助于解决使用传统关系数据库实现时难以实现或执行不佳的问题。Redis特点Redis 支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。Redis 不仅仅支持简单的 key-value 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。Redis 支持数据的备份,即 master-slave 模式的数据备份。Redis优势Redis性能极高 – Redis 能读的速度是 110000 次/s,写的速度是 81000 次/s 。丰富的数据类型 – Redis 支持二进制案例的 Strings, Lists, Hashes,Sets 及Ordered Sets 数据类型操作。原子性 – Redis 的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过 MULTI 和 EXEC指令包起来。丰富的特性 – Redis 还支持 publish/subscribe, 通知, key 过期等等特性。2、Redis 与其他 key-value 存储有什么不同?Redis与其他key-value Nosql数据库(Memcache等)不太一样,那么 Redis与 的区别都有哪些?(1)Redis 有着更为复杂的数据结构并且提供对他们的原子性操作,这是一个不同于其他数据库的进化路径。Redis 的数据类型都是基于基本数据结构的同时对程序员透明,无需进行额外的抽象。(2)Redis 运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,因为数据量不能大于硬件内存。(3)在内存数据库方面的另一个优点是,相比在磁盘上相同的复杂的数据结构,在内存中操作起来非常简单,这样 Redis可以做很多内部复杂性很强的事情。同时,在磁盘格式方面他们是紧凑的以追加的方式产生的,因为他们并不需要进行随机访问。3、Redis 的数据类型?Redis 支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及 zsetsorted set:有序集合)。我们实际项目中比较常用的是 string,hash 如果你是 Redis 中高级用户,还需要加上下面几种数据结构 HyperLogLog、Geo、Pub/Sub。4、使用 Redis 有哪些好处?1、速度快,因为数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是 O1)2、支持丰富数据类型,支持 string,list,set,Zset,hash 等3、支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行4、丰富的特性:可用于缓存,消息,按 key 设置过期时间,过期后将会自动删除5、Redis 相比 Memcached 有哪些优势?那么Redis相比Memcached又有那些独特的优势呢?1、Memcached 所有的值均是简单的字符串,redis 作为其替代者,支持更为丰富的数据类;2、Redis 的速度比 Memcached 快很多;3、Redis 可以持久化其数据;最后以上,我们就把Redis介绍完了,Redis 是一款非常实用,非常高效的Nosql数据库。作为开发者必须熟练掌握。后续我们还会详细介绍Redis的安装和使用。
前面我们介绍了Redis的安装和Redis的几个数据结构。但是,还是有些朋友会问我Redis的配置文件内容项,参数都有哪些?配置个主从、持久化等是怎么配置的?其实,这些都可以在Redis提供的配置文件:redis.conf中配置。那么接下来我们就来捋一捋Redis的配置文件(redis.conf :版本号-redis-3.0)。首先,Redis的配置文件redis.conf 在我们编译完 redis后,将在源码目录下 redis.conf ,将其拷贝到工作目录下使用即可。如果未找到对应的redis.conf 文件,可以全局搜索配置文件 redis.conf 所在目录,和Redis相关的参数配置都在此目录下设置。# find / -name redis.conf一、基本配置port 6379 # 监听端口号,默认为 6379,如果你设为 0 ,redis 将不在 socket 上监听任何客户端连接。daemonize no #是否以后台进程启动databases 16 #创建database的数量(默认选中的是database 0)save 900 1 #刷新快照到硬盘中,必须满足两者要求才会触发,即900秒之后至少1个关键字发生变化。save 300 10 #必须是300秒之后至少10个关键字发生变化。save 60 10000 #必须是60秒之后至少10000个关键字发生变化。stop-writes-on-bgsave-error yes #后台存储错误停止写。rdbcompression yes #使用LZF压缩rdb文件。rdbchecksum yes #存储和加载rdb文件时校验。dbfilename dump.rdb #设置rdb文件名。dir ./ #设置工作目录,rdb文件会写入该目录。二、主从配置slaveof 设为某台机器的从服务器masterauth 连接主服务器的密码slave-serve-stale-data yes # 当主从断开或正在复制中,从服务器是否应答slave-read-only yes #从服务器只读repl-ping-slave-period 10 #从ping主的时间间隔,秒为单位repl-timeout 60 #主从超时时间(超时认为断线了),要比period大slave-priority 100 #如果master不能再正常工作,那么会在多个slave中,选择优先值最小的一个slave提升为master,优先值为0表示不能提升为master。repl-disable-tcp-nodelay no #主端是否合并数据,大块发送给slaveslave-priority 100 从服务器的优先级,当主服挂了,会自动挑slave priority最小的为主服三、安全requirepass foobared # 需要密码rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52 #如果公共环境,可以重命名部分敏感命令 如config 四、限制maxclients 10000 #最大连接数maxmemory #最大使用内存maxmemory-policy volatile-lru #内存到极限后的处理volatile-lru -> LRU算法删除过期keyallkeys-lru -> LRU算法删除key(不区分过不过期)volatile-random -> 随机删除过期keyallkeys-random -> 随机删除key(不区分过不过期)volatile-ttl -> 删除快过期的keynoeviction -> 不删除,返回错误信息#解释 LRU ttl都是近似算法,可以选N个,再比较最适宜T踢出的数据maxmemory-samples 3五、日志模式appendonly no #是否仅要日志appendfsync no # 系统缓冲,统一写,速度快appendfsync always # 系统不缓冲,直接写,慢,丢失数据少appendfsync everysec #折衷,每秒写1次no-appendfsync-on-rewrite no #为yes,则其他线程的数据放内存里,合并写入(速度快,容易丢失的多)auto-AOF-rewrite-percentage 100 当前aof文件是上次重写是大N%时重写auto-AOF-rewrite-min-size 64mb aof重写至少要达到的大小六、慢查询slowlog-log-slower-than 10000 #记录响应时间大于10000微秒的慢查询slowlog-max-len 128 # 最多记录128条七、集群配置cluster-enabled yes #允许集群模式,只有以集群模式启动的Redis实例才能作为集群的节点。cluster-node-timeout 15000 #节点超时时间,集群模式下,master节点之间会互相发送PING心跳来检测集群master节点的存活状态,超过配置的时间没有得到响应,则认为该master节点主观宕机。cluster-migration-barrier 1 #设置master故障转移时保留的最少副本数,群集某个master的slave可以迁移到孤立的master,即没有工作slave的master。这提高了集群抵御故障的能力,因为如果孤立master没有工作slave,则在发生故障时无法对其进行故障转移。只有在slave的旧master的其他工作slave的数量至少为给定数量时,slave才会迁移到孤立的master。这个数字就是cluster-migration-barrier。值为1意味着slave只有在其master至少有一个其他工作的slave时才会迁移,以此类推。它通常反映集群中每个主机所需的副本数量。默认值为1(仅当副本的主副本至少保留一个副本时,副本才会迁移)。要禁用迁移,只需将其设置为非常大的值。可以设置值0,但仅对调试有用,并且在生产中很危险。cluster-require-full-coverage yes #哈希槽全覆盖检查,默认情况下,如果Redis群集节点检测到至少有一个未覆盖的哈希槽(没有可用的节点为其提供服务),它们将停止接受查询。这样,如果集群部分关闭(例如,一系列哈希槽不再被覆盖),那么所有集群最终都将不可用。一旦所有插槽再次被覆盖,它就会自动返回可用状态。然而,有时您希望正在工作的集群的子集继续接受对仍然覆盖的密钥空间部分的查询。为此,只需将cluster-require-full-coverage选项设置为no。cluster-replica-no-failover no 是否自动故障转移,当设置为“yes”时,此选项可防止副本在主机故障期间尝试故障切换master。但是,如果被迫这样做,主机仍然可以执行手动故障切换。八、常用运维命令1.服务器端命令time 返回时间戳+微秒dbsize 返回key的数量bgrewriteaof 重写aofbgsave 后台开启子进程dump数据save 阻塞进程dump数据lastsaveslaveof host port 做host port的从服务器(数据清空,复制新主内容)slaveof no one 变成主服务器(原数据不丢失,一般用于主服失败后)flushdb 清空当前数据库的所有数据flushall 清空所有数据库的所有数据(误用了怎么办?)shutdown [save/nosave] 关闭服务器,保存数据,修改AOF(如果设置)slowlog get 获取慢查询日志slowlog len 获取慢查询日志条数slowlog reset 清空慢查询2.设置config get 选项(支持*通配)config set 选项 值config rewrite 把值写到配置文件config restart 更新info命令的信息debug object key #调试选项,看一个key的情况debug segfault #模拟段错误,让服务器崩溃object key (refcount|encoding|idletime)monitor #打开控制台,观察命令(调试用)client list #列出所有连接client kill #杀死某个连接 CLIENT KILL 127.0.0.1:43501client getname #获取连接的名称 默认nilclient setname "名称" #设置连接名称,便于调试 2.连接命令auth 密码 #密码登陆(如果有密码)ping #测试服务器是否可用echo "some content" #测试服务器是否正常交互select 0/1/2... #选择数据库quit #退出连接
所谓工欲善其事,必先利其器。前面我们介绍了Redis在windows和在Linux下的安装和配置。Redis处理性能强大之外,还有一个优势就是提供了多种数据结构,应对不同的业务场景。接下来我们介绍Redis的常用数据类型。Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。一、String(字符串)string 是 redis 最基本的类型,你可以理解成与 Memcached 一模一样的类型,一个 key 对应一个 value。string 类型是二进制安全的,它可以包含任何数据类型。比如序列化的对象等。string 类型是 Redis 最基本最常用的数据类型,string 类型的值最大能存储 512MB。1.测试127.0.0.1:6379> SET weiz:string "hello weiz" OK 127.0.0.1:6379> GET weiz:string "hello weiz"在以上实例中我们使用了 Redis 的 SET 和 GET 命令新增和获取缓存数据。键为 "weiz:string",对应的值为"hello weiz"。如下图所示:2.常用命令除了上面的get,set命令外,Redis还提供了很多功能强大的命令,比如递增,递减等,具体如下所示:mset用于同时设置一个或多个键值对mget用于同时获取一个或多个 valueexpire用于给已经存在的键设置过期时间,单位为秒setnx用于如果键不存在,则会添加成功,否则将添加失败setex用于在添加键值对的时候就为其设置过期时间(set 方式添加是永不过期)msetnx用于批量添加,如果有一个键已经存在,则所有都将添加失败incr如果值是integer ,则会将其进行加一的操作,如果不是则会报错decr如果值是integer ,则会将其进行减一的操作,如果不是则会报错incrby用于自定义需要加多少decrby用于自定义需要减多少二、Hash(哈希)Redis hash 是一个键值(key=>value)对集合。它是一个 string 类型的 field 和 value 的映射表,hash 适合用于存储对象或map等集合数据。1.测试示例127.0.0.1:6379> HMSET weiz:hash field1 "Hello" field2 "World" "OK" 127.0.0.1:6379> HGET weiz:hash field1 "Hello" 127.0.0.1:6379> HGET weiz:hash field2 "World"上面的示例中,我们使用了 Redis的 HMSET, HGET 命令,HMSET 设置了两个 field=>value 的Hash数据集合, 然后通过HGET 获取对应 field 对应的 value。每个 hash 可以存储 232 -1 键值对(40多亿)。2.常用命令除了上面的HMSET和HGET 外,还有Redis还提供了其他的一些命令来操作Hash数据,常用的命令如下:hmset用于同时将多个 field-value (字段-值)对设置到哈希表中;hmget用于返回哈希表中,一个或多个给定字段的值;hexists用于查看哈希表的指定字段是否存在;hincrby用于为哈希表中的字段值加上指定增量值;hlen用于获取哈希表中字段的数量;hvals返回哈希表所有字段的值,返回一个包含哈希表中所有值的表;hkeys用于获取哈希表中的所有字段名;hsetnx用于为哈希表中不存在的的字段赋值;三、List(列表)List列表是简单有序的字符串列表,常用的操作是向列表两端添加元素,或者获得列表的某一个片段。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。通过List数据类型可以实现队列等功能。1.测试示例127.0.0.1:6379> lpush weiz:list redis (integer) 1 127.0.0.1:6379> lpush weiz:list mongodb (integer) 2 127.0.0.1:6379> lpush weiz:list rabbitmq (integer) 3 127.0.0.1:6379> lrange weiz:list 0 10 1) "rabbitmq" 2) "mongodb" 3) "redis" 127.0.0.1:6379>上面的示例中,我们使用Redis的lpush 命令往键为"weiz:list"的list中加入了3个值。然后通过lrange命令获取list中的数据。list列表最多可存储 232 - 1 元素 (4294967295, 每个列表可存储40多亿)。2.常用命令除了lpush,lpop,lrange命令外,还有rpush,rpop,rrange等命令,具体如下:LPUSH,用于向列表左边增加元素;RPUSH,用于向列表右边增加元素;LPOP,用于从列表左边弹出元素;RPOP,用于从列表右边弹出元素;LLEN,用于获取列表中元素的个数;LRANGE,用于获得列表片段;LREM,用于删除列表中指定值;LINDEX,用于获得指定索引的元素值; LSET,用于设置指定索引的元素值;LTRIM,用于只保留列表指定片段;LINSERT,用于向列表中插入元素;四、Set(集合)Redis 的 Set 是 string 类型的无序集合。用于保存多个字符串元素,集合中的元素不能重复,并且集合中的元素也是无序的,无法通过下标来获取集合中的元素,这些特性与java的set非常类似。Redis的 Set集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。1.sadd命令Redis提供了sadd命令添加一个 string 元素到 key 对应的 set 集合中,成功返回 1,如果元素已经在集合中返回 0。命令格式如下:sadd key member2.测试示例127.0.0.1:6379> sadd weiz:set redis (integer) 1 127.0.0.1:6379> sadd weiz:set mongodb (integer) 1 127.0.0.1:6379> sadd weiz:set rabbitmq (integer) 1 127.0.0.1:6379> sadd weiz:set rabbitmq (integer) 0 127.0.0.1:6379> smembers weiz:set上面的示例中,我们通过sadd命令添加数据到set中,然后通过smembers命令获取set中的数据,如下图所示:集合中最大的成员数为 232 - 1(4294967295, 每个集合可存储40多亿个成员)。3.常用命令除了sadd,smembers命令外,Redis还提供了交集,并集等功能强大的命令,具体如下:scard 获取集合的成员数sdiff 获取集合与集合的差集sinter 获取集合与集合的交集sismember 判断member元素是否是集合key的成员smembers 获取集合中的所有成员sunion 获取所有给定集合的并集srem 移除一个或多个元素sscan 迭代集合中元素五、zset(sorted set:有序集合)zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。不同的是zset为每个元素都关联了一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。zset的成员是唯一的,但分数(score)却可以重复。1.zadd 命令Redis提供了zadd命令添加元素到集合,如果元素在集合中存在则更新对应score,命令格式如下:zadd key score member 2.测试示例127.0.0.1:6379> zadd weiz:zset 0 redis (integer) 1 127.0.0.1:6379> zadd weiz:zset 1 mongodb (integer) 1 127.0.0.1:6379> zadd weiz:zset 2 rabbitmq (integer) 1 127.0.0.1:6379> zadd weiz:zset 3 rabbitmq (integer) 0 127.0.0.1:6379> ZRANGEBYSCORE weiz:zset 0 1000 1) "mongodb" 2) "rabbitmq" 3) "redis"上面的示例中,我们使用zadd添加了3个数据,然后通过 ZRANGEBYSCORE命令获取对应的数据,如下图所示:3.常用命令除了sadd,smembers命令外,Redis还提供了交集,并集等功能强大的命令,具体如下:zadd 用于在集合中增加序号为n的value;zrange 用于排序指定的rank(排名)范围内的元素并输出;zrevrange 用于反向排序;zrangebyscore 用于获取指定的score范围内的元素;zrangebylex 用于获取某个范围的数据,还可以使用limit分页;zincrby 用于为元素的score累加,新元素score基数为0zrem 用于删除集合中指定的元素;zrank 用于查询指定value的排名,注意不是score;zcard 用于获取集合的元素个数;zremrangebyrank 用于删除某个范围内的元素;zremrangebyscore 用于删除score在某个范围内的元素;最后以上,我们就把Redis的数据结构介绍完了,Redis的各种数据结构都有对应的使用场景。作为开发人员必须熟练掌握。磨刀不误砍柴工,只有掌握了这些基础,才能在实际项目中熟练使用Redis。
最近项目中需要使用Redis,刚好这两天有时间,便总结记录一下Redis的安装,以及如何在项目中使用Redis。 Redis是一个用的比较广泛的Key/Value的内存数据库。目前新浪微博、Github、StackOverflow 等大型应用中都用其作为缓存,和Memcached类似,但是支持数据的持久化,解决了断电后数据完全丢失的情况。而且它支持更多的类型,除了string外,还支持lists(链表)、sets(集合)和zsets(有序集合)几种数据类型。 Redis的官网为: http://redis.io/。 一、Windows安装1.1下载Redis的安装非常的简单,而且Redis并不依赖其他环境和标准库,很容易上手,这可能也是它流行的一个原因。这里为了测试方便,用的都是windows 环境下测试。下载Windows版本Redis。解压完成后,Redis 的文件非常简单,主要文件如下:redis.windows.conf 是redis的配置文件。redis-server.exe 服务器端。redis-cli 命令行客户端。redis-benchmark:Redis性能测试工具,测试Redis在你的系统及你的配置下的读写性能。 1.2启动服务在命令行输入如下命令 :redis-server redis.windows.conf。同时也可以该命令保存为文件 startup.bat,下次就可以直接启动了。 如果提示redis-server 不是内部命令。将该目录加到环境变量里面即可。 1.3Redis基本配置Redis的配置比较多,都在redis.windows.conf 文件中。基本上默认配置即可,常用的配置项如下:1. port 端口号,例如63792. bind 实例绑定的访问地址127.0.0.13. requirepass 访问的密码4. maxheap 记得把这个配置节点打开,否者redis 服务无法启动。例如maxheap 10240000005. timeout:请求超时时间6. logfile:log文件位置7. databases:开启数据库的数量8. dbfilename:数据快照文件名(只是文件名,不包括目录) 1.4连接测试在命令行输入如下命令:redis-cli –h 127.0.0.1 –p 6379参数分别为host、port,如果设置了密码,则必须要加上-a 123456; 123456为登录密码。否则会提示没有权限登录系统。如下图所示。 二、Linux安装2.1下载Redis# 进入安装系统路径 # cd /usr/local # 创建下载redis安装包的目录 # mkdir redis # 进入创建好的目录路径 # cd /usr/local/redis # 在线下载redis安装包 # wget http://download.redis.io/releases/redis-5.0.7.tar.gz 注意,如果Linux系统未安装wget,请先安装:yum -y install wget。2.2编译&安装# 1.解压redis安装包 # tar -zxvf redis-5.0.7.tar.gz # 进入解压后的redis安装包 # cd redis-5.0.7 # 2.编译 # make # 3.安装,设置安装路径为/usr/local/redis 下 # make install PREFIX=/usr/local/redis2.3启动运行编译安装成功后,bin目录下还会生成相应的可执行文件。通过相关命令启动即可。1.启动Redis服务启动redis并查进程 # ./redis-server redis.conf # ps -ef | grep redis2.关闭Redis服务如果要关闭Redis服务,运行如下命令,不关闭不运行即可:./redis-cli shutdown三、设置Rdis开机自启在服务器上我们可能需要将 Redis 设置为开机自启动,其实这个也非常简单,我们只需要做以下四步操作即可。3.1 编写配置脚本 首先别写配置脚本:vim /etc/init.d/redis,脚本如下:#!/bin/sh # # Simple Redis init.d script conceived to work on Linux systems # as it does use of the /proc filesystem. #chkconfig: 2345 80 90 #description:auto_run # 端口号 REDISPORT=6379 # 启动命令 EXEC=/usr/local/redis/src/redis-server # shell 交付命令 CLIEXEC=/usr/local/redis/src/redis-cli # pid 存放位置 PIDFILE=/var/run/redis_${REDISPORT}.pid # redis 配置文件 CONF="/usr/local/redis/redis.conf" case "$1" in start) if [ -f $PIDFILE ] then echo "$PIDFILE exists, process is already running or crashed" else echo "Starting Redis server..." $EXEC $CONF fi ;; stop) if [ ! -f $PIDFILE ] then echo "$PIDFILE does not exist, process is not running" else PID=$(cat $PIDFILE) echo "Stopping ..." $CLIEXEC -p $REDISPORT shutdown while [ -x /proc/${PID} ] do echo "Waiting for Redis to shutdown ..." sleep 1 done echo "Redis stopped" fi ;; *) echo "Please use start or stop as first argument" ;; esac3.2 设置 redis 为守护进程方式运行修改 redis.conf,设置redis 为守护进程方式运行。################################# GENERAL ##################################### # By default Redis does not run as a daemon. Use 'yes' if you need it. # Note that Redis will write a pid file in /var/run/redis.pid when daemonized. daemonize yes3.3 修改文件执行权限chmod +x /etc/init.d/redis3.4 设置开机启动# 启动 redis service redis start # 停止 redis service redis stop # 开启服务自启动 chkconfig redis on最后以上,我们就把Redis在Window下和Linux下的安装和配置介绍完了。
之前介绍了运维监控系统Prometheus,然后就有同鞋问我关于时序数据库的情况,所以这里总结一下时序数据库,并以InfluxDB为例,介绍时序数据库的功能特性和使用方式,希望能对大家有所帮助。一、时序数据库概述1.1 什么是时序数据库时序数据是一组按照时间维度索引的数据。时序数据在日常生活中随处可见,比如每个整点的温度、湿度等天气数据,每分钟的股票价格数据等。我们常用曲线图、柱状图等形式去展现时序数据,也就是我们常常听到的“数据可视化”。时序数据库是一种非关系型数据库,以时间作为数据主键,专门用来存储时序数据。1.2 时序数据库的特点高压缩比:由于数据每分每秒都在变化,海量的时序数据往往体量巨大,占用大量硬件资源,所以需要优化数据压缩算法提高数据压缩比。高并发写入:时序数据库采用持续高并发写入数据,无更新的方式,对于时间相同的重复的数据,只保留一份数据。低延时、高并发查询:通过索引降低查询延时,通过缓存等技术提高数据并发能力。1.3 时序数据库的使用场景IOT行业:电力、化工等工业物联网数据监测金融行业:各类金融产品及其衍生品、数字货币数据存储与量化研究IT行业:服务器、虚拟机、容器等的状态数据实时监测互联网行业:用户行为轨迹,日志等数据。目前比较流行的时序数据库有:InfluxDB、Prometheus、OpenTSDB、TDengine等,其中使用最广泛的当属InfluxDB,行业内应用最广泛。还有就是刚进入业内视野的国产时序数据库TDengine。而Prometheus则是Prometheus监控系统自带的数据库。二、InfluxDB简介2.1 什么是InfluxDBInfluxDB 是一个用于存储和分析时间序列数据的开源数据库。由 Golang 语言编写,也是由 Golang 编写的软件中比较著名的一个,在很多 Golang 的沙龙或者文章中可能都会把 InfluxDB 当标杆来介绍,这也间接帮助 InfluxDB 提高了知名度。2.2 InfluxDB的特性内置 HTTP 接口,使用方便数据可以打标记,这样查询可以很灵活类 SQL 的查询语句安装管理很简单,并且读写数据很高效能够实时查询,数据在写入时被索引后就能够被立即查出在最新的 DB-ENGINES 给出的时间序列数据库的排名中,InfluxDB 高居第一位,可以预见,InfluxDB 会越来越得到广泛的使用。2.3 InfluxDB几个基本概念时序数据库由于其存储海量时序数据的特性,因此与传统数据库有些许不同,下面先对influxdb中涉及的基本概念作出解释。influxdb数据库由database、measurement、point等三部分构成。分别对应关系数据库中的数据库、表、数据行。database:数据库,同Mysql等关系型数据库中的“数据库Database”measurement:数据表,相当于关系型数据库中的“表Table”point:数据点,表示单条数据记录,相当于关系型数据库中的“一行数据”概念MySQLInfluxDB数据库(同)databasedatabase表(不同)tablemeasurement(测量; 度量)列(不同)columnPoint,包括:tag(带索引的,非必须)、field(不带索引)、timestemp(唯一主键)2.4 Point数据构成由于database和measurement与传统数据库基本相同,这里不做过多解释,以下针对influxdb中特有的Point进行讲解。Point是InfluxDB中独有的概念,由时间(time)、数据(field)、标签(tags)三类字段组成。(1)time:代表每条数据的时间字段,是measurement中的数据主键,因此time字段具有索引属性。一条point只能有一个time。(2)field:代表各种数据的字段,例如气温、压力、股价等,field字段没有索引属性。一条point可以包括多个field。(3)tag:代表各类非数据字段,例如设备编码、地区、姓名等,tag字段有索引属性。一条point可以包括多个tag。例如:监控系统系统中,保存某个服务器的cpu和内存等资源使用情况,使用cpu_usage_total 的表名(measurement)保存数据。以下表示某一个point的样例数据:其中time为time字段,记录数据产生的时间;cpu_usage和memory_usage分别代表CPU使用率和内存使用率,因此他们是field字段,真正的监控数据;cpu 和host代表CPU的名字和服务器IP,所以,他们是tag字段,用于查询和检索。在使用和设计InfluxDB数据结构时,需要注意以下几点:1. tag 只能为字符串类型,可以加索引;2. field 类型无限制,不能加索引;3. InfluxDB不支持 join;4. InfluxDB支持连续查询操作(汇总统计数据):CONTINUOUS QUERY;三、InfluxDB安装InfluxDB安装非常简单,根据操作系统执行对应的安装命令即可。这里以window为例,演示如何安装InfluxDB。 3.1 下载InfluxDB:https://dl.influxdata.com/influxdb/releases/influxdb-1.7.4_windows_amd64.zip chronograf :https://dl.influxdata.com/chronograf/releases/chronograf-1.7.8_windows_amd64.zipchronograf为InfluxDB的Web后台管理端,InfluxDB提供了控制台命令端,如果使用不习惯,可以使用chronograf。3.2 解压安装包软件下载成功后,解压。3.3 修改配置文件InfluxDB 的数据存储主要有三个目录。默认情况下是 meta, wal 以及 data 三个目录,程序启动后会自动生成。meta 用于存储数据库的一些元数据,meta 目录下有一个 meta.db 文件。wal 目录存放预写日志文件,以 .wal 结尾。data 目录存放实际存储的数据文件,以 .tsm 结尾。接下来修改influxdb.conf 配置文件,修改以下部分的路径。另外,InfluxDB服务默认端口为8086,如果需要更改端口号,则增加以下配置。3.4 启动InfluxDB服务配置文件修改完成后,接下来启动InfluxDB服务。直接运行Influxd.exe使用默认配置运行即可。如果需要使用自定义的配置文件,则指定conf文件进行启动,启动命令如下:#先cmd 进入influxDB目录 influxd.exe -config influxdb.conf看到如下输出,说明InfluxDB启动成功。四、InfluxDB使用InfluxQL是一种类似于SQL的查询语言,用于与InfluxDB进行交互。如果你使用过关系数据库及SQL,那么你可以很快速的掌握InfluxQL。但是,InfluxQL又不完全是SQL,缺乏SQL中的一些高级的语法,例如UNION,JOIN,HAVING等。那么InfluxDB的到底如何操作呢?接下来介绍InfluxQL语言的使用方法。4.1 连接InfluxDB服务进入到InfluxDB目录后,在cmd中输入influx命令即可,命令如下:# 使用Command命令行进入influxdb influx -port 8086如果使用的是默认配置,可以不需要加端口,直接influx即可。4.2 操作InfluxDBInfluxQL与SQL命令语法类似。接下来我们看一看InfluxQL 是怎么使用的?4.2.1创建数据库# 创建数据库 CREATE DATABASE weiz_tes # 显示所有数据库 SHOW DATABASES # 删除数据库 DROP DATABASE weiz_test # 使用数据库 USE weiz_test4.2.2 表操作1.创建表InfluxDB没有专门的创建表的命令,当插入一条数据point至某A表时,此A表会自动创建,并且表的格式、字段名、字段类型也由此条插入命令决定。2.修改表InfluxDB没有修改表的命令,但当插入一条新数据point至表A时,如果此point中的字段多于原A表的字段,会自动修改A表与此条插入数据的格式字段等一致。注意:此种情况仅限于新插入的数据字段与表A字段的交集即表A的情况,如果新插入数据字段与表A完全不同则会插入失败。3.查询表# 显示该数据库中的表 SHOW MEASUREMENTS4.删除表:DROP MEASUREMENT "measurementName"5.插入数据insert host_cpu_usage_total,host_name=host1,cpu_core=core1 cpu_usage=0.26,cpu_idle=0.76上面,我们新增一条数据,measurement为host_cpu_usage_total, tag为host_name,cpu_core, field为cpu_usage,cpu_idle。我们简单小结一下插入的语句写法:基本格式:.insert + measurement + "," + tag=value,tag=value +空格+ field=value,field=value ;tag与tag之间用逗号分隔;field与field之间用逗号分隔;tag与field之间用空格分隔;tag都是string类型,不需要引号将value包裹;field如果是string类型,需要加引号;6.查询数据select * from host_cpu_usage_total查询语句使用select 关键字,格式与mysql 基本一致。4.2.3 用户管理InfluxDB 默认管理员账号:admin,密码为空。我们可以新增用户和权限。命令如下:#显示用户 show users #创建用户 create user "username" with password 'password' #创建管理员权限用户 create user "username" with password 'password' with all privileges #删除用户 drop user "username"以上是对InfluxDB数据库操作的基本总结,其他复杂的用法可以参考官网教程。官网教程地址:https://docs.influxdata.com/influxdb/v1.7/。五、SpringBoot整合InfluxDB前面介绍了InfluxDB的基本安装和使用。接下来我们介绍SpringBoot项目如何整合InfluxDB,实现数据的增删改查。这里使用的Spring Boot版本为2.4.1。接下来看看如何实现的。5.1 添加依赖首先创建springboot项目spring-boot-starter-influxdb,并添加相关依赖,具体依赖如下: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.influxdb</groupId> <artifactId>influxdb-java</artifactId> <version>2.14</version> </dependency>5.2 修改application.properties 配置接下来修改application.properties 配置文件,增加InfluxDB的相关配置,具体如下:#influxdb 配置 spring.influx.url=http://localhost:8086 spring.influx.user=admin spring.influx.password= spring.influx.database=weiz_test上面配置的是InfluxDB数据库连接配置,默认url为:http://localhost:8086 ,数据库为之前创建的weiz_test数据库。用户名为admin,密码默认为空。5.3 读取配置文件创建InfluxDBConfig类,负责读取Influx的数据库连接配置。具体代码如下:@Configuration public class InfluxDBConfig { @Value("${spring.influx.user}") public String userName; @Value("${spring.influx.password}") public String password; @Value("${spring.influx.url}") public String url; //数据库 @Value("${spring.influx.database}") public String database; } 5.4 数据库操作类创建数据库操作类InfluxDBService,负责数据库的初始化,增删改查等操作的具体实现,示例代码如下:@Service public class InfluxDBService { @Autowired private InfluxDBConfig influxDBConfig; @PostConstruct public void initInfluxDb() { this.retentionPolicy = retentionPolicy == null || "".equals(retentionPolicy) ? "autogen" : retentionPolicy; this.influxDB = influxDbBuild(); } //保留策略 private String retentionPolicy; private InfluxDB influxDB; /** * 设置数据保存策略 defalut 策略名 /database 数据库名/ 30d 数据保存时限30天/ 1 副本个数为1/ 结尾DEFAULT * 表示 设为默认的策略 */ public void createRetentionPolicy() { String command = String.format("CREATE RETENTION POLICY \"%s\" ON \"%s\" DURATION %s REPLICATION %s DEFAULT", "defalut", influxDBConfig.database, "30d", 1); this.query(command); } /** * 连接时序数据库;获得InfluxDB **/ private InfluxDB influxDbBuild() { if (influxDB == null) { influxDB = InfluxDBFactory.connect(influxDBConfig.url, influxDBConfig.userName, influxDBConfig.password); influxDB.setDatabase(influxDBConfig.database); } return influxDB; } /** * 插入 * @param measurement 表 * @param tags 标签 * @param fields 字段 */ public void insert(String measurement, Map<String, String> tags, Map<String, Object> fields) { influxDbBuild(); Point.Builder builder = Point.measurement(measurement); builder.time(System.currentTimeMillis(), TimeUnit.MILLISECONDS); builder.tag(tags); builder.fields(fields); influxDB.write(influxDBConfig.database, "", builder.build()); } /** * @desc 插入,带时间time * @date 2021/3/27 *@param measurement *@param time *@param tags *@param fields * @return void */ public void insert(String measurement, long time, Map<String, String> tags, Map<String, Object> fields) { influxDbBuild(); Point.Builder builder = Point.measurement(measurement); builder.time(time, TimeUnit.MILLISECONDS); builder.tag(tags); builder.fields(fields); influxDB.write(influxDBConfig.database, "", builder.build()); } /** * @desc influxDB开启UDP功能,默认端口:8089,默认数据库:udp,没提供代码传数据库功能接口 * @date 2021/3/13 *@param measurement *@param time *@param tags *@param fields * @return void */ public void insertUDP(String measurement, long time, Map<String, String> tags, Map<String, Object> fields) { influxDbBuild(); Point.Builder builder = Point.measurement(measurement); builder.time(time, TimeUnit.MILLISECONDS); builder.tag(tags); builder.fields(fields); int udpPort = 8089; influxDB.write(udpPort, builder.build()); } /** * 查询 * @param command 查询语句 * @return */ public QueryResult query(String command) { influxDbBuild(); return influxDB.query(new Query(command, influxDBConfig.database)); } /** * @desc 查询结果处理 * @date 2021/5/12 *@param queryResult */ public List<Map<String, Object>> queryResultProcess(QueryResult queryResult) { List<Map<String, Object>> mapList = new ArrayList<>(); List<QueryResult.Result> resultList = queryResult.getResults(); //把查询出的结果集转换成对应的实体对象,聚合成list for(QueryResult.Result query : resultList){ List<QueryResult.Series> seriesList = query.getSeries(); if(seriesList != null && seriesList.size() != 0) { for(QueryResult.Series series : seriesList){ List<String> columns = series.getColumns(); String[] keys = columns.toArray(new String[columns.size()]); List<List<Object>> values = series.getValues(); if(values != null && values.size() != 0) { for(List<Object> value : values){ Map<String, Object> map = new HashMap(keys.length); for (int i = 0; i < keys.length; i++) { map.put(keys[i], value.get(i)); } mapList.add(map); } } } } } return mapList; } /** * @desc InfluxDB 查询 count总条数 * @date 2021/4/8 */ public long countResultProcess(QueryResult queryResult) { long count = 0; List<Map<String, Object>> list = queryResultProcess(queryResult); if(list != null && list.size() != 0) { Map<String, Object> map = list.get(0); double num = (Double)map.get("count"); count = new Double(num).longValue(); } return count; } /** * 查询 * @param dbName 创建数据库 * @return */ public void createDB(String dbName) { influxDbBuild(); influxDB.createDatabase(dbName); } /** * 批量写入测点 * * @param batchPoints */ public void batchInsert(BatchPoints batchPoints) { influxDbBuild(); influxDB.write(batchPoints); } /** * 批量写入数据 * * @param database 数据库 * @param retentionPolicy 保存策略 * @param consistency 一致性 * @param records 要保存的数据(调用BatchPoints.lineProtocol()可得到一条record) */ public void batchInsert(final String database, final String retentionPolicy, final InfluxDB.ConsistencyLevel consistency, final List<String> records) { influxDbBuild(); influxDB.write(database, retentionPolicy, consistency, records); } /** * @desc 批量写入数据 * @date 2021/3/19 *@param consistency *@param records */ public void batchInsert(final InfluxDB.ConsistencyLevel consistency, final List<String> records) { influxDbBuild(); influxDB.write(influxDBConfig.database, "", consistency, records); } } 5.5 测试验证接下来,我们写几个单元测试,验证数据的增删改查等操作是否成功。单元测试代码如下: @SpringBootTest class Example01ApplicationTests { @Autowired private InfluxDBService influxDBService; @Test void contextLoads() { } @Test public void testSave(){ String measurement = "host_cpu_usage_total"; Map<String,String> tags = new HashMap<>(); tags.put("host_name","host2"); tags.put("cpu_core","core0"); Map<String, Object> fields = new HashMap<>(); fields.put("cpu_usage",0.22); fields.put("cpu_idle",0.56); influxDBService.insert(measurement, tags, fields); } @Test public void testGetdata(){ String command = "select * from host_cpu_usage_total"; QueryResult queryResult = influxDBService.query(command); List<Map<String, Object>> result = influxDBService.queryResultProcess(queryResult); for (Map map: result) { System.out.println("time:"+ map.get("time") +" host_name:" + map.get("host_name") +" cpu_core:" + map.get("cpu_core") +" cpu_usage:" + map.get("host_name") +" cpu_idle:" + map.get("host_name")); } } }运行上面的新增和查询等单元测试,单击Run Test或在方法上右击,选择Run 'testSave' ,查看单元测试结果,运行结果如下图所示。单元测试运行成功,说明InfluxDB的增加和查询操作执行成功。最后以上,我们就把时序数据库InfluxDB介绍完了,并通过示例介绍了如何在SpringBoot项目中整合InfluxDB。示例代码也会同步上传:https://gitee.com/weizhong1988/spring-boot-starter 。如有疑问,请在下方留言!InfluxDB在系统监控、物联网等方面的应用越来越多,希望大家能够熟练掌握。
之前我们搭建好了监控环境并且监控了服务器、数据库、应用,运维人员可以实时了解当前被监控对象的运行情况,但是他们不可能时时坐在电脑边上盯着DashBoard,这就需要一个告警功能,当服务器或应用指标异常时发送告警,通过邮件或者短信的形式告诉运维人员及时处理。接下来就来介绍非常重要的功能——告警。一、告警的实现方式Prometheus将数据采集和告警分成了两个模块。报警规则配置在Prometheus Servers上,然后发送报警信息到AlertManger等告警系统,然后在告警系统管理这些报警信息、聚合报警信息、然后通过email、短信等方式发送消息提。目前,实现告警功能主要有以下几种方式:使用prometheus提供的Alertmanager告警组件(功能全面,告警规则配置比较复杂)。OneAlert等其他第三方组件(配置简单,可以实现短信、电话、微信等多种告警方式,但是依赖第三方平台,而且是收费的)Grafana 等自带的告警功能(配置简单)相比于Grafana的图形化界面,Alertmanager需要依靠配置文件实现,虽然配置比较繁琐,但是胜在功能强大灵活。接下来我们就使用Alertmanager一步一步实现告警通知。二、Grafana告警新版本的Grafana提供了告警配置,直接在dashboard监控panel中设置告警即可。Grafana 支持多种告警方式,这里以邮件为例,演示Grafana如何设置邮件告警功能。2.1 配置邮件服务step1:要启用 email 报警需要在启动配置文件中 /conf/default.ini开启 SMTP 服务,具体配置如下:[smtp] enabled = true host = smtp.163.com:25 user = xxx@163.com # 邮件地址 # If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;""" password = xxx # 授权码 cert_file = key_file = skip_verify = true from_address = xxx@163.com # 发件人地址 from_name = Grafana ehlo_identity = dashboard.example.com [emails] welcome_email_on_sign_up = false templates_pattern = emails/*.html content_types = text/html这里的邮箱服务使用的是163的邮箱服务,需要提前打开邮箱的SMTP服务并申请授权码,示例中的邮箱地址和密码请换成自己的邮箱和授权码。step2:重启Grafana,验证邮件是否配置成功,点击页面上的 Alerting | Contact points 添加Contact Points。 点击页面上的 New contact point 按钮,添加一个邮件通知渠道。选择邮件方式,并输入收件人的邮箱后保存即可,验证邮箱是否配置成功,点击 Test 按钮,Grafana 会发送一封测试邮件到收件人邮箱。如果能收到邮件,说明配置成功。2.2 配置告警规则配置好邮件发送和接收的Contact Points 之后,接下来我们配置Grafana的告警规则。step1:创建告警告警规则,首先在某个Panel 上的下拉箭头中,选择 Edit | Alert。step2:接下来点击 Create alert from this panel 按钮,给此panel 创建告警规则。如上图所示,我们以node_memory_MemFree_bytes (服务器可用内存)指标为例,设置告警规则:当服务器可用内存低于4.65G 时告警。step3:告警名称,间隔时间等设置。step4:设置完其他相关的参数之后,点击Save 保存,即可查看告警的情况。如上图所示,Grafana已经产生了一条Pending状态的告警的记录,当此记录变为Firing状态就说明已经告警成功,并发送了邮件通知。以上,我们把Grafana的告警功能介绍完了,Grafana虽然比较直观,但是相比Alertmanager而言不够灵活,不支持变量,如果系统不复杂的话,可以考虑使用Grafana。三、Alertmanager3.1 告警类型Alertmanager提供了以下两种告警方式: 邮件接收器 email_config,发送邮件通知;Webhook接收器 webhook_config,使用post方式向配置的url地址发送数据请求。3.2 安装alertmanagerstep1:安装Alertmanager首先在prometheus官网,下载Alertmanager组件,并上传到服务器解压即可。# 解压到/usr/local/prometheus目录下: tar -zxvf alertmanager-0.24.0.linux-amd64.tar.gz -C /usr/local/prometheus # 修改目录名: cd /usr/local/prometheus mv alertmanager-0.24.0.linux-amd64 alertmanager-0.24.0step 2:配置Alertmanager修改alertmanager.yml 文件,增加Email等相关配置,具体如下:global: resolve_timeout: 5m # alertmanager在持续多久没有收到新告警后标记为resolved smtp_from: 'xxx@163.com' # 发件人邮箱地址 smtp_smarthost: 'smtp.163.com:25' # 邮箱smtp地址 smtp_auth_username: 'xxx@163.com' # 发件人的登陆用户名,默认和发件人地址一致 smtp_auth_password: 'xxx' # 发件人的登陆密码,有时候是授权码 smtp_hello: '163.com' smtp_require_tls: # 是否需要tls协议。默认是true route: group_by: [alertname] # 通过alertname的值对告警进行分类 group_wait: 10s # 一组告警第一次发送之前等待的时延,即产生告警10s将组内新产生的消息合并发送,通常是0s~几分钟(默认是30s) group_interval: 2m # 一组已发送过初始告警通知的告警,接收到新告警后,下次发送通知前等待时延,通常是5m或更久(默认是5m) repeat_interval: 5m # 一组已经发送过通知的告警,重复发送告警的间隔,通常设置为3h或者更久(默认是4h) receiver: 'default-receiver' # 设置告警接收人 receivers: - name: 'default-receiver' email_configs: - to: 'xxx@163.com' send_resolved: true # 发送恢复告警通知 inhibit_rules: # 抑制规则 - source_match: # 源匹配级别,当匹配成功发出通知,其他级别产生告警将被抑制 severity: 'critical' # 告警时间级别(告警级别根据规则自定义) target_match: severity: 'warning' # 匹配目标成功后,新产生的目标告警为'warning'将被抑制 equal: ['alertname','dev','instance'] # 基于这些标签抑制匹配告警的级别 这里的邮箱服务使用的是163的邮箱服务,需要提前打开邮箱的SMTP服务并申请授权码,示例中的邮箱地址和密码请换成自己的邮箱和授权码。配置文件比较复杂,可以使用./amtool check-config alertmanager.yml 命令校验文件是否正确。step 3:启动运行配置文件修改完成后,接下来我们将Alertmanager运行起来,具体命令如下:#alertmanager cd /usr/local/prometheus/alertmanager-0.24.0 #执行启动命令,指定数据访问的url alertmanager --config.file=/usr/local/prometheus/alertmanager-0.24.0/alertmanager.yml命令执行成功后,在浏览器中访问:http://10.2.1.231:9093/ 默认端口9093如上图所示,我们成功打开Alertmanager 管理页面,说明Alertmanager配置、启动成功。3.3 将Alertmanager添加到Prometheus前面我们说了,告警规则是配置在Prometheus Servers上,然后发送报警信息到AlertManger中的,那么接下来我们把Alertmanager添加到Prometheus中。step1: Prometheus告警配置修改prometheus.yml 配置文件,增加告警地址和告警规则,具体配置如下:# Alertmanager configuration alerting: alertmanagers: - static_configs: - targets: [10.2.1.231:9093] # Load rules once and periodically evaluate them according to the global 'evaluation_interval'. # 加载指定的规则文件 rule_files: - "first.rules" - "rules/*.yml"上面的配置,主要是配置Alertmanager的地址和规则文件加载路径。告警规则读取prometheus目录的rule下的所有以yml结尾的文件。step2:配置告警规则前面在prometheus.yml 中配置了规则的路径,所以,接下来在prometheus的根目录下创建rules目录。这里以服务器资源状态状态为例,制定cpu、内存、磁盘的告警。创建pods_rule.yaml文件。具体配置如下:groups: - name: alertmanager_pod.rules rules: - alert: Pod_all_cpu_usage expr: (sum by(name)(rate(container_cpu_usage_seconds_total{image!=""}[5m]))*100) > 1 for: 2m labels: serverity: critical service: pods annotations: description: 容器 {{ $labels.name }} CPU 资源利用率大于 10% , (current value is {{ $value }}) summary: Dev CPU 负载告警 - alert: Pod_all_memory_usage expr: sort_desc(avg by(name)(irate(node_memory_MemFree_bytes {name!=""}[5m]))) > 2147483648 # 内存大于2G for: 2m labels: severity: critical annotations: description: 容器 {{ $labels.name }} Memory 资源利用大于 2G , (current value is {{ $value }}) summary: Dev Memory 负载告警 - alert: Pod_all_network_receive_usage expr: sum by(name)(irate(container_network_reveive_bytes_total{container_name="POD"}[1m])) > 52428800 # 大于50M for: 2m labels: severity: critical annotations: description: 容器 {{ $labels.name }} network_receive 资源利用大于 50M , (current value is {{ $value }}) - alert: node内存可用大小 expr: node_memory_MemFree_bytes < 5368709120 # 内存小于5G for: 2m labels: severity: critical annotations: description: node可用内存小于5Gstep3: 重启Prometheus配置完成后重启Prometheus,访问Prometheus查看告警配置。在浏览器中输入:http://10.2.1.231:9090/alerts 查看告警配置是否成功。说上图所示,我们在Prometheus的Alerts下可以看到对应的告警配置。FIRING说明告警已成功。此时Alertmanager应该相关的告警数据。打开http://10.2.1.231:9093/#/alerts 查看报警情况。如上图所示,Alertmanager收到了Prometheus发过来的告警消息,前面我们在Alertmanager中配置了邮件地址,可以去邮箱中查看是否收到邮件。Alertmanager的告警内容支持使用模板配置,可以使用邮件模板进行渲染,感兴趣的话也可以试试!最后以上,我们就把Prometheus如何告警介绍完了,告警功能非常重要,告警规则的设置比较复杂,最好能够多熟悉熟悉相关的设置。
前面我们介绍了使用Prometheus + Grafana 构建了监控系统,那么我们的应用平台怎么监控呢?应用平台中的核心业务的执行情况能否监控呢?那么接下来我们使用Actuator,Micrometer,Prometheus和Grafana监控Spring Boot应用程序,自定义应用监控指标。应用程序在生产环境中运行时,监控其运行状况是非常必要的。通过实时了解应用程序的运行状况,才能在问题出现之前得到警告,也可以通监控应用系统的运行状况,优化性能,提高运行效率。一、监控Spring Boot应用下面我们以Spring Boot 为例,演示Prometheus如何监控应用系统。1.1 项目环境:Spring Boot 2.3.7.releasemicrometer-registry-prometheus 1.5.9需要注意Spring Boot 和 micrometer的版本号。不同的micrometer版本支持的Spring Boot 版本也不相同。1.2 Spring Boot集成 Micrometerstep1:首先创建Spring Boot项目,首先添加依赖如下:<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> <version>1.5.9</version> </dependency> </dependencies>这里引入了 io.micrometer 的 micrometer-registry-prometheus 依赖以及 spring-boot-starter-actuator 依赖,因为该包对 Prometheus 进行了封装,可以很方便的集成到 Spring Boot 工程中。需要注意Spring Boot 和 micrometer的版本号。不同的micrometer版本支持的Spring Boot 版本也不相同。step2:修改配置文件,打开Actuator监控端点在 application.yml 中配置如下:spring: application: name: PrometheusApp #Prometheus springboot监控配置 management: endpoints: web: exposure: include: '*' metrics: export: prometheus: enabled: true tags: application: ${spring.application.name} # 暴露的数据中添加application label上面的配置中, include=* 配置为开启 Actuator 服务,Spring Boot Actuator 自带了一个/actuator/Prometheus 的监控端点供给Prometheus 抓取数据。不过默认该服务是关闭的,所以,使用该配置将打开所有的 Actuator 服务。step3:最后,启动服务,然后在浏览器访问 http://10.2.1.159:8080/actuator/prometheus ,就可以看到服务的一系列不同类型 metrics 信息,例如 http_server_requests_seconds summary、jvm_memory_used_bytes gauge、jvm_gc_memory_promoted_bytes_total counter 等等。到此,Spring Boot 工程集成 Micrometer 就已经完成,接下里就要与 Prometheus 进行集成了。1.3 将应用添加到Prometheus前面Spring Boot应用已经启动成功,并暴露了/actuator/Prometheus的监控端点。接下来我们将此应用添加到Prometheus。step1:首先,修改 Prometheus 的配置文件 prometheus.yml ,添加上边启动的服务地址来执行监控。vim /usr/local/etc/prometheus.yml 。具体配置如下:global: scrape_interval: 15s scrape_configs: - job_name: "prometheus" # metrics_path defaults to '/metrics' # scheme defaults to 'http'. static_configs: - targets: ["localhost:9090"] # 采集node exporter监控数据 - job_name: 'node' static_configs: - targets: ['10.2.1.231:9527'] - job_name: 'prometheusapp' metrics_path: '/actuator/prometheus' static_configs: - targets: ['10.2.1.159:8080']上面的prometheusapp 就是前面创建的Spring Boot 应用程序,也就是 Prometheus 需要监控的服务地址。step2:然后,重启 Prometheus 服务,查看 Prometheus UI 界面确认 Target 是否添加成功。我们也可以在 Graph 页面执行一个简单的查询,也是获取 PrometheusApp服务的相关性能指标值。二、使用 Grafana Dashboard 展示应用数据前面我们已经在Prometheus正常监控Spring Boot应用的JVM性能指标数据,接下来,我们配置 Grafana Dashboard 来优雅直观的展示出来这些监控指标。2.1 下载Grafana模板之前介绍过Grafana 使用Dashboard 模板展示Prometheus的数据,这里就不再重复了,直接在https://grafana.com/dashboards 下载Spring Boot的模板(这里使用的是编号4701)。2.2 导入模板下载成功后直接在Dashboards | Import 将json模板导入到Grafana 即可。2.3 查看应用信息导入完毕后,就可以看到 JVM的各项监控指标,如果有多个应用,可以通过Application选择我们想要查看的应用即可。三、自定义监控指标前面我们在Spring Boot项目中集成Actuator和Micrometer实现了Spring Boot应用监控,基本上覆盖 JVM 各个层间的参数指标,并且配合 Grafana Dashboard 模板基本可以满足我们日常对Spring Boot应用的监控。但是,对于核心业务改是否也能够监控它们的执行情况呢?答案是肯定的,Micrometer支持自定义监控指标,实现业务方面的数据监控。例如统计访问某一个 API 接口的请求数,统计实时在线人数、统计实时接口响应时间等。接下来,我们以监控所有API请求次数为例,演示如何自定义监控指标并展示到Grafana 。3.1 添加指标统计step1:首先,在之前的Spring Boot项目中,创建CustomMetricsController 控制器,具体示例代码如下:@RestController @RequestMapping("/custom/metrics") public class CustomMetricsController { @Autowired private MeterRegistry meterRegistry; /** * 订单请求测试 */ @GetMapping("/order/{appId}") public String orderTest(@PathVariable("appId") String appId) { Counter.builder("metrics.request.count").tags("apiCode", "order").register(meterRegistry).increment(); return "order请求成功:" +appId ; } /** * 产品请求测试 */ @GetMapping("/product/{appId}") public String productTest(@PathVariable("appId") String appId) { Counter.builder("metrics.request.count").tags("apiCode", "product").register(meterRegistry).increment(); return "product请求成功:" +appId ; } }如上所示,使用Counter 计数器定义了自定义指标参数:metrics_request_count,来统计相关接口的请求次数。这里只是测试,所以直接在Controller类中进行统计。实际项目项目中,应该是使用AOP,或是拦截器的方式统计所有接口的请求信息,减少这种非关键代码的侵入性。step2: 验证测试,重新启动Spring Boot 应用。分别访问:http://10.2.1.159:8080/custom/metrics/order/{appId}和http://10.2.1.159:8080/custom/metrics/product/{appId} 接口,然后在 Promtheus 中查看自定义的指标数据:metrics_request_count_total。如上图所示,我们自定义的监控指标已经在Prometheus中显示了,说明我们在应用中配置的自定义监控指标已经成功。3.2 创建Grafana数据面板接下来,我们在 Grafana Dashboard展示我们自定义的监控指标。其实也非常简单,创建一个新的数据面板Panel 并添加 Query 查询,相关的监控指标就图形化展示出来了。接下来演示在Grafana上创建数据面板。step1:首先,页面的右上角的Add panel | Add a new Panel,添加一个 Panel,并命名为:统计接口请求次数。可以选择选择想要展示的图形,如:连线图、柱状图等。step2:然后,在panel的下方增加 Query 查询,选择数据源为之前定义的Prometheus-1,指标选择之前自定义的指标数据:metrics_request_count_total,点击applay 保存之后,返回首页就可以看到刚添加的 panel。具体如下图所示:如上图所示,上面我们新增加的panel中成功显示了我们自定义的监控数据。继续请求之前的应用接口,数据会正常刷新。说明Grafana上的指标数据展示配置成功。以上,我们就把如何自定义监控指标并在Grafana 的图形界面展示介绍完了。最后以上,我们就把Prometheus如何监控Spring Boot应用,自定义应用监控指标!介绍完了。
监控是运维系统的基础,我们衡量一个公司/部门的运维水平,看他们的监控系统就可以了。一个完善的监控系统可以提高应用的可用性和可靠性,在提供更优质服务的前提下,降低运维的投入和工作量,为用户带来更多的商业利益和客户体验。下面就带大家彻底搞懂监控系统,使用Prometheus +Grafana搭建完整的应用监控系统。一、监控系统简介1.1 什么是监控系统?监控系统顾名思义就是监控服务器、应用系统以及其他第三方组件运行状态的系统。对于平台系统而言,监控系统就是我们第三只眼,监控系统会实时跟踪应用平台的运行状态,如果有应用系统出现问题或是服务器内存爆满,我们通过监控系统就可以快速定位问题所在,甚至可以设置预警,对一些将要出现的问题进行提前预防处理,及时避免问题的发生。1.2 监控系统的作用监控是运维系统的基础,我们衡量一个公司/部门的运维水平,看他们的监控系统就可以了。监控系统的作用不言而喻,能帮我们快速定位问题,减少故障,容量规划,性能优化等。1)定位故障:在发生故障时,我们可以通过查看监控系统的各项指标数据,辅助故障分析和定位。2)减少故障率:对于即将可能产生的故障能够及时发出预警信息,做好提前预防处理。3)容量规划:为服务器、中间件以及应用集群的容量规划提供数据支撑。4)性能调优:JVM垃圾回收次数、接口响应时间、慢SQL等等都可以监控优化。总而言之,一个完善的监控系统可以提高应用的可用性和可靠性,在提供更优质服务的前提下,降低运维的投入和工作量,为用户带来更多的商业利益和客户体验。1.3 常见的监控对象和指标都有哪些?应用系统的监控主要分为指标监控和日志监控两大部分:指标监控主要是对一定时间段内性能指标进行测量,然后再通过时间序列的方式,进行处理、存储和告警。日志监控则可以提供更详细的上下文信息,通常通过 ELK 技术栈来进行收集、索引和图形化展示。指标监控可以说是系统监控最核心的功能。主要有服务器资源、应用监控、数据库中间件等。服务器资源监控:CPU使用率、内存使用率、磁盘使用率、磁盘读写的吞吐量、网络出入流量等等。数据库监控:TPS、QPS、数据库连接数、慢SQL、InnoDB缓冲池命中率等。Redis监控:内存使用率、缓存命中率、key值总数、Redis响应请求时间、客户端连接数、持久性指标等。MQ消息监控:连接数、队列数、生产速率、消费速率、消息堆积量等等。应用监控:包括HTTP请求,JVM,线程池等。1.4 监控系统的架构一个完整的监控系统通常由数据采集、数据传输、数据存储、数据展示、监控告警等多个模块组成。数据采集,采集的方式有很多种,包括日志埋点进行采集,JMX标准接口输出监控指标,被监控对象提供REST API进行数据采集(如Hadoop、ES),系统命令行,统一的SDK进行侵入式的埋点和上报等。数据传输,将采集的数据以TCP、UDP或者HTTP协议的形式上报给监控系统,有主动Push模式,也有被动Pull模式。数据存储,有使用MySQL、Oracle等关系数据库存储的,也有使用时序数据库RRDTool、OpentTSDB、InfluxDB存储的,还有使用HBase存储的。数据展示,数据指标的图形化展示。监控告警,灵活的告警设置,以及支持邮件、短信、IM等多种通知通道。二、当前流行的监控系统目前大部分厂商都采用自研或是基于开源组件的方式搭建自己的监控平台。当然也有很多非常流行的开源监控系统,其中,最流行的莫过于Zabbix和Prometheus。下面就对这两个监控系统进行介绍,同时总结下各自的优劣势。2.1 Zabbix Zabbix 1998年诞生,核心组件采用C语言开发,Web端采用PHP开发。它属于老牌监控系统中的优秀代表,功能全面,使用广泛,是最优秀的监控解决方案之一。2.1.1 Zabbix的优势产品成熟:由于诞生时间长且使用广泛,拥有丰富的文档资料以及各种开源的数据采集插件,能覆盖绝大部分监控场景。采集方式丰富:支持Agent、SNMP、JMX、SSH等多种采集方式,以及主动和被动的数据传输方式。2.1.2 Zabbix的劣势Zabbix需要在被监控主机上安装Agent,所有的数据都存在数据库里,产生的数据很大,瓶颈主要在数据库。2.2 Prometheus随着微服务架构和容器的兴起,Zabbix对容器监控显得力不从心。为解决监控容器的问题 Prometheus 应运而生。Prometheus 是一套开源的系统监控报警框架,采用Go语言开发。得益于Google与k8s的强力支持,自带云原生的光环,天然能够友好协作,使得Prometheus 在开源社区异常火爆。2.2.1 Prometheus优点(1)提供多维度数据模型和灵活的查询方式通过将监控指标关联多个 tag,来将监控数据进行任意维度的组合,并且提供简单的 PromQL 查询方式,还提供 HTTP 查询接口,可以很方便地结合 Grafana 等 GUI 组件展示数据。(2)基于时序数据库,支持服务器节点的本地存储通过 Prometheus 自带的时序数据库,可以完成每秒千万级的数据存储;不仅如此,在保存大量历史数据的场景中,Prometheus 可以对接第三方时序数据库和 OpenTSDB 等。(3)定义了开放指标数据标准以基于 HTTP 的 Pull 方式采集时序数据,只有实现了Prometheus监控数据才可以被 Prometheus 采集、汇总、并支持 Push 方式向中间网关推送时序列数据,能更加灵活地应对多种监控场景。(4)支持通过静态文件配置和动态发现机制发现监控对象自动完成数据采集。Prometheus 目前已经支持 Kubernetes、etcd、Consul 等多种服务发现机制。(5)易于维护可以通过二进制文件直接启动,并且提供了容器化部署镜像。(6)集群支持支持数据的分区采样和集群部署,支持大规模集群监控。2.2.2 Prometheus缺点Prometheus 是基于 Metric 的监控,不适用于日志(Logs)、事件(Event)、调用链(Tracing)。由于Prometheus采用的是Pull模型拉取数据,意味着所有被监控的endpoint必须是可达的,需要合理规划网络的安全配置。指标众多,需进行适当裁剪。2.3 综合对比下表通过多维度展现了各自监控系统的优缺点:综合来看,Zabbix 的成熟度更高,上手更快,但灵活性较差。而且,监控数据的复杂度增加后,Zabbix 做进一步定制难度很高,即使做好了定制,也没法利用之前收集到的数据了(关系型数据库造成的问题)。Prometheus 基本上是正相反,上手难度大一些,但由于定制灵活度高,数据也有更多的聚合可能,起步后的使用难度远小于 Zabbix。如果监控的是物理机,用 Zabbix 没毛病,Zabbix 在传统监控系统中,尤其是在服务器相关监控方面,占据绝对优势;但如果是云环境的话,除非是 Zabbix 玩的非常溜,可以做各种定制,否则还是 Prometheus 吧,毕竟人家就是干这个的。Prometheus 号称下一代监控系统,已经成为主导及容器监控方面的标配,并且在未来可见的时间内被广泛应用。三、使用Prometheus+grafana搭建监控系统前面,我们了解了一些监控系统的区别和优缺点,下面我们以Prometheus为例,带大家一步一步搭建监控系统。3.1 下载Prometheus需要下载prometheus(Prometheus主服务)、node_exporter(服务器监控)、mysqld_exporter(Mysql数据库监控-可选)、pushgateway(数据网关-可选)、alertmanager(告警组件-可选)下载地址:https://prometheus.io/download/Grafana为数据展示界面,下载地址:https://grafana.com/grafana/download3.2 架构图3.3 安装 Prometheus ServerPrometheus 的架构设计中,Prometheus Server 主要负责数据的收集,存储并且对外提供数据查询支持。下面开始安装Prometheus Server。step1:首先,下载prometheus,并上传到服务器# 解压到/usr/local/prometheus目录下: tar -zxvf prometheus-2.37.0.linux-amd64.tar.gz -C /usr/local/prometheus # 修改目录名: cd /usr/local/prometheus mv prometheus-2.37.0.linux-amd64 prometheus-2.37.0setp2:启动prometheus Server 服务。prometheus启动非常简单,只需要一个命令即可,进入到/usr/local/prometheus/prometheus-2.37.0后执行如下命令:#进入prometheus目录 cd /usr/local/prometheus/prometheus-2.37.0 #执行启动脚本 ./prometheus --web.enable-admin-api --config.file=prometheus.ymlstep3:验证prometheus是否启动成功,prometheus默认端口为:9090,我们在浏览器中输入:http://10.2.1.231:9090/graph,进入prometheus数据展示页面,说明prometheus启动成功。3.4 安装 Node Exporter实际的监控样本数据的由 Exporter 负责收集,如node_exporter 就是负责服务器的资源信息,同时提供了对外访问的HTTP服务地址(通常是/metrics)给prometheus拉取监控样本数据。下面开始安装node_exporter。step1:首先,下载node_exporter,并上传到服务器# 解压到/usr/local/prometheus目录下: tar -zxvf node_exporter-1.3.1.linux-amd64.tar.gz -C /usr/local/prometheus # 修改目录名: cd /usr/local/prometheus mv node_exporter-1.3.1.linux-amd64 node_exporter-1.3.1step2:启动node_exporler,输入如下命令启动:#node_exporter cd /usr/local/prometheus/node_exporter-1.3.1 #执行启动命令,指定数据访问的url ./node_exporter --web.listen-address 10.2.1.231:9527step3:验证node_exporler是否启动成功,我们在浏览器中输入上面指定的地址:http://10.2.1.231:9527/metrics,可以看到当前 node_exporter 获取到的当前主机的所有监控数据。说明node_exporler启动成功。step4:最后,配置prometheus,将新增加的node配置到prometheus。修改prometheus-2.37.0 文件夹下的prometheus.yml文件。增加新的node配置,具体配置如下:scrape_configs: # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config. - job_name: "prometheus" # metrics_path defaults to '/metrics' # scheme defaults to 'http'. static_configs: - targets: ["localhost:9090"] # 采集node exporter监控数据 - job_name: 'node' static_configs: - targets: ['10.2.1.231:9527'] 修改完prometheus.yml 文件后,重新启动prometheus。再次访问prometheus数据展示页面,选择status | target,可以看到新的node已经添加进来了。在Graph 页面,在查询框中输入: process_cpu_seconds_total3.5 安装grafana前面已经把prometheus和node exporter 安装并集成成功。prometheus虽然有自带的数据展示界面,但是不够全面也不直观。接下来集成grafana 完成数据展示。下载地址:https://grafana.com/grafana/downloadstep1:首先,下载Grafana,并上传到服务器。# 下载grafana wget https://dl.grafana.com/enterprise/release/grafana-enterprise-9.0.3.linux-amd64.tar.gz # 解压到 tar -zxvf grafana-enterprise-9.0.3.linux-amd64.tar.gz -C /usr/local/prometheus # 修改目录名: cd /usr/local/prometheus mv ngrafana-enterprise-9.0.3.linux-amd64 grafana-9.0.3step2:启动Grafana,输入如下命令:#grafana cd /usr/local/prometheus/grafana-9.0.3/bin #执行启动命令,指定数据访问的url ./grafana-server --homepath /usr/local/prometheus/grafana-9.0.3 webstep3:验证是否安装成功,Grafana默认端口:3000。在浏览器中输入:http://10.2.1.231:3000/ 输入默认账号密码:admin\admin。能正常进入Grafana,说明Grafana安装成功。step4:配置prometheus数据源,点击 设置 | Data Source ,按照操作添加prometheus数据源。点击add data source,后选择prometheus数据源。输入data source 的名字以及prometheus的地址:http://10.2.1.231:9090/ 后点击Save&Test 即可。step5:创建仪表盘 DashboardGrafana 支持手动创建仪表盘 Dashboard 和自动导入Dashboard模板两种方式,手动一个个添加Dashboard 比较繁琐,Grafana 社区鼓励用户分享 Dashboard,通过https://grafana.com/dashboards 网站,可以找到大量可直接使用的Dashboard模板。Grafana 中所有的Dashboard 通过 JSON 进行共享,下载并且导入这些 JSON 文件,就可以直接使用这些已经定义好的 Dashboard:选择自己喜欢的模板后,点击 Download JSON下载对应的json 文件。然后在Grafana系统中导入相应的json即可。接下来回到Grafana页面,点击DashBoards|Import 选择之前下载好的json文件,导入即可。点击Import后,我们就可以看到详细的服务器资源监控数据。如下图所示:最后以上,我们就把监控系统介绍完了,并使用Prometheus + Grafana 构建了一个初步的监控系统。监控是运维系统的基础,在DevOps大行其道的今天,运维监控不再是运维工程师的工作,而是程序员和架构师的必备技能。希望大家能够熟练掌握。
单点登录是目前比较流行的企业业务整合的解决方案之一。单点登录是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。例如:百度旗下有很多的产品,比如百度贴吧、百度知道、百度文库等,只要登录百度账号,在任何一个地方都是已登录状态,不需要重新登录。单点登录是互联网应用和企业级平台中的基础组件服务。接下来就介绍单点登录的原理,并基于SpringBoot +JWT实现单点登录解决方案。一、什么是单点登录?单点登录(Single Sign On 简称 SSO)是一种统一认证和授权机制,指在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统,不再需要重新登录验证。单点登录一般是用于互相授信的系统,实现单一位置登录,其他信任的应用直接免登录的方式。在多个应用系统中,只需要登录一次,就可以访问其他相互信任的应用系统。单点登录是互联网应用和企业级平台中的基础组件服务。比如百度贴吧、百度知道、百度文库等,只要登录百度账号,在任何一个地方都是已登录状态,不需要重新登录。此外,第三方授权登录,如在京东中使用微信登录。解决信息孤岛和用户不对等的实现方案。随着时代的演进,大型web系统早已从单体应用架构发展为如今的多系统分布式应用群。但无论系统内部多么复杂,对用户而言,都是一个统一的整体,访问web系统的整个应用群要和访问单个系统一样,登录/注销只要一次就够了,不可能让一个用户在每个业务系统上都进行一次登录验证操作,这时就需要独立出一个单独的认证系统,它就是单点登录系统。二、单点登录的优点和不足单点登录解决了用户只需要登录一次就可以访问所有相互信任的应用系统,而不用重复登录。除此之外,还有以下优点:1)提高用户的效率。用户不再被多次登录困扰,也不需要记住多个 ID 和密码。另外,用户忘记密码并求助于支持人员的情况也会减少。 2)提高开发人员的效率。SSO 为开发人员提供了一个通用的身份验证框架。实际上,如果 SSO 机制是独立的,那么开发人员就完全不需要为身份验证操心。他们可以假设,只要对应用程序的请求附带一个用户名,身份验证就已经完成了。 3)简化管理。如果应用程序加入了单点登录协议,管理用户帐号的负担就会减轻。简化的程度取决于应用程序,因为 SSO 只处理身份验证。所以,应用程序可能仍然需要设置用户的属性(比如访问特权)。三、单点登录实现机制单点登录听起来很复杂,实际上架构非常简单,具体如下图所示:如上图所示,当用户第一次访问应用系统A时,因为还没有登录,会被跳转到认证系统进行登录;认证系统根据用户提供的登录信息进行身份效验,如果通过效验,则返回给用户一个认证凭据(ticket);用户访问其他应用系统时,将会带上此ticket作为自己认证的凭据,应用系统B接受到请求之后会把ticket送到认证系统进行效验,检查ticket的合法性。如果通过,则成功登录应用系统B。四、单点登录常见的解决方案实现单点登录的方式有很多种,常见的有基于Cookie、Session共享、Token机制、JWT机制等方式。4.1 基于Cookie基于Cookie是最简单的单点登录实现方式是使用cookie作为媒介存放用户凭证。用户登录成功后,返回一个加密的cookie,当用户访问其他应用时,携带此cookie,授权应用解密cookie并进行校验,校验通过则登录当前用户。当然,我们不难发现以上方式将信任数据存储在客户端的Cookie中,这种方式存在两个问题:1、Cookie不安全,CSRF(跨站请求伪造);2、Cookie不能跨域,无法解决跨域系统的问题。对于第一个问题,通过加密Cookie可以保证安全性,当然这是在源代码不泄露的前提下。如果Cookie的加密算法泄露,攻击者通过伪造Cookie则可以伪造特定用户身份,这是很危险的。对于第二个问题,不能跨域是Cookie的硬伤。可以将生成ticket作为参数传递到其他应用系统。这样可以避免跨域问题。4.2 基于Session共享所谓基于Session共享,主要是将Session会话信息保存到公共的平台,如Redis,数据库等,各应用系统共用一个会话状态sessionId,实现登录信息的共享,从而实现单点登录。基于Session解决了Cookie不能跨域的问题,但也存在其他问题。早期的单体应用使用Session实现单点登录,但现在大部分情况下都需要集群,由于存在多台服务器,Session在多台服务器之间是不共享的,因此,还需解决Session共享的问题解决系统之间的 Session 不共享问题有以下几种方案:1)Tomcat集群Session全局复制【会影响集群的性能呢,不建议】2)分布式Session,即把Session数据放在Redis中(使用Redis模拟Session)【建议】4.3 Token机制其实,Token就是一串加密(使用MD5,等不可逆加密算法)的字符串。具体流程如下:1.客户端使用用户名跟密码请求登录,2.服务端收到请求,去验证用户名与密码,3.验证成功后,服务端会签发一个加密的字符串(Token)保存到(Session,Redis,Mysql)中,并把这个Token发送给客户端,4.客户端收到Token后存储在本地,如:Cookie 或 Local Storage 中,5.客户端每次向服务端请求资源的时候需要带着服务端签发的 Token,6.服务端收到请求,验证密客户端请求里面带着的 Token和服务器中保存的Token进行对比效验, 如果验证成功,就向客户端返回请求的数据。使用Token验证的优势:无状态、可扩展;在客户端存储的Token是无状态的,并且可扩展。基于这种无状态和不存储Session信息,负载负载均衡器能够将用户信息从一个服务传到其他服务器上;安全性,请求资源时发送token而不再是发送cookie能够防止CSRF(跨站请求伪造)即使在客户端使用cookie存储token,cookie也仅仅是一个存储机制而不是用于认证。不将信息存储在Session中,让我们少了对session操作。4.4 JWT 机制JWT(JSON Web Token的缩写)它将用户信息加密到token里,服务器不保存任何用户信息。服务器通过使用保存的密钥验证JWTToken的正确性,只要正确即通过验证。4.4.1 JWT的特点紧凑:数据小,可以通过URL,POST参数,请求头发送。且数据小代表传输速度快。自包含:使用payload数据块记录用户必要且不隐私的数据,可以有效的减少数据库访问次数,提高代码性能。JWT一般用于处理用户身份验证或数据信息交换。用户身份验证:一旦用户登录,每个后续请求都将包含JWT,允许用户访问该令牌允许的路由,服务和资源。单点登录是当今广泛使用JWT的一项功能,因为它的开销很小,并且能够轻松地跨不同域使用。数据信息交换:JWT是一种非常方便的多方传递数据的载体,因为其可以使用数据前面来保证数据的有效性和安全性。4.4.2 JWT数据结构JWT的结构包含三个部分: Header头部,Payload负载和Signature签名。三部分之间用“.”号做分割。 校验也是JWT内部自己实现的 ,并且可以将你存储时候的信息从token中取出来无须查库。header数据结构: {“alg”: “加密算法名称”, “typ” : “JWT”}alg是加密算法定义内容,如:HMAC SHA256 或 RSAtyp是token类型,这里固定为JWT。payload在payload数据块中一般用于记录实体(通常为用户信息)或其他数据的。主要分为三个部分,分别是:已注册信息(registered claims),公开数据(public claims),私有数据(private claims)。payload中常用信息有:iss(发行者),exp(到期时间),sub(主题),aud(受众)等。前面列举的都是已注册信息。公开数据部分一般都会在JWT注册表中增加定义。避免和已注册信息冲突。公开数据和私有数据可以由程序员任意定义。注意:即使JWT有签名加密机制,但是payload内容都是明文记录,除非记录的是加密数据,否则不排除泄露隐私数据的可能。不推荐在payload中记录任何敏感数据。Signature签名信息。这是一个由开发者提供的信息。是服务器验证的传递的数据是否有效安全的标准。在生成JWT最终数据的之前。先使用header中定义的加密算法,将header和payload进行加密,并使用点进行连接。如:加密后的head.加密后的payload。再使用相同的加密算法,对加密后的数据和签名信息进行加密。得到最终结果。4.4.3 JWT执行流程JWT的请求流程也特别简单,首先使用账号登录获取Token,然后后面的各种请求,都带上这个Token即可。具体流程如下:1. 客户端发起登录请求,传入账号密码;2. 服务端使用私钥创建一个Token;3. 服务器返回Token给客户端;4. 客户端向服务端发送请求,在请求头中该Token;5. 服务器验证该Token;6. 返回结果。可能有些小伙伴会觉得,Token 和JWT有什么区别呢?其实Token和JWT确实比较类似,只不过,Token需要查库验证token 是否有效,而JWT不用查库,直接在服务端进行校验,因为用户的信息及加密信息,和过期时间,都在JWT里,只要在服务端进行校验就行,并且校验也是JWT自己实现的。五、基于JWT机制的单点登录JWT提供了基于Java组件:java-jwt帮助我们在Spring Boot项目中快速集成JWT,接下来进行SpringBoot和JWT的集成。接下来我们通过项目示例,演示如何基于SpringBoot+JWT实现单点登录。5.1 项目结构项目结构如下图所示:如上图所示,weiz-sso为认证系统,weiz-app-a和weiz-app-b为两个独立的应用系统。5.2 创建认证系统5.2.1.创建项目并引入JWT等依赖首先,创建普通的Spring Boot项目weiz-sso,修改项目中的pom.xml文件,引入JWT等依赖。示例代码如下:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <exclusions> <exclusion> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> </exclusion> </exclusions> </dependency> <!-- SpringBoot集成thymeleaf模板 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.83</version> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.10.3</version> </dependency>5.2.2.创建&验证JWT工具类创建通用的处理类TokenUtil,负责创建和验证Token。示例代码如下:@/** * 类功能简述: * 类功能详述: * * @author weiz */ public class JwtUtil { /** * Description: 生成一个jwt字符串 * * @param name 用户名 * @param secret 秘钥 * @param timeOut 超时时间(单位s) * @return java.lang.String * @author weiz */ public static String encode(String name, String secret, long timeOut) { Algorithm algorithm = Algorithm.HMAC256(secret); String token = JWT.create() //设置过期时间为一个小时 .withExpiresAt(new Date(System.currentTimeMillis() + timeOut)) //设置负载 .withClaim("name", name) .sign(algorithm); return token; } /** * Description: 解密jwt * * @param token token * @param secret secret * @return java.util.Map<java.lang.String , com.auth0.jwt.interfaces.Claim> * @author weiz */ public static Map<String, Claim> decode(String token, String secret) { if (token == null || token.length() == 0) { throw new CustomException("token为空:" + token); } Algorithm algorithm = Algorithm.HMAC256(secret); JWTVerifier jwtVerifier = JWT.require(algorithm).build(); DecodedJWT decodedJWT = jwtVerifier.verify(token); return decodedJWT.getClaims(); } }5.2.3 登录功能接下来创建AuthController控制器,实现登录,退出等请求接口,示例代码如下:/** * 类功能简述: * 类功能详述: * * @author weiz */ @Controller @RequestMapping("/sso") public class AuthController { private JwtService service; @Autowired public AuthController(JwtService service) { this.service = service; } @RequestMapping({"/","/index"}) public String index(){ return "index"; } @RequestMapping("/login") public String login(){ return "login"; } @RequestMapping(value = "/login",method = RequestMethod.POST) @ResponseBody public ReturnResult login(@RequestBody User user) { String token = service.login(user); return ReturnResult.successResult(token); } @RequestMapping("/checkJwt") @ResponseBody public ReturnResult checkJwt(String token) { return ReturnResult.successResult(service.checkJwt(token)); } @RequestMapping("/refreshJwt") @ResponseBody public ReturnResult refreshJwt(String token){ return ReturnResult.successResult(service.refreshJwt(token)); } @RequestMapping("/inValid") @ResponseBody public ReturnResult inValid(String token) { service.inValid(token); return ReturnResult.successResult(null); } }5.3 创建应用系统5.3.1创建应用系统weiz-app-a和weiz-app-b首先,分别创建两个Spring Boot项目weiz-app-a和weiz-app-b。并引入相关依赖,示例代码如下: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- SpringBoot集成thymeleaf模板 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>3.14.9</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.83</version> </dependency>5.3.2 登录验证拦截器在两个项目中分别添加登录拦截器LoginFilter,负责拦截所有Http请求,验证Token是否有效。示例代码如下:/** * 类功能简述: * 类功能详述: * * @author weiz */ @Component @WebFilter(urlPatterns = "/**", filterName = "loginFilter") public class LoginFilter implements Filter { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Value("${sso_server}") private String serverHost; @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void destroy() { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; String token = httpServletRequest.getParameter("token"); if (this.check(token)) { filterChain.doFilter(servletRequest, servletResponse); } else { HttpServletResponse response = (HttpServletResponse) servletResponse; String redirect = serverHost + "/login?redirect=" + httpServletRequest.getRequestURL(); //response.setContentType("application/json;charset=utf-8"); //response.setCharacterEncoding("utf-8"); //response.getWriter().write(JSON.toJSONString(new ReturnEntity(-1, "未登录", null))); response.sendRedirect(redirect); } } private boolean check(String jwt) { try { if (jwt == null || jwt.trim().length() == 0) { return false; } JSONObject object = HttpClient.get(serverHost + "/checkJwt?token=" + jwt); return object.getBoolean("data"); } catch (Exception e) { logger.error("向认证中心请求失败", e); return false; } } }5.3.3.创建控制器在两个项目中分别创建IndexController控制器,处理HTTP请求。示例代码如下:/** * 类功能简述: * 类功能详述: * * @author weiz */ @Controller public class IndexController { @Value("${sso_server}") private String serverHost; @RequestMapping({"/","/index"}) public String index() { return "index"; } @RequestMapping("/test") @ResponseBody public ReturnEntity test() { return new ReturnEntity(1, "通过验证", null); } @RequestMapping("/logout") public void logout(HttpServletRequest request, HttpServletResponse response,String token) throws Exception { HttpClient.get(serverHost + "/inValid?token=" + token); String requestHost = request.getScheme() +"://"+ request.getServerName() + ":"+request.getServerPort() +"/"; String redirect = serverHost + "/login?redirect=" + requestHost; System.out.println(redirect); response.sendRedirect(redirect); } }5.4 测试验证集成JWT成功之后,接下来验证单点登录系统是否成功,分别启动wei-sso、weiz-app-a和weiz-app-b。验证功能是否正常。首先,在浏览器中访问应用系统A:http://localhost:8081/如上图所示,由于没有登录,自动跳转到了单点登录系统进行登录验证。输入用户名、密码(admin\admin)登录成功并返回到应用系统A。接下来,使用此token 访问应用系统B,在浏览器中输入:http://localhost:8082/?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiYWRtaW4iLCJleHAiOjE2NTc4MDA4MTB9.7NLXFJwGKxgnEwBsE25OredrpLIaanAoqeHXSZjO6QA 如上图所示,通过之前的token,无需登录即可成功进入了应用系统B。说明我们的单点登录系统搭建成功。总结以上,我们就把单点登录(SSO)的所有流程都介绍完了,原理大家都清楚了。单点登录是互联网应用和企业级平台中的基础组件服务,希望大家能明白其中的原理,熟练掌握。
后台定时任务系统在应用平台中的重要性不言而喻,特别是互联网电商、金融等行业更是离不开定时任务。在任务数量不多、执行频率不高时,单台服务器完全能够满足。但是随着业务逐渐增加,定时任务系统必须具备高可用和水平扩展的能力,单台服务器已经不能满足需求。因此需要把定时任务系统部署到集群中,实现分布式定时任务系统集群。分布式任务调度框架几乎是每个大型应用必备的工具,下面我们结合项目实践,对业界普遍使用的开源分布式任务调度框架的使用进行了探究实践,并分析了这几种框架的优劣势和对自身业务的思考。一、分布式定时任务简介1.什么是分布式任务?分布式定时任务就是把分散的、批量的后台定时任务纳入统一的管理调度平台,实现任务的集群管理、调度和分布式部署管理方式。2.分布式定时任务的特点实际项目中涉及到分布式任务的业务场景非常多,这就使得我们的定时任务系统应该集管理、调度、任务分配、监控预警为一体的综合调度系统,如何打造一套健壮的、适应不同场景的系统,技术选型尤其重要。针对以上场景我们需要我们的分布式任务系统具备以下能力:支持多种任务类型(shell任务/Java任务/web任务)支持HA,负载均衡和失败转移支持弹性扩容(应对开门红以及促销活动)支持Job Timeout 处理支持统一监控和告警支持任务统一配置支持资源隔离和作业隔离二、为什么需要分布式定时任务?定时任务系统在应用平台中的重要性不言而喻,特别是互联网电商、金融等行业更是离不开定时任务。在任务数量不多、执行频率不高时,单台服务器完全能够满足。但是,为什么还需要分布式呢?主要有如下两点原因:高可用:单机版的定式任务调度只能在一台机器上运行,如果系统出现异常,就会导致整个后台定时任务不可用。这对于互联网企业来说是不可接受的。单机处理极限:单机处理的数据,任务数量是有限的。原本1分钟内需要处理1万个订单,但是现在需要1分钟内处理10万个订单;原来一个统计需要1小时,现在业务方需要10分钟就统计出来。你也许会说,你也可以多线程、单机多进程处理。的确,多线程并行处理可以提高单位时间的处理效率,但是单机能力毕竟有限(主要是CPU、内存和磁盘),始终会有单机处理不过来的情况。但我们遇到的问题还不止这些,比如容错功能、失败重试、分片功能、路由负载均衡、管理后台等。这些都是单机的定时任务系统所不具备的,因此需要把定时任务系统部署到集群中,实现分布式定时任务系统集群。三、常见开源方案目前,分布式定时任务框架非常多,而且大部分都已经开源,比较流行的有:xxl-job、elastic-job、quartz等。elastic-job,是由当当网基于quartz 二次开发之后的分布式调度解决方案 , 由两个相对独立的子项目Elastic-Job-Lite和Elastic-Job-Cloud组成 。xxl-job,是由个人开源的一个轻量级分布式任务调度框架 ,主要分为 调度中心和执行器两部分 , 调度中心在启动初始化的时候,会默认生成执行器的RPC代理对象(http协议调用), 执行器项目启动之后, 调度中心在触发定时器之后通过jobHandle 来调用执行器项目里面的代码,核心功能和elastic-job差不多,同时技术文档比较完善quartz 的常见集群方案如下,通过在数据库中配置定时器信息, 以数据库悲观锁的方式达到同一个任务始终只有一个节点在运行,以表列出了几个代表性的开源分布式任务框架的:功能quartzelastic-jobxxl-jobHA多节点部署,通过数据库锁来保证只有一个节点执行任务通过zookeeper的注册与发现,可以动态的添加服务器。支持水平扩容集群部署任务分片—支持支持文档完善完善完善完善管理界面无支持支持难易程度简单较复杂简单公司OpenSymphony当当网个人缺点没有管理界面,以及不支持任务分片等。不适用于分布式场景需要引入zookeeper , mesos, 增加系统复杂度, 学习成本较高通过获取数据库锁的方式,保证集群中执行任务的唯一性,性能不好。四、基于Quartz实现分布式定时任务解决方案1.Quartz的集群解决方案Quartz的单机版本相比大家应该比较熟悉,它的集群方案则是在单机的基础上加上一个公共数据库。通过在数据库中配置定时器信息, 以数据库悲观锁的方式达到同一个任务始终只有一个节点在运行,集群架构如下:通过上面的架构图可以看到,三个Quartz服务节点共享同一个数据库,如果某一个服务节点失效,那么Job会在其他节点上执行。各个Quartz服务器都遵守基于数据库锁的调度原则,只有获取了锁才能调度后台任务,从而保证了任务执行的唯一性。同时多个节点的异步运行保证了服务的可靠性。2.实现基于Quartz的分布式定时任务下面就通过示例,演示如何基于Quartz实现分布式定时任务。1. 添加Quartz依赖由于分布式的原因,Quartz中提供分布式处理的JAR包以及数据库和连接相关的依赖。示例代码如下:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency> <!-- mysql --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- orm --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency>在上面的示例中,除了添加Quartz依赖外,还需要添加mysql-connector-java和spring-boot-starter-data-jpa两个组件,这两个组件主要用于JOB持久化到MySQL数据库。2. 初始化Quartz数据库分布式Quartz定时任务的配置信息存储在数据库中,数据库初始化脚本可以在官方网站中查找,默认保存在quartz-2.2.3-distribution\src\org\quartz\impl\jdbcjobstore\tables-mysql.sql目录下。首先创建quartz_jobs数据库,然后在数据库中执行tables-mysql.sql初始化脚本。3. 配置数据库和Quartz修改application.properties配置文件,配置数据库与Quartz。具体操作如下:# server.port=8090 # Quartz 数据库 spring.datasource.url=jdbc:mysql://localhost:3306/quartz_jobs?useSSL=false&serverTimezone=UTC spring.datasource.username=root spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.max-active=1000 spring.datasource.max-idle=20 spring.datasource.min-idle=5 spring.datasource.initial-size=10 # 是否使用properties作为数据存储 org.quartz.jobStore.useProperties=false # 数据库中表的命名前缀 org.quartz.jobStore.tablePrefix=QRTZ_ # 是否是一个集群,是不是分布式的任务 org.quartz.jobStore.isClustered=true # 集群检查周期,单位为毫秒,可以自定义缩短时间。当某一个节点宕机的时候,其他节点等待多久后开始执行任务 org.quartz.jobStore.clusterCheckinInterval=5000 # 单位为毫秒,集群中的节点退出后,再次检查进入的时间间隔 org.quartz.jobStore.misfireThreshold=60000 # 事务隔离级别 org.quartz.jobStore.txIsolationLevelReadCommitted=true # 存储的事务管理类型 org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX # 使用的Delegate类型 org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate # 集群的命名,一个集群要有相同的命名 org.quartz.scheduler.instanceName=ClusterQuartz # 节点的命名,可以自定义。AUTO代表自动生成 org.quartz.scheduler.instanceId=AUTO # rmi远程协议是否发布 org.quartz.scheduler.rmi.export=false # rmi远程协议代理是否创建 org.quartz.scheduler.rmi.proxy=false # 是否使用用户控制的事务环境触发执行任务 org.quartz.scheduler.wrapJobExecutionInUserTransaction=false上面的配置主要是Quartz数据库和Quartz分布式集群相关的属性配置。分布式定时任务的配置存储在数据库中,所以需要配置数据库连接和Quartz配置信息,为Quartz提供数据库配置信息,如数据库、数据表的前缀之类。4. 定义定时任务后台定时任务与普通Quartz任务并无差异,只是增加了@PersistJobDataAfterExecution注解和@DisallowConcurrentExecution注解。创建QuartzJob定时任务类并实现Quartz定时任务的具体示例代码如下:// 持久化 @PersistJobDataAfterExecution // 禁止并发执行 @DisallowConcurrentExecution public class QuartzJob extends QuartzJobBean { private static final Logger log = LoggerFactory.getLogger(QuartzJob.class); @Override protected void executeInternal(JobExecutionContext context) throws JobExecutionException { String taskName = context.getJobDetail().getJobDataMap().getString("name"); log.info("---> Quartz job, time:{"+new Date()+"} ,name:{"+taskName+"}<----"); } }在上面的示例中,创建了QuartzJob定时任务类,使用@PersistJobDataAfterExecution注解持久化任务信息。DisallowConcurrentExecution禁止并发执行,避免同一个任务被多次并发执行。5. SchedulerConfig配置创建SchedulerConfig配置类,初始化Quartz分布式集群相关配置,包括集群设置、数据库等。示例代码如下:@Configuration public class SchedulerConfig { @Autowired private DataSource dataSource; /** * 调度器 * * @return * @throws Exception */ @Bean public Scheduler scheduler() throws Exception { Scheduler scheduler = schedulerFactoryBean().getScheduler(); return scheduler; } /** * Scheduler工厂类 * * @return * @throws IOException */ @Bean public SchedulerFactoryBean schedulerFactoryBean() throws IOException { SchedulerFactoryBean factory = new SchedulerFactoryBean(); factory.setSchedulerName("Cluster_Scheduler"); factory.setDataSource(dataSource); factory.setApplicationContextSchedulerContextKey("applicationContext"); factory.setTaskExecutor(schedulerThreadPool()); //factory.setQuartzProperties(quartzProperties()); factory.setStartupDelay(10);// 延迟10s执行 return factory; } /** * 配置Schedule线程池 * * @return */ @Bean public Executor schedulerThreadPool() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(Runtime.getRuntime().availableProcessors()); executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors()); executor.setQueueCapacity(Runtime.getRuntime().availableProcessors()); return executor; } }在上面的示例中,主要是配置Schedule线程池、配置Quartz数据库、创建Schedule调度器实例等初始化配置。6. 触发定时任务配置完成之后,还需要触发定时任务,创建JobStartupRunner类以便在系统启动时触发所有定时任务。示例代码如下:@Component public class JobStartupRunner implements CommandLineRunner { @Autowired SchedulerConfig schedulerConfig; private static String TRIGGER_GROUP_NAME = "test_trigger"; private static String JOB_GROUP_NAME = "test_job"; @Override public void run(String... args) throws Exception { Scheduler scheduler; try { scheduler = schedulerConfig.scheduler(); TriggerKey triggerKey = TriggerKey.triggerKey("trigger1", TRIGGER_GROUP_NAME); CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey); if (null == trigger) { Class clazz = QuartzJob.class; JobDetail jobDetail = JobBuilder.newJob(clazz).withIdentity("job1", JOB_GROUP_NAME).usingJobData("name","weiz QuartzJob").build(); CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 * * * * ?"); trigger = TriggerBuilder.newTrigger().withIdentity("trigger1", TRIGGER_GROUP_NAME) .withSchedule(scheduleBuilder).build(); scheduler.scheduleJob(jobDetail, trigger); System.out.println("Quartz 创建了job:...:" + jobDetail.getKey()); } else { System.out.println("job已存在:{}" + trigger.getKey()); } TriggerKey triggerKey2 = TriggerKey.triggerKey("trigger2", TRIGGER_GROUP_NAME); CronTrigger trigger2 = (CronTrigger) scheduler.getTrigger(triggerKey2); if (null == trigger2) { Class clazz = QuartzJob2.class; JobDetail jobDetail2 = JobBuilder.newJob(clazz).withIdentity("job2", JOB_GROUP_NAME).usingJobData("name","weiz QuartzJob2").build(); CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 * * * * ?"); trigger2 = TriggerBuilder.newTrigger().withIdentity("trigger2", TRIGGER_GROUP_NAME) .withSchedule(scheduleBuilder).build(); scheduler.scheduleJob(jobDetail2, trigger2); System.out.println("Quartz 创建了job:...:{}" + jobDetail2.getKey()); } else { System.out.println("job已存在:{}" + trigger2.getKey()); } scheduler.start(); } catch (Exception e) { System.out.println(e.getMessage()); } } }在上面的示例中,为了适应分布式集群,我们在系统启动时触发定时任务,判断任务是否已经创建、是否正在执行。如果集群中的其他示例已经创建了任务,则启动时无须触发任务。7. 验证测试配置完成之后,接下来启动任务,测试分布式任务配置是否成功。启动一个实例,可以看到定时任务执行了,然后每10秒钟打印输出一次,如下图所示。接下来,模拟分布式部署的情况。我们再启动一个测试程序实例,这样就有两个后台定时任务实例,如下所示。后台定时任务实例1的日志输出:后台定时任务实例2的日志输出:从上面的日志中可以看到,Quartz Job和Quartz Job2交替地在两个任务实例进程中执行,同一时刻同一个任务只有一个进程在执行,这说明已经达到了分布式后台定时任务的效果。接下来,停止任务实例1,测试任务实例2是否会接管所有任务继续执行。如下图所示,停止任务实例1后,任务实例2接管了所有的定时任务。这样如果集群中的某个实例异常了,其他实例能够接管所有的定时任务,确保任务集群的稳定运行。最后以上,就把分布式后台任务介绍完了,并通过Spring Boot + Quartz 实现了基于Quartz的分布式定时任务解决方案!分布式任务调度框架几乎是每个大型应用必备的工具,作为程序员、架构师必须熟练掌握。
在微服务飞速发展的今天,在高并发的分布式的系统中,缓存是提升系统性能的重要手段。没有缓存对后端请求的拦截,大量的请求将直接落到系统的底层数据库。系统是很难撑住高并发的冲击,下面就以Redis为例来聊聊分布式系统中关于缓存的设计以及过程中遇到的一些问题。一、分布式缓存简介1. 什么是分布式缓存分布式缓存:指将应用系统和缓存组件进行分离的缓存机制,这样多个应用系统就可以共享一套缓存数据了,它的特点是共享缓存服务和可集群部署,为缓存系统提供了高可用的运行环境,以及缓存共享的程序运行机制。2、本地缓存VS分布式缓存本地缓存:是应用系统中的缓存组件,其最大的优点是应用和cache是在同一个进程内部,请求缓存非常快速,没有过多的网络开销等,在单应用不需要集群支持的场景下使用本地缓存较合适;但是,它的缺点也是应为缓存跟应用程序耦合,多个应用程序无法共享缓存数据,各应用或集群的各节点都需要维护自己的单独缓存。很显然,这是对内存是一种浪费。分布式缓存:与应用分离的缓存组件或服务,分布式缓存系统是一个独立的缓存服务,与本地应用隔离,这使得多个应用系统之间可直接的共享缓存数据。目前分布式缓存系统已经成为微服务架构的重要组成部分,活跃在成千上万的应用服务中。但是,目前还没有一种缓存方案可以解决一切的业务场景或数据类型,我们需要根据自身的特殊场景和背景,选择最适合的缓存方案。3、分布式缓存的特性相对于本地应用缓存,分布式缓存具有如下特性: 1) 高性能:当传统数据库面临大规模数据访问时,磁盘I/O 往往成为性能瓶颈,从而导致过高的响应延迟。分布式缓存将高速内存作为数据对象的存储介质,数据以key/value 形式存储。2) 动态扩展性:支持弹性扩展,通过动态增加或减少节点应对变化的数据访问负载,提供可预测的性能与扩展性;同时,最大限度地提高资源利用率;3) 高可用性:高可用性包含数据可用性与服务可用性两方面,故障的自动发现,自动转义。确保不会因服务器故障而导致缓存服务中断或数据丢失。4) 易用性:提供单一的数据与管理视图;API 接口简单,且与拓扑结构无关;动态扩展或失效恢复时无需人工配置;自动选取备份节点;多数缓存系统提供了图形化的管理控制台,便于统一维护;4、分布式缓存的应用场景 分布式缓存的典型应用场景可分为以下几类:1) 页面缓存:用来缓存Web 页面的内容片段,包括HTML、CSS 和图片等,多应用于社交网站等;2) 应用对象缓存:缓存系统作为ORM 框架的二级缓存对外提供服务,目的是减轻数据库的负载压力,加速应用访问;3) 状态缓存:缓存包括Session 会话状态及应用横向扩展时的状态数据等,这类数据一般是难以恢复的,对可用性要求较高,多应用于高可用集群;4) 并行处理:通常涉及大量中间计算结果需要共享;5) 事件处理:分布式缓存提供了针对事件流的连续查询(continuous query)处理技术,满足实时性需求;6) 极限事务处理:分布式缓存为事务型应用提供高吞吐率、低延时的解决方案,支持高并发事务请求处理,多应用于铁路、金融服务和电信等领域;二、 为什么要用分布式缓存?在传统的后端架构中,由于请求量以及响应时间要求不高,我们经常采用单一的数据库结构。这种架构虽然简单,但随着请求量的增加,这种架构存在性能瓶颈导致无法继续稳定提供服务。通过在应用服务与DB中间引入缓存层,我们可以得到如下三个好处:(1)读取速度得到提升。(2)系统扩展能力得到大幅增强。我们可以通过加缓存,来让系统的承载能力提升。(3)总成本下降,单台缓存即可承担原来的多台DB的请求量,大大节省了机器成本。三、常用的缓存技术目前最流行的分布式缓存技术有redis和memcached两种,1. MemcacheMemcached 是一个高性能,分布式内存对象缓存系统,通过在内存里维护一个统一的巨大的 Hash 表,它能够用来存储各种格式的数据,包括图像、视频、文件以及数据库检索的结果等。简单的说就是:将数据缓存到内存中,然后从内存中读取,从而大大提高读取速度。Memcached 特性:使用物理内存作为缓存区,可独立运行在服务器上。每个进程最大 2G,如果想缓存更多的数据,可以开辟更多的 Memcached 进程(不同端口)或者使用分布式 Memcached 进行缓存,将数据缓存到不同的物理机或者虚拟机上。使用 key-value 的方式来存储数据。这是一种单索引的结构化数据组织形式,可使数据项查询时间复杂度为 O(1)。协议简单,基于文本行的协议。直接通过 telnet 在 Memcached 服务器上可进行存取数据操作,简单,方便多种缓存参考此协议;基于 libevent 高性能通信。Libevent 是一套利用 C 开发的程序库,它将 BSD 系统的 kqueue,Linux 系统的 epoll 等事件处理功能封装成一个接口,与传统的 select 相比,提高了性能。分布式能力取决于 Memcached 客户端,服务器之间互不通信。各个 Memcached 服务器之间互不通信,各自独立存取数据,不共享任何信息。服务器并不具有分布式功能,分布式部署取决于 Memcached 客户端。采用 LRU 缓存淘汰策略。在 Memcached 内存储数据项时,可以指定它在缓存的失效时间,默认为永久。当 Memcached 服务器用完分配的内时,失效的数据被首先替换,然后也是最近未使用的数据。在 LRU 中,Memcached 使用的是一种 Lazy Expiration 策略,自己不会监控存入的 key/vlue 对是否过期,而是在获取 key 值时查看记录的时间戳,检查 key/value 对空间是否过期,这样可减轻服务器的负载。内置了一套高效的内存管理算法。这套内存管理效率很高,而且不会造成内存碎片,但是它最大的缺点就是会导致空间浪费。当内存满后,通过 LRU 算法自动删除不使用的缓存。不支持持久化。Memcached 没有考虑数据的容灾问题,重启服务,所有数据会丢失。2. RedisRedis 是一个开源(BSD 许可),基于内存的,支持网络、可基于内存、分布式、可选持久性的键值对(Key-Value)存储数据库,并提供多种语言的 API。可以用作数据库、缓存和消息中间件。Redis 支持多种数据类型 - string、Hash、list、set、sorted set。提供两种持久化方式 - RDB 和 AOF。通过 Redis cluster 提供集群模式。Redis的优势:性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。原子 – Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性执行。(事务)丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。3. 分布式缓存技术对比不同的分布式缓存功能特性和实现原理方面有很大的差异,因此他们所适应的场景也有所不同。四、分布式缓存实现方案缓存的目的是为了在高并发系统中有效降低DB的压力,高效的数据缓存可以极大地提高系统的访问速度和并发性能。分布式缓存有很多实现方案,下面将讲一讲分布式缓存实现方案。1、缓存实现方案如上图所示,系统会自动根据调用的方法缓存请求的数据。当再次调用该方法时,系统会首先从缓存中查找是否有相应的数据,如果命中缓存,则从缓存中读取数据并返回;如果没有命中,则请求数据库查询相应的数据并再次缓存。如上图所示,每一个用户请求都会先查询缓存中的数据,如果缓存命中,则会返回缓存中的数据。这样能减少数据库查询,提高系统的响应速度。2、使用Spring Boot+Redis实现分布式缓存解决方案接下来,以用户信息管理模块为例演示使用Redis实现数据缓存框架。1.添加Redis Cache的配置类RedisConfig类为Redis设置了一些全局配置,比如配置主键的生产策略KeyGenerator()方法,此类继承CachingConfigurerSupport类,并重写方法keyGenerator(),如果不配置,就默认使用参数名作为主键。@Configuration @EnableCaching public class RedisConfig extends CachingConfigurerSupport { / ** * 采用RedisCacheManager作为缓存管理器 * 为了处理高可用Redis,可以使用RedisSentinelConfiguration来支持Redis Sentinel */ @Bean public CacheManager cacheManager(RedisConnectionFactory connectionFactory) { RedisCacheManager redisCacheManager = RedisCacheManager.builder(connectionFactory).build(); return redisCacheManager; } / ** * 自定义生成key的规则 */ @Override public KeyGenerator keyGenerator() { return new KeyGenerator() { @Override public Object generate(Object o, Method method, Object...objects) { // 格式化缓存key字符串 StringBuilder stringBuilder = new StringBuilder(); // 追加类名 stringBuilder.append(o.getClass().getName()); // 追加方法名 stringBuilder.append(method.getName()); // 遍历参数并且追加 for (Object obj :objects) { stringBuilder.append(obj.toString()); } System.out.println("调用Redis缓存Key: " + stringBuilder.toString()); return stringBuilder.toString(); } }; } }在上面的示例中,主要是自定义配置RedisKey的生成规则,使用@EnableCaching注解和@Configuration注解。@EnableCaching:开启基于注解的缓存,也可以写在启动类上。@Configuration:标识它是配置类的注解。2.添加@Cacheable注解在读取数据的方法上添加@Cacheable注解,这样就会自动将该方法获取的数据结果放入缓存。@Repository public class UserRepository { / ** * @Cacheable应用到读取数据的方法上,先从缓存中读取,如果没有,再从DB获取数据,然后把数据添加到缓存中 * unless表示条件表达式成立的话不放入缓存 * @param username * @return */ @Cacheable(value = "user") public User getUserByName(String username) { User user = new User(); user.setName(username); user.setAge(30); user.setPassword("123456"); System.out.println("user info from database"); return user; } }在上面的实例中,使用@Cacheable注解标注该方法要使用缓存。@Cacheable注解主要针对方法进行配置,能够根据方法的请求对参数及其结果进行缓存。1)这里缓存key的规则为简单的字符串组合,如果不指定key参数,则自动通过keyGenerator生成对应的key。2)Spring Cache提供了一些可以使用的SpEL上下文数据,通过#进行引用。3.测试数据缓存创建单元测试方法调用getUserByName()方法,测试代码如下:@Test public void testGetUserByName() { User user = userRepository.getUserByName("weiz"); System.out.println("name: "+ user.getName()+",age:"+user.getAge()); user = userRepository.getUserByName("weiz"); System.out.println("name: "+ user.getName()+",age:"+user.getAge()); }上面的实例分别调用了两次getUserByName()方法,输出获取到的User信息。最后,单击Run Test或在方法上右击,选择Run 'testGetUserByName',运行单元测试方法,结果如下图所示。通过上面的日志输出可以看到,首次调用getPersonByName()方法请求User数据时,由于缓存中未保存该数据,因此从数据库中获取User信息并存入Redis缓存,再次调用会命中此缓存并直接返回。五、常见问题及解决方案1.缓存预热缓存预热指在用户请求数据前先将数据加载到缓存系统中,用户查询 事先被预热的缓存数据,以提高系统查询效率。缓存预热一般有系统启动 加载、定时加载等方式。5.热key问题所谓热key问题就是,突然有大量的请求去访问redis上的某个特定key,导致请求过于集中,达到网络IO的上限,从而导致这台redis的服务器宕机引发雪崩。针对热key的解决方案:1. 提前把热key打散到不同的服务器,降低压力2. 加二级缓存,提前加载热key数据到内存中,如果redis宕机,则内存查询2.缓存击穿缓存击穿是指大量请求缓存中过期的key,由于并发用户特别多,同时新的缓存还没读到数据,导致大量的请求数据库,引起数据库压力瞬间增大,造成过大压力。缓存击穿和热key的问题比较类似,只是说的点在于过期导致请求全部打到DB上而已。解决方案:1. 加锁更新,假设请求查询数据A,若发现缓存中没有,对A这个key加锁,同时去数据库查询数据,然后写入缓存,再返回给用户,这样后面的请求就可以从缓存中拿到数据了。2. 将过期时间组合写在value中,通过异步的方式不断的刷新过期时间,防止此类现象发生。3.缓存穿透缓存穿透是指查询不存在缓存中的数据,每次请求都会打到DB,就像缓存不存在一样。解决方案:接口层增加参数校验,如用户鉴权校验,请求参数校验等,对 id<=0的请求直接拦截,一定不存在请求数据的不去查询数据库。布隆过滤器:指将所有可能存在的Key通过Hash散列函数将它映射为一个位数组,在用户发起请求时首先经过布隆过滤器的拦截,一个一定不存在的数据会被这个布隆过滤器拦截,从而避免对底层存储系统带来查询上 的压力。cache null策略:指如果一个查询返回的结果为null(可能是数据不存在,也可能是系统故障),我们仍然缓存这个null结果,但它的过期 时间会很短,通常不超过 5 分钟;在用户再次请求该数据时直接返回 null,而不会继续访问数据库,从而有效保障数据库的安全。其实cache null策略的核心原理是:在缓存中记录一个短暂的(数据过期时间内)数据在系统中是否存在的状态,如果不存在,则直接返回null,不再查询数据库,从而避免缓存穿透到数据库上。布隆过滤器布隆过滤器的原理是在保存数据的时候,会通过Hash散列函数将它映射为一个位数组中的K个点,同时把他的值置为1。这样当用户再次来查询A时,而A在布隆过滤器值为0,直接返回,就不会产生击穿请求打到DB了。4.缓存雪崩缓存雪崩指在同一时刻由于大量缓存失效,导致大量原本应该访问缓存的请求都去查询数据库,而对数据库的CPU和内存造成巨大压力,严重的话会导致数据库宕机,从而形成一系列连锁反应,使得整个系统崩溃。雪崩和击穿、热key的问题不太一样的是,缓存雪崩是指大规模的缓存都过期失效了。针对雪崩的解决方案:1. 针对不同key设置不同的过期时间,避免同时过期2. 限流,如果redis宕机,可以限流,避免同时刻大量请求打崩DB3. 二级缓存,同热key的方案。六、缓存与数据库数据一致性缓存与数据库的一致性问题分为两种情况,一是缓存中有数据,则必须与数据库中的数据一致;二是缓存中没数据,则数据库中的数据必须是最新的。3.1删除和修改数据第一种情况:我们先删除缓存,在更新数据库,潜在的问题:数据库更新失败了,get请求进来发现没缓存则请求数据库,导致缓存又刷入了旧的值。第二种情况:我们先更新数据库,再删除缓存,潜在的问题:缓存删除失败,get请求进来缓存命中,导致读到的是旧值。3.2先删除缓存再更新数据库假设有2个线程A和B,A删除缓存之后,由于网络延迟,在更新数据库之前,B来访问了,发现缓存未命中,则去请求数据库然后把旧值刷入了缓存,这时候姗姗来迟的A,才把最新数据刷入数据库,导致了数据的不一致性。解决方案针对多线程的场景,可以采用延迟双删的解决方案,我们可以在A更新完数据库之后,线程A先休眠一段时间,再删除缓存。需要注意的是:具体休眠的时间,需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。当然这种策略还要考虑redis和数据库主从同步的耗时。3.3先更新数据库再删除缓存这种场景潜在的风险就是更新完数据库,删缓存之前,会有部分并发请求读到旧值,这种情况对业务影响较小,可以通过重试机制,保证缓存能得到删除。最后以上,就把分布式缓存介绍完了,并使用Spring Boot+Redis实现分布式缓存解决方案。缓存的使用是程序员、架构师的必备技能,好的程序员能根据数据类型、业务场景来准确判断使用何种类型的缓存,如何使用这种缓存,以最小的成本最快的效率达到最优的目的。
前面介绍了分布式锁以及如何使用Redis实现分布式锁,接下来介绍分布式系统中另外一个非常重要的组件:消息队列。消息队列是大型分布式系统不可缺少的中间件,也是高并发系统的基石中间件,所以掌握好消息队列MQ就变得极其重要。接下来我就将从零开始介绍什么是消息队列?消息队列的应用场景?如何进行选型?如何在Spring Boot项目中整合集成消息队列。一、消息队列概述消息队列(Message Queue,简称MQ)指保存消息的一个容器,其实本质就是一个保存数据的队列。消息中间件是指利用高效可靠的消息传递机制进行与平台无关的数据交流,并基于数据通信来进行分布式系统的构建。消息中间件是分布式系统中重要的组件,主要解决应用解耦,异步消息,流量削峰等问题,实现高性能,高可用,可伸缩和最终一致性的系统架构。目前使用较多的消息队列有ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ等。二、消息队列应用场景消息中间件在互联网公司使用得越来越多,主要用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。以下介绍消息队列在实际应用中常用的使用场景。异步处理,应用解耦,流量削峰和消息通讯四个场景。2.1 异步处理异步处理,就是将一些非核心的业务流程以异步并行的方式执行,从而减少请求响应时间,提高系统吞吐量。以下单为例,用户下单后需要生成订单、赠送活动积分、赠送红包、发送下单成功通知等一系列业务处理。假设三个业务节点每个使用100毫秒钟,不考虑网络等其他开销,则串行方式的时间是400毫秒,并行的时间只需要200毫秒。这样就大大提高了系统的吞吐量。2.2 应用解耦应用解耦,顾名思义就是解除应用系统之间的耦合依赖。通过消息队列,使得每个应用系统不必受其他系统影响,可以更独立自主。以电商系统为例,用户下单后,订单系统需要通知积分系统。一般的做法是:订单系统直接调用积分系统的接口。这就使得应用系统间的耦合特别紧密。如果积分系统无法访问,则积分处理失败,从而导致订单失败。加入消息队列之后,用户下单后,订单系统完成下单业务后,将消息写入消息队列,返回用户订单下单成功。积分系统通过订阅下单消息的方式获取下单通知消息,从而进行积分操作。实现订单系统与库存系统的应用解耦。如果,在下单时积分系统系统异常,也不影响用户正常下单,因为下单后,订单系统写入消息队列就不再关心其他的后续操作。2.3 流量削峰流量削峰也是消息队列中的常用场景,一般在秒杀或团抢活动中使用广泛。以秒杀活动为例,一般会因为流量过大,导致流量暴增,应用挂掉。为解决这个问题,一般需要在应用前端加入消息队列,秒杀业务处理系统根据消息队列中的请求信息,再做后续处理。如上图所示,服务器接收到用户的请求后,首先写入消息队列,秒杀业务处理系统根据消息队列中的请求信息,做后续业务处理。假如消息队列长度超过最大数量,则直接抛弃用户请求或跳转到错误页面。2.4 消息通讯消息通讯是指应用间的数据通信。消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等点对点通讯。以上实际是消息队列的两种消息模式,点对点或发布订阅模式。三、如何选择合适的消息队列目前使用较多的消息队列有ActiveMQ,RabbitMQ,Kafka,RocketMQ等。面对这么多的中消息队列中间件,如何选择适合我们自身业务的消息中间件呢?3.1 衡量标准虽然这些消息队列在功能和特性方面各有优劣,但我们在选型时要有基本衡量标准:1、首先,是开源。开源意味着,如果有一天你使用的消息队列遇到了一个影响你系统业务的Bug,至少还有机会通过修改源代码来迅速修复或规避这个Bug,解决你的系统的问题,而不是等待开发者发布的下一个版本来解决。2、其次,是社区活跃度。这个产品必须是近年来比较流行并且有一定社区活跃度的产品。我们知道,开源产品越流行 Bug 越少,因为大部分遇到的 Bug,其他人早就遇到并且修复了。而且在使用过程中遇到的问题,也比较容易在网上搜索到类似的问题并快速找到解决方案。同时,流行开源产品一般与周边生态系统会有一个比较好的集成和兼容。3、最后,作为一款及格的消息队列,必须具备的几个特性包括:消息的可靠传递:确保不丢消息;支持集群:确保不会因为某个节点宕机导致服务不可用,当然也不能丢消息;性能:具备足够好的性能,能满足绝大多数场景的性能要求。3.2 选型对比接下来我们一起看一下有哪些符合上面这些条件,可供选择的开源消息队列产品。以下是关于各个消息队列中间件的选型对比:特性KafkaRocketMQRabbitMQActiveMQ单机吞吐量10万级10万级万级10万级开发语言ScalaJavaErlangJava高可用分布式分布式主从分布式消息延迟ms级ms级us级ms级消息丢失理论上不会丢失理论上不会丢失低低消费模式拉取推拉推拉 持久化 文件内存,文件内存,文件,数据库支持协议自定义协议自定义协议AMQP,XMPP, SMTP,STOMPAMQP,MQTT,OpenWire,STOMP社区活跃度高中高高管理界面 web console好一般部署难度中 低 部署方式独立独立独立独立,嵌入成熟度成熟比较成熟成熟成熟综合评价优点:拥有强大的性能及吞吐量,兼容性很好。 缺点:由于支持消息堆积,导致延迟比较高。优点:性能好,稳定可靠,有活跃的中文社区,特点响应快。 缺点:兼容性较差,但随着影响力的扩大,该问题会有改善。优点:产品成熟,容易部署和使用,拥有灵活的路由配置。 缺点:性能和吞吐量较差,不易进行二次开发。优点:产品成熟,支持协议多,支持多种语言的客户端。 缺点:社区不活跃,存在消息丢失的可能。以上四种消息队列都有各自的优劣势,需要根据现有系统的情况,选择最适合的消息队列。总结起来,电商、金融等对事务性要求很高的,可以考虑RocketMQ;技术挑战不是特别高,用 RabbitMQ 是不错的选择;如果是大数据领域的实时计算、日志采集等场景可以考虑 Kafka。四、Spring Boot整合RabbitMQ实现消息队列Spring Boot提供了spring-bootstarter-amqp组件对消息队列进行支持,使用非常简单,仅需要非常少的配置即可实现完整的消息队列服务。接下来介绍Spring Boot对RabbitMQ的支持。如何在SpringBoot项目中使用RabbitMQ?4.1 Spring Boot集成RabbitMQSpring Boot提供了spring-boot-starter-amqp组件,只需要简单的配置即可与Spring Boot无缝集成。下面通过示例演示集成RabbitMQ实现消息的接收和发送。第一步,配置pom包。创建Spring Boot项目并在pom.xml文件中添加spring-bootstarter-amqp等相关组件依赖:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>在上面的示例中,引入Spring Boot自带的amqp组件spring-bootstarter-amqp。第二步,修改配置文件。修改application.properties配置文件,配置rabbitmq的host地址、端口以及账户信息。spring.rabbitmq.host=10.2.1.231 spring.rabbitmq.port=5672 spring.rabbitmq.username=zhangweizhong spring.rabbitmq.password=weizhong1988 spring.rabbitmq.virtualHost=order在上面的示例中,主要配置RabbitMQ服务的地址。RabbitMQ配置由spring.rabbitmq.*配置属性控制。virtual-host配置项指定RabbitMQ服务创建的虚拟主机,不过这个配置项不是必需的。第三步,创建消费者消费者可以消费生产者发送的消息。接下来创建消费者类Consumer,并使用@RabbitListener注解来指定消息的处理方法。示例代码如下:@Component public class Consumer { @RabbitHandler @RabbitListener(queuesToDeclare = @Queue("rabbitmq_queue")) public void process(String message) { System.out.println("消费者消费消息111=====" + message); } }在上面的示例中,Consumer消费者通过@RabbitListener注解创建侦听器端点,绑定rabbitmq_queue队列。(1)@RabbitListener注解提供了@QueueBinding、@Queue、@Exchange等对象,通过这个组合注解配置交换机、绑定路由并且配置监听功能等。(2)@RabbitHandler注解为具体接收的方法。第四步,创建生产者生产者用来产生消息并进行发送,需要用到RabbitTemplate类。与之前的RedisTemplate类似,RabbitTemplate是实现发送消息的关键类。示例代码如下:@Component public class Producer { @Autowired private RabbitTemplate rabbitTemplate; public void produce() { String message = new Date() + "Beijing"; System.out.println("生产者产生消息=====" + message); rabbitTemplate.convertAndSend("rabbitmq_queue", message); } }如上面的示例所示,RabbitTemplate提供了 convertAndSend方法发送消息。convertAndSend方法有routingKey和message两个参数:(1)routingKey为要发送的路由地址。(2)message为具体的消息内容。发送者和接收者的queuename必须一致,不然无法接收。第五步,测试验证。创建对应的测试类ApplicationTests,验证消息发送和接收是否成功。@RunWith(SpringRunner.class) @SpringBootTest public class ApplicationTests { @Autowired Producer producer; @Test public void contextLoads() throws InterruptedException { producer.produce(); Thread.sleep(1*1000); } }在上面的示例中,首先注入生产者对象,然后调用produce()方法来发送消息。最后,单击Run Test或在方法上右击,选择Run 'contextLoads()',运行单元测试程序,查看后台输出情况,结果如下图所示。通过上面的程序输出日志可以看到,消费者已经收到了生产者发送的消息并进行了处理。这是常用的简单使用示例。4.2 发送和接收实体对象Spring Boot支持对象的发送和接收,且不需要额外的配置。下面通过一个例子来演示RabbitMQ发送和接收实体对象。4.2.1 定义消息实体首先,定义发送与接收的对象实体User类,代码如下:public class User implements Serializable { public String name; public String password; // 省略get和set方法 }在上面的示例中,定义了普通的User实体对象。需要注意的是,实体类对象必须继承Serializable序列化接口,否则会报数据无法序列化的错误。4.2.2 定义消费者修改Consumer类,将参数换成User对象。示例代码如下:@Component public class Consumer { @RabbitHandler @RabbitListener(queuesToDeclare = @Queue("rabbitmq_queue_object")) public void process(User user) { System.out.println("消费者消费消息111user=====name:" + user.getName()+",password:"+user.getPassword()); } }其实,消费者类和消息处理方法和之前的类似,只不过将参数换成了实体对象,监听rabbitmq_queue_object队列。4.2.3 定义生产者修改Producer类,定义User实体对象,并通过convertAndSend方法发送对象消息。示例代码如下:@Component public class Producer { @Autowired private RabbitTemplate rabbitTemplate; public void produce() { User user=new User(); user.setName("weiz"); user.setPassword("123456"); System.out.println("生产者生产消息111=====" + user); rabbitTemplate.convertAndSend("rabbitmq_queue_object", user); } }在上面的示例中,还是调用convertAndSend()方法发送实体对象。convertAndSend()方法支持String、Integer、Object等基础的数据类型。4.2.4 验证测试创建单元测试类,注入生产者对象,然后调用produceObj()方法发送实体对象消息,从而验证消息能否被成功接收。@RunWith(SpringRunner.class) @SpringBootTest public class ApplicationTests { @Autowired Producer producer; @Test public void testProduceObj() throws InterruptedException { producer.produceObj(); Thread.sleep(1*1000); } }最后,单击Run Test或在方法上右击,选择Run 'contextLoads()',运行单元测试程序,查看后台输出情况,运行结果如下图所示。通过上面的示例成功实现了RabbitMQ发送和接收实体对象,使得消息的数据结构更加清晰,也更加贴合面向对象的编程思想。五、实现消息的100%可靠性发送5.1 什么是实现消息的100%可靠性发送?在使用消息队列时,因为生产者和消费者不直接交互,所以面临下面几个问题:1)要把消息添加到队列中,怎么保证消息成功添加?2)如何保证消息发送出去时一定会被消费者正常消费?3)消费者正常消费了,生产者或者队列如何知道消费者已经成功消费了消息?要解决前面这些问题,就要保证消息的可靠性发送,实现消息的100%可靠性发送。5.2 技术实现方案RabbitMQ为我们提供了解决方案,下面以常见的创建订单业务为例进行介绍,假设订单创建成功后需要发送短信通知用户。实现消息的100%可靠性发送需要以下条件:1)完成订单业务处理后,生产者发送一条消息到消息队列,同时记录这条操作日志(发送中)。2)消费者收到消息后处理进行;3)消费者处理成功后给消息队列发送ack应答;4)消息队列收到ack应答后,给生成者的Confirm Listener发送确认;5)生产者对消息日志表进行操作,修改之前的日志状态(发送成功);6)在消费端返回应答的过程中,可能发生网络异常,导致生产者未收到应答消息,因此需要一个定时任务去提取其状态为“发送中”并已经超时的消息集合;7)使用定时任务判断为消息事先设置的最大重发次数,大于最大重发次数就判断消息发送失败,更新日志记录状态为发送失败。具体流程如下图所示 5.3 实现消息的100%可靠性发送前面介绍了实现消息的100%可靠性发送的解决方案,接下来从项目实战出发演示如何实现消息的可靠性发送。1. 创建生产者首先把核心生产者的代码编写好,生产者由基本的消息发送和监听组成:@Component public class RabbitOrderSender { private static Logger logger = LoggerFactory.getLogger(RabbitOrderSender.class); @Autowired private RabbitTemplate rabbitTemplate; @Autowired private MessageLogMapper messageLogMapper; /** * Broker应答后,会调用该方法区获取应答结果 */ final RabbitTemplate.ConfirmCallback confirmCallback = new RabbitTemplate.ConfirmCallback() { @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { logger.info("correlationData:"+correlationData); String messageId = correlationData.getId(); logger.info("消息确认返回值:"+ack); if (ack){ //如果返回成功,则进行更新 messageLogMapper.changeMessageLogStatus(messageId, Constans.ORDER_SEND_SUCCESS,new Date()); }else { //失败进行操作:根据具体失败原因选择重试或补偿等手段 logger.error("异常处理,返回结果:"+cause); } } }; /** * 发送消息方法调用: 构建自定义对象消息 * @param order * @throws Exception */ public synchronized void sendOrder(OrderInfo order) throws Exception { // 通过实现 ConfirmCallback 接口,消息发送到 Broker 后触发回调,确认消息是否到达 Broker 服务器,也就是只确认是否正确到达 Exchange 中 rabbitTemplate.setConfirmCallback(confirmCallback); //消息唯一ID CorrelationData correlationData = new CorrelationData(order.getMessageId()); rabbitTemplate.convertAndSend("order.exchange", "order.message", order, correlationData); } }上面的消息发送示例代码和之前的没什么区别,只是增加了confirmCallback应答结果回调。通过实现ConfirmCallback接口,消息发送到Broker后触发回调,确认消息是否到达Broker服务器。因此,ConfirmCallback只能确认消息是否正确到达交换机中。2. 消息重发定时任务实现消息重发的定时任务,示例代码如下:@Component public class RetryMessageTasker { private static Logger logger = LoggerFactory.getLogger(RetryMessageTasker.class); @Autowired private RabbitOrderSender rabbitOrderSender; @Autowired private MessageLogMapper messageLogMapper; /** * 定时任务 */ @Scheduled(initialDelay = 5000, fixedDelay = 10000) public void reSend(){ logger.info("-----------定时任务开始-----------"); //抽取消息状态为0且已经超时的消息集合 List<MessageLog> list = messageLogMapper.query4StatusAndTimeoutMessage(); list.forEach(messageLog -> { //投递三次以上的消息 if(messageLog.getTryCount() >= 3){ //更新失败的消息 messageLogMapper.changeMessageLogStatus(messageLog.getMessageId(), Constans.ORDER_SEND_FAILURE, new Date()); } else { // 重试投递消息,将重试次数递增 messageLogMapper.update4ReSend(messageLog.getMessageId(), new Date()); OrderInfo reSendOrder = JsonUtil.jsonToObject(messageLog.getMessage(), OrderInfo.class); try { rabbitOrderSender.sendOrder(reSendOrder); } catch (Exception e) { e.printStackTrace(); logger.error("-----------异常处理-----------"); } } }); } }在上面的定时任务程序中,每10秒钟提取状态为0且已经超时的消息,重发这些消息,如果发送次数已经在3次以上,则认定为发送失败。3. 创建消费者创建消费者程序负责接收处理消息,处理成功后发送消息确认。示例代码如下:@Component public class OrderReceiver { /** * @RabbitListener 消息监听,可配置交换机、队列、路由key * 该注解会创建队列和交互机 并建立绑定关系 * @RabbitHandler 标识此方法如果有消息过来,消费者要调用这个方法 * @Payload 消息体 * @Headers 消息头 * @param order */ @RabbitListener(bindings = @QueueBinding( value = @Queue(value = "order.queue",declare = "true"), exchange = @Exchange(name = "order.exchange",declare = "true",type = "topic"), key = "order.message" )) @RabbitHandler public void onOrderMessage(@Payload Order order, @Headers Map<String,Object> headers, Channel channel) throws Exception{ //消费者操作 try { System.out.println("------收到消息,开始消费------"); System.out.println("订单ID:"+order.getId()); Long deliveryTag = (Long)headers.get(AmqpHeaders.DELIVERY_TAG); //现在是手动确认消息 ACK channel.basicAck(deliveryTag,false); } finally { channel.close(); } } } 消息处理程序和一般的接收者类似,都是通过@RabbitListener注解监听消息队列。不同的是,发送程序处理成功后,通过channel.basicAck(deliveryTag,false)发送消息确认ACK。4. 运行测试创建单元测试程序。创建一个生成订单的测试方法,测试代码如下:@RunWith(SpringRunner.class) @SpringBootTest public class MqApplicationTests { @Autowired private OrderService orderService; /** * 测试订单创建 */ @Test public void createOrder(){ OrderInfo order = new OrderInfo(); order.setId("201901236"); order.setName("测试订单6"); order.setMessageId(System.currentTimeMillis() + "$" + UUID.randomUUID().toString()); try { orderService.createOrder(order); } catch (Exception e) { e.printStackTrace(); } } }启动消费者程序,启动成功之后,运行createOrder创建订单测试方法。结果表明发送成功并且入库正确,业务表和消息记录表均有数据且status状态=1,表示成功。如果消费者程序处理失败或者超时,未返回ack确认;则生产者的定时程序会重新投递消息。直到三次投递均失败。六、MQ常见问题总结6.1 怎么保证消息没有重复消费?使用消息队列如何保证幂等性?幂等性:就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用问题出现原因我们先来了解一下产生消息重复消费的原因,对于MQ的使用,有三个角色:生产者、MQ、消费者,那么消息的重复这三者会出现:生产者:生产者可能会推送重复的数据到MQ中,有可能controller接口重复提交了两次,也可能是重试机制导致的MQ:假设网络出现了波动,消费者消费完一条消息后,发送ack时,MQ还没来得及接受,突然挂了,导致MQ以为消费者还未消费该条消息,MQ回复后会再次推送了这条消息,导致出现重复消费。消费者:消费者接收到消息后,正准备发送ack到MQ,突然消费者挂了,还没得及发送ack,这时MQ以为消费者还没消费该消息,消费者重启后,MQ再次推送该条消息。解决方案在正常情况下,生产者是客户,我们很难避免出现用户重复点击的情况,而MQ是允许存在多条一样的消息,但消费者是不允许出现消费两条一样的数据,所以幂等性一般是在消费端实现的:状态判断:消费者把消费消息记录到redis中,再次消费时先到redis判断是否存在该数据,存在则表示消费过,直接丢弃业务判断:消费完数据后,都是需要插入到数据库中,使用数据库的唯一约束防止重复消费。插入数据库前先查询是否存在该数据,存在则直接丢弃消息,这种方式是比较简单粗暴地解决问题6.2 消息丢失的情况消息丢失属于比较常见的问题。一般有生产端丢失、MQ服务丢失、消费端丢失等三种情况。针对各种情况应对方式也不一样。1.生产端丢失的解决方案主要有开启confirm模式,生产着收到MQ发回的confirm确认之后,再进行消息删除,否则消息重推。生产者端消息保存的数据库,由后台定时程序异步推送,收到confirm确认则认为成功,否则消息重推,重推多次均未成功,则认为发送失败。2.MQ服务丢失则主要是开启消息持久化,让消息及时保存到磁盘。3.消费端消息丢失则关闭自动ack确认,消息消费成功后手动发送ack确认。消息消费失败,则重新消费。6.3 消息的传输顺序性解决思路在生产端发布消息时,每次法发布消息都把上一条消息的ID记录到消息体中,消费者接收到消息时,做如下操作先根据上一条Id去检查是否存在上一条消息还没被消费,如果不存在(消费后去掉id),则正常进行,如果正常操作如果存在,则根据id到数据库检查是否被消费,如果被消费,则正常操作如果还没被消费,则休眠一定时间(比如30ms),再重新检查,如被消费,则正常操作如果还没被消费,则抛出异常6.4 怎么解决消息积压问题?所谓的消息积压,就是生成者生成消息太快,而消费者处理消息太慢,从而导致消费端消息积压在MQ中无法处理的问题。遇到这种消息积压的情况,可以根据消息重要程度,分为两种情况处理:如果消息可以被丢弃,那么直接丢弃就好了一般情况下,消息是不可以被丢弃的,那么这样需要考虑策略了,我们可以把原来的消费端重新当做生产端,重新部署一天MQ,再后面出现增加消费端,这样形成另一条生产-消息-消费的线路最后以上,我们就把消息队列介绍完了。消息中间件在互联网公司使用得越来越多,希望大家能够熟悉其使用。
随着现在分布式架构越来越盛行,在很多场景下需要使用到分布式锁。很多小伙伴对于分布式锁还不是特别了解,所以特地总结了一篇文章,让大家一文读懂分布式锁的前世今生。分布式锁的实现有很多种,比如基于数据库、Redis 、 zookeeper 等实现,本文的示例主要介绍使用Redis实现分布式锁。一、什么是分布式锁分布式锁,即分布式系统中的锁,分布式锁是控制分布式系统有序的对共享资源进行操作,在单体应用中我们通过锁实现共享资源访问,而分布式锁,就是解决了分布式系统中控制共享资源访问的问题。可能初学的小伙伴就会有疑问,Java多线程中的公平锁、非公平锁、自旋锁、可重入锁、读写锁、互斥锁这些都还没闹明白呢?怎么有出来一个分布式锁?其实,可以这么理解:Java的原生锁是解决多线程下对于共享资源的操作,而分布式锁则是多进程下对于共享资源的操作。分布式系统中竞争共享资源的最小粒度从线程升级成了进程。分布式锁已经被应用到各种高并发的场景下,典型场景案例包括:秒杀、车票、订单、退款、库存等场景。二、为什么要使用分布式锁在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,这个时候,便需要使用到分布式锁。目前几乎很多大型网站及应用都是分布式部署的,如何保证分布式场景中的数据一致性问题一直是一个比较重要的话题。在某些场景下,为了保证数据的完整性和一致性,我们需要保证一个方法在同一时间内只能被同一个线程执行,这就需要使用分布式锁。如上图所示,假设用户A和用户B同时购买了某款商品,订单创建成功后,下单系统A和下单系统B就会同时对数据库中的该款商品的库存进行扣减。如果此时不加任何控制,系统B提交的数据更新就会覆盖系统A的数据,导致库存错误,超卖等问题。三、分布式锁应该具备哪些条件在介绍分布式锁的实现方式之前,先了解一下分布式锁应该具备哪些条件:1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;2、高可用的获取锁与释放锁;3、高性能的获取锁与释放锁;4、具备可重入特性;5、具备锁失效机制,防止死锁;6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。四、分布式锁的实现方式随着业务发展的需要,原来的单体应用被演化成分布式集群系统后,由于系统分布在不同机器上,这就使得原有的并发控制锁策略失效,为了解决这个问题就需要一种跨进程的互斥机制来控制共享资源的访问,这就需要用到分布式锁!分布式锁的实现有多种方式,下面介绍下这几种分布式锁的实现:基于数据库实现分布式锁,(适用于并发小的系统);基于缓存(Redis等)实现分布式锁,(效率高,最流行,存在锁超时的问题);基于Zookeeper实现分布式锁,(可靠,但是效率不高);尽管有这三种方案,但是不同的业务也要根据自己的情况进行选型,他们之间没有最好只有更适合!五、基于Redis实现分布式锁使用Redis实现分布式锁是目前比较流行的解决方案,主要是使用Redis 获取锁与释放锁效率都很高,实现方式也特别简单。实现原理:(1)获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为线程ID,通过此在释放锁的时候进行判断。(2)获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。(3)释放锁的时候,通过线程ID判断是不是该锁,若是该锁,则执行delete进行锁释放。说完了Redis分布式锁的实现原理,接下来就带大家一步一步在SpringBoot项目中使用Redis 实现分布式锁。第一步,创建Spring Boot项目,并引入相关依赖。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.72</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>第二步,创建Redis分布式锁通用操作类,示例代码如下:import com.alibaba.fastjson.JSON; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.context.annotation.Bean; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * Lua脚本 * // 加锁 * if * redis.call('setNx',KEYS[1],ARGV[1]) * then * if redis.call('get',KEYS[1])==ARGV[1] * return redis.call('expire',KEYS[1],ARGV[2]) * else * return 0 * end * end * * // 解锁 * redis.call('get', KEYS[1]) == ARGV[1] * then * return redis.call('del', KEYS[1]) * else * return 0 * * * //更新时间 * if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end * * * * */ @Slf4j @Component public class RedisLockUtils { @Resource private RedisTemplate redisTemplate; private static Map<String, LockInfo> lockInfoMap = new ConcurrentHashMap<>(); private static final Long SUCCESS = 1L; public static class LockInfo { private String key; private String value; private int expireTime; //更新时间 private long renewalTime; //更新间隔 private long renewalInterval; public static LockInfo getLockInfo(String key, String value, int expireTime) { LockInfo lockInfo = new LockInfo(); lockInfo.setKey(key); lockInfo.setValue(value); lockInfo.setExpireTime(expireTime); lockInfo.setRenewalTime(System.currentTimeMillis()); lockInfo.setRenewalInterval(expireTime * 2000 / 3); return lockInfo; } public String getKey() { return key; } public void setKey(String key) { this.key = key; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } public int getExpireTime() { return expireTime; } public void setExpireTime(int expireTime) { this.expireTime = expireTime; } public long getRenewalTime() { return renewalTime; } public void setRenewalTime(long renewalTime) { this.renewalTime = renewalTime; } public long getRenewalInterval() { return renewalInterval; } public void setRenewalInterval(long renewalInterval) { this.renewalInterval = renewalInterval; } } /** * 使用lua脚本加锁 * @param lockKey 锁 * @param value 身份标识(保证锁不会被其他人释放) * @param expireTime 锁的过期时间(单位:秒) * @Desc 注意事项,redisConfig配置里面必须使用 genericToStringSerializer序列化,否则获取不了返回值 */ public boolean tryLock(String lockKey, String value, int expireTime) { String luaScript = "if redis.call('setNx',KEYS[1],ARGV[1]) then if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('expire',KEYS[1],ARGV[2]) else return 0 end end"; DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>(); redisScript.setResultType(Boolean.class); redisScript.setScriptText(luaScript); List<String> keys = new ArrayList<>(); keys.add(lockKey); //Object result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey),value,expireTime + ""); // Object result = redisTemplate.execute(redisScript, new StringRedisSerializer(), new StringRedisSerializer(), Collections.singletonList(lockKey), identity, expireTime); Object result = redisTemplate.execute(redisScript, keys, value, expireTime); log.info("已获取到{}对应的锁!", lockKey); if (expireTime >= 10) { lockInfoMap.put(lockKey + value, LockInfo.getLockInfo(lockKey, value, expireTime)); } return (boolean) result; } /** * 使用lua脚本释放锁 * @param lockKey * @param value * @return 成功返回true, 失败返回false */ public boolean unlock(String lockKey, String value) { lockInfoMap.remove(lockKey + value); String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>(); redisScript.setResultType(Boolean.class); redisScript.setScriptText(luaScript); List<String> keys = new ArrayList<>(); keys.add(lockKey); Object result = redisTemplate.execute(redisScript, keys, value); log.info("解锁成功:{}", result); return (boolean) result; } /** * 使用lua脚本更新redis锁的过期时间 * @param lockKey * @param value * @return 成功返回true, 失败返回false */ public boolean renewal(String lockKey, String value, int expireTime) { String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end"; DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>(); redisScript.setResultType(Boolean.class); redisScript.setScriptText(luaScript); List<String> keys = new ArrayList<>(); keys.add(lockKey); Object result = redisTemplate.execute(redisScript, keys, value, expireTime); log.info("更新redis锁的过期时间:{}", result); return (boolean) result; } /** * * @param lockKey 锁 * @param value 身份标识(保证锁不会被其他人释放) * @param expireTime 锁的过期时间(单位:秒) * @return 成功返回true, 失败返回false */ public boolean lock(String lockKey, String value, long expireTime) { return redisTemplate.opsForValue().setIfAbsent(lockKey, value, expireTime, TimeUnit.SECONDS); } /** * redisTemplate解锁 * @param key * @param value * @return 成功返回true, 失败返回false */ public boolean unlock2(String key, String value) { Object currentValue = redisTemplate.opsForValue().get(key); boolean result = false; if (StringUtils.isNotEmpty(String.valueOf(currentValue)) && currentValue.equals(value)) { result = redisTemplate.opsForValue().getOperations().delete(key); } return result; } /** * 定时去检查redis锁的过期时间 */ @Scheduled(fixedRate = 5000L) @Async("redisExecutor") public void renewal() { long now = System.currentTimeMillis(); for (Map.Entry<String, LockInfo> lockInfoEntry : lockInfoMap.entrySet()) { LockInfo lockInfo = lockInfoEntry.getValue(); if (lockInfo.getRenewalTime() + lockInfo.getRenewalInterval() < now) { renewal(lockInfo.getKey(), lockInfo.getValue(), lockInfo.getExpireTime()); lockInfo.setRenewalTime(now); log.info("lockInfo {}", JSON.toJSONString(lockInfo)); } } } /** * 分布式锁设置单独线程池 * @return */ @Bean("redisExecutor") public Executor redisExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(1); executor.setMaxPoolSize(1); executor.setQueueCapacity(1); executor.setKeepAliveSeconds(60); executor.setThreadNamePrefix("redis-renewal-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy()); return executor; } }第三步,创建RedisTemplate 配置类,配置Redistemplate,示例代码如下:@Configuration public class RedisConfig { @Bean public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) throws Exception { RedisTemplate redisTemplate = new RedisTemplate(); redisTemplate.setConnectionFactory(redisConnectionFactory); // 创建 序列化类 GenericToStringSerializer genericToStringSerializer = new GenericToStringSerializer(Object.class); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(genericToStringSerializer); return redisTemplate; } }第四步,实现业务调用,这里以扣减库存为例,示例代码如下:@RestController public class IndexController { @Resource private RedisTemplate redisTemplate; @Autowired private RedisLockUtils redisLock; @RequestMapping("/deduct-stock") public String deductStock() { String productId = "product001"; System.out.println("---------------->>>开始扣减库存"); String key = productId; String requestId = productId + Thread.currentThread().getId(); try { boolean locked = redisLock.lock(key, requestId, 10); if (!locked) { return "error"; } //执行业务逻辑 //System.out.println("---------------->>>执行业务逻辑:"+appTitle); int stock = Integer.parseInt(redisTemplate.opsForValue().get("product001-stock").toString()); int currentStock = stock-1; redisTemplate.opsForValue().set("product001-stock",currentStock); try { Random random = new Random(); Thread.sleep(random.nextInt(3) *1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("---------------->>>扣减库存结束:current stock:" + currentStock); return "success,current stock:" + currentStock; } finally { redisLock.unlock2(key, requestId); } } }六、验证测试代码完成之后,开始测试。我们同时启动两个实例,端口号为:8888和8889模拟分布式系统。接下来,我们分别请求:http://localhost:8888/deduct-stock和http://localhost:8889/deduct-stock,或者使用JMater分别请求这两个地址,模拟高并发的情况。通过上图我们可以看到,在批量请求的情况下,库存扣减也没有出现问题。说明分布式锁生效了。最后以上,我们就把什么是分布式锁,如何基于Redis 实现分布式锁的解决方案介绍完了。分布式锁是分布式系统中的重要功能组件,希望大家能够熟练掌握。
本文将从项目实战出发来介绍分布式定时任务的实现。在某些应用场景下要求任务必须具备高可用性和可扩展性,单台服务器不能满足业务需求,这时就需要使用Quartz实现分布式定时任务。一、分布式任务应用场景定时任务系统在应用平台中的重要性不言而喻,特别是互联网电商、金融等行业更是离不开定时任务。在任务数量不多、执行频率不高时,单台服务器完全能够满足。但是随着业务逐渐增加,定时任务系统必须具备高可用和水平扩展的能力,单台服务器已经不能满足需求。因此需要把定时任务系统部署到集群中,实现分布式定时任务系统集群。Quartz的集群功能通过故障转移和负载平衡功能为调度程序带来高可用性和可扩展性。Quartz是通过数据库表来存储和共享任务信息的。独立的Quartz节点并不与另一个节点或者管理节点通信,而是通过数据库锁机制来调度执行定时任务。需要注意的是,在集群环境下,时钟必须同步,否则执行时间不一致。二、Quartz实现分布式定时任务1. 添加Quartz依赖首先,引入Quartz中提供分布式处理的JAR包以及数据库和连接相关的依赖。示例代码如下:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency> <!-- mysql --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- orm --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency>在上面的示例中,除了添加Quartz依赖外,还需要添加mysql-connector-java和spring-boot-starter-data-jpa两个组件,这两个组件主要用于JOB持久化到MySQL数据库。2. 初始化Quartz数据库分布式Quartz定时任务的配置信息存储在数据库中,数据库初始化脚本可以在官方网站中查找,默认保存在quartz-2.2.3-distribution\src\org\quartz\impl\jdbcjobstore\tables-mysql.sql目录下。首先创建quartz_jobs数据库,然后在数据库中执行tables-mysql.sql初始化脚本。具体示例如下:DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS; DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS; DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE; DROP TABLE IF EXISTS QRTZ_LOCKS; DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS; DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS; DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS; DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS; DROP TABLE IF EXISTS QRTZ_TRIGGERS; DROP TABLE IF EXISTS QRTZ_JOB_DETAILS; DROP TABLE IF EXISTS QRTZ_CALENDARS; CREATE TABLE QRTZ_JOB_DETAILS ( SCHED_NAME VARCHAR(120) NOT NULL, JOB_NAME VARCHAR(200) NOT NULL, JOB_GROUP VARCHAR(200) NOT NULL, DESCRIPTION VARCHAR(250) NULL, JOB_CLASS_NAME VARCHAR(250) NOT NULL, IS_DURABLE VARCHAR(1) NOT NULL, IS_NONCONCURRENT VARCHAR(1) NOT NULL, IS_UPDATE_DATA VARCHAR(1) NOT NULL, REQUESTS_RECOVERY VARCHAR(1) NOT NULL, JOB_DATA BLOB NULL, PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) ); CREATE TABLE QRTZ_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, JOB_NAME VARCHAR(200) NOT NULL, JOB_GROUP VARCHAR(200) NOT NULL, DESCRIPTION VARCHAR(250) NULL, NEXT_FIRE_TIME BIGINT(13) NULL, PREV_FIRE_TIME BIGINT(13) NULL, PRIORITY INTEGER NULL, TRIGGER_STATE VARCHAR(16) NOT NULL, TRIGGER_TYPE VARCHAR(8) NOT NULL, START_TIME BIGINT(13) NOT NULL, END_TIME BIGINT(13) NULL, CALENDAR_NAME VARCHAR(200) NULL, MISFIRE_INSTR SMALLINT(2) NULL, JOB_DATA BLOB NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP) ); CREATE TABLE QRTZ_SIMPLE_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, REPEAT_COUNT BIGINT(7) NOT NULL, REPEAT_INTERVAL BIGINT(12) NOT NULL, TIMES_TRIGGERED BIGINT(10) NOT NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_CRON_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, CRON_EXPRESSION VARCHAR(200) NOT NULL, TIME_ZONE_ID VARCHAR(80), PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_SIMPROP_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, STR_PROP_1 VARCHAR(512) NULL, STR_PROP_2 VARCHAR(512) NULL, STR_PROP_3 VARCHAR(512) NULL, INT_PROP_1 INT NULL, INT_PROP_2 INT NULL, LONG_PROP_1 BIGINT NULL, LONG_PROP_2 BIGINT NULL, DEC_PROP_1 NUMERIC(13,4) NULL, DEC_PROP_2 NUMERIC(13,4) NULL, BOOL_PROP_1 VARCHAR(1) NULL, BOOL_PROP_2 VARCHAR(1) NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_BLOB_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, BLOB_DATA BLOB NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_CALENDARS ( SCHED_NAME VARCHAR(120) NOT NULL, CALENDAR_NAME VARCHAR(200) NOT NULL, CALENDAR BLOB NOT NULL, PRIMARY KEY (SCHED_NAME,CALENDAR_NAME) ); CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP) ); CREATE TABLE QRTZ_FIRED_TRIGGERS ( SCHED_NAME VARCHAR(120) NOT NULL, ENTRY_ID VARCHAR(95) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, INSTANCE_NAME VARCHAR(200) NOT NULL, FIRED_TIME BIGINT(13) NOT NULL, SCHED_TIME BIGINT(13) NOT NULL, PRIORITY INTEGER NOT NULL, STATE VARCHAR(16) NOT NULL, JOB_NAME VARCHAR(200) NULL, JOB_GROUP VARCHAR(200) NULL, IS_NONCONCURRENT VARCHAR(1) NULL, REQUESTS_RECOVERY VARCHAR(1) NULL, PRIMARY KEY (SCHED_NAME,ENTRY_ID) ); CREATE TABLE QRTZ_SCHEDULER_STATE ( SCHED_NAME VARCHAR(120) NOT NULL, INSTANCE_NAME VARCHAR(200) NOT NULL, LAST_CHECKIN_TIME BIGINT(13) NOT NULL, CHECKIN_INTERVAL BIGINT(13) NOT NULL, PRIMARY KEY (SCHED_NAME,INSTANCE_NAME) ); CREATE TABLE QRTZ_LOCKS ( SCHED_NAME VARCHAR(120) NOT NULL, LOCK_NAME VARCHAR(40) NOT NULL, PRIMARY KEY (SCHED_NAME,LOCK_NAME) );使用tables-mysql.sql创建表的语句执行完成后,说明Quartz的数据库和表创建成功,我们查看数据库的ER图,如下图所示。3. 配置数据库和Quartz修改application.properties配置文件,配置数据库与Quartz。具体操作如下:# server.port=8090 # Quartz 数据库 spring.datasource.url=jdbc:mysql://localhost:3306/quartz_jobs?useSSL=false&serverTimezone=UTC spring.datasource.username=root spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.max-active=1000 spring.datasource.max-idle=20 spring.datasource.min-idle=5 spring.datasource.initial-size=10 # 是否使用properties作为数据存储 org.quartz.jobStore.useProperties=false # 数据库中表的命名前缀 org.quartz.jobStore.tablePrefix=QRTZ_ # 是否是一个集群,是不是分布式的任务 org.quartz.jobStore.isClustered=true # 集群检查周期,单位为毫秒,可以自定义缩短时间。当某一个节点宕机的时候,其他节点等待多久后开始执行任务 org.quartz.jobStore.clusterCheckinInterval=5000 # 单位为毫秒,集群中的节点退出后,再次检查进入的时间间隔 org.quartz.jobStore.misfireThreshold=60000 # 事务隔离级别 org.quartz.jobStore.txIsolationLevelReadCommitted=true # 存储的事务管理类型 org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX # 使用的Delegate类型 org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate # 集群的命名,一个集群要有相同的命名 org.quartz.scheduler.instanceName=ClusterQuartz # 节点的命名,可以自定义。AUTO代表自动生成 org.quartz.scheduler.instanceId=AUTO # rmi远程协议是否发布 org.quartz.scheduler.rmi.export=false # rmi远程协议代理是否创建 org.quartz.scheduler.rmi.proxy=false # 是否使用用户控制的事务环境触发执行任务 org.quartz.scheduler.wrapJobExecutionInUserTransaction=false上面的配置主要是Quartz数据库和Quartz分布式集群相关的属性配置。分布式定时任务的配置存储在数据库中,所以需要配置数据库连接和Quartz配置信息,为Quartz提供数据库配置信息,如数据库、数据表的前缀之类。4. 定义定时任务后台定时任务与普通Quartz任务并无差异,只是增加了@PersistJobDataAfterExecution注解和@DisallowConcurrentExecution注解。创建QuartzJob定时任务类并实现Quartz定时任务的具体示例代码如下:// 持久化 @PersistJobDataAfterExecution // 禁止并发执行 @DisallowConcurrentExecution public class QuartzJob extends QuartzJobBean { private static final Logger log = LoggerFactory.getLogger(QuartzJob.class); @Override protected void executeInternal(JobExecutionContext context) throws JobExecutionException { String taskName = context.getJobDetail().getJobDataMap().getString("name"); log.info("---> Quartz job, time:{"+new Date()+"} ,name:{"+taskName+"}<----"); } }在上面的示例中,创建了QuartzJob定时任务类,使用@PersistJobDataAfterExecution注解持久化任务信息。DisallowConcurrentExecution禁止并发执行,避免同一个任务被多次并发执行。5. SchedulerConfig配置创建SchedulerConfig配置类,初始化Quartz分布式集群相关配置,包括集群设置、数据库等。示例代码如下:@Configuration public class SchedulerConfig { @Autowired private DataSource dataSource; /** * 调度器 * * @return * @throws Exception */ @Bean public Scheduler scheduler() throws Exception { Scheduler scheduler = schedulerFactoryBean().getScheduler(); return scheduler; } /** * Scheduler工厂类 * * @return * @throws IOException */ @Bean public SchedulerFactoryBean schedulerFactoryBean() throws IOException { SchedulerFactoryBean factory = new SchedulerFactoryBean(); factory.setSchedulerName("Cluster_Scheduler"); factory.setDataSource(dataSource); factory.setApplicationContextSchedulerContextKey("applicationContext"); factory.setTaskExecutor(schedulerThreadPool()); //factory.setQuartzProperties(quartzProperties()); factory.setStartupDelay(10);// 延迟10s执行 return factory; } /** * 配置Schedule线程池 * * @return */ @Bean public Executor schedulerThreadPool() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(Runtime.getRuntime().availableProcessors()); executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors()); executor.setQueueCapacity(Runtime.getRuntime().availableProcessors()); return executor; } }在上面的示例中,主要是配置Schedule线程池、配置Quartz数据库、创建Schedule调度器实例等初始化配置。6. 触发定时任务配置完成之后,还需要触发定时任务,创建JobStartupRunner类以便在系统启动时触发所有定时任务。示例代码如下:@Component public class JobStartupRunner implements CommandLineRunner { @Autowired SchedulerConfig schedulerConfig; private static String TRIGGER_GROUP_NAME = "test_trigger"; private static String JOB_GROUP_NAME = "test_job"; @Override public void run(String... args) throws Exception { Scheduler scheduler; try { scheduler = schedulerConfig.scheduler(); TriggerKey triggerKey = TriggerKey.triggerKey("trigger1", TRIGGER_GROUP_NAME); CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey); if (null == trigger) { Class clazz = QuartzJob.class; JobDetail jobDetail = JobBuilder.newJob(clazz).withIdentity("job1", JOB_GROUP_NAME).usingJobData("name","weiz QuartzJob").build(); CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 * * * * ?"); trigger = TriggerBuilder.newTrigger().withIdentity("trigger1", TRIGGER_GROUP_NAME) .withSchedule(scheduleBuilder).build(); scheduler.scheduleJob(jobDetail, trigger); System.out.println("Quartz 创建了job:...:" + jobDetail.getKey()); } else { System.out.println("job已存在:{}" + trigger.getKey()); } TriggerKey triggerKey2 = TriggerKey.triggerKey("trigger2", TRIGGER_GROUP_NAME); CronTrigger trigger2 = (CronTrigger) scheduler.getTrigger(triggerKey2); if (null == trigger2) { Class clazz = QuartzJob2.class; JobDetail jobDetail2 = JobBuilder.newJob(clazz).withIdentity("job2", JOB_GROUP_NAME).usingJobData("name","weiz QuartzJob2").build(); CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 * * * * ?"); trigger2 = TriggerBuilder.newTrigger().withIdentity("trigger2", TRIGGER_GROUP_NAME) .withSchedule(scheduleBuilder).build(); scheduler.scheduleJob(jobDetail2, trigger2); System.out.println("Quartz 创建了job:...:{}" + jobDetail2.getKey()); } else { System.out.println("job已存在:{}" + trigger2.getKey()); } scheduler.start(); } catch (Exception e) { System.out.println(e.getMessage()); } } }在上面的示例中,为了适应分布式集群,我们在系统启动时触发定时任务,判断任务是否已经创建、是否正在执行。如果集群中的其他示例已经创建了任务,则启动时无须触发任务。三、 验证测试配置完成之后,接下来启动任务,测试分布式任务配置是否成功。启动一个实例,可以看到定时任务执行了,然后每10秒钟打印输出一次,如下图所示。接下来,模拟分布式部署的情况。我们再启动一个测试程序实例,这样就有两个后台定时任务实例。实例1:实例2:从上面的日志中可以看到,Quartz Job和Quartz Job2交替地在两个任务实例进程中执行,同一时刻同一个任务只有一个进程在执行,这说明已经达到了分布式后台定时任务的效果。接下来,停止任务实例1,测试任务实例2是否会接管所有任务继续执行。如图10-11所示,停止任务实例1后,任务实例2接管了所有的定时任务。这样如果集群中的某个实例异常了,其他实例能够接管所有的定时任务,确保任务集群的稳定运行。最后以上,我们就把Spring Boot集成Quartz实现分布式定时任务的功能介绍完了。分布式定时任务在应用开发中非常重要的功能模块,希望大家能够熟练掌握。
前面介绍了Go语言的基础语法,所谓磨刀不误砍柴工,希望大家还是能熟悉掌握这些基础知识,这样后面真正学起Go来才会得心应手。作为初学者。Go语言的语法有些和java类似,但也有很多不一样的地方。刚开始都会遇到各种各样的坑。下面就来总结下学习go语言的过程中,遇到的各种坑。一、基础事项1. 写C# 的人都会将 “{” 独立一行,但是这在go 里面是错误的 “{” 必须更方法体 在同一行。我第一次写go 的就犯了这个错误,还不知道错误在哪。func main() { fmt.Println("Hello, World!") } 2. if…else 语句中的 else 必须和 if 的 ’ } ’ 在同一行,否则编译错误var a int = 30 if a < 20 { fmt.Print("a<20") } else { fmt.Print("a>=20") } 3. 包名的定义。你必须在源文件中非注释的第一行声明包名,如:package main。package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。package main 4. 在 Go 程序中,一行代表一个语句结束。每个语句不需要像 C 家族中的其它语言一样以分号 ; 结尾,因为这些工作都将由 Go 编译器自动完成。如果你打算将多个语句写在同一行,则必须使用 ; 人为区分,但在实际开发中我们并不鼓励这种做法。fmt.Println("Hello, World!") fmt.Println("www.fpeach.com") 5. main()函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数。然而,每个package 中,只能有一个main() 函数,否则会报main redeclared in this block previous declaration at .. 的错误。package main import "fmt" func main() { /* 这是我的第一个简单的程序 */ fmt.Println("Hello, World!") } 二、变量使用1. 标识符用来命名变量、类型等程序实体。一个标识符实际上就是一个或是多个字母(A~Z和a~z)数字(0~9)、下划线_组成的序列,但是第一个字符必须是字母或下划线而不能是数字。以下是无效的标识符:1ab(以数字开头)case(Go 语言的关键字)a+b(运算符是不允许的)2. 定义了变量但未使用,会编译不通过,全局变量除外var g_name = 10 //全局变量,定义了可以不使用 func main() { var person_name int // 局部变量定义后未使用,编译会报错。 fmt.Println(person_name) }3. 已经定义过的变量,不能再使用变量定义赋值符(:=)。否则会报“no new variables on left side of := ”的错误,意思是,“左边一个新的变量也没有!”。func main() { var b int = 20 b := 30 fmt.Print(b) }4. 变量定义赋值符(:=)只能在函数内部使用,不能在外部使用。package main import "fmt" b:=4 // 定义赋值操作符,不能在外面使用,外面只能用var定义 //var b=4 func main(){ a :=3 //只能在内部使用这种操作符 fmt.Println(a) }5. struct成员变量不能使用简短操作符(:=),否则提示“ expected, got ':='”type UserInfo struct { UserName string Password string } func main(){ a :=3 //只能在内部使用这种操作符 var u UserInfo u.UserName := "zhangsan" //u.UserName为struct的字段,不能直接使用 :=,需要用预定义实现 fmt.Println(a) }三、其他1. 当函数、结构等标识符以一个大写字母开头,如:GetInfo,那么使用这种形式的标识符的对象就可以被外部包的代码所使用,这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 protected )。// 公有函数,可以被外部包的代码所使用 func Test() { . . . } // 私有函数,包的内部是可见、 func test2() { . . . }2. 不能使用++自增或- -自减运算符初始化变量和对变量赋值 package main import "fmt" func main(){ var a int = 10 var b int = a++ var c int = 20 c = a++ fmt.Print(a, b, c) }3. Go语言使用nil 表示数据为null,判断为空使用 nil 判断。if err != nil { . . return }最后以上就是我刚开始接触Go语言时所遇到的各种坑。作为初学者,遇到坑并不可怕,这些坑都能让你快速成长。你们再学习Go 的过程中遇到了哪些坑呢?欢迎一起交流!
前面已经了 Go 环境的配置和初学Go时,容易遇到的坑。我们知道Go语言和我们以前的Java或是C#哈时候很大差别的。所以在学习Go,首先必须要熟悉Go语言的基础语法。接下来就为初学者大致介绍下Go语言基础语法。 一、Go 程序的基本结构下面是一个Go 程序的基本结构,包含(包声明,引入包,函数等)package main // 定义包名,package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。 import "fmt" // 导入需要使用的包(的函数,或其他元素) func main() { // 程序的入口函数。main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数。 fmt.Println("Hello, World!") } 上面的示例,就是最基本的Go 程序,其中:package 表示程序所在的包名。import 表示导入需要使用的包。main 为程序的入口函数。需要注意的是:同一文件夹下,只能有一个man函数,否则会报错。二、Go语言的数据类型Go提供了int、bool等基本数据类型,也有array、slice、map等高级数据类型。下面就来介绍这些数据类型的用法。1. 基本数据类型Go提供的布尔(bool)、数值类型(int,float)和字符串类型(string)等基本数据类型。用法与其他语言类似。 布尔类型(bool),布尔型的值只可以是常量 true 或者 false。一个简单的例子:var b bool = true。数值类型,又可以分为整数类型、浮点类型、和复数类型。 整数类型包括(byte,rune,int/uint,int8/uint8,int16/uint16,int32/uint32,int64/uint64) 浮点类型包括(float32,float64) 复数类型包括(complex64,complex128)字符串类型,Go的字符串是由单个字节连接起来的。使用UTF-8编码标识Unicode文本。2. 高级数据类型Go语言除了提供以上一些基本数据类型,为了方便开发者,也提供了array、slice、map等高级数据类型。数组(array)切片(slice)字典(map)通道(channel)函数(function)结构体(function)接口(interface)指针(*Xxx,Pointer,uintptr)这些高级数据类型中,array和map与java中的作用比较类似。但是,slice、channel等是Go独有的。后面会详细介绍。除此之外,如果按照底层结构划分可以分为值类型和引用类型。值类型包括(所有基本数据类型,数组,结构体)。引用类型包括(slice,map,channel,function,interface,指针)。三、变量&常量Go 语言的变量名由字母、数字、下划线组成,其中首个字母不能为数字,声明变量的一般形式是使用 var 关键字。例如:var name string1.声明变量a.指定变量类型,声明后若不赋值,使用默认值。var name string name = "李四"b.根据值自行判定变量类型。var name = "李四"上面的示例,Go会根据值判定变量name 的数据类型为string。c.简短形式,省略varage := 10在使用简略形式定义变量时,需要注意以下几点:(:=)是使用变量的首选形式(:=)只能被用在函数体内,而不可以用于全局变量的声明与赋值。(:=)左侧的变量不应该是已经声明过的,否则会导致编译错误。 2.声明常量 Go语言中,常量的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。使用const 关键字定义。例如:const name string = "abc"或者还可以简化的方式定义,与变量的声明类似。具体如下:const name = "abc"我们知道常量的值是不能被修改的。但是,Go语言提供了iota关键字来定义这种可以被修改的常量。使用方式如下:const ( a = iota b = "weiz" c = 9 d = iota )上面的示例中,使用const关键字定义了四个常量。iota在 const关键字中,默认为 0,const 中每新增一行常量声明将使 iota 加1。所以,a的值就为0,d的值为3。我们可以输出a、b、c、d四个常量的值。看看是否如此:iota 看起来挺饶的,使用时只要注意一下几点,多写多用几次就熟悉了:iota表示连续的,无类型的整数常量,以const开始的常量声明语句为单位,从0开始,每赋给一个常量就递增一次一旦跨越以const开始的常量声明语句就归0四、运算符Go 语言提供的运算符有:算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符、其他运算符。接下来让我们来详细看看各个运算符的介绍。 1. 算术运算符算术运算符,包括:+,-,*,/,%,++,--package main import "fmt" func main() { var a int = 20 var b int = 10 var c int c = a + b fmt.Printf("加法运算的值为 %d\n", c ) c = a - b fmt.Printf("减法运算的值为 %d\n", c ) c = a * b fmt.Printf("乘法运算的值为 %d\n", c ) c = a / b fmt.Printf("除法运算的值为 %d\n", c ) c = a % b fmt.Printf("求余运算的值为 %d\n", c ) a++ fmt.Printf("递增运算的值为 %d\n", a ) a=20 a-- fmt.Printf("递减运算的值为 %d\n", a ) }2. 关系运算符关系运算符,返回True或False ,a == b ,包括:==,!=,>,<,>=,<=。package main import "fmt" func main() { var a = 20 var b = 10 fmt.Printf("a == b : %t\n", a == b) fmt.Printf("a != b : %t\n", a != b) fmt.Printf("a > b : %t\n", a > b) fmt.Printf("a < b : %t\n", a < b) a = 5 b = 20 fmt.Printf("a <= b : %t\n", a <= b) fmt.Printf("a >= b : %t\n", a >= b) }3.逻辑运算符逻辑运算符,返回True或False ,包括:&&,||,!。package main import "fmt" func main() { var a bool = true var b bool = false fmt.Printf("a && b 结果: %t\n", a && b) fmt.Printf("a || b 结果: %t\n" ,a || b) fmt.Printf("!b 结果: %t\n" ,!b) }4.位运算符逻辑运算符对整数在内存中的二进制位进行操作,包括: &,|,^,<<和>>。package main import ( "fmt" ) func main() { var a int = 2 /* 2 = 0000 0010 */ var b int = 7 /* 7 = 0000 0111 */ fmt.Printf("a & b 的值为 %d\n", a&b) fmt.Printf("a | b 的值为 %d\n", a|b) fmt.Printf("a ^ b 的值为 %d\n", a^b) fmt.Printf("a << 2 的值为 %d\n", a<<1) fmt.Printf("b >> 2 的值为 %d\n", b>>1) }5. 地址运算符 地址运算符用于返回变量存储地址和定义支持变量。包括:&和*& : 返回变量存储地址 (&originalValue)* :指针变量 (*pointerValue) package main import "fmt" func main() { var a = 4 var ptr *int /* & 和 * 运算符 */ ptr = &a /* 'ptr' 包含了 'a' 变量的地址 */ fmt.Printf("a 的值为 %d\n", a) fmt.Printf("*ptr 为 %d\n", *ptr) }6. 接收运算符接收运算符用于接收通道的数据或者给将数据加入通道。例如:(intChan<-1, <-intChan)。这个后面讲到channel的时候会详细介绍。五、函数Go语言的核心就是函数,它是基本的代码块,用于实现各种业务逻辑。Go 语言标准库同样也提供了非常多的内置的函数。例如,len() 函数可以接受不同类型参数并返回该类型的长度。我们知道,Java或是C#通过public、private 控制函数的可见。但是,Go语言没有public等关键字。它是通过首字母大小写的方式区分的。当函数、结构等标识符以一个大写字母开头,如:GetInfo,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 protected )。1.函数声明Go 语言函数定义格式如下:func function_name( [parameter list] ) [return_types] { 函数体 }函数声明告诉调用者函数的名称,返回类型,和参数。函数定义解析:func:Go语言关键字,声明该代码块为函数。function_name:函数名称。parameter list:参数列表,指定的是参数类型、顺序、及参数个数。参数是可选的。return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types 不是必须的。函数体:函数定义的代码集合。下面是一个具体的函数定义:func add(a int,b int ) int{ return a+b }上面的示例是简单的加法运算,传入两个int参数,返回计算的值。Go语言支持如果所有的参数类型一样,可以使用简单写法:func add(a,b int) int{。这正是Go语言设计者的初衷:最大可能的简化程序,使其更加优雅。2.函数返回多个值Go 语言支持函数返回多个值,即一个函数,同时返回多个返回值。这正是Go语言比其他语言优雅、方便的地方之一。package main import ( "fmt" "strconv" ) func convert(a string, b string) (int, int) { x, _ :=strconv.Atoi(a) y, _ :=strconv.Atoi(b) return x, y } func main() { a, b := convert("5", "2") fmt.Println(a, b) }上面的示例我们可以看看到。如果函数返回多个值,但是我们不需要该值时,可使用“_”代替。最后以上,就把Go语言的基本语法介绍完了。所谓磨刀不误砍柴工,Go语言的语法有些和java类似,但也有很多不一样的地方。希望大家还是能熟悉掌握这些基础知识,这样后面真正学起Go来才会得心应手。
前面我们介绍了Spring Boot 整合 Elasticsearch 实现数据查询检索的功能,在实际项目中,我们的数据一般存储在数据库中,而且随着业务的发送,数据也会随时变化。那么如何保证数据库中的数据与Elasticsearch存储的索引数据保持一致呢? 最原始的方案就是:当数据发生增删改操作时同步更新Elasticsearch。但是这样的设计耦合太高。接下来我们介绍一种非常简单的数据同步方式:Logstash 数据同步。一、Logstash简介1.什么是Logstashlogstash是一个开源的服务器端数据处理工具。简单来说,就是一根具备实时数据传输能力的管道,负责将数据信息从管道的输入端传输到管道的输出端;与此同时这根管道还可以让你根据自己的需求在中间加上滤网,Logstash提供里很多功能强大的滤网以满足你的各种应用场景。Logstash常用于日志系统中做日志采集设备,最常用于ELK中作为日志收集器使用。2.Logstash的架构原理Logstash的基本流程架构:input=》 filter =》 output 。input(输入):采集各种样式,大小和来源数据,从各个服务器中收集数据。常用的有:jdbc、file、syslog、redis等。filter(过滤器)负责数据处理与转换。主要是将event通过output发出之前对其实现某些处理功能。output(输出):将我们过滤出的数据保存到那些数据库和相关存储中,。3.Logstash如何与Elasticsearch数据同步实际项目中,我们不可能通过手动添加的方式将数据插入索引库,所以需要借助第三方工具,将数据库的数据同步到索引库。此时,Logstash出现了,它可以将不同数据库的数据同步到Elasticsearch中。保证数据库与Elasticsearch的数据保持一致。目前支持数据库与ES数据同步的插件有很多,个人认为Logstash是众多同步mysql数据到es的插件中,最稳定并且最容易配置的一个。二、安装LogstashLogstash的使用方法也很简单,下面讲解一下,Logstash是如何使用的。需要说明的是:这里以windows 环境为例,演示Logstash的安装和配置。1.下载Logstash首先,下载对应版本的Logstash包,可以通过上面提供下载elasticsearch的地址进行下载,完成后解压。上面是Logstash解压后的目录,我们需要关注是bin目录中的执行文件和config中的配置文件。一般生产情况下,会使用Linux服务器,并且会将Logstash配置成自启动的服务。这里测试的话,直接启动。2.配置Logstash接下来,配置Logstash。需要我们编写配置文件,根据官网和网上提供的配置文件,将其进行修改。第一步:在Logstash根目录下创建mysql文件夹,添加mysql.conf配置文件,配置Logstash需要的相应信息,具体配置如下:input { stdin { } jdbc { # mysql数据库连接 jdbc_connection_string => "jdbc:mysql://localhost:3306/book_test?characterEncoding=utf-8&useSSL=false&serverTimezone=UTC" # mysqly用户名和密码 jdbc_user => "root" jdbc_password => "root" # 驱动配置 jdbc_driver_library => "C:\Users\Administrator\Desktop\logstash-7.5.1\mysql\mysql-connector-java-8.0.20.jar" # 驱动类名 jdbc_driver_class => "com.mysql.cj.jdbc.Driver" #jdbc_paging_enabled => "true" #jdbc_page_size => "50000" jdbc_default_timezone => "Asia/Shanghai" # 执行指定的sql文件 statement_filepath => "C:\Users\Administrator\Desktop\logstash-7.5.1\mysql\sql\bookquery.sql" use_column_value => true # 是否将字段名转换为小写,默认true(如果有数据序列化、反序列化需求,建议改为false); lowercase_column_names => false # 需要记录的字段,用于增量同步,需是数据库字段 tracking_column => updatetime # Value can be any of: numeric,timestamp,Default value is "numeric" tracking_column_type => timestamp # record_last_run上次数据存放位置; record_last_run => true #上一个sql_last_value值的存放文件路径, 必须要在文件中指定字段的初始值 last_run_metadata_path => "C:\Users\Administrator\Desktop\logstash-7.5.1\mysql\sql\logstash_default_last_time.log" # 是否清除last_run_metadata_path的记录,需要增量同步时此字段必须为false; clean_run => false # 设置监听 各字段含义 分 时 天 月 年 ,默认全部为*代表含义:每分钟都更新 schedule => "* * * * *" # 索引类型 type => "id" } } output { elasticsearch { #es服务器 hosts => ["10.2.1.231:9200"] #ES索引名称 index => "book" #自增ID document_id => "%{id}" } stdout { codec => json_lines } }第二步:将mysql-connector-java.jar 拷贝到前面配置的目录下。上面的mysql.conf配置的是:C:\Users\Administrator\Desktop\logstash-7.5.1\mysql\mysql-connector-java-8.0.20.jar。那么jar包拷贝到此目录下即可:上面是mysql的驱动,如果是sqlserver数据库,下载SqlServer对应的驱动即可。放置的位置要与mysql.conf 配置文件中的jdbc_driver_library 地址保持一致。第三步:创建sql目录,创建bookquery.sql文件用于保存需要执行的sql 脚本。示例代码如下:select * from book where updatetime >= :sql_last_value order by updatetime desc这里使用的增量更新,所以使用:sql_last_value 记录上一次记录的最后时间。3.启动Logstash进入logstash的bin目录,执行如下命令:logstash.bat -f C:\Users\Administrator\Desktop\logstash-7.5.1\mysql\mysql.conf启动成功之后,Logstash就会自动定时将数据写入到Elasticsearch。如下图所示:同步完成后,我们使用Postman查询Elasticsearch,验证索引是否都创建成功。在postman中,发送 Get 请求:http://10.2.1.231:9200/book/_search 。返回结果如下图所示:可以看到,数据库中的数据已经通过Logstash同步至Elasticsearch。说明Logstash配置成功。三、创建查询服务数据同步完成后,接下来我们使用Spring Boot 构建Elasticsearch查询服务。首先创建Spring Boot项目并整合Elasticsearch,这个之前都已经介绍过,不清楚的朋友可以看我之前的文章。接下来演示如何封装完整的数据查询服务。1.数据实体@Document( indexName = "book" , replicas = 0) public class Book { @Id private Long id; @Field(analyzer = "ik_max_word",type = FieldType.Text) private String bookName; @Field(analyzer = "ik_max_word",type = FieldType.Text) private String author; private float price; private int page; @Field(type = FieldType.Date,format = DateFormat.custom,pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") private Date createTime; @Field(type = FieldType.Date,format = DateFormat.custom,pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") private Date updateTime; @Field(analyzer = "ik_max_word",type = FieldType.Text) private String category; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getBookName() { return bookName; } public void setBookName(String bookName) { this.bookName = bookName; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } public float getPrice() { return price; } public void setPrice(float price) { this.price = price; } public int getPage() { return page; } public void setPage(int page) { this.page = page; } public String getCategory() { return category; } public void setCategory(String category) { this.category = category; } public Book(){ } public Date getCreateTime() { return createTime; } public void setCreateTime(Date createTime) { this.createTime = createTime; } public Date getUpdateTime() { return updateTime; } public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } }2.请求封装类public class BookQuery { public String category; public String bookName; public String author; public int priceMin; public int priceMax; public int pageMin; public int pageMax; public String sort; public String sortType; public int page; public int limit; }3.创建Controller控制器@RestController public class ElasticSearchController { @Autowired private ElasticsearchRestTemplate elasticsearchRestTemplate; /** * 查询信息 * @param * @return */ @PostMapping(value = "/book/query") public JSONResult query(@RequestBody BookQuery bookQuery){ Query query= getQueryBuilder(bookQuery); SearchHits<Book> searchHits = elasticsearchRestTemplate.search(query, Book.class); List<SearchHit<Book>> result = searchHits.getSearchHits(); return JSONResult.ok(result); } public Query getQueryBuilder(BookQuery query) { BoolQueryBuilder builder = boolQuery(); // 匹配器 模糊查询部分,分析器使用ik (ik_max_word) List<QueryBuilder> must = builder.must(); if (query.getBookName()!=null && !query.getBookName().isEmpty()) must.add(wildcardQuery("bookName", "*" +query.getBookName()+ "*")); if (query.getCategory()!=null && !query.getCategory().isEmpty()) must.add(wildcardQuery("category", "*" +query.getCategory()+ "*")); if (query.getAuthor()!=null && !query.getAuthor().isEmpty()) must.add(wildcardQuery("author", "*" +query.getAuthor()+ "*")); // 筛选器 精确查询部分 List<QueryBuilder> filter = builder.filter(); // 范围查询 if (query.getPriceMin()>0 && query.getPriceMax()>0) { RangeQueryBuilder price = rangeQuery("price").gte(query.getPriceMin()).lte(query.getPriceMax()); filter.add(price); } // 范围查询 if (query.getPageMin()>0 && query.getPageMax()>0) { RangeQueryBuilder page = rangeQuery("page").gte(query.getPageMin()).lte(query.getPageMax()); filter.add(page); } // 分页 PageRequest pageable = PageRequest.of(query.getPage() - 1, query.getLimit()); // 排序 SortBuilder sort = SortBuilders.fieldSort("price").order(SortOrder.DESC); //设置高亮效果 String preTag = "<font color='#dd4b39'>";//google的色值 String postTag = "</font>"; HighlightBuilder.Field highlightFields = new HighlightBuilder.Field("category").preTags(preTag).postTags(postTag); Query searchQuery = new NativeSearchQueryBuilder() .withQuery(builder) .withHighlightFields(highlightFields) .withPageable(pageable) .withSort(sort) .build(); return searchQuery; } }4.测试验证启动项目,在Postman中,请求http://localhost:8080/book/query 接口查询书籍信息数据。查看接口返回情况。我们看到接口成功返回数据。说明数据查询服务创建成功。 最后以上,我们就把使用Spring Boot + Elasticsearch + Logstash 实现完整的数据查询检索服务介绍完了。
默认情况下,Spring Boot定时任务是按单线程方式执行的,也就是说,如果同一时刻有两个定时任务需要执行,那么只能在一个定时任务完成之后再执行下一个。如果只有一个定时任务,这样做肯定没问题;当定时任务增多时,如果一个任务被阻塞,则会导致其他任务无法正常执行。要解决这个问题,需要配置任务调度线程池。一、实现多线程定时任务下面通过示例演示Spring Boot 实现多线程定时任务。1. 增加多线程配置类在config目录下增加SchedulerConfig配置类,代码如下:public class SchedulerConfig { @Bean public Executor taskScheduler() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(3); executor.setMaxPoolSize(10); executor.setQueueCapacity(3); executor.initialize(); return executor; } }设置执行线程池为3,最大线程数为10。2. 修改SchedulerTask定时任务修改之前定义的SchedulerTask定时任务的类,在方法上增加@Async注解,使得后台任务能够异步执行,代码如下:@EnableAsync // 开启异步事件的支持 @Component public class SchedulerTask { private static final Logger logger = LoggerFactory.getLogger(SchedulerTask.class); @Async @Scheduled(cron="*/10 * * * * ?") public void taskCron() { SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss"); logger.info("SchedulerTask taskCron 现在时间: " + dateFormat.format(new Date())); } @Async @Scheduled(fixedRate = 5000) public void taskFixed() { SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss"); logger.info("SchedulerTask taskFixed 现在时间: " + dateFormat.format(new Date())); } }在上面的示例中,定时任务类SechedulerTask增加了@EnableAsync注解,开启了异步事件支持。同时,在定时方法上增加@Async注解,使任务能够异步执行,这样各个后台任务就不会阻塞。二、测试验证配置修改完成后,重新启动项目,查看后台任务的运行情况。如图10-2所示,全部的后台任务分成了多个线程执行,这样任务之间不会相互影响。通过后台日志可以看到,Spring Boot启动线程池负责调度执行后台任务,各个后台任务之间相对独立、互不影响。最后以上,我们就把Spring Boot实现多线程定时任务介绍完了。
前面介绍了Spring Boot 使用JWT实现Token验证,其实Spring Boot 有完整的安全认证框架:Spring Security。接下来我们介绍如何集成Security 实现安全验证。一、Security简介安全对于企业来说至关重要,必要的安全认证为企业阻挡了外部非正常的访问,保证了企业内部数据的安全。当前,数据安全问题越来越受到行业内公司的重视。数据泄漏很大一部分原因是非正常权限访问导致的,于是使用合适的安全框架保护企业服务的安全变得非常紧迫。在Java领域,Spring Security无疑是最佳选择之一。Spring Security 是 Spring 家族中的一个安全管理框架,能够基于 Spring 的企业应用系统提供声明式的安全访问控制解决方案。它提供了一组可以在Spring应用系统中灵活配置的组件,充分利用了 Spring的IoC、DI和AOP等特性,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。二、Spring Boot对Security的支持虽然,在Spring Boot出现之前,Spring Security已经发展多年,但是使用并不广泛。安全管理这个领域一直是Shiro的天下,因为相对于Shiro,在项目中集成Spring Security还是一件麻烦的事情,所以Spring Security虽然比Shiro强大,但是却没有Shiro受欢迎。随着Spring Boot的出现,Spring Boot 对Spring Security 提供了自动化配置方案,可以零配置使用 Spring Security。这使得Spring Security重新焕发新的活力。Spring Boot 提供了集成 Spring Security 的组件包 spring-boot-starter-security,方便我们在 Spring Boot 项目中使用 Spring Security进行权限控制。三、集成Security在Spring Boot 项目中集成Spring Boot Security 非常简单,只需在项目中增加Spring Boot Security的依赖即可。下面通过示例演示Spring Boot中基础Security的登录验证。1. 添加依赖Spring Boot 提供了集成 Spring Security 的组件包 spring-boot-starter-security,方便我们在 Spring Boot 项目中使用 Spring Security。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>上面除了引入Security组件外,因为我们要做Web系统的权限验证,所以还添加了Web和Thymeleaf组件。2. 配置登录用户名和密码用户名和密码在application.properties中进行配置。# security spring.security.user.name=admin spring.security.user.password=admin在application.properties配置文件中增加了管理员的用户名和密码。3. 添加Controller创建SecurityController 类,在类中添加访问页面的入口。@Controller public class SecurityController { @RequestMapping("/") public String index() { return "index"; } }4. 创建前端页面在resources/templates 目录下创建页面 index.html,这个页面就是具体的需要增加权限控制的页面,只有登录了才能进入此页。<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <body> <h1>Hello</h1> <p>我是登录后才可以看的页面</p> </body> </html>5. 测试验证配置完成后,重启项目,访问地址:http://localhost:8080/,页面会自动弹出一个登录框,如下图所示。系统自动跳转到Spring Security默认的登录页面,输入之前配置的用户名和密码就可以登录系统,登录后的页面如下图所示。 通过上面的示例,我们看到Spring Security自动给所有访问请求做了登录保护,实现了页面权限控制。四、登录验证前面演示了在Spring Boot项目中集成Spring Security 实现简单的登录验证功能,在实际项目使用过程中,可能有的功能页面不需要进行登录验证,而有的功能页面只有进行登录验证才能访问。下面通过完整的示例程序演示如何实现Security的登录认证。 1. 创建页面content.html先创建页面content.html,此页面只有登录用户才可查看,否则会跳转到登录页面,登录成功后才能访问。示例代码如下:<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <body> <h1>content</h1> <p>我是登录后才可以看的页面</p> <form method="post" action="/logout"> <button type="submit">退出</button> </form> </body> </html>在上面的示例中,我们看到退出使用post请求,因为Security退出请求默认只支持 post 。2. 修改index.html 页面修改之前的index.html页面,增加登录按钮。<p>点击 <a th:href="@{/content}">这里</a>进入管理页面</p>在上面的示例中,index页面属于公共页面,无权限验证,从index页面进入content页面时需要登录验证。3. 修改Controller控制器修改之前的SecurityController控制器,增加content页面路由地址,示例代码如下:@RequestMapping("/") public String index() { return "index"; } @RequestMapping("/content") public String content() { return "content"; }4. 创建 SecurityConfig 类创建 Security的配置文件SecurityConfig类,它继承于 WebSecurityConfigurerAdapter,现自定义权限验证配置。示例代码如下:@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/", "/home").permitAll() .anyRequest().authenticated() .and() .formLogin() .permitAll() .and() .logout() .permitAll() .and() .csrf() .ignoringAntMatchers("/logout"); } }在上面的示例程序中,SecurityConfig类中配置 index.html 可以直接访问,但 content.html 需要登录后才可以查看,没有登录自动跳转到登录页面。@EnableWebSecurity:开启 Spring Security 权限控制和认证功能。antMatchers("/", "/home").permitAll():配置不用登录可以访问的请求。anyRequest().authenticated():表示其他的请求都必须有权限认证。formLogin():定制登录信息。loginPage("/login"):自定义登录地址,若注释掉,则使用默认登录页面。logout():退出功能,Spring Security自动监控了/logout。ignoringAntMatchers("/logout"):Spring Security 默认启用了同源请求控制,在这里选择忽略退出请求的同源限制。5. 测试验证修改完成之后重启项目,访问地址http://localhost:8080/可以看到 index 页面的内容,单击链接跳转到content页面时会自动跳转到登录页面,登录成功后才会自动跳转到http://localhost:8080/content,在 content 页面单击“退出”按钮,会退出登录状态,跳转到登录页面并提示已经退出。登录、退出、请求受限页面退出后跳转到登录页面是常用的安全控制案例,也是账户系统基本的安全保障。最后以上,我们就把Spring Boot如何集成Security实现安全认证介绍完了。推荐阅读:SpringBoot从入门到精通(三十四)如何集成JWT实现Token验证实战:使用Spring Boot Admin实现运维监控平台Druid数据库连接 | Spring Boot 集成 Druid实现数据库连接和完善的SQL执行监控非常简单 | 使用Actuator 从零开始搭建Spring Boot 应用监控系统极简教程 | Spring Boot整合Elasticsearch实现数据搜索引擎
近年来,随着前后端分离、微服务等架构的兴起,传统的cookie+session身份验证模式已经逐渐被基于Token的身份验证模式取代。接下来介绍如何在Spring Boot项目中集成JWT实现Token验证。一、JWT入门1.什么是JWTJWT (Json web token)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519)。它定义了一种紧凑的,自包含的方式,用于通信双方之间以JSON对象的形式安全传递信息。JWT使用HMAC算法或者是RSA的公私秘钥的数字签名技术,所以这些信息是可被验证和信任的。JWT官网: https://jwt.io/JWT(Java版)的github地址:https://github.com/jwtk/jjwt2.JWT的结构在使用 JWT 前,需要先了解它的组成结构。它是由以下三段信息构成的:Header 头部(包含签名和/或加密算法的类型)Payload 载荷 (存放有效信息)Signature 签名/签证将这三段信息文本用‘.’连接一起就构成完整的JWT字符串,也是就我们需要的Token。如下所示:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0aW1lU3RhbXAiOjE2MzkwNDc1NTMxNjksInVzZXJSb2xlIjoiUk9MRV9BRE1JTiIsInVzZXJJZCI6ImFkbWluIn0.UFQLvaiQ1AThx9Fa4SRqNg-b9HPJ9y1TlgQB4-F3pi0JWT的数据结构还是比较复杂的,Header,Payload,Signature中包含了很多信息,建议大家最好是能够了解。3.JWT的请求流程JWT的请求流程也特别简单,首先使用账号登录获取Token,然后后面的各种请求,都带上这个Token即可。具体流程如下:1. 客户端发起登录请求,传入账号密码;2. 服务端使用私钥创建一个Token;3. 服务器返回Token给客户端;4. 客户端向服务端发送请求,在请求头中该Token;5. 服务器验证该Token;6. 返回结果。二、Spring Boot 如何集成JWTJWT提供了基于Java组件:java-jwt帮助我们在Spring Boot项目中快速集成JWT,接下来进行SpringBoot和JWT的集成。1.引入JWT依赖创建普通的Spring Boot项目,修改项目中的pom.xml文件,引入JWT等依赖。示例代码如下:<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.10.3</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>2.创建&验证Token创建通用的处理类TokenUtil,负责创建和验证Token。示例代码如下:@Component public class TokenUtil { @Value("${token.secretKey}") private String secretKey; /** * 加密token. */ public String getToken(String userId, String userRole) { //这个是放到负载payLoad 里面,魔法值可以使用常量类进行封装. String token = JWT .create() .withClaim("userId" ,userId) .withClaim("userRole", userRole) .withClaim("timeStamp", System.currentTimeMillis()) .sign(Algorithm.HMAC256(secretKey)); return token; } /** * 解析token. * { * "userId": "weizhong", * "userRole": "ROLE_ADMIN", * "timeStamp": "134143214" * } */ public Map<String, String> parseToken(String token) { HashMap<String, String> map = new HashMap<String, String>(); DecodedJWT decodedjwt = JWT.require(Algorithm.HMAC256(secretKey)) .build().verify(token); Claim userId = decodedjwt.getClaim("userId"); Claim userRole = decodedjwt.getClaim("userRole"); Claim timeStamp = decodedjwt.getClaim("timeStamp"); map.put("userId", userId.asString()); map.put("userRole", userRole.asString()); map.put("timeStamp", timeStamp.asLong().toString()); return map; } }3.创建拦截器,验证Token创建一个拦截器AuthHandlerInterceptor,负责拦截所有Http请求,验证Token是否有效。示例代码如下:@Slf4j @Component public class AuthHandlerInterceptor implements HandlerInterceptor { @Autowired TokenUtil tokenUtil; @Value("${token.refreshTime}") private Long refreshTime; @Value("${token.expiresTime}") private Long expiresTime; /** * 权限认证的拦截操作. */ @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception { log.info("=======进入拦截器========"); // 如果不是映射到方法直接通过,可以访问资源. if (!(object instanceof HandlerMethod)) { return true; } //为空就返回错误 String token = httpServletRequest.getHeader("token"); if (null == token || "".equals(token.trim())) { return false; } log.info("==============token:" + token); Map<String, String> map = tokenUtil.parseToken(token); String userId = map.get("userId"); String userRole = map.get("userRole"); long timeOfUse = System.currentTimeMillis() - Long.parseLong(map.get("timeStamp")); //1.判断 token 是否过期 if (timeOfUse < refreshTime) { log.info("token验证成功"); return true; } //超过token刷新时间,刷新 token else if (timeOfUse >= refreshTime && timeOfUse < expiresTime) { httpServletResponse.setHeader("token",tokenUtil.getToken(userId,userRole)); log.info("token刷新成功"); return true; } //token过期就返回 token 无效. else { throw new TokenAuthExpiredException(); } } }拦截器创建之后,需要将拦截器注册到Spring Boot中。这和其他的拦截器注册是一样的。示例代码如下:@Configuration public class AuthWebMvcConfigurer implements WebMvcConfigurer { @Autowired AuthHandlerInterceptor authHandlerInterceptor; /** * 给除了 /login 的接口都配置拦截器,拦截转向到 authHandlerInterceptor */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authHandlerInterceptor) .addPathPatterns("/**") .excludePathPatterns("/login"); } }4.创建控制器创建TokenTestController控制器,处理HTTP请求。示例代码如下:@RestController public class TokenTestController { @Autowired TokenUtil tokenUtil; @PostMapping("/login") public String login(@RequestBody LoginUser user){ // 先验证用户的账号密码,账号密码验证通过之后,生成Token String role = "ROLE_ADMIN"; String token = tokenUtil.getToken(user.username,role); return token; } @PostMapping("/testToken") public String testToken(HttpServletRequest request){ String token = request.getHeader("token"); tokenUtil.parseToken(token); return "请求成功"; } }5.测试验证集成JWT成功之后,接下来验证Token是否成功,启动项目。在Postman中调用相关接口,验证功能是否正常。首先,调用http://localhost:8080/testToken,获取token然后,调用http://localhost:8080/testToken 验证token是否有效。最后以上,我们就把Spring Boot集成JWT实现Token验证介绍完了。身份验证是Web开发中非常基础的功能,后面还会介绍授权及权限管理等内容。
我们知道,使用Actuator可以收集应用系统的健康状态、内存、线程、堆栈、配置等信息,比较全面地监控了Spring Boot应用的整个生命周期。但是还有一个问题:如何呈现这些采集到的应用监控数据、性能数据呢?在这样的背景下,就诞生了另一个开源软件Spring Boot Admin。下面就来介绍什么是Spring Boot Admin以及如何使用Spring Boot Admin搭建完整的运维监控平台。一、什么是Spring Boot AdminSpring Boot Admin是一个管理和监控Spring Boot应用程序的开源项目,在对单一应用服务监控的同时也提供了集群监控方案,支持通过eureka、consul、zookeeper等注册中心的方式实现多服务监控与管理。Spring Boot Admin UI部分使用Vue JS将数据展示在前端。Spring Boot Admin分为服务端(spring-boot-admin-server)和客户端(spring-boot-admin-client)两个组件:spring-boot-admin-server通过采集actuator端点数据显示在spring-boot-admin-ui上,已知的端点几乎都有进行采集。spring-boot-admin-client是对Actuator的封装,提供应用系统的性能监控数据。此外,还可以通过spring-boot-admin动态切换日志级别、导出日志、导出heapdump、监控各项性能指标等。Spring Boot Admin服务器端负责收集各个客户的数据。各台客户端配置服务器地址,启动后注册到服务器。服务器不停地请求客户端的信息(通过Actuator接口)。具体架构如下图所示。上图为Spring Boot Admin的整体架构,在每个Spring Boot应用程序上增加Spring Boot Admin Client组件。这样每个Spring Boot应用即Admin客户端,Admin服务端通过请求Admin客户端的接口收集所有的Spring Boot应用信息并进行数据呈现,从而实现Spring Boot应用监控。二、使用Spring Boot Admin搭建运维监控平台下面就通过示例,演示如何使用Spring Boot Admin 搭建运维监控平台。1、创建服务器端Spring Boot Admin服务器端主要负责收集各个客户的数据。建立一个Spring Boot Admin服务器端只需要简单的两步。下面通过示例演示创建Spring Boot Admin服务器端的过程。1. 配置依赖创建新的Spring Boot项目,在新建的项目中添加Spring Boot Admin服务器端的依赖JAR包:spring-boot-admin-starter-server。<dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-server</artifactId> <version>2.1.3</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>添加spring-boot-starter-web是为了让应用处于启动状态。2. 配置启动端口修改系统配置文件application.properties,配置服务端的启动端口为8000:server.port=80003. 启用Admin服务器使用@EnableAdminServer注解启动Admin服务器,示例代码如下:@SpringBootApplication // 启用Admin服务器 @EnableAdminServer public class AdminServerApplication { public static void main(String[] args) { SpringApplication.run(AdminServerApplication.class, args); } }4. 运行测试完成以上3步之后,启动服务器端,在浏览器中访问http://localhost:8000,可以看到如下所示的界面。从Admin服务端的启动界面可以看到,Applications页面会展示应用数量、实例数量和状态3个信息。这里由于没有启动客户端,因此显示出“No applications registered.”的信息。2、创建客户端接下来我们创建一个客户端并注册到服务器端。1. 配置依赖创建新的Spring Boot项目,在新建的项目中添加Spring Boot Admin客户端的依赖JAR包:spring-boot-admin-starter-server。<dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-client</artifactId> <version>2.1.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>spring-boot-admin-starter-client会自动添加Actuator相关依赖,所以这里不需要重复添加Actuator的相关依赖。2. 配置客户端修改application.properties配置文件,增加如下配置:server.port=8001 spring.application.name=Admin Client spring.boot.admin.client.url=http://localhost:8000 management.endpoints.web.exposure.include=*相关配置说明如下:server.port:当前应用设置端口为8001。spring.application.name:设置Application名称,其默认名称都是spring-boot-application。spring.boot.admin.client.url:配置Admin服务器的地址。management.endpoints.web.exposure.include=*:打开客户端Actuator的监控。3. 运行验证配置完成后启动客户端,客户端会自动注册到Admin服务器,Admin服务器检查到客户端的变化并展示其应用信息。重新刷新地址http://localhost:8000后,可以看到如图15-7所示的页面。客户端启动之后,Admin服务器界面的Application数量会增加。单击Application下的数值可以查看完整的应用信息。页面会展示被监控的应用列表,单击应用名称会进入此应用的详细监控信息页面。这个页面会实时显示应用的运行监控信息,包括之前介绍的Actuator所有的端点数据信息。Spring Boot Admin以图形化的形式展示了应用的各项信息,这些信息大多来自于Spring Boot Actuator提供的接口。利用图形化的形式很容易看到应用的各项参数变化,甚至有些页面还可以进行一些配置操作,比如改变打印日志的级别等。三、告警提醒功能虽然Spring Boot Admin提供了强大的监控功能,但它不能存储历史数据,我们不可能一直盯着系统,为此,Spring Boot Admin提供了强大的提醒功能,能够在发生服务状态变更的时候发出告警。支持的Email等提醒功能,同时也支持自定义告警提醒。下面就来介绍Spring Boot Admin的告警提醒功能。1、邮件提醒设置Spring Boot Admin的邮件提醒,需要用到Spring Boot的邮件组件:spring-boot-starter-mail。这里只展示邮件提醒功能的使用。配置依赖修改前面的服务器端,在pom.xml 文件中,增加邮件组件,示例代码如下: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency>修改系统配置修改application.properties 系统配置文件,增加邮件发送配置,和告警通知配置,示例代码如下:# 邮件服务配置 spring.mail.host=smtp.163.com spring.mail.username=18618243664@163.com spring.mail.password=#邮箱授权码 spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.ssl.enable=true #告警接收 spring.boot.admin.notify.mail.enabled=true spring.boot.admin.notify.mail.to=417114764@qq.com spring.boot.admin.notify.mail.from=18618243664@163.com上面,我们配置了Spring Boot 发送邮件的相关配置,这个是通用的。然后配置了告警消息的接收地址。验证测试我们再次启动服务端和客户端,然后停止客户端,模拟应用宕机的情况。这样Spring Boot Admin 就会发送告警邮件提醒。2、自定义告警提醒除了邮件提醒之外,通常我们还需要其他的提醒方式,比如:短信,日志等。我们可以通过自定义的方式实现自定义的消息告警方式。Spring Boot Admin 实现自定义告警提醒也非常简单,只要实现Notifier接口即可。具体实现方式:继承AbstractEventNotifier 或AbstractStatusChangeNotifier这两个类。然后重写doNotify中实现具体的业务逻辑。下面通过示例演示自定义告警提醒功能:首先,创建AppStatusNotifier类,实现告警提醒功能,示例代码如下:** * 自定义的事件通知者 * @author weiz * */ @Service public class AppStatusNotifier extends AbstractEventNotifier { private static final Logger LOGGER = LoggerFactory.getLogger(LoggingNotifier.class); public AppStatusNotifier(InstanceRepository repository) { super(repository); } @Override protected Mono<Void> doNotify(InstanceEvent event, Instance instance) { return Mono.fromRunnable(() -> { if (event instanceof InstanceStatusChangedEvent) { LOGGER.info("Instance {} ({}) is {}", instance.getRegistration().getName(), event.getInstance(), ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus()); } else { LOGGER.info("Instance {} ({}) {}", instance.getRegistration().getName(), event.getInstance(), event.getType()); } }); } }然后,再次运行服务端和客户端。启动成功之后,再关掉客户端,模拟应用宕机的情况。我们看到服务端后台日志显示,服务端已经收到了客户端状态改变的告警消息。客户端状态已经变为OFFLINE。最后以上,就将Spring Boot Admin搭建运维监控平台介绍完毕,
我们知道,应用系统最频繁,最主要的操作还是数据库的操作,所以数据库的性能和安全对于整个系统平台的重要性不言而喻。为了提高数据库性能,我们可以使用数据库连接池,有时候我们需要增加一些列的日志或是数据库性能监控工具来确保数据库的性能,同时还得防范数据库的SQL注入等安全问题。所以,今天我们来介绍一款集数据库连接池、数据库监控、SQL执行日志于一身的神器:Druid。一、Druid简介Druid 是阿里巴巴开源平台上的一个数据库连接池项目,它结合了 C3P0、DBCP 等数据库池的优点,同时加入了SQL日志和SQL性能监控的功能。可以很好的监控数据库池连接和 SQL 的执行情况,可以说是针对监控而生的数据库连接池框架。Druid 是目前比较流行的高性能的,分布式列存储的OLAP框架(具体来说是MOLAP)。它有如下几个特点:1、亚秒级查询:Druid提供了快速的聚合能力以及亚秒级的OLAP查询能力,多租户的设计,是面向用户分析应用的理想方式。2、实时数据注入:Druid支持流数据的注入,并提供了数据的事件驱动,保证在实时和离线环境下事件的实效性和统一性3、可扩展的PB级存储:Druid集群可以很方便的扩容到PB的数据量,每秒百万级别的数据注入。即便在加大数据规模的情况下,也能保证时其效性4、多环境部署:Druid既可以运行在商业的硬件上,也可以运行在云上。它可以从多种数据系统中注入数据,包括hadoop,spark,kafka,storm和samza等目前 Druid 已经在阿里巴巴部署了超过600个应用,经过生产环境大规模部署的严苛考验。二、Druid对Spring Boot的支持Driud 同样对Spring Boot 提供了支持。为Spring Boot项目提供了druid-spring-boot-starter组件,可以帮助我们在Spring Boot项目中轻松集成Druid数据库连接池和监控。Github地址:https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter我们知道 Spring Boot 2.0 以上默认使用 Hikari 数据源,可以说 Hikari 与 Driud 是当前 Java Web 开发中最优秀的数据源。三、Spring Boot集成DruidDruid 提供的druid-spring-boot-starter组件可以帮助我们在Spring Boot 项目中轻松集成Druid。下面通过示例演示如何在Spring Boot 项目中集成Druid。1、引入依赖包修改pom.xml 文件,引入druid、jdbc等依赖包,具体如下所示: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- SPRINGBOOT DRUID --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <!-- SPRINGBOOT JDBC --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- MYSQL --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>上面的示例中,我们引入了druid、jdbc、mysql-connector等依赖组件,其中druid的组件包不是Spring Boot 提供,所以版本号与Spring Boot不一致,我们需要单独添加对应的版本号:1.1.10。2、修改配置文件接下来,修改application.properties 配置文件,配置数据库连接,Druid等相关配置。具体如下所示: spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.url=jdbc:mysql://localhost:3306/druid_test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true spring.datasource.username=root spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # 初始化最大、最⼩、最连接数 spring.datasource.druid.initial-size=3 spring.datasource.druid.min-idle=3 spring.datasource.druid.max-active=10 # 配置获取连接等待超时的时间 spring.datasource.druid.max-wait=60000 # 监控后台账号和密码 spring.datasource.druid.stat-view-servlet.login-username=admin spring.datasource.druid.stat-view-servlet.login-password=123456 # 配置 StatFilter spring.datasource.druid.filter.stat.log-slow-sql=true spring.datasource.druid.filter.stat.slow-sql-millis=2000上面的示例,我们配置了Mysql数据库连接,已经Druid的基础配置和后台监控的账号密码。通过此账号密码,即可登录Druid后台,查看SQL的执行情况。3、运行验证前面这两步,我们就把druid集成到Spring Boot项目中了。启动项目访问地址: http://localhost:8080/druid,就会出现 Druid 监控后台的登录页面,输入前面配置的账户和密码后,就会进入首页。通过上图可以看到,Druid展示了Spring Boot 项目中使用的 JDK 版本、数据库驱动、 JVM 等相关统计信息,同时还有,数据源、 SQL 监控、 SQL 防火墙、 URI 监控、Session监控等诸多监控功能。从这里也可以看出 Druid 的功能非常强大。四、Druid+jdbcTemplate实现数据库操作前面我们在Spring Boot项目中集成了Druid, 操作非常的简单,只需要添加依赖,简单配置即可实现。接下来我们通过Druid+jdbcTemplate实现数据库操作,演示Druid 是如何监控SQL执行的。1、创建数据库及表首先,创建druid_test数据库和student 表。具体脚本如下:CREATE TABLE `student` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id', `name` varchar(32) DEFAULT NULL COMMENT '姓名', `sex` int(11) DEFAULT NULL, `age` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of student -- ---------------------------- INSERT INTO `student` VALUES ('3', 'zhangsan', '1', '20'); INSERT INTO `student` VALUES ('5', 'weiz多数据源', '0', '30'); INSERT INTO `student` VALUES ('6', 'weiz', '1', '30'); INSERT INTO `student` VALUES ('7', 'weiz2', '1', '30'); INSERT INTO `student` VALUES ('10', '李四', '0', '18'); INSERT INTO `student` VALUES ('11', 'weiz11', '1', '23');2、创建Student实体类接下来,创建student表对应的Student实体类,示例代码如下:public class Student { private Long id; private String name; private int sex; private int age; public Student(){ } public Student(String name, int sex, int age) { this.name = name; this.sex = sex; this.age = age; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getSex() { return sex; } public void setSex(int sex) { this.sex = sex; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }3、创建Service 及Impl实现创建数据库操作的方法StudentRepository及实现StudentRepositoryImpl。具体示例代码如下:// StudentRepository public interface StudentRepository { int save(Student user); int update(Student user); int delete(long id); Student findById(long id); } @Repository public class StudentRepositoryImpl implements StudentRepository { @Autowired private JdbcTemplate jdbcTemplate; @Override public int save(Student user) { return jdbcTemplate.update("INSERT INTO Student(name, sex, age) values(?, ?, ?)", user.getName(), user.getSex(), user.getAge()); } @Override public int update(Student user) { return jdbcTemplate.update("UPDATE Student SET name = ? , sex = ? , age = ? WHERE id=?",user.getName(), user.getSex(), user.getAge(), user.getId()); } @Override public int delete(long id) { return jdbcTemplate.update("DELETE FROM Student where id = ? ",id); } @Override public Student findById(long id) { return jdbcTemplate.queryForObject("SELECT * FROM Student WHERE id=?", new Object[] { id }, new BeanPropertyRowMapper<Student>(Student.class)); } } 4、创建Controller调用最后,创建StudentController,并调用相关的数据操作方法。示例代码如下:@RestController @RequestMapping("/student") public class StudentController { @Autowired StudentRepository studentRepository; @RequestMapping("/findById/{id}") public Student findById(@PathVariable Long id){ return studentRepository.findById(id); } }5、运行验证接下来,启动项目,验证jdbcTemplate 数据操作是否成功。访问地址: http://localhost:8080/student/findById/3, 查询学生信息。我们看到,后台成功返回了该学生的相关信息,接下来,我们在Druid中查看SQL的执行情况。通过http://localhost:8080/druid 进入监控后台,查看SQL的执行情况,具体如下图所示:如上图所示,Druid的 SQL 监控会将项目中执行的所有SQL 打印出来,展示 SQL执行了多少次、每次返回多少数据、执行的时间分布是什么。这些功能非常的实用,方便我们在实际生产中查找出慢 SQL,最后对 SQL 进行调优。最后以上,我们介绍了如何在Spring Boot 中集成Druid 实现数据库连接和监控的功能。然后通过Druid + jdbcTemplate 实现完整的数据操作。
我们知道Spring Boot 提供了Actuator组件,方便我们对应用程序进行监控和维护。接下来,就来介绍Actuator到底是什么? 如何在Spring Boot项目中快速集成Actuator?一、Actuator简介1.Actuator是什么?Actuator是Spring Boot提供的应用系统监控的开源框架,它是Spring Boot体系中非常重要的组件。它可以轻松实现应用程序的监控治理。支持通过众多 REST接口、远程Shell和JMX收集应用的运行情况。2.端点(Endpoint)Actuator的核心是端点(Endpoint),它用来监视、提供应用程序的信息,Spring Boot提供的spring-boot-actuator组件中已经内置了非常多的 Endpoint(health、info、beans、metrics、httptrace、shutdown等),每个端点都可以启用和禁用。Actuator也允许我们扩展自己的端点。通过JMX或HTTP的形式暴露自定义端点。Actuator会将自定义端点的ID默认映射到一个带/actuator前缀的URL。比如,health端点默认映射到/actuator/health。这样就可以通过HTTP的形式获取自定义端点的数据。Actuator同时还可以与外部应用监控系统整合,比如 Prometheus, Graphite, DataDog, Influx, Wavefront, New Relic等。这些系统提供了非常好的仪表盘、图标、分析和告警等功能,使得你可以通过统一的接口轻松的监控和管理你的应用系统。这对于实施微服务的中小团队来说,无疑快速高效的解决方案。二、Spring Boot集成Actuator在Spring Boot项目中集成Actuator非常简单,只需要在项目中添加spring-boot-starter-actuator组件,就能自动启动应用监控的功能。首先,创建一个Spring Boot项目来添加spring-boot-starter-actuator依赖:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>如上面的示例所示,我们添加了actuator和web两个组件。spring-boot-starter-actuator除了可以监控Web系统外,还可以监控后台服务等Spring Boot应用。然后,修改配置文件,配置Actuator端点# 打开所有的监控点management.endpoints.web.exposure.include=* management.endpoint.health.show-details=always最后,启动项目并在浏览器中输入http://localhost:8080/actuator,我们可以看到返回的是Actuator提供的各种数据接口信息。Actuator提供了丰富的数据接口,包括/health、/env、/metrics等。下面我们请求其中的一个地址/actuator/health,查看接口返回的详细信息。如图上图所示,/health接口返回了系统详细的健康状态信息,包括系统的状态(UP为正常)、磁盘使用情况等信息。三、自定义端点Spring Boot支持自定义端点,只需要在我们定义的类中使用@Endpoint、@JmxEndpoint、@WebEndpoint等注解,实现对应的方法即可定义一个Actuator中的自定义端点。从Spring Boot 2.x版本开始,Actuator支持CRUD(增删改查)模型,而不是旧的RW(读/写)模型。我们可以按照3种策略来自定义:使用@Endpoint注解,同时支持JMX和HTTP方式。使用@JmxEndpoint 注解,只支持JMX技术。使用@WebEndpoint注解,只支持HTTP。编写自定义端点类很简单,首先需要在类前面使用@Endpoint注解,然后在类的方法上使用@ReadOperation、@WriteOperation或@DeleteOperation(分别对应HTTP中的GET、POST、DELETE)等注解获取、设置端点信息。下面我们创建一个获取系统当前时间的自定义端点。首先,创建自定义端点类SystemTimeEndpoint,使用@Endpoint注解声明端点ID,同时需要使用@Component注解,将此类交给Spring Boot管理。示例代码如下:/* * 自定义端点类 * @Endpoint //表示这是一个自定义事件端点类 * Endpoint 中有一个id //它是设置端点的URL路径 * */ @Endpoint(id="systemtime") //端点路径不要与系统自带的重合 @Component public class SystemTimeEndpoint { //一般端点都是对象,或者一个json返回的格式,所以通常我们会将端点定义一个MAP的返回形式 //通过ReadOperation //访问地址是根据前缀+ endpoint 的ID ///actuator/systemtime private String format = "yyyy-MM-dd HH:mm:ss"; @ReadOperation //显示监控指标 public Map<String,Object> info(){ Map<String,Object> info = new HashMap<>(); info.put("system","数据管理服务"); info.put("memo","系统当前时间端点"); info.put("datetime",new SimpleDateFormat(format).format(new Date())); return info; } //动态修改指标 @WriteOperation //动态修改指标,是以post方式修改 public void setFormat(String format){ this.format = format; } }在上面的示例中,我们通过@Endpoint注解定义一个自定义端点,参数id为自定义端点的唯一标识和访问路径,必须唯一不重复。做好这些配置后,就能访问http://127.0.0.1:8080/actuator/systemtime端点了,如图下图所示。 最后以上,Actuator到底是什么,如何在Spring Boot项目中快速集成Actuator介绍完了。Actuator是Spring Boot 提供的非常重要的应用监控组件,希望大家能熟悉掌握。后面还会介绍搭建完整的Spring Boot应用监控平台。敬请关注。
前面介绍了Elasticsearch的特点和优势,接下来在Spring Boot项目中使用Elasticsearch一步一步地实现搜索引擎的功能。一、Spring Boot对Elasticsearch的支持在没有Spring Boot之前使用Elasticsearch非常痛苦,需要对Elasticsearch客户端进行一系列的封装等操作,使用复杂,配置烦琐。所幸,Spring Boot提供了对Spring Data Elasticsearch的封装组件spring-boot-starter-data-elasticsearch,它让Spring Boot项目可以非常方便地去操作Elasticsearch中的数据。值得注意的是,Elasticsearch的5.x、6.x、7.x版本之间的差别还是很大的。Spring Data Elasticsearch、Spring Boot与Elasticsearch之间有版本对应关系,不同的版本之间不兼容,Spring Boot 2.1对应的是Spring Data Elasticsearch 3.1.2版本。对应关系如表13-1所示。表13-1 Spring Data Elasticsearch、Spring Boot与Elasticsearch的对应关系Spring Data ElasticsearchSpring BootElasticsearch3.2.x2.2.x6.8.43.1.x2.1.x6.2.23.0.x2.0.x5.5.02.1.x1.5.x2.4.0这是官方提供的版本对应关系,建议按照官方的版本对应关系进行选择,以避免不必要的麻烦。二、Spring Boot操作Elasticsearch的方式由于Elasticsearch和Spring之间存在版本兼容的问题,导致在Spring Boot项目中操作Elasticsearch的方式有很多种,如Repositories、JestClient、Rest API等。因此有必要梳理一下主流的Spring Boot操作Elasticsearch的方式。目前,Spring推荐使用Elasticsearch的方式,如下图所示:我们看到Spring Boot提供了ElasticSearchRepository和ElasticsearchRestTemplate实现索引数据的增删改查。ElasticSearchRepository:继承自Spring Data中的Repository接口,所以支持以数据库的方式对数据进行增删改查的操作,而且支持已命名查询等数据查询。ElasticsearchRestTemplate:spring-data-Elasticsearch项目中的一个类,和其他Spring项目中的Template类似。ElasticsearchRestTemplate是Spring对ES的Rest API进行的封装,提供了大量相关的类来完成复杂的查询功能。三、在Spring Boot项目中集成ElasticsearchSpring Boot提供的spring-boot-starter-data-Elasticsearch组件为我们提供了非常便捷的数据检索功能。下面就来演示Spring Boot项目如何集成Elasticsearch。1. 添加Elasticsearch依赖首先在pom.xml中添加spring-boot-starter-data-Elasticsearch组件依赖,代码如下:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-Elasticsearch</artifactId> </dependency>2. 配置Elasticsearch在application.properties项目配置文件中添加Elasticsearch服务器的地址,代码如下:spring.Elasticsearch.rest.uris=http://10.2.1.231:9200主要用来配置Elasticsearch服务地址,多个地址用逗号分隔。需要注意的是,Spring Data Elasticsearch各版本的配置属性可能不一样。本示例中使用的是7.6.2版本。3. 创建文档对象创建实体对象类Book,然后使用@Document注解定义文档对象,示例代码如下:@Document( indexName = "book" , replicas = 0) public class Book { @Id private Long id; @Field(analyzer = "ik_max_word",type = FieldType.Text) private String bookName; @Field(type = FieldType.Keyword) private String author; private float price; private int page; @Field(type = FieldType.Keyword, fielddata = true) private String category; // 省略get、set方法 public Book(){ } public Book(Long id,String bookName, String author,float price,int page,String category) { this.id = id; this.bookName = bookName; this.author = author; this.price = price; this.page = page; this.category = category; } @Override public String toString() { final StringBuilder sb = new StringBuilder( "{\"Book\":{" ); sb.append( "\"id\":" ).append( id ); sb.append( ",\"bookName\":\"" ).append( bookName ).append( '\"' ); sb.append( ",\"page\":\"" ).append( page ).append( '\"' ); sb.append( ",\"price\":\"" ).append( price ).append( '\"' ); sb.append( ",\"category\":\"" ).append( category ).append( '\"' ); sb.append( ",\"author\":\"" ).append( author ).append( '\"' ); sb.append( "}}" ); return sb.toString(); } }如上面的示例所示,通过@Document注解将数据实体对象与Elasticsearch中的文档和属性一一对应。(1)@Document注解会对实体中的所有属性建立索引:indexName = "customer":表示创建一个名为customer的索引。type="customer":表示在索引中创建一个名为customer的类别,而在Elasticsearch 7.x版本中取消了类别的概念。shards = 1:表示只使用一个分片,默认为5。replicas = 0:表示副本数量,默认为1,0表示不使用副本。refreshInterval = "-1":表示禁止索引刷新。(2)@Id作用在成员变量,标记一个字段作为id主键。(3)@Field作用在成员变量,标记为文档的字段,并指定字段映射属性:type:字段类型,取值是枚举:FieldType。index:是否索引,布尔类型,默认是true。store:是否存储,布尔类型,默认是false。analyzer:分词器名称是ik_max_word。4. 创建操作的Repository创建CustomerRepository接口并继承ElasticsearchRepository,新增两个简单的自定义查询方法。示例代码如下:public interface BookRepository extends ElasticsearchRepository<Book, Integer>{ List<Book> findByBookNameLike(String bookName); }通过上面的示例代码,我们发现其使用方式和JPA的语法是一样的。5. 验证测试首先创建BookRepositoryTest单元测试类,在类中注入BookRepository,最后添加一个数据插入测试方法。@Test public void testSave() { Book book = new Book(); book.setId(1); book.setBookName("西游记"); book.setAuthor("吴承恩"); repository.save(book); Book newbook=repository.findById(1).orElse(null); System.out.println(newbook); }单击Run Test或在方法上右击,选择Run 'testSave',运行单元测试方法,查看索引数据是否插入成功,运行结果如下图所示:结果表明索引数据保存成功,并且通过id能查询到保存的索引数据信息,说明在Spring Boot中成功集成Elasticsearch。最后以上,介绍了Spring Boot项目中使用Elasticsearch,一步一步地实现搜索引擎的功能。
随着互联网的发展,国内的大厂开始全面拥抱 Go 语言,包括阿里巴巴、京东、今日头条、小米、滴滴、七牛云、360等互联网公司。这么多大厂开始使用 Go 语言,可以说, Go语言入门快、程序库多、运行迅速,很适合快速构建互联网软件产品。当然,Go语言也有其不足之处,与 Java 相比,还很年轻,很多框架、库、包比较少,这也比较正常,毕竟生态还不健全。综合来讲,Go语言旨在不损失应用程序性能的情况下降低代码的复杂性,具有“部署简单、并发性好、语言设计良好、执行性能好”等优势,目前国内诸多 IT 公司均已采用Go语言开发项目。一、Go语言简介Go语言(或 Golang)起源于 2007 年,并在 2009 年正式对外发布。Go 是非常年轻的一门语言,它的主要目标是“兼具 Python 等动态语言的开发速度和 C/C++ 等编译型语言的性能与安全性”。Go语言是编程语言设计的又一次尝试,是对类C语言的重大改进,它不但能让你访问底层操作系统,还提供了强大的网络编程和并发编程支持。Go语言的用途众多,可以进行网络编程、系统编程、并发编程、分布式编程。Go 从C语言继承了相似的表达式语法、控制流结构、基础数据类型、调用参数传值、指针等很多思想,还有C语言一直所看中的编译后机器码的运行效率以及和现有操作系统的无缝适配。因为Go语言没有类和继承的概念,所以它和 Java 或 C++ 看起来并不相同。但是它通过接口(interface)的概念来实现多态性。Go语言有一个清晰易懂的轻量级类型系统,在类型之间也没有层级之说。因此可以说Go语言是一门混合型的语言。作为一个开源项目,Go 语言借助开源社区的有生力量达到快速地发展,很多重要的开源项目都是使用Go语言开发的,其中包括 Docker、Go-Ethereum、Thrraform 和 Kubernetes。二、Go语言优势Golang 真的好用吗?它到底有哪些令人着迷的地方呢?带着这些问题,我们一起来看看Go语言到底都有哪些优势:1.简单易学Go语言的作者都有C的基因,Go 自然而然也有了 C 的基因,但是 Go 的语法比 C 还简单, 并且几乎支持大多数你在其他语言见过的特性:封装、继承、多态、反射等。2.丰富的标准库Go 目前已经内置了大量的库,特别是网络库非常强大前面说了作者是 C的作者,所以 Go 里面也可以直接包含c代码,利用现有的丰富的C库跨平台编译和部署Go 代码可直接编译成机器码,不依赖其他库,部署就是扔一个文件上去就完事了. 并且 Go 代码还可以做到跨平台编译。3.内置强大的工具Go 语言里面内置了很多工具链,最好的应该是 gofmt 工具,自动化格式化代码,能够让团队review 变得如此的简单,代码格式一模一样,想不一样都很困难。4.性能优势Go 极其地快。其性能与 C 或 C++ 相似。在我们的使用中,Go 一般比 Python 要快 30 倍左右。语言层面支持并发,这个就是Go最大的特色,天生的支持并发,可以充分的利用多核,很容易的使用并发。内置runtime,支持垃圾回收。三、适用场景Go 语言提供了海量并行的支持,高度的抽象化和高性能。对于高性能分布式系统领域而言,Go 语言无疑比大多数其它语言有着更高的开发效率。非常适合复杂事件处理(CEP)。服务器编程,拥有提供了海量并发性能,非常适合游戏服务端,视频服务等。分布式系统,数据库代理,存储集群等基础服务,如:日志处理、数据打包、虚拟机处理、文件系统、内存数据库等中间件。云平台,目前国外很多云平台在采用Go开发。四、Go语言官网Go语言的官方网站为:https://golang.org ,它提供了完善的参考文档,包括编程语言规范和标准库等诸多权威的帮助信息。同时也包含了如何编写更地道的Go程序的基本教程,还有各种各样的在线文本资源和视频资源。五、吉祥物Go语言有一个吉祥物,在会议、文档页面和博文中,大多会包含下图所示的 Go Gopher,这是才华横溢的插画家 Renee French 设计的,她也是 Go 设计者之一 Rob Pike 的妻子。最后Go语言入门快、程序库多、运行迅速,很适合快速构建互联网软件产品。目前国内的大厂开始全面拥抱 Go 语言。建议大家学习好好学习Go这门语言。
前面我们讲了maven项目中的最重要的文件:pom.xml 配置文件相关内容。介绍了pom 是如何定义项目,如何添加依赖的jar 包的等。我们知道,在Maven的生命周期中,存在编译、测试、运行等过程,那么有些依赖只用于测试,比如junit;有些依赖编译用不到,只有运行的时候才能用到,比如mysql的驱动包在编译期就用不到(编译期用的是JDBC接口),而是在运行时用到的;还有些依赖,编译期要用到,而运行期不需要提供,因为有些容器已经提供了,比如servlet-api在tomcat中已经提供了,我们只需要的是编译期提供而已。那么Maven是如何管理各个jar包的依赖关系,jar包之间的依赖传递和依赖冲突呢?所以接下来就讲一讲maven的依赖管理。 一、声明依赖 Maven 项目使用pom.xml 文件,可以方便的管理项目中所有依赖的jar包,之前也介绍过pom.xml 文件是如何定义依赖的。下面就以一段 junit依赖声明,演示pom.xml 是如何定义相关依赖的:<dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <type>jar</type> <scope>test</scope> <optional>false</optional> <exclusions> <exclusion></exclusion> </exclusions> </dependency> </dependencies>上面的示例我们定义了junit的依赖,而且junit只在测试阶段生效。具体配置参数如下:type:依赖类型,对应构件中定义的 packaging,可不声明,默认为 jar;scope:依赖范围,大致有compile、provided、runtime、test、system等几个;optional:依赖是否可选;exclusions:排除传递依赖。总结说来,在pom.xml 配置文件中,配置节点引入了节点,它主要管理依赖的范围。大致有compile、provided、runtime、test、system等几个。引入 节点排除某些依赖。解决了控制各个jar包的依赖范围,jar包之间的依赖传递和依赖冲突的问题。 二、依赖范围Maven在执行不同的命令时(mvn package,mvn test,mvn install ……),会使用不同的 classpath,Maven 对应的有三套 classpath,即:编译classpath、测试classpath,运行classpath。我们可在dependency中的scope元素中进行配置。从而决定了该依赖构件会被引入到哪一个 classpath 中。默认为:compile。Maven提供的依赖范围如下:compile:编译依赖范围,默认值。此选项对编译、测试、运行三种 classpath 都有效,如 hibernate-core-3.6.5.Final.jar,表明在编译、测试、运行的时候都需要该依赖;test:测试依赖范围。只对测试有效,表明只在测试的时候需要,在编译和运行时将无法使用该类依赖,如 junit;provided:已提供依赖范围。编译和测试有效,运行无效。如 servlet-api ,在项目运行时,tomcat 等容器已经提供,无需 Maven 重复引入;runtime:运行时依赖范围。测试和运行有效,编译无效。如 jdbc 驱动实现,编译时只需接口,测试或运行时才需要具体的 jdbc 驱动实现;system:系统依赖范围。和 provided 依赖范围一致,也是编译和测试有效,需要通过 显示指定,且可以引用环境变量;import:导入依赖范围。使用该选项,通常需要 pom,将目标 pom 的 dependencyManagement 配置导入合并到当前 pom 的 dependencyManagement 元素。需要注意的是,在打包阶段,使用的是运行classpath。即引入到运行classpath中的Maven依赖会被一起打包。三、依赖传递所谓依赖传递,就是A依赖B,B依赖C,A就间接依赖C,那么A与C直接的依赖关系就叫传递性依赖。直接依赖又叫第一直接依赖,传递性依赖又叫第二直接依赖,其中第一直接依赖和第二直接依赖的依赖范围,决定了传递性依赖的依赖范围。下面使用hibernate-core的依赖关系详细介绍所谓的依赖传递:如上图所示,hibernate-core 依赖 hibernate-commons-annotations ,而 hibernate-commons-annotations 又依赖 slf4j-api ,hibernate-core 对 slf4j-api 的依赖就是传递依赖。pom.xml 文件中我们只需要引入 hibernate-core 构件的依赖,Maven 会自动为我们引入依赖jar包及传递依赖的jar包,无需再手动引一遍相关依赖。所以不用担心依赖冲突的问题。那么如何判定一个间接依赖是否有必要被引入呢?间接依赖被引入后其依赖范围又是什么呢?其实很简单,就是通过第一直接依赖的依赖范围和第二直接依赖的依赖范围之间的关系,来判定是否有必要引入间接依赖以及确定引入间接依赖后其依赖范围,如下表所示:所以,通过上表我们可以发现:1)第二直接依赖的依赖范围为compile : 传递依赖的依赖范围同第一直接依赖的依赖范围一样。2)第二直接依赖的依赖范围为test : 传递依赖不会被引入。3)第二直接依赖的依赖范围为provided : 只有当第一直接依赖的依赖范围亦为provided时,传递依赖才会被引入,且依赖范围依然是provided。4)第二直接依赖的依赖范围为runtime : 除了第一直接依赖的依赖范围为compile时传递依赖的依赖范围为runtime外,其余情况下,传递依赖的依赖范围同第一直接依赖的依赖范围一样。四、依赖冲突通常我们不需要关心传递性依赖,但是当多个传递性依赖中有对同一构件不同版本的依赖时,如何解决呢?通常我们有一下两个原则:短路径优先:假如有以下依赖:A -> B -> C ->X(版本 1.0) 和 A -> D -> X(版本 2.0),则优先解析较短路径的 X(版本 2.0);先声明优先:即谁先声明,谁被解析。针对依赖冲突中的“短路径优先”,那如果我们就想使用长路径的依赖怎么办呢?这时可以使用依赖排除元素,显示排除短路径依赖。在非冲突的情况下,这种方法同样有效。五、解决依赖冲突一般情况下我们不需要关心依赖的问题。而当依赖出问题时,我们需要知道该如何解决依赖冲突。解决依赖冲突的方式如下:(1)首先,使用:mvn dependency:tree 命令查看项目的依赖列表,(2)然后,通过依赖列表,找出冲突的jar包。(3)最后,可选依赖 option或是排除依赖 exclusions 处理相关冲突。1.可选依赖 option通过项目中的pom.xml中的dependency下的option元素中进行配置,只有显式地配置项目中某依赖的option元素为true时,该依赖才是可选依赖;不设置该元素或值为false时,作用是:当某个间接依赖是可选依赖时,无论依赖范围是什么,其都不会因为传递性依赖机制而被引入。 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <!-- optional=true,依赖不会传递,该项目依赖devtools;之后依赖boot项目的项目如果想要使用devtools,需要重新引入 --> <optional>true</optional> </dependency>上面的示例,当optional=true,devtools的依赖不会传递,该项目依赖devtools;之后依赖该项目的项目则不会依赖devtools组件,如果想要使用devtools,需要重新引入。2.排除依赖 exclusions我们知道,由于Maven的的传递性依赖机,有时候第三方组件B的C依赖由于版本(1.0)过低时。我们期望能够将该间接依赖直接剔除出去,这样不会影响到项目中其他依赖组件。这时可以通过exclusions元素实现。<dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <exclusions> <exclusion> <artifactId>commons-logging</artifactId> <groupId>commons-logging</groupId> </exclusion> </exclusions> </dependency>上面,我们排除了spring-core组件忠的commons-logging。最后以上,我们就把Maven的依赖关系,Jar包之间的依赖传递和依赖冲突介绍完了。
前面我们介绍了Golang的环境搭建,如何安装、配置Golang环境、配置Go工作目录:GOPATH。通过前面学习想必大家已经对Golang有了一定的了解,那要怎么来创建一个Go语言程序呢?下面就来领大家实现一个简单的程序:helloworld。一、创建第一个GO程序下面创建我们第一个Go程序,helloworld。其实非常简单,仅需要几行代码就可以搞定,如下所示:package main import ( "fmt" ) func main() { fmt.Println("Hello World!") }将上面的程序保存成 helloworld.go,然后在命令行中执行:go run helloworld.go我们看到程序成功输出:Hello World!,说明我们的第一个Golang程序运行成功。二、GO程序的基本结构上面,我们成功创建并运行了一个helloworld.go 。虽然程序运行了,但是,想必大家也许不明白这些代码的含义,没关系,下面就来一一介绍:1. package(声明main包)Go语言以“包”作为管理单位,每个 Go 源文件必须先声明它所属的包,所以我们会看到每个 Go 源文件的开头都是一个 package 声明,格式如下:package mian其中 package 是声明包名的关键字,main 为具体包的名字。Go语言的包与文件夹是一一对应的,它具有以下几点特性:一个目录下的同级文件属于同一个包。包名可以与其目录名不同。main 包是Go语言程序的入口包,一个Go语言程序必须有且仅有一个 main 包。如果一个程序没有 main 包,那么编译时将会出错,无法生成可执行文件。2. import(导入包)在包声明之后,是 import 语句,用于导入程序中所依赖的包,导入的包名使用双引号""包围,格式如下:import "fmt"其中 import 是导入包的关键字,fmt为所导入包的名字,是Go提供的标准库。需要注意的是,导入的包中不能含有代码中没有使用到的包,否则Go编译器会报编译错误,例如 imported and not used: "xxx","xxx" 表示包名。3. main 函数通过func main() 创建了一个 main 函数,它是Go语言程序的入口函数,也即程序启动后运行的第一个函数。main 函数只能声明在 main 包中,不能声明在其他包中,并且,一个 main 包中也必须有且仅有一个 main 函数。这个跟C或是C++类似。main 函数是自定义函数的一种,在Go语言中,所有函数都以关键字 func 开头的,定义格式如下所示:func 函数名 (参数列表) (返回值列表){ }格式说明如下:函数名:由字母、数字、下画线_组成,其中,函数名的第一个字母不能为数字,并且,在同一个包内,函数名称不能重名。参数列表:一个参数由参数变量和参数类型组成,例如func foo( a int, b string )。返回值列表:可以是返回值类型列表,也可以是参数列表那样变量名与类型的组合,函数有返回值时,必须在函数体中使用 return 语句返回。注意:Go语言函数的左大括号{必须和函数名称在同一行,否则会报错。4. fmt.Println("Hello World!")调用fmt包中的Println函数格式化输出相关的内容。比如字符串、整数、小数等,与C语言中printf 函数类似。另外,我们看到Go语言不需要使用;来作为结束符,Go 编译器会自动帮我们添加,当然,在这里加上;也是可以的。三、编译&运行上面的helloworld示例程序编写完之后,接下来我们运行helloworld.go,验证是否执行成功。在命令行中执行:go run helloworld.go 编译运行该程序,输出结果如下:Golang还提供了go test、go run、go build 、命令主要是用于测试、编译、运行、打包程序。在包的编译过程中,若有必要,会同时编译与之相关联的包。go build 命令主要用于打包项目go run 命令主要用于编译并运行Go程序。例如:go run helloworld.go go test 命令会自动读取源码目录下面名为*_test.go的文件,生成并运行测试用的可执行文件。最后以上,我们第一个Go语言程序:helloworld 创建并运行成功了,相信大家已经知道如何创建一个简单的Go语言程序了,赶快动手试试吧!
前面介绍Go(或者Golang)的起源,Go语言的优势、最后还介绍了Golang的特别可爱的吉祥物。想必对Golang有了个大致的认识,接下来我们就开始搭建Golang的开发环境。开始我们的Go语言学习之旅。一、安装 GoGo 的官方网站:http://golang.org/(需要FQ软件)国内下载地址:http://www.golangtc.com/download下载对应平台的安装包。注意区分32位还是64位操作系统。安装包下载完成之后,安装过程很简单,傻瓜式下一步到底就好了。 二、Go 环境变量安装go 的时候,安装程序会自动把相关目录写到系统环境。但是如果是zip 的安装,需要自己手动添加。主要配置以下几个:GOROOT:Go 安装后的根目录(例如:D:\Go),安装过程中会由安装程序自动写入系统环境变量中。GOBIN:Go 的二进制文件存放目录(%GOROOT%\bin)PATH:需要将 %GOBIN% 加在 PATH 变量的最后,方便在命令行下运行。当环境变量都配置完成之后,Go 就已经安装完毕了。打开命令行,运行 go version命令查看go的版本,就可以看到如下的提示,说明Go的环境就搭建成功了。三、Go 工作目录Go的工作目录主要用于存储我们的开发和依赖包的目录(例如:D:\Go_Path\go) ,此目录需要手动配置到系统环境变量。GOPATH 工作空间是一个目录层次结构,其根目录包含三个子目录:src:包含 Go 源文件,注意:你自己创建依赖的package,也要放到GOPATH 目录下,这样才能够被引用到。pkg:包含包对象,编译好的库文件bin:包含可执行命令注意:1. 需要将GOPATH 路径,手动写入到系统环境变量。2. 不要把 GOPATH 设置成 Go 的安装路径3. 你自己创建依赖的package,也要放到GOPATH 目录下,这样才能够被引用到。 配置好之后,通过 go env 命令来查看go环境是否配置正确: 从命令行输出中,可以看到 GOPATH 设定的路径为:D:\Go_Path\go。四、其他1. IDE 的下载安装这里就不说,大家直接去这个地址下载就行。 Goland:https://www.jetbrains.com/go/download/#section=windows LiteIDE: https://studygolang.com/dl 这个是最新的1.10.3,免费的IDE 2. 我用的是Goland编辑器。非常好用。建议大家使用这个,其他的IDE配置都比较繁琐。最后以上,我们就把Golang的环境搭建介绍完了,所谓工欲善其事必先利其器,这些都是基础的工作,作为开发人员还是得牢牢掌握的。推荐阅读:一起学Golang系列(一)Go语言入门:起源、优势和应用场景
上一章,我们讲了Maven的坐标和仓库的概念,介绍了Maven是怎么通过坐标找到依赖的jar包的。同时也介绍了Maven的中央仓库、本地仓库、私服等概念及其作用。这些东西都是Maven最基本、最核心的概念,大家一定要搞明白。所谓工欲善其事必先利其器,这些基础的东西一定要掌握。其实,Maven项目中还有一个最核心的文件:pom.xml 文件。pom.xml 文件是Maven项目中的核心项目管理文件,用于项目描述、依赖管理、构建信息管理、组织信息管理等。pom.xml 文件中包含了许多标签。接下来介绍一些Maven常用的标签。一、文件结构Maven项目根目录下的pom.xml文件是Maven项目中非常重要的配置文件。主要描述项目包的依赖和项目构建时的配置。pom.xml配置文件主要分4部分,分别是:项目的描述信息项目的依赖配置信息构建时需要的公共变量构建配置下面就来一一介绍pom.xml文件各个组成部分以及它们的作用。 二、各部分说明1. 项目的描述信息pom.xml中最重要的就是项目的坐标信息,主要包含之前介绍的:<groupId>、<artifactId>、<version>、<packaging>等标签。pom.xml 文件中定义如下:<groupId>com.wei</groupId> <artifactId>hello</artifactId> <version>2.0.5.RELEASE</version> <packaging>jar</packaging> <name>hello</name> <description>Demo project for Spring Boot</description>上面的配置内容基本是创建项目时定义的有关项目的基本描述信息,其中比较重要的是groupId、artifactId。各个属性说明如下:groupId:项目的包路径。artifactId:项目名称。version:项目版本号。packaging:一般有jar、war两个值,表示使用 Maven打包时是构建成JAR包还是WAR包。name:项目名称。description:项目描述。2. 项目的依赖配置信息此部分为项目的依赖信息,主要包括Spring Boot的版本信息和第三方组件的版本信息。示例代码如下:<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.5.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>上述描述项目的依赖信息主要分为parent和dependencies两部分。parent:配置父级项目的信息。Maven支持项目的父子结构,引入后会默认继承父级的配置。此项目中引入spring-boot-starter-parent 定义Spring Boot的基础版本。dependencies:配置项目所需要的依赖包,Spring Boot体系内的依赖组件不需要填写具体版本号,spring-boot-starter-parent维护了体系内所有依赖包的版本信息。dependency:Maven项目定义依赖库的重要标签,通过groupId、artifactId等“坐标”信息定义依赖库的路径信息。scope:指依赖的范围, 非常重要,也非常难懂接下来我会专门讲maven的依赖范围。主要包含: compile(编译范围):默认的scope,运行期有效,需要打入包中 provided:编译期有效,运行期不需要提供,不会打入包中 runtime:编译不需要,在运行期有效,需要导入包中。(接口与实现分离) test:测试需要,不会打入包中 system:非本地仓库引入、存在系统的某个路径下的jar。(一般不使用)optional:设置依赖是否可选,有true和false,默认是false。exclusions:排除依赖传递列表。此外,Maven的项目支持父子继承,子项目的pom文件继承父项目的pom文件中的配置。假如某个的模块很多,一些公共的jar包,每个模块都需要引用一遍很麻烦。为了项目的正确运行,必须让所有的子项目使用依赖项的统一版本,必须确保应用的各个项目的依赖项和版本一致,才能保证测试的和发布的是相同的结果。所以如果抽象出一个父工程来管理子项目的公共的依赖。在我们项目顶层的pom文件中,我们会看到《dependencyManagement》元素。通过它元素来管理jar包的版本,让子项目中引用一个依赖而不用显示的列出版本号。Maven会沿着父子层次向上走,直到找到一个拥有dependencyManagement元素的项目,然后它就会使用在这个dependencyManagement元素中指定的版本号。如上图所示,我们可以通过<modules> 来聚合多个maven 模块,假如我们项目中有多个模块,那么通过<modules> 标签将这些子模块聚合,统一编译。假如我们的项目分成了好几个模块,那么我们构建的时候是不是有几个模块就需要构建几次了(到每个模块的目录下执行mvn命令)?当然,你逐个构建没问题,但是非要这么麻烦的一个一个的构建吗,那么简单的做法就是使用聚合,一次构建全部模块。3. 构建构建时需要的公共变量这里面定义pom中的公共变量:<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties>上面配置了项目构建时所使用的编码,输出所使用的编码,最后指定了项目使用的JDK版本。4. 构建配置此部分为构建配置信息,这里使用Maven构建Spring Boot项目,所以必须需要在<plugins>中添加 spring-boot-maven-plugin 插件,它能够以Maven的方式为应用提供Spring Boot的支持。<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>配置spring-boot-maven-plugin构建插件,将Spring Boot应用打包为可执行的JAR或WAR文件,然后以简单的方式运行Spring Boot应用。如果需要更改为Docker相关的配置,则只要更改此部分即可。最后以上,就把Maven项目中的pom文件的常用标签介绍完。磨刀不误砍柴工,pom.xml 文件虽然简单,但是还是必须牢牢掌握。接下来会讲Maven 中最重要,也是最麻烦的依赖关系。
之前通过一个helloworld的例子来说一说如何创建maven项目以及maven项目的项目结构,然后讲maven如何编译运行项目。接下来介绍maven中几个比较重要的概念:坐标和仓库。 一、 坐标maven中,所有的依赖、插件和生成的jar包统称为构件,坐标就是所有的构件的唯一标识。所有构件均通过坐标进行组织和管理。maven 的坐标通过 5 个元素进行定义,其中 groupId、artifactId、version 是必须的,packaging 是可选的(默认为jar),classifier 是不能直接定义的。groupId:定义当前 Maven 项目所属的实际项目,跟 Java 包名类似,通常与域名反向一一对应。artifactId:定义当前 Maven 项目的一个模块,默认情况下,Maven 生成的构件,其文件名会以 artifactId 开头,如 hibernate-core-3.6.5.Final.jar。version:定义项目版本。packaging:定义项目打包方式,如 jar,war,pom,zip ……,默认为 jar。classifier:定义项目的附属构件,如 hibernate-core-3.6.6.Final-sources.jar,hibernate-core-3.6.6.Final-javadoc.jar,其中 sources 和 javadoc 就是这两个附属构件的 classifier。classifier 不能直接定义,通常由附加的插件帮助生成。 项目中pom.xml 文件中定义:所以,一般我们实际项目的开发过程中,java的包名一般对应groupId,项目名对应artifactId。当我们需要引用某个Jar包时,在pom.xml文件中配置对应的坐标(groupId和artifactId)即可。 二、 仓库所谓仓库,就是Maven 根据构件的坐标统一存储这些构件的唯一副本的目录。在项目中通过坐标依赖声明,可以方便的引用构件。Maven 仓库分为本地仓库和远程仓库,Maven寻找构件时,首先从本地仓库找,本地找不到则到远程仓库找,再找不到就报错;在远程仓库中找到了,就下载到本地仓库再使用。本地仓库是存储在本机的构件仓库,默认地址为:${user.home}/.m2/repository。中央仓库是 Maven 核心自带的远程仓库,默认地址:http://repo1.maven.org/maven2。除了中央仓库,还有其它很多公共的远程仓库。私服是架设在本机或局域网中的一种特殊的远程仓库,通过私服可以方便、统一的管理其它所有的外部远程仓库。实际项目中为方便、统一的管理,一般会创建私服以确保所有的项目环境使用的都是同一个版本的构件。1 . 本地仓库Maven 本地仓库默认地址为:${user.home}/.m2/repository。安装完 Maven ,本地仓库几乎是空的,这时需要从远程仓库下载所需构件。上图就是本地的Maven仓库,默认在${user.home}/.m2/repository,那么如何修改本地仓库默认地址?通过修改 %MAVEN_HOME%/conf/settings.xml 下的配置文件可以更改本地仓库的位置。 2 . 中央仓库Maven 配置了一个默认的远程仓库,即中央仓库,找到 %MAVEN_HOME%/lib/maven-model-builder-3.2.1.jar,我们打开 org/apache/maven/model/pom-4.0.0.xml 文件,这是Maven的超级POM文件,所有的项目都会继承这个POM文件:<repositories> <repository> <id>central</id> <name>Central Repository</name> <url>https://repo.maven.apache.org/maven2</url> <layout>default</layout> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories>示例说明:<id>central</id> 标识中央仓库的唯一标识。<url> 就是中央仓库的地址。<snapshots> 配置的是false,就是不下载版本为快照的构件。中央仓库包含的绝大多数开源项目的构件。基本上平时开发用到的框架这里都能找到。 3. 其他公共远程仓库除了maven的中央仓库,由于网络的原因,很多其他的大公司也提供了公共的远程仓库,又叫镜像仓库。修改%MAVEN_HOME%/conf/settings.xml ,默认配置了nexus-aliyun 镜像仓库。<mirror> <id>nexus-aliyun</id> <mirrorOf>*,!jeecg,!jeecg-snapshots</mirrorOf> <name>Nexus aliyun</name> <url>http://maven.aliyun.com/nexus/content/groups/public</url> </mirror> 注意:如果配置了镜像仓库,那么所有的对中央仓库的访问,都会自动转到镜像仓库。 最后以上,介绍了Maven的两个重要的概念坐标和仓库。理解起来比较简单,这些是开发者必学必会的基础技能。请大家关注!
之前讲过Maven介绍及环境搭建,介绍了maven的作用和如何搭建maven环境。接下来就以一个helloworld的例子来说一说如何创建Maven项目以及Maven的项目结构,最后讲Maven如何编译运行项目。 一、创建Maven项目其实所谓创建Maven项目,说白了就是创建一个符合Maven约定的项目骨架,也就是项目目录。这些项目的目录可以手动创建,也可以用maven插件。这里我就介绍使用archetype插件自动建立目录。首先,创建项目存放的目录(例如d:\maven_project),然后打开终端或者命令行并切换到d:\maven_project目录下,执行以下Maven命令:mvn archetype:generate -DgroupId=com.weiz.hellomaven -DartifactId=hello-maven-test -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false参数说明groupId : 标识package命名空间artifactId: 创建的项目名称命令行输出如下结果说明项目创建成功。Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8 -Dgroovy.source.encoding=UTF-8 [INFO] Scanning for projects... . .省略输出 . [INFO] Parameter: basedir, Value: D:\maven_project [INFO] Parameter: package, Value: com.weiz.hellomaven [INFO] Parameter: groupId, Value: com.weiz.hellomaven [INFO] Parameter: artifactId, Value: hello-maven-test [INFO] Parameter: packageName, Value: com.weiz.hellomaven [INFO] Parameter: version, Value: 1.0-SNAPSHOT [INFO] project created from Old (1.x) Archetype in dir: D:\maven_project\hello-m aven-test [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 4.039 s [INFO] Finished at: 2020-05-23T16:40:59+08:00 [INFO] ------------------------------------------------------------------------注意:如果是刚安装的Maven,第一次创建项目时可能需要一段时间,因为Maven需要从网上下载大部分最近的artifacts (plugin jars and other files)到你的本地仓库。如果失败了,再执行一次该命令即可。输出了“BUILD SUCCESS”时表示项目创建成功了,创建一个hello-maven-test的项目。在目录下,可以看到刚创建成功的maven项目。 二、Maven项目结构Maven工程与以往的java工程目录结构类似,不同的地方在于:以往的java工程目录目录一般只有一个src用于存放包及java文件。具体Maven项目结构如下: $ MavenProject |-- pom.xml |-- src | |-- main | | `-- java | | `-- resources | `-- test | | `-- java | | `-- resources上面我们看到maven创建的项目结构,与普通的Java项目结构差不多。一般将java的功能代码,放在main/java下面,而测试代码放在test/java下,这样在运行时,maven才可以识别目录并进行编译。src/main/java - 存放项目.java文件;src/main/resources - 存放项目资源文件;src/test/java - 存放测试类.java文件;src/test/resources - 存放测试资源文件;target - 项目输出目录;pom.xml - Maven核心文件(Project Object Model) 三、POM文件详解1.什么是pom文件POM代表工程对象模型(Project Object Model)它是使用Maven工作的基本组件,pom.xml位于工程根目录,它是Maven项目中非常重要的文件。下面就是pom.xml示例:<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/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.weiz.hellomaven</groupId> <artifactId>hello-maven-test</artifactId> <packaging>jar</packaging> <version>1.0-SNAPSHOT</version> <name>hello-maven-test</name> <url>http://maven.apache.org</url> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> </dependencies> </project> 2.Maven的坐标maven 的所有构件均通过坐标进行组织和管理。maven 的坐标通过 5 个元素进行定义,其中 groupId、artifactId、version 是必须的,packaging 是可选的(默认为jar),classifier 是不能直接定义的。groupId 这是工程组的标示,它在一个组织或项目中通常是唯一的,例如,上述项目中com.weiz.hellomaven拥有所有当前组织的项目。artifactId 当前工程标识。通常是工程的名称,如上述中的hello-maven-test。groupId和artifactId一起定位了当前项目的仓库中的位置信息version 工程版本号,如:com.weiz.hellomaven:hello-maven-test:1.0-SNAPSHOT 如果要在项目中引用其他第三方的Jar包,只需要在pom.xml中添加该Jar包的Maven坐标即可: <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.15</version> </dependency>四、编译、运行上面讲了项目的目录结构,已经如何创建项目,那么怎么项目怎么编译、运行呢?1、编译打开控制台,进入到新创建的工程的目录下,执行命令:mvn compile[INFO] Scanning for projects... [INFO] [INFO] ----------------< com.weiz.hellomaven:hello-maven-test >---------------- [INFO] Building hello-maven-test 1.0-SNAPSHOT [INFO] --------------------------------[ jar ]---------------------------------. .省略输出 . [INFO] Nothing to compile - all classes are up to date [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 1.923 s [INFO] Finished at: 2020-05-23T17:41:56+08:00 [INFO] ------------------------------------------------------------------------输出了“BUILD SUCCESS”时表示项目编译成功。项目编译成功后会在项目下生成一个target文件夹,里面存放编译后的文件。 2、运行测试类编译成功后执行mvn test命令,运行测试类:mvn test[INFO] Scanning for projects... . 省略输出 . . ------------------------------------------------------- T E S T S ------------------------------------------------------- Running com.weiz.hellomaven.AppTest Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.012 sec Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8 -Dgroovy.source.encoding=UTF-8 Results : Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 3.131 s [INFO] Finished at: 2020-05-23T17:53:57+08:00 [INFO] ------------------------------------------------------------------------ 输出信息里会显示单元测试的成功,失败数。五、Maven常用命令来看一下maven几个常用的构建命令,格式为mvn xxx。命令功能备注mvn compile编译源代码这个过程会下载工程所有依赖的jar包mvn clean清理环境清理target目录mvn test执行单元测试用例 mvn install安装jar包到本地仓库 mvn dependency:tree树型显示maven依赖关系用于排查依赖冲突的问题mvn dependency:list显示maven依赖列表 mvn package打包,将java工程打成jar包或war包 除了以上命令之外,还有之前介绍的查看maven版本的命令:mvn -v 。 最后以上,用hellomaven为例,首先介绍了如何创建maven项目、然后maven项目的结构,最后将如何编译运行maven项目。是不是特别简单。虽然简单,但这是所有开发者必学必会的基础技能!
做开发的程序员都知道,在系统开发需要各自各样的框架、工具。其中有一种工具不管你是初级程序员还是高级程序员都必须熟练掌握的,那就是项目管理工具(maven、ant、gradle)。接下来就总结Maven快速入门的系列文章,希望能帮到一些正在学习的朋友们。 一、Maven介绍1.什么是Maven?Maven 是基于项目对象模型(POM),可以通过一小段描述信息来管理项目的构建、报告和文档的软件项目管理工具。简单来说Maven 可以帮助我们更有效的管理项目。同时也是一套强大的自动化构建工具。覆盖了编译、测试、运行、清理,打包和部署整个项目构建周期。Maven 提供了仓库的概念,统一管理项目依赖的第三方jar包。最大限度的避免因环境配置不同导致编译出错的问题,比如在我的电脑上能运行,在其他电脑不能运行的尴尬问题。目前大部分互联网公司都在适用Maven 管理项目。2.Maven官网 maven官网:http://maven.apache.org/maven下载地址:https://mirror.bit.edu.cn/apache/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.zip注意:maven 依赖java环境,所以安装maven之前,需先安装jdk 环境。 二、Maven环境搭建1、maven目录介绍首先在Maven官网下载Maven程序包,然后将下载下来的maven安装包加压之后,可以看到主要有bin,boot,conf,lib等目录。如下图所示: 说明: 1.bin目录主要包含maven的运行脚本。 2.boot目录包含一个类加载器框架,maven就是通过它来加载自己使用的类库。 3.conf目录主要是存放maven的配置文件。比如:settings.xml 配置各种maven仓库。 4.lib目录包含maven运行时用到的所有类库。 2、配置maven环境变量 1)在系统属性=》环境变量中,创建M2_HOME的系统环境变量,M2_HOME : C:\Program Files\apache-maven-3.6.12)修改PATH,在PATH的最后加上:;%M2_HOME%\bin;以上两步Maven的环境变量就配置完成了。Maven一般不需要安装,只是把程序包下载下来之后,配置环境变量即可。3、验证maven安装成功接下来验证Maven是否安装成功,在命令行输入maven命令:mvn -v 。 检查Maven是否安装成功,如下图所示:我们可以看到,输出了Maven 的版本号为:3.6.1,以及Maven Home的地址。同时也输出了JDK的版本号。说明Maven已经安装成功。 最后以上,就把Maven介绍及环境搭建介绍完了,本章是maven 入门系列的第一章,后面会陆续介绍maven创建工程等。请大家关注!
前面介绍了Spring Boot项目的打包、发布和部署。我们知道Spring Boot打包时,默认是会把resource目录下的静态资源文件和配置文件统一打包到jar文件中。这样部署到生产环境中一旦需要修改配置文件,则非常麻烦。所以,在实际项目中,需要将静态文件、配置文件和jar包分离。将Jar包的依赖文件、资源文件、配置文件与Jar包分离,如下所示:如上图所示,lib目录为依赖jar包目录,html为存放配置文件和静态资源文件目录。这样如果需要修改配置文件、js、css等文件时,直接改html中的相关文件即可,无需更新打包。下面通过示例演示如何Spring Boot项目打包,实现静态文件、配置文件与jar分离!一、修改配置Spring Boot 使用Maven创建的项目能够非常轻松地实现静态文件、配置文件与jar包的分离。首先修改项目中的pom.xml文件,将pom.xml 配置文件中的节点,修改为自定义maven打包插件即可,配置示例如下:<build> <plugins> <!--定义项目的编译环境--> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> <!-- 打JAR包 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <configuration> <!-- 不打包资源文件(配置文件和依赖包分开) --> <excludes> <exclude>*.yml</exclude> <exclude>*.properties</exclude> <exclude>mapper/**</exclude> <exclude>static/**</exclude> <include>templates/**</include> </excludes> <archive> <manifest> <addClasspath>true</addClasspath> <!-- MANIFEST.MF 中 Class-Path 加入前缀 --> <classpathPrefix>lib/</classpathPrefix> <!-- jar包不包含唯一版本标识 --> <useUniqueVersions>false</useUniqueVersions> <!--指定入口类 --> <mainClass>com.weiz.example01.Example01Application</mainClass> </manifest> <manifestEntries> <!--MANIFEST.MF 中 Class-Path 加入资源文件目录 --> <Class-Path>./html/</Class-Path> </manifestEntries> </archive> <outputDirectory>${project.build.directory}</outputDirectory> </configuration> </plugin> <!-- 该插件的作用是用于复制依赖的jar包到指定的文件夹里 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <executions> <execution> <id>copy-dependencies</id> <phase>package</phase> <goals> <goal>copy-dependencies</goal> </goals> <configuration> <outputDirectory>${project.build.directory}/lib/</outputDirectory> </configuration> </execution> </executions> </plugin> <!-- 该插件的作用是用于复制指定的文件 --> <plugin> <artifactId>maven-resources-plugin</artifactId> <executions> <execution> <!-- 复制配置文件 --> <id>copy-resources</id> <phase>package</phase> <goals> <goal>copy-resources</goal> </goals> <configuration> <resources> <resource> <directory>src/main/resources</directory> <includes> <include>mapper/**</include> <include>static/**</include> <include>templates/**</include> <include>*.yml</include> <include>*.properties</include> </includes> </resource> </resources> <outputDirectory>${project.build.directory}/html</outputDirectory> </configuration> </execution> </executions> </plugin> </plugins> </build>上面的示例,看起来很复杂。其实,就实现了3个功能:(1)打包时排查src/main/resources目录下的静态文件和配置文件。(2)将项目中的依赖库拷贝到lib目录(2)将src/main/resources目录下静态文件和配置文件拷贝到target目录下。二、打包项目在项目根目录下,在控制台执行如下命令:mvn clean package -Dmaven.test.skip=true命令执行完之后,就可以看到target目录下,生成了jar包、资源文件和配置文件。jar包文件也变得非常小了。最后以上就把Spring Boot项目打包,资源文件分离介绍完了。我们可以发现生成了jar包、资源文件和配置文件。jar包文件也变得非常小。
我们知道Spring Boot使用了内嵌容器,因此它的部署方式也变得非常简单灵活,一方面可以将Spring Boot项目打包成独立的jar或者war包来运行,也可以单独打包成war包部署到Tomcat容器中运行,如果涉及到大规模的部署Jinkins成为最佳选择之一。接下来,开始介绍Spring Boot项目是如何打包、发布的。一、项目打包现在Maven、Gradle已经成了我们日常开发必不可少的构建工具,使用这些工具很容易地将项目打包成jar或者war包。下面就以Maven项目为例演示Spring Boot项目如何打包发布。1. 生成jar包Maven默认会将项目打成jar包,也可以在pom.xml文件中指定打包方式。配置示例如下:<groupId>com.weiz</groupId> <artifactId>spring-boot-package</artifactId> <version>1.0.0</version> <name>spring-boot-package</name> <!--指定打包方式--> <packaging>jar</packaging>上面的示例中,使用packaging标签知道打包方式,版本号为1.0.0。Maven打包会根据pom包中的packaging配置来决定是生成jar包或者war包。然后,在项目根目录下,在控制台执行如下命令:mvn clean package -Dmaven.test.skip=true(1)mvn clean package其实是两条命令,mvn clean是清除项目target目录下的文件,mvn package打包命令。两个命令可以一起执行。(2)-Dmaven.test.skip=true:排除测试代码后进行打包。命令执行完成后,jar包会生成到target目录下,命名一般是“项目名+版本号.jar”的形式。如下图所示。2. 生成war包Spring Boot项目既可以生成war发布,也可以生成jar包发布。那么他们有什么区别呢?jar包:通过内置tomcat运行,不需要额外安装tomcat。如需修改内置tomcat的配置,只需要在spring boot的配置文件中配置。内置tomcat没有自己的日志输出,全靠jar包应用输出日志。但是部署简单方便,适合快速部署。war包:传统的应用交付方式,需要安装tomcat,然后将war包放到waeapps目录下运行,这样可以灵活选择tomcat版本,也可以直接修改tomcat的配置,同时有自己的tomcat日志输出,可以灵活配置安全策略。相对jar包来说没那么快速方便。Spring Boot生成war包的方式和生成jar包的方式基本一样。只需要添加一些额外的配置,下面演示生成war包的方式:步骤1:修改项目中的pom.xml文件将<packaging>jar</packaging>改为<packaging>war</packaging>。示例代码如下:<groupId>com.weiz</groupId> <artifactId>spring-boot-package</artifactId> <version>1.0.0</version> <name>spring-boot-package</name> <!--指定打包方式--> <packaging>war</packaging>上面的示例中,修改packaging标签,将jar包的形式改成了war包的形式,版本号为1.0.0。步骤2:排除Tomcat部署war包在Tomcat中运行,并不需要Spring Boot自带的Tomcat组件,所以需要在pom.xml文件中排除自带的Tomcat。示例代码如下:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope> </dependency>上面的示例中,将Tomcat组件的scope属性设置为provided,这样在打包产生的war中就不会包含Tomcat相关的jar。步骤3:注册启动类在项目的启动类中继承SpringBootServletInitializer并重写configure( )方法,示例代码如下所示:@SpringBootApplication public class PackageApplication extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(PackageApplication.class); } public static void main(String[] args) { SpringApplication.run(PackageApplication.class, args); } }步骤4:生成war包生成war包的命令与jar包的方式是一样的,具体命令如下:mvn clean package -Dmaven.test.skip=true执行完成后,会在target目录下生成:项目名+版本号.war文件,将打包好的war包复制到Tomcat服务器中webapps目录下启动即可。二、运行部署以往我们开发部Web项目时非常繁琐,而使用Spring Boot开发部署一个命令就能解决,不需要再关注容器的环境问题,专心写业务代码即可。Spring Boot内嵌的内置Tomcat、Jetty等容器对项目部署带来了更多的改变,在服务器上仅仅需要几条命令即可部署项目。一般开发环境直接java -jar命令启动,正式环境需要将程序部署成服务。下面开始演示Spring Boot项目是如何运行、部署的。1. 启动运行简单就是直接启动jar包。启动jar包命令如下:java -jar spring-boot-package-1.0.0.jar这种方式是前台运行的,只要将控制台关闭,服务就会停止。实际生产中我们肯定不会前台运行,一般使用后台运行的方式来启动。nohup java -jar spring-boot-package-1.0.0.jar &上面的示例中,使用nohup java –jar xxx.jar &命令让程序以后台运行的方式执行。日志会被重定向到nohup.out文件中。也可以用“>filename 2>&1”来更改缺省的重定向文件名。命令如下:nohup java -jar spring-boot-package-1.0.0.jar >spring.log 2>&1 &上面的示例中,使用“>spring.log 2>&1”参数将系统的运行日志保存到spring.log中。以上就是简单的启动jar包的方式,使用简单。Spring Boot支持在启动时添加定制,比如设置应用的堆内存、垃圾回收机制、日志路径等。(1)设置jvm参数通过设置jvm的参数,优化程序的性能。java -Xms10m -Xmx80m -jar spring-boot-package-1.0.0.jar(2)选择运行环境在配置多运行环境一节中,介绍了如何配置多运行环境。那么在启动项目时,选择对应的启动环境即可:java -jar spring-boot-package-1.0.0.jar --spring.profiles.active=dev一般项目打包时指定默认的运行环境,在启动运行时也可以再次设置运行的环境。2. 生产环境部署上一节介绍的运行方式比较传统和简单的,实际生产环境中考虑到后期的运维,建议大家使用服务的方式来部署。下面通过示例演示Spring Boot项目配置成系统服务。步骤1:将之前的jar包spring-boot-package-1.0.0.jar复制到/usr/local/目录下。步骤2:进入服务文件目录,命令如下:cd /etc/systemd/system/步骤3:使用vim springbootpackage.service创建服务文件,示例代码如下:[Unit] Description=springbootpackage After=syslog.target [Service] ExecStart=/usr/java/jdk1.8.0_221-amd64/bin/java -Xmx4096m -Xms4096m -Xmn1536m -jar /usr/local/spring-boot-package-1.0.0.jar [Install] WantedBy=multi-user.target上面的示例中,主要是定义服务的名字,以及启动的命令和参数。使用只要修改Description和ExecStart即可。步骤4:启动服务// 启动服务 systemctl start springbootpackage // 停止服务 systemctl stop springbootpackage // 查看服务状态 systemctl status springbootpackage // 查看服务日志 journalctl -u springbootpackage上面的命令,通过systemctl start|stop|status springbootpackage命令启动、停止创建的springbootpackage服务。如上图所示就是使用systemctl status springbootpackage命令查看服务状态,同时还可以通过journalctl -u springbootpackage命令查看服务完整日志。此外,还需要设置服务开机启动,使用如下命令:// 开机启动 systemctl enable springbootpackage以上是打包成独立的jar包部署到服务器。如果是部署到Tomcat中,就按照Tomcat的相关命令来重新启动。最后以上,我们就把Spring Boot项目的打包和部署的方式题介绍完了。
我们知道Spring Boot使用了内嵌容器,因此它的部署方式也变得非常简单灵活,一方面可以将Spring Boot项目打包成独立的jar或者war包来运行,也可以单独打包成war包部署到Tomcat容器中运行,如果涉及到大规模的部署Jinkins成为最佳选择之一。接下来,开始介绍Spring Boot项目是如何打包、发布的。一、项目打包现在Maven、Gradle已经成了我们日常开发必不可少的构建工具,使用这些工具很容易地将项目打包成jar或者war包。下面就以Maven项目为例演示Spring Boot项目如何打包发布。1. 生成jar包Maven默认会将项目打成jar包,也可以在pom.xml文件中指定打包方式。配置示例如下:<groupId>com.weiz</groupId> <artifactId>spring-boot-package</artifactId> <version>1.0.0</version> <name>spring-boot-package</name> <!--指定打包方式--> <packaging>jar</packaging>上面的示例中,使用packaging标签知道打包方式,版本号为1.0.0。Maven打包会根据pom包中的packaging配置来决定是生成jar包或者war包。然后,在项目根目录下,在控制台执行如下命令:mvn clean package -Dmaven.test.skip=true(1)mvn clean package其实是两条命令,mvn clean是清除项目target目录下的文件,mvn package打包命令。两个命令可以一起执行。(2)-Dmaven.test.skip=true:排除测试代码后进行打包。命令执行完成后,jar包会生成到target目录下,命名一般是“项目名+版本号.jar”的形式。如下图所示。2. 生成war包Spring Boot项目既可以生成war发布,也可以生成jar包发布。那么他们有什么区别呢?jar包:通过内置tomcat运行,不需要额外安装tomcat。如需修改内置tomcat的配置,只需要在spring boot的配置文件中配置。内置tomcat没有自己的日志输出,全靠jar包应用输出日志。但是部署简单方便,适合快速部署。war包:传统的应用交付方式,需要安装tomcat,然后将war包放到waeapps目录下运行,这样可以灵活选择tomcat版本,也可以直接修改tomcat的配置,同时有自己的tomcat日志输出,可以灵活配置安全策略。相对jar包来说没那么快速方便。Spring Boot生成war包的方式和生成jar包的方式基本一样。只需要添加一些额外的配置,下面演示生成war包的方式:步骤1:修改项目中的pom.xml文件将<packaging>jar</packaging>改为<packaging>war</packaging>。示例代码如下:<groupId>com.weiz</groupId> <artifactId>spring-boot-package</artifactId> <version>1.0.0</version> <name>spring-boot-package</name> <!--指定打包方式--> <packaging>war</packaging>上面的示例中,修改packaging标签,将jar包的形式改成了war包的形式,版本号为1.0.0。步骤2:排除Tomcat部署war包在Tomcat中运行,并不需要Spring Boot自带的Tomcat组件,所以需要在pom.xml文件中排除自带的Tomcat。示例代码如下:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope> </dependency>上面的示例中,将Tomcat组件的scope属性设置为provided,这样在打包产生的war中就不会包含Tomcat相关的jar。步骤3:注册启动类在项目的启动类中继承SpringBootServletInitializer并重写configure( )方法,示例代码如下所示:@SpringBootApplication public class PackageApplication extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(PackageApplication.class); } public static void main(String[] args) { SpringApplication.run(PackageApplication.class, args); } }步骤4:生成war包生成war包的命令与jar包的方式是一样的,具体命令如下:mvn clean package -Dmaven.test.skip=true执行完成后,会在target目录下生成:项目名+版本号.war文件,将打包好的war包复制到Tomcat服务器中webapps目录下启动即可。二、运行部署以往我们开发部Web项目时非常繁琐,而使用Spring Boot开发部署一个命令就能解决,不需要再关注容器的环境问题,专心写业务代码即可。Spring Boot内嵌的内置Tomcat、Jetty等容器对项目部署带来了更多的改变,在服务器上仅仅需要几条命令即可部署项目。一般开发环境直接java -jar命令启动,正式环境需要将程序部署成服务。下面开始演示Spring Boot项目是如何运行、部署的。1. 启动运行简单就是直接启动jar包。启动jar包命令如下:java -jar spring-boot-package-1.0.0.jar这种方式是前台运行的,只要将控制台关闭,服务就会停止。实际生产中我们肯定不会前台运行,一般使用后台运行的方式来启动。nohup java -jar spring-boot-package-1.0.0.jar &上面的示例中,使用nohup java –jar xxx.jar &命令让程序以后台运行的方式执行。日志会被重定向到nohup.out文件中。也可以用“>filename 2>&1”来更改缺省的重定向文件名。命令如下:nohup java -jar spring-boot-package-1.0.0.jar >spring.log 2>&1 &上面的示例中,使用“>spring.log 2>&1”参数将系统的运行日志保存到spring.log中。以上就是简单的启动jar包的方式,使用简单。Spring Boot支持在启动时添加定制,比如设置应用的堆内存、垃圾回收机制、日志路径等。(1)设置jvm参数通过设置jvm的参数,优化程序的性能。java -Xms10m -Xmx80m -jar spring-boot-package-1.0.0.jar(2)选择运行环境在配置多运行环境一节中,介绍了如何配置多运行环境。那么在启动项目时,选择对应的启动环境即可:java -jar spring-boot-package-1.0.0.jar --spring.profiles.active=dev一般项目打包时指定默认的运行环境,在启动运行时也可以再次设置运行的环境。2. 生产环境部署上一节介绍的运行方式比较传统和简单的,实际生产环境中考虑到后期的运维,建议大家使用服务的方式来部署。下面通过示例演示Spring Boot项目配置成系统服务。步骤1:将之前的jar包spring-boot-package-1.0.0.jar复制到/usr/local/目录下。步骤2:进入服务文件目录,命令如下:cd /etc/systemd/system/步骤3:使用vim springbootpackage.service创建服务文件,示例代码如下:[Unit] Description=springbootpackage After=syslog.target [Service] ExecStart=/usr/java/jdk1.8.0_221-amd64/bin/java -Xmx4096m -Xms4096m -Xmn1536m -jar /usr/local/spring-boot-package-1.0.0.jar [Install] WantedBy=multi-user.target上面的示例中,主要是定义服务的名字,以及启动的命令和参数。使用只要修改Description和ExecStart即可。步骤4:启动服务// 启动服务 systemctl start springbootpackage // 停止服务 systemctl stop springbootpackage // 查看服务状态 systemctl status springbootpackage // 查看服务日志 journalctl -u springbootpackage上面的命令,通过systemctl start|stop|status springbootpackage命令启动、停止创建的springbootpackage服务。如上图所示就是使用systemctl status springbootpackage命令查看服务状态,同时还可以通过journalctl -u springbootpackage命令查看服务完整日志。此外,还需要设置服务开机启动,使用如下命令:// 开机启动 systemctl enable springbootpackage以上是打包成独立的jar包部署到服务器。如果是部署到Tomcat中,就按照Tomcat的相关命令来重新启动。最后以上,我们就把Spring Boot项目的打包和部署的方式题介绍完了。推荐阅读:SpringBoot从入门到精通(三十)如何使用JdbcTemplate操作数据库?SpringBoot从入门到精通(二十九)使用Redis实现分布式Session共享SpringBoot从入门到精通(二十八)JPA 的实体映射关系,一对一,一对多,多对多关系映射!SpringBoot从入门到精通(二十七)JPA实现自定义查询,完全不需要写SQL!SpringBoot从入门到精通(二十六)超级简单的数据持久化框架!Spring Data JPA 的使用!SpringBoot从入门到精通(二十五)搞懂自定义系统配置SpringBoot从入门到精通(二十四)3分钟搞定Spring Boot 多环境配置!SpringBoot从入门到精通(二十三)Mybatis系列之——实现Mybatis多数据源配置SpringBoot从入门到精通(二十二)使用Swagger2优雅构建 RESTful API文档
消息中间件在互联网公司使用得越来越多,主要用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。如上如图所示,消息队列实现系统之间的双向解耦,生产者往消息队列中发送消息,消费者从队列中拿取消息并处理,生产者不用关心是谁来消费,消费者不用关心谁在生产消息,从而达到系统解耦的目的,也大大提高了系统的高可用性和高并发能力。Spring Boot提供了spring-bootstarter-amqp组件对消息队列进行支持,使用非常简单,仅需要非常少的配置即可实现完整的消息队列服务。接下来介绍Spring Boot对RabbitMQ的支持。如何在SpringBoot项目中使用RabbitMQ?一、Spring Boot集成RabbitMQSpring Boot提供了spring-boot-starter-amqp组件,只需要简单的配置即可与Spring Boot无缝集成。下面通过示例演示集成RabbitMQ实现消息的接收和发送。第一步,配置pom包。创建Spring Boot项目并在pom.xml文件中添加spring-bootstarter-amqp等相关组件依赖:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>在上面的示例中,引入Spring Boot自带的amqp组件spring-bootstarter-amqp。第二步,修改配置文件。修改application.properties配置文件,配置rabbitmq的host地址、端口以及账户信息。spring.rabbitmq.host=10.2.1.231 spring.rabbitmq.port=5672 spring.rabbitmq.username=zhangweizhong spring.rabbitmq.password=weizhong1988 spring.rabbitmq.virtualHost=order在上面的示例中,主要配置RabbitMQ服务的地址。RabbitMQ配置由spring.rabbitmq.*配置属性控制。virtual-host配置项指定RabbitMQ服务创建的虚拟主机,不过这个配置项不是必需的。第三步,创建消费者消费者可以消费生产者发送的消息。接下来创建消费者类Consumer,并使用@RabbitListener注解来指定消息的处理方法。示例代码如下:@Component public class Consumer { @RabbitHandler @RabbitListener(queuesToDeclare = @Queue("rabbitmq_queue")) public void process(String message) { System.out.println("消费者消费消息111=====" + message); } }在上面的示例中,Consumer消费者通过@RabbitListener注解创建侦听器端点,绑定rabbitmq_queue队列。(1)@RabbitListener注解提供了@QueueBinding、@Queue、@Exchange等对象,通过这个组合注解配置交换机、绑定路由并且配置监听功能等。(2)@RabbitHandler注解为具体接收的方法。第四步,创建生产者生产者用来产生消息并进行发送,需要用到RabbitTemplate类。与之前的RedisTemplate类似,RabbitTemplate是实现发送消息的关键类。示例代码如下:@Component public class Producer { @Autowired private RabbitTemplate rabbitTemplate; public void produce() { String message = new Date() + "Beijing"; System.out.println("生产者产生消息=====" + message); rabbitTemplate.convertAndSend("rabbitmq_queue", message); } }如上面的示例所示,RabbitTemplate提供了 convertAndSend方法发送消息。convertAndSend方法有routingKey和message两个参数:(1)routingKey为要发送的路由地址。(2)message为具体的消息内容。发送者和接收者的queuename必须一致,不然无法接收。第五步,测试验证。创建对应的测试类ApplicationTests,验证消息发送和接收是否成功。@RunWith(SpringRunner.class) @SpringBootTest public class ApplicationTests { @Autowired Producer producer; @Test public void contextLoads() throws InterruptedException { producer.produce(); Thread.sleep(1*1000); } }在上面的示例中,首先注入生产者对象,然后调用produce()方法来发送消息。最后,单击Run Test或在方法上右击,选择Run 'contextLoads()',运行单元测试程序,查看后台输出情况,结果如下图所示。通过上面的程序输出日志可以看到,消费者已经收到了生产者发送的消息并进行了处理。这是常用的简单使用示例。二、发送和接收实体对象Spring Boot支持对象的发送和接收,且不需要额外的配置。下面通过一个例子来演示RabbitMQ发送和接收实体对象。1. 定义消息实体首先,定义发送与接收的对象实体User类,代码如下:public class User implements Serializable { public String name; public String password; // 省略get和set方法 }在上面的示例中,定义了普通的User实体对象。需要注意的是,实体类对象必须继承Serializable序列化接口,否则会报数据无法序列化的错误。2. 定义消费者修改Consumer类,将参数换成User对象。示例代码如下:@Component public class Consumer { @RabbitHandler @RabbitListener(queuesToDeclare = @Queue("rabbitmq_queue_object")) public void process(User user) { System.out.println("消费者消费消息111user=====name:" + user.getName()+",password:"+user.getPassword()); } }其实,消费者类和消息处理方法和之前的类似,只不过将参数换成了实体对象,监听rabbitmq_queue_object队列。3. 定义生产者修改Producer类,定义User实体对象,并通过convertAndSend方法发送对象消息。示例代码如下:@Component public class Producer { @Autowired private RabbitTemplate rabbitTemplate; public void produce() { User user=new User(); user.setName("weiz"); user.setPassword("123456"); System.out.println("生产者生产消息111=====" + user); rabbitTemplate.convertAndSend("rabbitmq_queue_object", user); } }在上面的示例中,还是调用convertAndSend()方法发送实体对象。convertAndSend()方法支持String、Integer、Object等基础的数据类型。4. 验证测试创建单元测试类,注入生产者对象,然后调用produceObj()方法发送实体对象消息,从而验证消息能否被成功接收。@RunWith(SpringRunner.class) @SpringBootTest public class ApplicationTests { @Autowired Producer producer; @Test public void testProduceObj() throws InterruptedException { producer.produceObj(); Thread.sleep(1*1000); } }最后,单击Run Test或在方法上右击,选择Run 'contextLoads()',运行单元测试程序,查看后台输出情况,运行结果如下图所示。通过上面的示例成功实现了RabbitMQ发送和接收实体对象,使得消息的数据结构更加清晰,也更加贴合面向对象的编程思想。最后以上,我们就把Spring Boot使用RabbitMQ的问题介绍完了。消息中间件在互联网公司使用得越来越多,希望大家能够熟悉其使用。推荐阅读:SpringBoot从入门到精通(三十)如何使用JdbcTemplate操作数据库?SpringBoot从入门到精通(二十九)使用Redis实现分布式Session共享SpringBoot从入门到精通(二十八)JPA 的实体映射关系,一对一,一对多,多对多关系映射!SpringBoot从入门到精通(二十七)JPA实现自定义查询,完全不需要写SQL!SpringBoot从入门到精通(二十六)超级简单的数据持久化框架!Spring Data JPA 的使用!SpringBoot从入门到精通(二十五)搞懂自定义系统配置SpringBoot从入门到精通(二十四)3分钟搞定Spring Boot 多环境配置!SpringBoot从入门到精通(二十三)Mybatis系列之——实现Mybatis多数据源配置SpringBoot从入门到精通(二十二)使用Swagger2优雅构建 RESTful API文档
前面介绍了Mybatis数据持久化框架,Mybatis虽然功能强大,但是,使用起来还是比较复杂的。所以接下来介绍一个简单的数据持久化框架——JdbcTemplate。一、什么是JdbcTemplateJDBC作为Java访问数据库的API规范,统一了各种数据库的访问方式。但是,直接在Java程序中使用JDBC还是非常复杂和繁琐的。所以Spring对JDBC进行了更深层次的封装,而JdbcTemplate就是Spring提供的一个操作数据库的便捷工具。JdbcTemplate实现了数据库连接的管理,我们可以借助JdbcTemplate来执行所有数据库操作,例如插入、更新、删除和从数据库中检索数据,并且有效避免直接使用JDBC带来的烦琐编码。Spring Boot作为Spring的集大成者,自然会将JdbcTemplate集成进去。Spring Boot针对JDBC的使用提供了对应的Starter包:spring-boot-starter-jdbc,它其实就是在Spring JDBC上做了进一步的封装,方便在 Spring Boot 项目中更好地使用JDBC。1、JdbcTemplate的特点速度快,相对于ORM框架,JDBC的方式是最快的。配置简单,Spring封装的,除了数据库连接之外,几乎没有额外的配置。使用方便,就像DBUtils工具类,只需注入JdbcTemplate对象即可。2、JdbcTemplate的几种类型的方法JdbcTemplate虽然简单,功能却非常强大。它提供了非常丰富、实用的方法,归纳起来主要有以下几种类型的方法:(1)execute方法:可以用于执行任何SQL语句,一般用于执行DDL语句。(2)update、batchUpdate方法:用于执行新增、修改与删除等语句。(3)query和queryForXXX方法:用于执行查询相关的语句。(4)call方法:用于执行数据库存储过程和函数相关的语句。总的来说,新增、删除与修改三种类型的操作主要使用update和batchUpdate方法来完成。query和queryForObject方法中主要用来完成查询功能。 execute方法可以用来执行任意的SQL、call方法来调用存储过程。二、Spring Boot集成JdbcTemplateSpring Boot集成JDBC很简单,需要引入依赖并做基础配置即可。接下来,我们就以一个具体的例子来学习如何利用Spring的JdbcTemplate进行数据库操作。第一步,添加依赖配置首先,项目pom.xml 配置文件中增加 JDBC等相关依赖:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>上面的示例,在pom.xml文件中引入spring-boot-starterjdbc依赖。同时,项目中使用 MySQL作为数据库,因此项目中需要引入MySQL驱动包。spring-boot-starter-jdbc则直接依赖于HikariCP和spring-jdbc。HikariCP是Spring Boot 2.0默认使用的数据库连接池,也是传说中最快的数据库连接池。spring-jdbc是Spring封装对JDBC操作的工具包。第二步,创建数据库及表结构首先创建jdbctest测试数据库,然后再创建student表。包括id、name、sex、age等字段,对应的SQL脚本如下:DROP TABLE IF EXISTS `student`; CREATE TABLE `student` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id', `name` varchar(32) DEFAULT NULL COMMENT '姓名', `sex` int DEFAULT NULL, `age` int DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;第三步,配置数据源在application.properties配置MYSQL数据库连接相关配置。具体配置如下:spring.datasource.url=jdbc:mysql://localhost:3306/jdbctest?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true spring.datasource.username=root spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver上面的示例,数据库连接配置非常简单,包括数据库连接地址、数据库用户名、密码以及数据驱动,无需其他额外配置。在Spring Boot 2.0中,com.mysql.jdbc.Driver已经过期,推荐使用com.mysql.cj.jdbc.Driver。第四步,使用JdbcTemplate上面已经就把JdbcTemplate整合到Spring Boot项目中,并创建好数据。接下来创建一个单元测试类JdbcTests,验证JdbcTemplate操作数据库。示例代码如下:@RunWith(SpringRunner.class) @SpringBootTest class JdbcTests { @Autowired JdbcTemplate jdbcTemplate; @Test void querytest() throws SQLException { List<Map<String, Object>> list = jdbcTemplate.queryForList("select * from student "); System.out.println(list.size()); Assert.assertNotNull(list); Assert.assertEquals(1,list.size()); } }上面是简单使用JdbcTemplate的测试示例,Spring的JdbcTemplate是自动配置的。使用@Autowired将JdbcTemplate注入到需要的bean中即可直接调用。运行Run Test或在方法上右键|Run ‘querytest’,运行测试方法。运行结果如下图所示:如上图所示,单元测试方法queryTest运行成功,并输出相应的结果。说明JdbcTemplate已经连接上数据库,并成功执行了数据查询操作。以上就把JdbcTemplate整合到Spring Boot 项目中了。三、封装Repository类第一步,创建实体类根据之前创建的Student表结构,创建对应的实体类Student。具体代码如下:public class Student { private Long id; private String name; private int sex; private int age; public Student(){ } public Student(String name, int sex, int age) { this.name = name; this.sex = sex; this.age = age; } //省略get、set方法 }需要注意,实体类的数据类型要和数据库字段一一对应。第二步,封装Repository实现增删改查首先,创建StudentRepository接口并定义常用的增删改查的接口方法,示例代码如下:public interface StudentRepository { int save(Student student); int update(Student student); int delete(long id); Student findById(long id); }上面的示例,在StudentRepository中定义了save、update、delete、findAll和findById等常用方法。然后,创建StudentRepositoryImpl类,继承StudentRepository接口,实现接口中的增删改查等方法,示例代码如下:@Repository public class StudentRepositoryImpl implements StudentRepository { @Autowired private JdbcTemplate jdbcTemplate; }上面的示例,在StudentRepositoryImpl类上使用 @Repository 注解用于标注数据访问组件JdbcTemplate,同时在类中注入 JdbcTemplate实例。接下来逐个实现对应的增删查改方法。四、实现数据增删改查(1)新增在StudentRepositoryImpl类中实现StudentRepository接口中的save()方法。示例代码如下:@Override public int save(Student student) { return jdbcTemplate.update("INSERT INTO Student(name, sex, age) values(?, ?, ?)", student.getName(),student.getSex(),student.getAge()); }在JdbcTemplate中,除了查询有几个API之外,新增、删除与修改统一都使用update来操作,传入SQL即可。update方法的返回值就是SQL执行受影响的行数。(2)修改更新和新增类似,在StudentRepositoryImpl类中实现StudentRepository接口的update()方法。示例代码如下:@Override public int update(Student student) { return jdbcTemplate.update("UPDATE Student SET name = ? , password = ? , age = ? WHERE id=?", student.getName(),student.getSex(),student.getAge(),student.getId()); }(3)删除通过用户id删除用户信息,在StudentRepositoryImpl类中实现StudentRepository接口的update()方法。示例代码如下:@Override public int delete(long id) { return jdbcTemplate.update("DELETE FROM Student where id = ? ",id); }看到这里大家可能会有疑问:怎么新增、修改、删除,都调用update方法,这跟其他的框架不一样?严格来说,新增、修改、删除都属于数据写入,通过update执行对应的SQL语句,实现对数据库中数据的变更。(4)查询根据用户id查询用户信息,同样在StudentRepositoryImpl类中实现StudentRepository接口的findById ()方法。示例代码如下:@Override public Student findById(long id) { return jdbcTemplate.queryForObject("SELECT * FROM Student WHERE id=?", new Object[] { id }, new BeanPropertyRowMapper<Student>(Student.class)); }上面的示例,JdbcTemplate执行查询相关的语句使用query方法及queryForXXX方法。查询对象使用queryForObject 方法。JdbcTemplate支持将查询结果转换为实体对象,使用new BeanPropertyRowMapper<Student>(Student.class)对返回的数据进行封装,它通过名称匹配的方式,自动将数据列映射到指定类的实体类中。在执行查询操作时,需要有一个RowMapper将查询出来的列和实体类中的属性一一对应起来:l 如果列名和属性名都是相同的,那么可以直接使用BeanPropertyRowMapper。l 如果列名和属性名不同,就需要开发者自己实现 RowMapper 接口,将数据列与实体类属性字段映射。五、如何调用接下来对封装好的StudentRepository进行测试,测试StudentRepository中的各个方法是否正确。创建StudentRepositoryTests类,将studentRepository注入到测试类中。@SpringBootTest class StudentRepositoryImplTest { @Autowired private StudentRepository studentRepository; @Test void save() { Student student =new Student("weiz",1,30); studentRepository.save(student); } @Test void update() { Student student =new Student("weiz",1,18); student.setId(1L); studentRepository.update(student); } @Test void delete() { studentRepository.delete(1L); } @Test void findById() { Student student = studentRepository.findById(1L); System.out.println("student == " + student.toString()); } }如上面的测试示例,我们依次执行测试方法,执行成功后会在数据库查看数据是否符合预期。测试执行正常,则表明StudentRepository中方法正确。最后以上,我们就把Spring Boot使用JdbcTemplate的问题介绍完了。推荐阅读:SpringBoot从入门到精通(二十九)使用Redis实现分布式Session共享SpringBoot从入门到精通(二十八)JPA 的实体映射关系,一对一,一对多,多对多关系映射!SpringBoot从入门到精通(二十七)JPA实现自定义查询,完全不需要写SQL!SpringBoot从入门到精通(二十六)超级简单的数据持久化框架!Spring Data JPA 的使用!SpringBoot从入门到精通(二十五)搞懂自定义系统配置SpringBoot从入门到精通(二十四)3分钟搞定Spring Boot 多环境配置!SpringBoot从入门到精通(二十三)Mybatis系列之——实现Mybatis多数据源配置SpringBoot从入门到精通(二十二)使用Swagger2优雅构建 RESTful API文档
前面介绍了Spring Boot如何使用Redis缓存。接下来从项目实战出发,介绍使用Redis实现Session共享。在分布式或微服务系统中,会出现这样一个问题:用户在服务器A上登录以后,假如后续的业务操作被负载均衡服务转发到服务器B上面,服务器B上没有这个用户的Session状态,就会强制让用户重新登录,导致业务无法顺利完成。因此,这就需要将Session进行共享,保证每个系统都能获取用户的Session状态。一、分布式Session共享解决方案目前主流的分布式Session共享主要有以下几种解决方案:客户端存储,使用Cookie来完成,其缺点是不安全、不可靠。Session绑定,使用Nginx中的IP绑定策略,同一个IP指定访问同一个机器,其缺点是容易造成单点故障。如果某一台服务器宕机,那么该台服务器上的Session信息将会丢失。Session同步,使用tomcat内置的Session同步,其缺点是同步可能会产生延迟。Session共享,将Session存储在Redis等缓存中间件中。以上解决方案各有优缺点,其中,比较流行的是使用Redis等缓存中间件的Session共享解决方案。将所有的Session会话信息存入Redis缓存中,然后Web应用从Redis中取出Session信息实现所有应用的Session共享。具体示意图如下图所示。从上图可以看出,所有的服务都将Session的信息存储到Redis中,无论是对Session的注销、更新都会同步到Redis中,从而达到Session共享的目的。二 、使用Redis实现Session共享前面介绍了使用Redis实现Session共享的解决方案。下面通过示例演示使用Redis实现Session信息存储,并实现多系统的Session信息共享。1.引入依赖<dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency> <!-- 引入 redis 依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>上面的示例中,引入除了Redis组件外,还需要引入spring-session-data-redis依赖。通过此组件实现session信息的管理。2.添加Session配置类创建SessionConfig配置类,配置打开Session,示例代码如下:@Configuration @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 86400*30) public class SessionConfig { }上面的示例,配置Session的缓存时间。maxInactiveIntervalInSeconds:设置Session失效时间,使用Redis共享Session之后,原Spring Boot的server.session.timeout属性不再生效。经过上面的配置后,Session调用就会自动去Redis上存取。另外,想要达到Session共享的目的,只需要在其他的系统上做同样的配置即可。三、测试验证首先,增加Session的测试方法。@RequestMapping("/uid") String uid(HttpSession session) { UUID uid = (UUID) session.getAttribute("uid"); if (uid == null) { uid = UUID.randomUUID(); } session.setAttribute("uid", uid); return session.getId(); }然后,启动项目,运行一个程序实例,启动端口号为8080,在浏览器中输入地址:“http://localhost:8080/uid”,页面返回会话的sessionId:我们可以登录Redis客户端,查看session是否已经保存到Redis,输入“keys '*sessions*'”查看所有的Session信息:从上面的输出可以看到,sessionId是7433a35d-a086-4b7d-bb64-37cf8b4e18f7,与页面返回的sessionId一致。说明Redis中缓存的SessionId和实际使用的Session一致,Session已经在Redis中进行有效的管理。最后,我们模拟分布式系统,再启动一个程序实例,启动端口号为8081,在浏览器中输入“http://localhost:8081/uid”,页面返回会话的SessionId为:从输出结果可以看到,程序实例1和程序实例2获取到的是同一个Session,这说明两个程序实现了Session共享。最后以上,我们就把Spring Boot使用Redis实现Session共享的问题介绍完了。Session共享是分布式和微服务的基础,通过Redis可以快速实现各服务、各系统之间的Session共享。推荐阅读:SpringBoot从入门到精通(二十八)JPA 的实体映射关系,一对一,一对多,多对多关系映射!SpringBoot从入门到精通(二十七)JPA实现自定义查询,完全不需要写SQL!SpringBoot从入门到精通(二十六)超级简单的数据持久化框架!Spring Data JPA 的使用!SpringBoot从入门到精通(二十五)搞懂自定义系统配置SpringBoot从入门到精通(二十四)3分钟搞定Spring Boot 多环境配置!SpringBoot从入门到精通(二十三)Mybatis系列之——实现Mybatis多数据源配置SpringBoot从入门到精通(二十二)使用Swagger2优雅构建 RESTful API文档
前面讲了Spring Boot 使用 JPA,实现JPA 的增、删、改、查的功能,同时也介绍了JPA的一些查询,自定义SQL查询等使用。JPA使用非常简单,功能非常强大的ORM框架,无需任何数据访问层和sql语句即可实现完整的数据操作方法。但是,之前都是介绍的单表的增删改查等操作,多表多实体的数据操作怎么实现呢?接下来聊一聊 JPA 的一对一,一对多,多对一,多对多等实体映射关系。 一、常用注解详解1、实体定义注解(1) @JoinColumn指定该实体类对应的表中引用的表的外键,name属性指定外键名称,referencedColumnName指定应用表中的字段名称(2) @JoinColumn(name=”role_id”): 标注在连接的属性上(一般多对一中的‘一’方),指定了本类的外键名叫什么。(3) @JoinTable(name="permission_role") :标注在连接的属性上(一般多对多),指定了多对多的中间表叫什么。备注:Join的标注,和下面几个标注的mappedBy属性互斥!2、关系映射注解(1) @OneToOne 配置一对一关联,属性targetEntity指定关联的对象的类型 。(2) @OneToMany注解“一对多”关系中‘一’方的实体类属性(该属性是一个集合对象),targetEntity注解关联的实体类类型,mappedBy注解另一方实体类中本实体类的属性名称(3)@ManyToOne注解“一对多”关系中‘多’方的实体类属性(该属性是单个对象),targetEntity注解关联的实体类类型属性1: mappedBy="permissions" 表示,当前类不维护状态,属性值其实是本类在被标注的链接属性上的链接属性,此案例的本类时Permission,连接属性是roles,连接属性的类的连接属性是permissions 属性2: fetch = FetchType.LAZY 表示是不是懒加载,默认是,可以设置成FetchType.EAGER属性3:cascade=CascadeType.ALL 表示当前类操作时,被标注的连接属性如何级联,比如班级和学生是一对多关系,cascade标注在班级类中,那么执行班级的save操作的时候(班级.学生s.add(学生)),能级联保存学生,否则报错,需要先save学生,变成持久化对象,在班级.学生s.add(学生)注意:只有OneToOne,OneToMany,ManyToMany上才有mappedBy属性,ManyToOne不存在该属性; 二、一对一首先,一对一的实体关系最常用的场景就是主表与从表,即主表存关键经常使用的字段,从表保存非关键字段,类似 User与UserDetail 的关系。主表和详细表通过外键一一映射。一对一的映射关系通过@OneToOne 注解实现。通过 @JoinColumn 配置一对一关系。其实,一对一有好几种,这里举例的是常用的一对一双向外键关联(改造成单向很简单,在对应的实体类去掉要关联其它实体的属性即可),并且配置了级联删除和添加,相关类如下:1、User 实体类定义:package com.weiz.pojo; import lombok.Getter; import lombok.Setter; import javax.persistence.*; @Getter @Setter @Entity @Table(name = "Users") public class Users { @Id @GeneratedValue private Long id; private String name; private String account; private String pwd; @OneToOne(cascade = {CascadeType.PERSIST,CascadeType.REMOVE}) @JoinColumn(name="detailId",referencedColumnName = "id") private UsersDetail userDetail; @Override public String toString() { return String.format("Book [id=%s, name=%s, user detail=%s]", id, userDetail.getId()); } }上面的示例中,@OneToOne注解关联实体映射,关联的实体的主键一般是用来做外键的。但如果此时不想主键作为外键,则需要设置referencedColumnName属性。当然这里关联实体(Address)的主键 id 是用来做主键,所以这里第20行的 referencedColumnName = "id" 实际可以省略。 2、从表 UserDetail 实体类定义package com.weiz.pojo; import lombok.Getter; import lombok.Setter; import javax.persistence.*; @Getter @Setter @Entity @Table(name = "UsersDetail") public class UsersDetail { @Id @GeneratedValue private Long id; @Column(name = "address") private String address; @Column(name = "age") private Integer age; @Override public String toString() { return String.format("UsersDetail [id=%s, address=%s, age=%s]", id,address,age); } }代码说明:子类无需任何定义,关系均在主类中维护。 3、验证测试创建单元测试方法,验证一对一关系的保存和查询功能。@Test public void testOneToOne(){ // 用户 User user = new User(); user.setName("one2one"); user.setPassword("123456"); user.setAge(20); // 详情 UserDetail userDetail = new UserDetail(); userDetail.setAddress("beijing,haidian,"); // 保存用户和详情 user.setUserDetail(userDetail); userRepository.save(user); User result = userRepository.findById(7L).get(); System.out.println("name:"+result.getName()+",age:"+result.getAge()+", address:"+result.getUserDetail().getAddress()); } 单击Run Test或在方法上右击,选择Run 'testOneToOne',运行单元测试方法,结果如下图所示。结果表明创建的单元测试运行成功,用户信息(User)和用户详细信息(UserDetail)保存成功,实现了一对一实体的级联保存和关联查询。二、一对多和对多对一一对多和多对一的关系映射,最常见的场景就是:人员角色关系。实体Users:人员。 实体 Roles:角色。 人员 和角色是一对多关系(双向)。那么在JPA中,如何表示一对多的双向关联呢?JPA使用@OneToMany和@ManyToOne来标识一对多的双向关联。一端(Roles)使用@OneToMany,多端(Users)使用@ManyToOne。在JPA规范中,一对多的双向关系由多端(Users)来维护。也就是说多端(Users)为关系维护端,负责关系的增删改查。一端(Roles)则为关系被维护端,不能维护关系。 一端(Roles)使用@OneToMany注释的mappedBy="role"属性表明Author是关系被维护端。 多端(Users)使用@ManyToOne和@JoinColumn来注释属性 role,@ManyToOne表明Article是多端,@JoinColumn设置在Users表中的关联字段(外键)。 1、原先的User 实体类修改如下:package com.weiz.pojo; import lombok.Getter; import lombok.Setter; import javax.persistence.*; @Getter @Setter @Entity @Table(name = "Users") public class Users { @Id @GeneratedValue private Long id; private String name; private String account; private String pwd; @OneToOne(cascade = {CascadeType.PERSIST,CascadeType.REMOVE}) @JoinColumn(name="detailId",referencedColumnName = "id") private UsersDetail userDetail; /**一对多,多的一方必须维护关系,即不能指定mapped=""**/ @ManyToOne(fetch = FetchType.LAZY,cascade=CascadeType.MERGE) @JoinColumn(name="role_id") private Roles role; @Override public String toString() { return String.format("Book [id=%s, name=%s, user detail=%s]", id, userDetail.getId()); } }2、角色实体类package com.weiz.pojo; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import java.util.HashSet; import java.util.Set; @Getter @Setter @Entity @Table(name = "Roles") public class Roles { @Id @GeneratedValue() private Long id; private String name; @OneToMany(mappedBy="role",fetch=FetchType.LAZY,cascade=CascadeType.ALL) private Set<Users> users = new HashSet<Users>(); }其中 @OneToMany 和 @ManyToOne 用得最多,这里再补充一下 关于级联,一定要注意,要在关系的维护端,即 One 端。比如 人员和角色,角色是One,人员是Many;cascade = CascadeType.ALL 只能写在 One 端,只有One端改变Many端,不准Many端改变One端。 特别是删除,因为 ALL 里包括更新,删除。如果删除一条评论,就把文章删了,那算谁的。所以,在使用的时候要小心。一定要在 One 端使用。最终生成的表结构 Users 表中会增加role_id 字段。 3、验证测试@Test public void testOneToMany() { // 保存角色 Role role = new Role(); role.setId(3L); role.setName("管理员"); roleRepository.save(role); // 修改人员角色 User user = userRepository.findById(7L).orElse(null); Role admin = roleRepository.findById(3L).orElse(null); if (user!=null){ user.setRole(admin); } userRepository.save(user); User result = userRepository.findById(7L).get(); System.out.println("name:"+result.getName()+",age:"+result.getAge()+", role:"+result.getRole().getName()); } 特别注意的是更新和删除的级联操作。单击Run Test或在方法上右击,选择Run 'testOneToMany',运行单元测试方法,结果下图所示。 三、多对多 多对多的映射关系最常见的场景就是:权限和角色关系。角色和权限是多对多的关系。一个角色可以有多个权限,一个权限也可以被很多角色拥有。 JPA中使用@ManyToMany来注解多对多的关系,由一个关联表来维护。这个关联表的表名默认是:主表名+下划线+从表名。(主表是指关系维护端对应的表,从表指关系被维护端对应的表)。这个关联表只有两个外键字段,分别指向主表ID和从表ID。字段的名称默认为:主表名+下划线+主表中的主键列名,从表名+下划线+从表中的主键列名。 需要注意的:1、多对多关系中一般不设置级联保存、级联删除、级联更新等操作。2、可以随意指定一方为关系维护端,在这个例子中,我指定 User 为关系维护端,所以生成的关联表名称为: role_permission,关联表的字段为:role_id 和 permission_id。3、多对多关系的绑定由关系维护端来完成,即由 role1.setPermissions(ps);来绑定多对多的关系。关系被维护端不能绑定关系,即permission不能绑定关系。4、多对多关系的解除由关系维护端来完成,即由 role1.getPermissions().remove(permission);来解除多对多的关系。关系被维护端不能解除关系,即permission不能解除关系。5、如果Role和Permission已经绑定了多对多的关系,那么不能直接删除Permission,需要由Role解除关系后,才能删除Permission。但是可以直接删除Role,因为Role是关系维护端,删除Role时,会先解除Role和Permission的关系,再删除Role。下面,看看角色Roles 和 权限 Permissions 的多对多的映射关系实现,具体代码如下:1、角色Roles 实体类定义:package com.weiz.pojo; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import java.util.HashSet; import java.util.Set; @Getter @Setter @Entity @Table(name = "Roles") public class Roles { @Id @GeneratedValue() private Long id; private String name; @ManyToMany(cascade = CascadeType.MERGE,fetch = FetchType.LAZY) @JoinTable(name="permission_role") private Set<Permissions> permissions = new HashSet<Permissions>(); @OneToMany(mappedBy="role",fetch=FetchType.LAZY,cascade=CascadeType.ALL) private Set<Users> users = new HashSet<Users>(); }代码说明:cascade表示级联操作,all是全部,一般用MERGE 更新,persist表示持久化即新增此类是维护关系的类,删除它,可以删除对应的外键,但是如果需要删除对应的权限就需要CascadeType.allcascade:作用在本放,对于删除或其他操作本方时,对标注连接方的影响!和数据库一样!! 2、权限Permissions 实体类定义:package com.weiz.pojo; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import java.util.Set; /** * 权限表 */ @Getter @Setter @Entity @Table(name="Permissions") public class Permissions { @Id @GeneratedValue private Long id; private String name; private String type; private String url; @Column(name="perm_code") private String permCode; @ManyToMany(mappedBy="permissions",fetch = FetchType.LAZY) private Set<Roles> roles; }注意:不能两边用mappedBy:这个属性就是维护关系的意思!谁主类有此属性谁不维护关系。比如两个多对多的关系是由role中的permissions维护的,那么,只有操作role实体对象时,指定permissions,才可建立外键的关系。只有OneToOne,OneToMany,ManyToMany上才有mappedBy属性,ManyToOne不存在该属性; 并且mappedBy一直和joinXX互斥。注解中属性的汉语解释:权限不维护关系,关系表是permission_role,全部懒加载,角色的级联是更新 (多对多关系不适合用all,不然删除一个角色,那么所有此角色对应的权限都被删了,级联删除一般用于部分一对多时业务需求上是可删的,比如品牌类型就不适合删除一个类型就级联删除所有的品牌,一般是把此品牌的类型设置为null(解除关系),然后执行删除,就不会报错了!)3、验证测试@Test public void testManyToMany(){ // 角色 Roles role1 = new Roles(); role1.setName("admin role"); // 角色赋权限 Set<Permissions> ps = new HashSet<Permissions>(); for (int i = 0; i < 3; i++) { Permission pm = new Permission(); pm.setName("permission"+i); permissionRespository.save(pm); /**由于Role类没有设置级联持久化,因此这里需要先持久化pm,否则报错!*/ ps.add(pm); } role1.setPermissions(ps); // 保存 roleRespository.save(role1); } 配置说明:由于多对一不能用mapped,那么它必然必须维护关系,维护关系是多的一方由User维护的,User的级联是更新,Role的级联是All,User的外键是role_id指向Role。最后维护关系是由mapped属性决定,标注在那,那个就不维护关系。级联操作是作用于当前类的操作发生时,对关系类进行级联操作。和hibernate使用没多大区别啊!推荐阅读:SpringBoot从入门到精通(二十七)JPA实现自定义查询,完全不需要写SQL!SpringBoot从入门到精通(二十六)超级简单的数据持久化框架!Spring Data JPA 的使用!SpringBoot从入门到精通(二十五)搞懂自定义系统配置SpringBoot从入门到精通(二十四)3分钟搞定Spring Boot 多环境配置!SpringBoot从入门到精通(二十三)Mybatis系列之——实现Mybatis多数据源配置SpringBoot从入门到精通(二十二)使用Swagger2优雅构建 RESTful API文档
前面讲了Spring Boot 使用 JPA,实现JPA 的增、删、改、查的功能,同时也介绍了JPA的一些查询,自定义SQL查询等使用。JPA使用非常简单,功能非常强大的ORM框架,无需任何数据访问层和sql语句即可实现完整的数据操作方法。但是,之前都是介绍的单表的增删改查等操作,多表多实体的数据操作怎么实现呢?接下来聊一聊 JPA 的一对一,一对多,多对一,多对多等实体映射关系。 一、常用注解详解1、实体定义注解(1) @JoinColumn指定该实体类对应的表中引用的表的外键,name属性指定外键名称,referencedColumnName指定应用表中的字段名称(2) @JoinColumn(name=”role_id”): 标注在连接的属性上(一般多对一中的‘一’方),指定了本类的外键名叫什么。(3) @JoinTable(name="permission_role") :标注在连接的属性上(一般多对多),指定了多对多的中间表叫什么。备注:Join的标注,和下面几个标注的mappedBy属性互斥!2、关系映射注解(1) @OneToOne 配置一对一关联,属性targetEntity指定关联的对象的类型 。(2) @OneToMany注解“一对多”关系中‘一’方的实体类属性(该属性是一个集合对象),targetEntity注解关联的实体类类型,mappedBy注解另一方实体类中本实体类的属性名称(3)@ManyToOne注解“一对多”关系中‘多’方的实体类属性(该属性是单个对象),targetEntity注解关联的实体类类型属性1: mappedBy="permissions" 表示,当前类不维护状态,属性值其实是本类在被标注的链接属性上的链接属性,此案例的本类时Permission,连接属性是roles,连接属性的类的连接属性是permissions 属性2: fetch = FetchType.LAZY 表示是不是懒加载,默认是,可以设置成FetchType.EAGER属性3:cascade=CascadeType.ALL 表示当前类操作时,被标注的连接属性如何级联,比如班级和学生是一对多关系,cascade标注在班级类中,那么执行班级的save操作的时候(班级.学生s.add(学生)),能级联保存学生,否则报错,需要先save学生,变成持久化对象,在班级.学生s.add(学生)注意:只有OneToOne,OneToMany,ManyToMany上才有mappedBy属性,ManyToOne不存在该属性; 二、一对一首先,一对一的实体关系最常用的场景就是主表与从表,即主表存关键经常使用的字段,从表保存非关键字段,类似 User与UserDetail 的关系。主表和详细表通过外键一一映射。一对一的映射关系通过@OneToOne 注解实现。通过 @JoinColumn 配置一对一关系。其实,一对一有好几种,这里举例的是常用的一对一双向外键关联(改造成单向很简单,在对应的实体类去掉要关联其它实体的属性即可),并且配置了级联删除和添加,相关类如下:1、User 实体类定义:package com.weiz.pojo; import lombok.Getter; import lombok.Setter; import javax.persistence.*; @Getter @Setter @Entity @Table(name = "Users") public class Users { @Id @GeneratedValue private Long id; private String name; private String account; private String pwd; @OneToOne(cascade = {CascadeType.PERSIST,CascadeType.REMOVE}) @JoinColumn(name="detailId",referencedColumnName = "id") private UsersDetail userDetail; @Override public String toString() { return String.format("Book [id=%s, name=%s, user detail=%s]", id, userDetail.getId()); } }上面的示例中,@OneToOne注解关联实体映射,关联的实体的主键一般是用来做外键的。但如果此时不想主键作为外键,则需要设置referencedColumnName属性。当然这里关联实体(Address)的主键 id 是用来做主键,所以这里第20行的 referencedColumnName = "id" 实际可以省略。 2、从表 UserDetail 实体类定义package com.weiz.pojo; import lombok.Getter; import lombok.Setter; import javax.persistence.*; @Getter @Setter @Entity @Table(name = "UsersDetail") public class UsersDetail { @Id @GeneratedValue private Long id; @Column(name = "address") private String address; @Column(name = "age") private Integer age; @Override public String toString() { return String.format("UsersDetail [id=%s, address=%s, age=%s]", id,address,age); } }代码说明:子类无需任何定义,关系均在主类中维护。 3、验证测试创建单元测试方法,验证一对一关系的保存和查询功能。@Test public void testOneToOne(){ // 用户 User user = new User(); user.setName("one2one"); user.setPassword("123456"); user.setAge(20); // 详情 UserDetail userDetail = new UserDetail(); userDetail.setAddress("beijing,haidian,"); // 保存用户和详情 user.setUserDetail(userDetail); userRepository.save(user); User result = userRepository.findById(7L).get(); System.out.println("name:"+result.getName()+",age:"+result.getAge()+", address:"+result.getUserDetail().getAddress()); } 单击Run Test或在方法上右击,选择Run 'testOneToOne',运行单元测试方法,结果如下图所示。结果表明创建的单元测试运行成功,用户信息(User)和用户详细信息(UserDetail)保存成功,实现了一对一实体的级联保存和关联查询。二、一对多和对多对一一对多和多对一的关系映射,最常见的场景就是:人员角色关系。实体Users:人员。 实体 Roles:角色。 人员 和角色是一对多关系(双向)。那么在JPA中,如何表示一对多的双向关联呢?JPA使用@OneToMany和@ManyToOne来标识一对多的双向关联。一端(Roles)使用@OneToMany,多端(Users)使用@ManyToOne。在JPA规范中,一对多的双向关系由多端(Users)来维护。也就是说多端(Users)为关系维护端,负责关系的增删改查。一端(Roles)则为关系被维护端,不能维护关系。 一端(Roles)使用@OneToMany注释的mappedBy="role"属性表明Author是关系被维护端。 多端(Users)使用@ManyToOne和@JoinColumn来注释属性 role,@ManyToOne表明Article是多端,@JoinColumn设置在Users表中的关联字段(外键)。 1、原先的User 实体类修改如下:package com.weiz.pojo; import lombok.Getter; import lombok.Setter; import javax.persistence.*; @Getter @Setter @Entity @Table(name = "Users") public class Users { @Id @GeneratedValue private Long id; private String name; private String account; private String pwd; @OneToOne(cascade = {CascadeType.PERSIST,CascadeType.REMOVE}) @JoinColumn(name="detailId",referencedColumnName = "id") private UsersDetail userDetail; /**一对多,多的一方必须维护关系,即不能指定mapped=""**/ @ManyToOne(fetch = FetchType.LAZY,cascade=CascadeType.MERGE) @JoinColumn(name="role_id") private Roles role; @Override public String toString() { return String.format("Book [id=%s, name=%s, user detail=%s]", id, userDetail.getId()); } }2、角色实体类package com.weiz.pojo; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import java.util.HashSet; import java.util.Set; @Getter @Setter @Entity @Table(name = "Roles") public class Roles { @Id @GeneratedValue() private Long id; private String name; @OneToMany(mappedBy="role",fetch=FetchType.LAZY,cascade=CascadeType.ALL) private Set<Users> users = new HashSet<Users>(); }其中 @OneToMany 和 @ManyToOne 用得最多,这里再补充一下 关于级联,一定要注意,要在关系的维护端,即 One 端。比如 人员和角色,角色是One,人员是Many;cascade = CascadeType.ALL 只能写在 One 端,只有One端改变Many端,不准Many端改变One端。 特别是删除,因为 ALL 里包括更新,删除。如果删除一条评论,就把文章删了,那算谁的。所以,在使用的时候要小心。一定要在 One 端使用。最终生成的表结构 Users 表中会增加role_id 字段。 3、验证测试@Test public void testOneToMany() { // 保存角色 Role role = new Role(); role.setId(3L); role.setName("管理员"); roleRepository.save(role); // 修改人员角色 User user = userRepository.findById(7L).orElse(null); Role admin = roleRepository.findById(3L).orElse(null); if (user!=null){ user.setRole(admin); } userRepository.save(user); User result = userRepository.findById(7L).get(); System.out.println("name:"+result.getName()+",age:"+result.getAge()+", role:"+result.getRole().getName()); } 特别注意的是更新和删除的级联操作。单击Run Test或在方法上右击,选择Run 'testOneToMany',运行单元测试方法,结果下图所示。 三、多对多 多对多的映射关系最常见的场景就是:权限和角色关系。角色和权限是多对多的关系。一个角色可以有多个权限,一个权限也可以被很多角色拥有。 JPA中使用@ManyToMany来注解多对多的关系,由一个关联表来维护。这个关联表的表名默认是:主表名+下划线+从表名。(主表是指关系维护端对应的表,从表指关系被维护端对应的表)。这个关联表只有两个外键字段,分别指向主表ID和从表ID。字段的名称默认为:主表名+下划线+主表中的主键列名,从表名+下划线+从表中的主键列名。 需要注意的:1、多对多关系中一般不设置级联保存、级联删除、级联更新等操作。2、可以随意指定一方为关系维护端,在这个例子中,我指定 User 为关系维护端,所以生成的关联表名称为: role_permission,关联表的字段为:role_id 和 permission_id。3、多对多关系的绑定由关系维护端来完成,即由 role1.setPermissions(ps);来绑定多对多的关系。关系被维护端不能绑定关系,即permission不能绑定关系。4、多对多关系的解除由关系维护端来完成,即由 role1.getPermissions().remove(permission);来解除多对多的关系。关系被维护端不能解除关系,即permission不能解除关系。5、如果Role和Permission已经绑定了多对多的关系,那么不能直接删除Permission,需要由Role解除关系后,才能删除Permission。但是可以直接删除Role,因为Role是关系维护端,删除Role时,会先解除Role和Permission的关系,再删除Role。下面,看看角色Roles 和 权限 Permissions 的多对多的映射关系实现,具体代码如下:1、角色Roles 实体类定义:package com.weiz.pojo; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import java.util.HashSet; import java.util.Set; @Getter @Setter @Entity @Table(name = "Roles") public class Roles { @Id @GeneratedValue() private Long id; private String name; @ManyToMany(cascade = CascadeType.MERGE,fetch = FetchType.LAZY) @JoinTable(name="permission_role") private Set<Permissions> permissions = new HashSet<Permissions>(); @OneToMany(mappedBy="role",fetch=FetchType.LAZY,cascade=CascadeType.ALL) private Set<Users> users = new HashSet<Users>(); }代码说明:cascade表示级联操作,all是全部,一般用MERGE 更新,persist表示持久化即新增此类是维护关系的类,删除它,可以删除对应的外键,但是如果需要删除对应的权限就需要CascadeType.allcascade:作用在本放,对于删除或其他操作本方时,对标注连接方的影响!和数据库一样!! 2、权限Permissions 实体类定义:package com.weiz.pojo; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import java.util.Set; /** * 权限表 */ @Getter @Setter @Entity @Table(name="Permissions") public class Permissions { @Id @GeneratedValue private Long id; private String name; private String type; private String url; @Column(name="perm_code") private String permCode; @ManyToMany(mappedBy="permissions",fetch = FetchType.LAZY) private Set<Roles> roles; }注意:不能两边用mappedBy:这个属性就是维护关系的意思!谁主类有此属性谁不维护关系。比如两个多对多的关系是由role中的permissions维护的,那么,只有操作role实体对象时,指定permissions,才可建立外键的关系。只有OneToOne,OneToMany,ManyToMany上才有mappedBy属性,ManyToOne不存在该属性; 并且mappedBy一直和joinXX互斥。注解中属性的汉语解释:权限不维护关系,关系表是permission_role,全部懒加载,角色的级联是更新 (多对多关系不适合用all,不然删除一个角色,那么所有此角色对应的权限都被删了,级联删除一般用于部分一对多时业务需求上是可删的,比如品牌类型就不适合删除一个类型就级联删除所有的品牌,一般是把此品牌的类型设置为null(解除关系),然后执行删除,就不会报错了!)3、验证测试@Test public void testManyToMany(){ // 角色 Roles role1 = new Roles(); role1.setName("admin role"); // 角色赋权限 Set<Permissions> ps = new HashSet<Permissions>(); for (int i = 0; i < 3; i++) { Permission pm = new Permission(); pm.setName("permission"+i); permissionRespository.save(pm); /**由于Role类没有设置级联持久化,因此这里需要先持久化pm,否则报错!*/ ps.add(pm); } role1.setPermissions(ps); // 保存 roleRespository.save(role1); } 配置说明:由于多对一不能用mapped,那么它必然必须维护关系,维护关系是多的一方由User维护的,User的级联是更新,Role的级联是All,User的外键是role_id指向Role。最后维护关系是由mapped属性决定,标注在那,那个就不维护关系。级联操作是作用于当前类的操作发生时,对关系类进行级联操作。和hibernate使用没多大区别啊!推荐阅读:SpringBoot从入门到精通(二十七)JPA实现自定义查询,完全不需要写SQL!SpringBoot从入门到精通(二十六)超级简单的数据持久化框架!Spring Data JPA 的使用!SpringBoot从入门到精通(二十五)搞懂自定义系统配置SpringBoot从入门到精通(二十四)3分钟搞定Spring Boot 多环境配置!SpringBoot从入门到精通(二十三)Mybatis系列之——实现Mybatis多数据源配置SpringBoot从入门到精通(二十二)使用Swagger2优雅构建 RESTful API文档
前面讲了Spring Boot 整合Spring Boot JPA,实现JPA 的增、删、改、查的功能。JPA使用非常简单,只需继承JpaRepository ,无需任何数据访问层和sql语句即可实现完整的数据操作方法。JPA除了这些功能和优势之外,还有非常强大的查询的功能。以前复查的查询都需要拼接很多查询条件,JPA 有非常方便和优雅的方式来解决。Spring Data JPA 查询分为三种:1、Spring Data JPA 默认实现的预定义的方法2、需要根据查询的情况定义查询条件3、通过@Query 注解,自定义hql 查询语句接下来就聊一聊JPA 自定义查询,体验Spring Data JPA 的强大。一、预定义查询预定义方法就是我们上面看到的那些自带的方法,因为UserRepository继承了 JpaRepository 拥有了父类的这些JPA自带的方法。如下图所示:我们看到,JpaRepository类默认自带了很多数据操作方法,包括save、delete、findAll、fidById、等方法。调用这些预定义的预定义方法也非常简单,具体如下所示:@RequestMapping("/test") public void test() { Users user = new Users(); user.setId((long) 1); userRespository.findById((long) 1); userRespository.findAll(); userRespository.delete(user); userRespository.deleteById((long) 1); userRespository.existsById((long) 1); }上面所有JpaRepository父类拥有的方法都可以直接调用 。二、自定义查询Spring Data JPA 支持根据实体的某个属性实现数据库操作,主要的语法是 findByXX、 readAByXX、queryByXX、 countByXX、 getByXX 后跟属性名称。利用这个功能仅需要在定义的 Repository 中添加对应的方法名即可,无需具体实现完整的方法,使用时 Spring Boot 会自动动帮我们实现对应的sql语句。1、属性查询根据姓名查询,示例如下:@Repository public interface UserRespository extends JpaRepository<Users, Long> { Users findByName(String name,String account); }上面的实例可以看到,我们可以在UserRepository 接口中进行接口声明。例如,如果想根据实体的 name和account 这两个属性来进行查询User的信息。那么直接在 UserRepository 中增加一个接口声明即可。2、组合条件查询JPA不仅支持单个属性查询,还能支持多个属性,根据And、or 等关键字组合查询:Users findByNameAndAccount(String name,String account);上面的例子,就是根据姓名和账号两个条件组合查询。这个是组合查询的例子,删除和统计也是类似的:deleteByXXAndXX、countByXXAndXX。可以根据查询的条件不断地添加和拼接, Spring Boot 都可以正确解析和执行。3、JPA关键字JPA的自定义查询除了And、or 关键字外,基本上SQL语法中的关键字,JPA都支持,比如:like,between 等。这个语句结构可以用下面的表来说明:关 键 字示例方法JPQL语句AndfindByLastnameAndFirstname… where x.lastname = ?1 and x.firstname = ?2OrfindByLastnameOrFirstname… where x.lastname = ?1 or x.firstname = ?2Is,EqualsfindByFirstname,findByFirstnameIs,findByFirstnameEquals… where x.firstname = ?1BetweenfindByStartDateBetween… where x.startDate between ?1 and ?2LessThanfindByAgeLessThan… where x.age < ?1LessThanEqualfindByAgeLessThanEqual… where x.age <= ?1GreaterThanfindByAgeGreaterThan… where x.age > ?1GreaterThanEqualfindByAgeGreaterThanEqual… where x.age >= ?1AfterfindByStartDateAfter… where x.startDate > ?1BeforefindByStartDateBefore… where x.startDate < ?1IsNullfindByAgeIsNull… where x.age is nullIsNotNull,NotNullfindByAge(Is)NotNull… where x.age not nullLikefindByFirstnameLike… where x.firstname like ?1NotLikefindByFirstnameNotLike… where x.firstname not like ?1三、自定义SQL语句上面介绍了JPA的很多条件查询的方法。但是,实际项目中,还是有些场景上面的查询条件无法满足。那么我们就可以通过 @Query 注解写hql 来实现。@Query("select u from Users u where u.name = :name1") List<UserDO> findByHql(@Param("name1") String name1);说明: 1、@Query 注解,表示用执行hql语句。 2、name1等参数对应定义的参数。上面是通过hql,如果hql 写着不习惯,也可以用本地 SQL 语句来完成查询:@Query(value = "select * from users where name = ?1",nativeQuery = true) List<User> findUserBySql(String name);上面示例中的 ?1 表示方法参数中的顺序,nativeQuery = true 表示执行原生sql语句。四、已命名查询除了使用 @Query注解外,还可以使用@NamedQuery与@NameQueries等注解定义命名查询。JPA的命名查询实际上就是给SQL查询语句起一个名字,执行查询时就是直接使用起的名字,避免重复写JPQL语句,使得查询方法能够复用。下面通过示例程序演示JPA已命名查询。1. 定义命名查询在实体类中,@NamedQuery注解定义一个命名查询语句,示例代码如下:@Entity @Table(name="t_user") @NamedQuery(name="findAllUser",query="SELECT u FROM User u") public class User { // Entity实体类相关定义 }在上面的示例中,@NamedQuery中的name属性指定命名查询的名称,query属性指定命名查询的语句。如果要定义多个命名查询方法,则需要使用@NamedQueries注解:@Entity @Table(name="users") @NamedQueries({ @NamedQuery(name="findAllUser",query="SELECT u FROM User u"), @NamedQuery(name="findUserWithId",query="SELECT u FROM User u WHERE u.id = ?1"), @NamedQuery(name="findUserWithName",query="SELECT u FROM User u WHERE u.name = :name") }) public class User { // Entity实体类相关定义 }在上面的示例中,在User实体类中定义了findAllUser()、findUserWithId()、findUserWithName()三种方法。2. 调用命名查询定义命名查询后,可以使用EntityManager类中的createNamedQuery()方法传入命名查询的名称来创建查询:@Resource EntityManagerFactory emf; @Test public void testNamedQuery() { EntityManager em = emf.createEntityManager(); Query query = em.createNamedQuery("findUserWithName");// 根据User实体中定义的命名查询 query.setParameter("name", "weiz"); List<User> users = query.getResultList(); for (User u : users){ System.out.println("name:"+u.getName()+",age:"+u.getAge()); } }在上面的示例中,使用createNamedQuery创建对应的查询,JPA会先根据传入的查询名查找对应的NamedQuery,然后通过调用getResultList()方法执行查询并返回结果。3. 运行验证单击Run Test或在方法上右击,选择Run 'testNamedQuery',运行全部测试方法,结果如图9-7所示。定义的已命名查询findUserWithName的单元测试运行成功,并输出了相应的查询结果,说明使用JPA实现了自定义的已命名查询的功能。最后以上就把Spring Data JPA的查询功能介绍完了, JPA 简化了我们对数据库的操作,预定义很多常用的数据库方法,直接使用即可。另外 JPA 还有一个特点,就是不用关心数据库的表结构,需要更改的时候只需要修改对应 Model 的属性即可。推荐阅读:SpringBoot从入门到精通(二十六)超级简单的数据持久化框架!Spring Data JPA 的使用!SpringBoot从入门到精通(二十五)搞懂自定义系统配置SpringBoot从入门到精通(二十四)3分钟搞定Spring Boot 多环境配置!SpringBoot从入门到精通(二十三)Mybatis系列之——实现Mybatis多数据源配置SpringBoot从入门到精通(二十二)使用Swagger2优雅构建 RESTful API文档SpringBoot从入门到精通(二十一)如何优雅的设计 RESTful API 接口版本号,实现 API 版本控制!
之前讲了Nginx 如何实现负载均衡,以及如何实现动静分离。我们知道,Nginx服务在整个平台系统中, 处于非常重要的位置,Nginx的高可用影响到整个系统的稳定性。如果Nginx服务器宕机,整个后端web服务将无法提供服务,影响非常严重。所以,接下来就来介绍Nginx + keepalived 实现高可用的方案。 一、什么是高可用高可用HA(High Availability)是分布式系统架构设计中必须考虑的因素之一,它通常是指,通过系统架构设计,减少系统不能提供服务的时间。如果一个系统能够一直提供服务,那么这个可用性则是百分之百,但是天有不测风云。所以我们只能尽可能的去减少服务的故障。Nginx作为系统的核心基础服务,它的可用性至关重要。为了屏蔽Nginx服务器的宕机,需要建立一个备份机。主服务器和备份机上都运行高可用(High Availability)监控程序,通过传送诸如“I am alive”这样的信息来监控对方的运行状况。当备份机不能在一定的时间内收到这样的信息时,它就接管主服务器的服务IP并继续提供负载均衡服务;当备份管理器又从主管理器收到“I am alive”这样的信息时,它就释放服务IP地址,这样的主服务器就开始再次提供负载均衡服务。 二、Nginx高可用方案目前,比较流行的实现Nginx高可用方案就是:keepalived+nginx实现主备方案。这是国内企业中最为普遍的一种高可用方案。所谓,双机热备其实就是指一台服务器在提供服务时,另一台为某服务的备用状态;当主服务器不可用时,另外备份服务器会获取到主服务器的IP,自动顶替主服务器的功能。1、什么是keepalivedkeepalived是集群管理中保证集群高可用的一个服务软件,用来防止单点故障。Keepalived的作用是检测web服务器的状态,如果有一台web服务器死机,或工作出现故障,Keepalived将检测到,并将有故障的web服务器从系统中剔除,当web服务器工作正常后Keepalived自动将web服务器加入到服务器群中,这些工作全部自动完成,不需要人工干涉,需要人工做的只是修复故障的web服务器。 2、keepalived工作原理keepalived是以VRRP协议为实现基础的,VRRP全称Virtual Router Redundancy Protocol,即虚拟路由冗余协议。虚拟路由冗余协议,可以认为是实现路由器高可用的协议,即将N台提供相同功能的路由器组成一个路由器组,这个组里面有一个master和多个backup,master上面有一个对外提供服务的vip(VIP = Virtual IP Address,虚拟IP地址,该路由器所在局域网内其他机器的默认路由为该vip),master会发组播,当backup收不到VRRP包时就认为master宕掉了,这时就需要根据VRRP的优先级来选举一个backup当master。这样的话就可以保证路由器的高可用了。keepalived主要有三个模块,分别是core、check和VRRP。core模块为keepalived的核心,负责主进程的启动、维护以及全局配置文件的加载和解析。check负责健康检查,包括常见的各种检查方式。VRRP模块是来实现VRRP协议的。 3、keepalived+nginx实现主备过程下图是keepalived + nginx 实现主备的过程。从上图可以看到,主Nginx健康时,系统所有的请求通过主Nginx 转发到Tomcat服务器集群。当主Nginx 宕机后,会立马切换到备Nginx ,由备Nginx 提供转发服务。这样就保证系统的正常运行。 三、环境准备1、两天Nginx服务器和两台web服务器两台nginx,一主一备:192.168.101.3和192.168.101.4两台tomcat服务器:192.168.101.5、192.168.101.6 2、安装keepalived分别在主备nginx上安装keepalived,这里就讲解keepalived的安装了。 3、配置虚拟IP(vip:192.168.101.100) 四、配置Nginx高可用1、配置主nginx修改主nginx下/etc/keepalived/keepalived.conf文件,配置主Nginx。#全局配置 global_defs { notification_email { #指定keepalived在发生切换时需要发送email到的对象,一行一个 XXX@XXX.com } notification_email_from XXX@XXX.com #指定发件人 #smtp_server XXX.smtp.com #指定smtp服务器地址 #smtp_connect_timeout 30 #指定smtp连接超时时间 router_id LVS_DEVEL #运行keepalived机器的一个标识 } vrrp_instance VI_1 { state MASTER #标示状态为MASTER 备份机为BACKUP interface eth0 #设置实例绑定的网卡 virtual_router_id 51 #同一实例下virtual_router_id必须相同 priority 100 #MASTER权重要高于BACKUP 比如BACKUP为99 advert_int 1 #MASTER与BACKUP负载均衡器之间同步检查的时间间隔,单位是秒 authentication { #设置认证 auth_type PASS #主从服务器验证方式 auth_pass 8888 } virtual_ipaddress { #设置vip 192.168.101.100 #可以多个虚拟IP,换行即可 } }2、配置备nginx修改备nginx下/etc/keepalived/keepalived.conf文件,配置备Nginx配置备nginx时需要注意:需要修改state为BACKUP , priority比MASTER低,virtual_router_id和master的值一致#全局配置 global_defs { notification_email { #指定keepalived在发生切换时需要发送email到的对象,一行一个 XXX@XXX.com } notification_email_from XXX@XXX.com #指定发件人 #smtp_server XXX.smtp.com #指定smtp服务器地址 #smtp_connect_timeout 30 #指定smtp连接超时时间 router_id LVS_DEVEL #运行keepalived机器的一个标识 } vrrp_instance VI_1 { state BACKUP #标示状态为MASTER 备份机为BACKUP interface eth0 #设置实例绑定的网卡 virtual_router_id 51 #同一实例下virtual_router_id必须相同 priority 99 #MASTER权重要高于BACKUP 比如BACKUP为99 advert_int 1 #MASTER与BACKUP负载均衡器之间同步检查的时间间隔,单位是秒 authentication { #设置认证 auth_type PASS #主从服务器验证方式 auth_pass 8888 } virtual_ipaddress { #设置vip 192.168.101.100 #可以多个虚拟IP,换行即可 } } 五、测试验证配置完keepalived和nginx之后,接下来我们验证Nginx的双机备份设置是否成功。1、启动主备nginx,以及keepalived。service keepalived start ./nginx 2、启动之后,主Nginx正常工作,分别查看主nginx和 备nginx的eth0设置,如下图所示:我们可以看到:vip(192.168.101.100)绑定在主nginx的eth0上。说明此时主Nginx 正常。访问http://192.168.101.100,Nginx可以正常访问。 3、将主nginx服务停止或将主nginx关机(相当于模拟宕机),再次查看主nginx和备nginx的eth0设置,如下图所示:我们可以看到:vip(192.168.101.100)已经漂移到备nginx 服务器上。再次访问http://192.168.101.100,发现虽然主Nginx单击,但是系统依然可以访问。说明Nginx实现了双机热备份,达到了高可用的目的。 最后以上,keepalived+nginx 系统高可用的解决方案介绍完了,看上去复杂,其实配置还是比较简单的。推荐阅读:Nginx极简入门(九)Nginx实现动静分离!Nginx极简入门(八)Nginx性能监控及性能状态参数详解!Nginx极简入门(七)Nginx的日志管理及配置Nginx极简入门(六)配置Nginx负载均衡,提高系统并发性能!Nginx极简入门(五)配置Nginx反向代理Nginx极简入门(四)基于域名的虚拟主机配置Nginx极简入门(三)基于端口的虚拟主机配置Nginx极简入门(二)配置基于ip的虚拟主机Nginx极简入门(一)如何在Linux系统编译安装Nginx服务Nginx极简实战—Nginx服务器高性能优化配置,轻松实现10万并发访问量
前面介绍了Nginx的负载均衡,一般来说,都需要将动态资源和静态资源分开,这样可以很大程度的提升静态资源的访问速度,同时在开过程中也可以让前后端开发并行可以有效的提高开发时间,也可以有些的减少联调时间 。接下来介绍什么是动静分离以及如何使用Nginx实现动静分离。一、什么是动静分离 在Web开发中,通常来说,动态资源其实就是指那些后台资源,而静态资源就是指HTML,JavaScript,CSS,img等文件。动静分离,说白了,就是将网站静态资源(HTML,JavaScript,CSS,img等文件)与后台应用分开部署,提高用户访问静态代码的速度,降低对后台应用服务器的请求。后台应用服务器只负责动态数据请求。优势:分担负载,减轻web服务器的压力,适用于大负载。静态资源放置cdn,同时还可以通过配置缓存到客户浏览器中,这样极大减轻web服务器的压力。劣势:网络环境不佳时,ajax回应很慢,导致页面出现空白,出错处理会不好看。不利于网站SEO(搜索引擎优化),增加了开发复杂度。二、实现方案动静分离就是根据一定规则静态资源的请求全部请求Nginx服务器,后台数据请求转发到Web应用服务器上。从而达到动静分离的目的。目前比较流行的做法是将静态资源部署在Nginx上,而Web应用服务器只处理动态数据请求。这样减少Web应用服务器的并发压力。具体如下图所示: 三、配置Nginx动静分离1. 修改nginx.conf配置,其中第一个location负责处理后台请求,第二个负责处理静态资源, nginx 的其他配置,请参考前之前的文章。 具体如下所示: #拦截静态资源 location ~ .*\.(html|htm|gif|jpg|jpeg|bmp|png|ico|js|css)$ { root static; expires 30d; } 上面的示例,主要是配置image、js、css等资源文件的路径和地址。然后设置缓存失效的时间。完成的Nginx配置如下所示:worker_processes 1; events { worker_connections 1024; } http { server { listen 80; server_name localhost; #拦截后台请求 location / { proxy_pass http://localhost:81; proxy_set_header X-Real-IP $remote_addr; } #拦截静态资源 location ~ .*\.(html|htm|gif|jpg|jpeg|bmp|png|ico|js|css)$ { root static; expires 30d; } } } 2. 在Nginx 下 创建 static 目录,将图片,js, css 等文件 拷贝到该目录下3. 重启Nginx,使用命令: ./nginx -s reload 重新启动Nginx。四、验证测试Nginx 配置完成之后,在浏览器中访问:http://localhost/ 查看页面的请求效果。通过浏览器的调试工具,通过图中红框内容都可以看出来引用静态资源成功了。动态请求转发到了81端口的web应用服务器,而静态的资源文件,访问的是80端口。说明Nginx的动静分离配置成功。最后以上,就把如何配置Nginx动静分离介绍完了,是不是特别简单。因为后面还要介绍Nginx 的优化,免不了查看Nginx的状态。所以这里就提前介绍下。下篇会介绍Nginx的高性能优化,怎么让Nginx服务器实现10w的并发访问量。这是系列课程,大家关注我的微信公众号(架构师精进),随时交流。推荐阅读:Nginx极简入门(八)Nginx性能监控及性能状态参数详解!Nginx极简入门(七)Nginx的日志管理及配置Nginx极简入门(六)配置Nginx负载均衡,提高系统并发性能!Nginx极简入门(五)配置Nginx反向代理Nginx极简入门(四)基于域名的虚拟主机配置Nginx极简入门(三)基于端口的虚拟主机配置Nginx极简入门(二)配置基于ip的虚拟主机Nginx极简入门(一)如何在Linux系统编译安装Nginx服务Nginx极简实战—Nginx服务器高性能优化配置,轻松实现10万并发访问量
2023年02月
2023年01月
2022年11月
2022年10月
2022年09月
2022年07月
2022年06月
2022年03月
2022年02月
2021年12月
2021年11月
2021年10月
有可能是其中的某些数据有问题,导致的convert 失败,可以换cast 试试。
很全面,