经典再现,看到就是赚到。尚硅谷雷神 - SpringBoot 2.x 学习笔记 -高级与场景整合篇

SpringBoot 2.x 场景整合

在上一篇核心功能篇里,我们已了解SpringBoot的配置文件、web开发、数据访问、JUnit5单元测试、生产指标监控、SpringBoot启动流程等。然而SpringBoot是一个伟大的框架,它的知识点远不止这些,我们还要学习更多的技术并整合到SpringBoot中,如虚拟化技术、安全控制、缓存技术、消息中间件、对象存储、定时调度、异步任务、分布式系统等,才能迎接微服务时代!

整合示例:https://github.com/spring-projects/spring-boot/tree/master/spring-boot-samples

1、SpringBoot与缓存

缓存应该是每个系统都要考虑的架构,缓存不仅可以加速系统的访问速度还可以提升系统的性能。如我们需要经常访问的高频热点数据,如果把它缓存起来就能有效减少数据库服务器的压力。手机验证码等有一定的失效时间,我们就可以考虑使用缓存,等失效时间过了,就删掉验证码。

1.1、JSR107

Java Caching定义了5个核心接口,分别是CachingProvider,CacheManager,Cache,Entry和Expriy

  • CachingProvider:定义了创建、配置、获取、管理和控制多个CacheManager。一个应用可以在运行期间访问多个CachingProvider
  • CacheManager:定义了创建、配置、获取、管理和控制多个唯一命名的Cache,这些Cache存在于CacheManager的上下文中,一个CacheManager仅被一个CachingProvider所拥有。
  • Cache:是一个类似Map的数据结构并临时存储以Key为索引的值。一个Cache仅被一个CacheManager所拥有。
  • Entry:是一个存储在Cache中的key-value对
  • Expiry:每一个存储在Cache中的条目有一个定义有效期。一旦超过这个时间,条目为过期的状态。一旦过期,条目将不可访问、更新和删除。缓存有效期可以通过ExpiryPolicy

缓存在应用中的整体架构:
在这里插入图片描述
对于JSR107缓存规范,并不支持所有市面的缓存实现,不支持的需要我们自己编写实现,整合到我们的应用系统中,难度系数也比较大。为了简化开发Spring提供了自己的缓存抽象。但是上面的缓存概念是通用的比如CachingProviderCacheManager

1.2、Spring缓存抽象

为了简化缓存开发Spring从3.1开始定义了org.springframework.cache.Cacheorg.springframework.cache.CacheManager接口来统一不同的缓存技术。并支持使用JCache(JSR-107)注解简化我们开发。

  • Cache接口为缓存的组件规范定义,包含缓存的各种操作集合;
  • Cache接口下Spring提供了各种xxxCache的实现;如RedisCache,EhCacheCache ,ConcurrentMapCache等;
  • 每次调用需要缓存功能的方法时,Spring会检查检查指定参数的指定的目标方法是否已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。

使用Spring缓存抽象时我们需要关注以下两点:

  1. 确定方法需要被缓存以及他们的缓存策略
  2. 从缓存中读取之前缓存存储的数据

几个重要的概念常用注解:
在这里插入图片描述
实验环境搭建略~

快速体验缓存

  1. 开启基于注解的缓存@EnableCaching
  2. 标准缓存注解即可@Cacheable,@CacheEvict,@CachePut
/*
1、需要在主程序上加上@EnableCaching注解
2、配置文件更改日志的级别方便查看效果:
logging:
  level:
    com.lzh.excel.dao:
      debug
*/

/* @Cacheable几个属性:
cacheNames/value:缓存组件的名字 
key:指定缓存数据使用的key,默认使用方法参数的值,可以编写SpEL进行指定,如#id就是参数的值
keyGenerator:key的生成器,可以自己指定key的生成器的组件id,使用时key/keyGenerator只能二选一
cacheManager:指定缓存管理器,或者cacheResolver指定获取解析器
condition:指定复合条件的情况下才缓存,如condition = "#id > 0"
unless:否定缓存,当unless指定的条件为true,方法的返回值就不会被缓存,如unless = "#result == null"
sync:是否启用异步模式,如果启用unless就不支持了。默认为false

缓存的值就是方法返回的结果
*/
@Override
@Cacheable(cacheNames = {"employee"}, key = "#id")  // 将方法的运行结果进行缓存,以后再调用相同的数据,直接从缓存中获取,不用调用方法
public EmployeeEntity getEmployeeEntityById(Integer id) {
    return employeeMapper.getEmployeeEntityById(id);
}
// 清空控制台日志,调用两次方法即可看到效果

关于SpEL(Cache SpEL available metadata)表达式:

名字位置描述示例
methodNameroot object当前被调用的方法名#root.methodName
methodroot object当前被调用的方法#root.method.name
targetroot object当前被调用的目标对象#root.target
targetClassroot object当前被调用的目标对象类#root.targetClass
argsroot object当前被调用的方法的参数列表#root.args[0]
cachesroot object当前方法调用使用的缓存列表(如@Cacheable(value={“cache1”, “cache2”})),则有两个cache#root.caches[0].name
argument nameevaluation context方法参数的名字. 可以直接 #参数名 ,也可以使用 #p0或#a0 的 形式,0代表参数的索引;#iban 、 #a0 、 #p0
resultevaluation context方法执行后的返回值(仅当方法执行之后的判断有效,如 ‘unless’,’cache put’的表达式 ’cache evict’的表达式 beforeInvocation=false)#result

我们发现给方法加上@Cacheable注解后,如果方法参数相同,再次调用方法时,SpringBoot将不再走方法内容,而是直接返回了结果,实现这一过程的原理又是什么呢?

自动配置类:CacheAutoConfiguration

...
// 加载缓存的配置类(目前SpringBoot2.4中有10个,如下图所示)
@Import({ CacheConfigurationImportSelector.class, CacheManagerEntityManagerFactoryDependsOnPostProcessor.class })
public class CacheAutoConfiguration {
	...
}

CacheConfigurationImportSelector静态类所有加载缓存配置类:
在这里插入图片描述
那么那个配置类自动生效呢?在没有导入redis等配置类时,默认生效的是SimpleCacheConfiguration这个配置类:

@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(CacheManager.class) // 没有其他缓存配置类注入时,则生效
@Conditional(CacheCondition.class)
class SimpleCacheConfiguration {

	@Bean
	ConcurrentMapCacheManager cacheManager(CacheProperties cacheProperties,
			CacheManagerCustomizers cacheManagerCustomizers) {
		ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
		List<String> cacheNames = cacheProperties.getCacheNames();
		if (!cacheNames.isEmpty()) {
			cacheManager.setCacheNames(cacheNames);
		}
		return cacheManagerCustomizers.customize(cacheManager);
	}
	/*
	以上源码分析:
	1、给容器中注册了一个类型为ConcurrentMapCacheManager名字为cacheManager的缓存组件
	2、他的作用是将数据保存在ConcurrentMap中 private final ConcurrentMap<Object, Object> store;
	*/
}
// 可以通过配置文件设置debug = true进行验证

缓存组件已经注入容器了,那么它的作用或运行流程是什么呢?我们已@Cacheable为例:

1、方法运行之前,CacheManager先获取相应的缓存,再去查询Cache(缓存组件),按照caheNames指定的名字获取。根据caheNames第一次获取缓存,如果没有Cache组件会自动创建

@Override
@Nullable
public Cache getCache(String name) { // 这里的name参数就是@Cacheable注解指定caheNames的值
	Cache cache = this.cacheMap.get(name);
	if (cache == null && this.dynamic) {
		synchronized (this.cacheMap) {
			cache = this.cacheMap.get(name);
			if (cache == null) {
				cache = createConcurrentMapCache(name); // 没有则创建Cache组件
				this.cacheMap.put(name, cache);
			}
		}
	}
	return cache;
}

2、去Cache中查找缓存的内容,使用一个key,默认就是方法的参数。key是按照某种策略生成的,默认是使用keyGenerator生成的,默认使用SimpleKeyGenerator实现类生成key。(如果在@Cacheable中指定了key,如key = "#id"则使用指定的,不使用SimpleKeyGenerator实现类生成key)

@Nullable
protected Object generateKey(@Nullable Object result) {
	if (StringUtils.hasText(this.metadata.operation.getKey())) {
		EvaluationContext evaluationContext = createEvaluationContext(result);
		return evaluator.key(this.metadata.operation.getKey(), this.metadata.methodKey, evaluationContext); 
	}
	return this.metadata.keyGenerator.generate(this.target, this.metadata.method, this.args);
}

3、根据key如果没有查到缓存就调用目标方法

@Nullable
private Cache.ValueWrapper findInCaches(CacheOperationContext context, Object key) {
	for (Cache cache : context.getCaches()) {
		Cache.ValueWrapper wrapper = doGet(cache, key); // 调用doGet()方法查找缓存
		if (wrapper != null) {
			if (logger.isTraceEnabled()) {
				logger.trace("Cache entry for key '" + key + "' found in cache '" + cache.getName() + "'");
			}
			return wrapper;
		}
	}
	return null;
}

4、将目标方法返回的结果,放进缓存中

public void apply(@Nullable Object result) { // result就是方法返回结果
	if (this.context.canPutToCache(result)) {
		for (Cache cache : this.context.getCaches()) {
			doPut(cache, this.key, result); // 调用doPut()方法将结果放入缓存中
		}
	}
}

总结下来就是:@Cacheable标准的方法执行之前先来检查缓存中没有这个数据,默认安装参数的值作为key去查询缓存,如果没有就运行方法并将结果放入缓存。


上面我们已经介绍了@Cacheable注解的基本使用,也明白了缓存的原理与运行过程。Spring的缓存功能丰富,它还提供了很多注解,去完成不同的场景~

@CachePut注解:即调用方法,又更新缓存数据。如修改了某个数据库的某个数据,同时更新缓存。
运行市级:

  1. 先调用目标方法
  2. 将目标方法的结果缓存起来
@Override
@CachePut(cacheNames = {"employee"}, key = "#employeeEntity.id") // 这里的key必须和@Cacheable中指定的key一样
public EmployeeEntity updateEmployee(EmployeeEntity employeeEntity) {
    employeeMapper.updateEmployee(employeeEntity);
    return employeeEntity;
}
// 即调用方法也更新缓存
// key的写法还可以是这样:key = "#result.id"

@CacheEvict注解:缓存清除

@Override
@CacheEvict(cacheNames = {"employee"}, key = "#id")
public Integer deleteEmployee(Integer id) {
    return employeeMapper.deleteEmployee(id);
}
/*
key:指定要清除的缓存
allEntries:是否清除这个缓存(cacheNames)中的所有数据。默认false
beforeInvocation:是否在方法执行之前清除缓存,默认false,如果方法出现异常是不会清除缓存的
*/

@Caching注解:定义复杂的缓存规则

// @Caching注解可以组合@Cacheable、@CachePut、@CacheEvict
public @interface Caching {

	Cacheable[] cacheable() default {};

	CachePut[] put() default {};

	CacheEvict[] evict() default {};

}

// 如
@Override
@Caching(
        cacheable = {@Cacheable(cacheNames = "employee", key = "#name")},
        put = {@CachePut(cacheNames = "employee", key = "#result.id"), @CachePut(cacheNames = "employee", key = "#result.email")})
public EmployeeEntity getEmployeeByName(String name) {
    return employeeMapper.getEmployeeByName(name);
}

@CacheConfig注解(抽取缓存公共配置):以上注解都是加在方法上,而这个注解是加在类上,即可以指定公共的cacheNamescacheManager等,这样标注在方法上的注解,就可以不写cacheNames或其他属性。


1.3、以Redis作为缓存组件

前面呢,我们学习了缓存的基本注解、运行过程、自动配置原理等。对于Spring缓存抽象有了一定的了解,知道了如果没有集成其他缓存配置类如Redis,SpringBoot默认生效的是缓存配置类是SimpleCacheConfiguration,而注入容器的缓存组件类型是ConcurrentMapCacheManager,组件创建的缓存是ConcurrentMapCache,数据保存在private final ConcurrentMap<Object, Object> store;。之后一系列的缓存操作都是基于这个类型组件展开。

而真实的开发中,我们大多使用的一些缓存中间件如Redis、memcached、ehcache等。那么,我们集成其他的缓存配置类,如Redis又会达到什么样的效果呢?


整合Redis作为缓存:

1、引入场景启动器

<!--spring-boot-starter-data-redis-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2、配置Redis

spring:
  redis:
    host: 49.234.92.64

3、测试是否连接成功

@Autowired
private StringRedisTemplate redisTemplate;

@Test
public void redisTest() {
	ValueOperations<String, String> ops = redisTemplate.opsForValue();
	ops.set("msg", "hello redis");
	System.out.println(ops.get("msg")); // hello redis
}

4、保存Java对象,实体需要实现Serializable接口

// 默认情况,如果我们没有指定key和value的序列化规则,redis会采用默认的序列化机制如:
if (defaultSerializer == null) {

	defaultSerializer = new JdkSerializationRedisSerializer( // 使用jdk的序列化机制
			classLoader != null ? classLoader : this.getClass().getClassLoader());
}

// 实际开发中,我们为了方便查看缓存的java对象数据,常常以json数据格式进行缓存
// 1.编写配置类
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        // 我们为了开发方便,直接使用<String,Object>泛型
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        // 序列化配置
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // String序列化的配置
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化
        template.setKeySerializer(stringRedisSerializer);
        // Hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value采用Jackson2JsonRedisSerializer的序列化方式
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // Hash的value也采用jackson2JsonRedisSerializer的序列化方式
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}
// 2.测试
@Autowired
private RedisTemplate<String, Object> redisTemplate;

@Test
public void redisSaveJavaBeanTest() {
	EmployeeEntity employeeEntity = new EmployeeEntity();
	employeeEntity.setId(1);
	employeeEntity.setLastName("alex");
	employeeEntity.setEmail("123@qq.com");
	employeeEntity.setGender(1);
	employeeEntity.setdId(1);
	redisTemplate.opsForValue().set("employee", employeeEntity);
}

// 打开redis客户端
127.0.0.1:6379> get employee
"[\"com.lzh.excel.entity.EmployeeEntity\",{\"id\":1,\"lastName\":\"alex\",\"email\":\"123@qq.com\",\"gender\":1,\"dId\":1}]"

前面我们,整合好了Redis中间件,往后的数据都是缓存在Redis服务里,那么以Redis作为缓存(Cache),它的原理又是什么呢?

1、加载RedisCacheConfiguration缓存配置类,前面我们已经知道如果没有集成其他缓存中间件,SpringBoot默认加载的配置类是SimpleCacheConfiguration,并且加载顺序RedisCacheConfigurationSimpleCacheConfiguration靠前。

RedisCacheConfiguration matched: // 成功匹配RedisCacheConfiguration配置类
  - @ConditionalOnClass found required class 'org.springframework.data.redis.connection.RedisConnectionFactory' (OnClassCondition)
  - Cache org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration automatic cache type (CacheCondition)
  - @ConditionalOnBean (types: org.springframework.data.redis.connection.RedisConnectionFactory; SearchStrategy: all) found bean 'redisConnectionFactory'; @ConditionalOnMissingBean (types: org.springframework.cache.CacheManager; SearchStrategy: all) did not find any beans (OnBeanCondition)


// 引入redis的starter后,容器中保存的是RedisCacheManager类型的bean
@Bean
RedisCacheManager cacheManager(CacheProperties cacheProperties, CacheManagerCustomizers cacheManagerCustomizers,
		ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration,
		ObjectProvider<RedisCacheManagerBuilderCustomizer> redisCacheManagerBuilderCustomizers,
		RedisConnectionFactory redisConnectionFactory, ResourceLoader resourceLoader) {
	RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(
			determineConfiguration(cacheProperties, redisCacheConfiguration, resourceLoader.getClassLoader()));
	List<String> cacheNames = cacheProperties.getCacheNames();
	if (!cacheNames.isEmpty()) {
		builder.initialCacheNames(new LinkedHashSet<>(cacheNames));
	}
	if (cacheProperties.getRedis().isEnableStatistics()) {
		builder.enableStatistics();
	}
	redisCacheManagerBuilderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
	return cacheManagerCustomizers.customize(builder.build());
}

2、容器中RedisCacheManager类型的bean,帮我们创建RedisCache来作为缓存组件(Cache),通过redis缓存数据

protected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) {
	return new RedisCache(name, cacheWriter, cacheConfig != null ? cacheConfig : defaultCacheConfig);
	// 在这里创建RedisCache组件
}

3、明白原理后,我们就可以继续测试以前的接口

@Override
@Cacheable(cacheNames = {"employee"}, key = "#id") // 将方法的运行结果进行缓存,以后再调用相同的数据,直接从缓存中获取,不用调用方法
public EmployeeEntity getEmployeeEntityById(Integer id) {
    return employeeMapper.getEmployeeEntityById(id);
}

// 经测试发现,默认保存的数据 k-v 都是Object,利用JDK序列化保存
// 这是因为默认创建的RedisCacheManager,操作Redis的时候使用的是RedisTemplate<Object, Object> redisTemplate

// 并且key命名方式为 cacheNames::key
127.0.0.1:6379> keys *
1) "employee::2"
2) "employee::1"

4、如果希望redis缓存的值是json格式,我们还需要自定义RedisCacheManager,让它操作redis的时候,采用我们自定义的序列化机制。

@Bean
RedisCacheManager cacheManager( RedisConnectionFactory redisConnectionFactory) {
    // 使用缓存的默认配置
    RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
    // 使用 GenericJackson2JsonRedisSerializer 作为序列化器
    config = config.serializeValuesWith(
            RedisSerializationContext.SerializationPair.fromSerializer(
                    new GenericJackson2JsonRedisSerializer()));
    RedisCacheManager.RedisCacheManagerBuilder builder =
            RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(config);

    return builder.build();
}

重新启动项目,重新测试接口,这时候Redis中对象安装json格式进行存储,并且读取也没有问题!


2、SpringBoot与消息队列

2.1、消息队列概述

1、大多应用中,可通过消息服务中间件来提升系统异步通信、扩展解耦能力

2、消息服务中两个重要概念:

  • 消息代理(message broker)和目的地(destination)
  • 当消息发送者发送消息以后,将由消息代理接管,消息代理保证消息传递到指定目的地。

3、消息队列主要有两种形式的目的地

  1. 队列(queue):点对点消息通信(point-to-point)
  2. 主题(topic):发布(publish)/订阅(subscribe)消息通信

4、点对点式:

  • 消息发送者发送消息,消息代理将其放入一个队列中,消息接收者从队列中获取消息内容,
    消息读取后被移出队列
  • 消息只有唯一的发送者和接受者,但并不是说只能有一个接收者

5、发布订阅式:

  • 发送者(发布者)发送消息到主题,多个接收者(订阅者)监听(订阅)这个主题,那么就会在消息到达时同时收到消息

6、JMS(Java Message Service)JAVA消息服务:

  • 基于JVM消息代理的规范。ActiveMQ、HornetMQ是JMS实现

7、AMQP(Advanced Message Queuing Protocol)

  • 高级消息队列协议,也是一个消息代理的规范,兼容JMS
  • RabbitMQ是AMQP的实现

2.2、应用场景举例

异步处理
在这里插入图片描述
应用解耦:
在这里插入图片描述
流量削峰:
在这里插入图片描述

2.3、JMS 与 AMQP对比

JMSAMQP
定义Java API网络线协议
跨语言
跨平台
Model提供两种消息模型: (1)、Peer-2-Peer (2)、Pub/sub提供了五种消息模型: (1)、direct exchange (2)、fanout exchange (3)、topic change (4)、headers exchange (5)、system exchange 本质来讲,后四种和JMS的pub/sub模型没有太大差别,仅是在 路由机制上做了更详细的划分;
支持消息类型多种消息类型: TextMessage MapMessage BytesMessage StreamMessage ObjectMessage Message (只有消息头和属性)byte[] 当实际应用时,有复杂的消息,可以将消息序列化后发送。
综合评价JMS 定义了JAVA API层面的标准;在java体系中,多个client 均可以通过JMS进行交互,不需要应用修改代码,但是其对跨 平台的支持较差;AMQP定义了wire-level层的协议标准;天然具有跨平台、跨语 言特性。

2.4、RabbitMQ简介

1、RabbitMQ简介:

RabbitMQ是一个由erlang开发的AMQP(Advanved Message Queue Protocol)的开源实现。


2、核心概念

Message:消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。

Publisher:消息的生产者,也是一个向交换器发布消息的客户端应用程序。

Exchange:交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。
Exchange有4种类型:direct(默认),fanout, topic, 和headers,不同类型的Exchange转发消息的策略有
所区别

Queue:消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息
可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。

Binding:绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。
Exchange 和Queue的绑定可以是多对多的关系。

Connection:网络连接,比如一个TCP连接。

Channel:信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内的虚拟连接,AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接。

Consumer:消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。

Virtual Host:虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器,拥有自己的队列、交换器、绑定和权限机制。vhost 是 AMQP 概念的基础,必须在连接时指定,RabbitMQ 默认的 vhost 是/

Broker:表示消息队列服务器实体。

总结下来就是如下图所示:
在这里插入图片描述


2.5、RabbitMQ的运行机制


AMQP 中的消息路由:AMQP 中消息的路由过程和 Java 开发者熟悉的 JMS 存在一些差别,AMQP 中增加了ExchangeBinding 的角色。生产者把消息发布到 Exchange 上,消息最终到达队列并被消费者接收,而 Binding 决定交换器的消息应该发送到那个队列。

在这里插入图片描述


Exchange 类型:Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型:
direct、fanout、topic、headers

headers 匹配 AMQP 消息的 header而不是路由键, headers 交换器和 direct 交换器完全一致,但性能差很多,
目前几乎用不到了,所以直接看另外三种类型:

1、第一种
在这里插入图片描述

/*
消息中的路由键(routing key)如果和 Binding 中的 binding key 一致, 交换器就将消息发到对应的队列中。路
由键与队列名完全匹配,如果一个队列绑定到交换机要求路由键为“dog”,则只转发 routing key 标记为“dog”的消息,
不会转发“dog.puppy”,也不会转发“dog.guard”等等。它是完全匹配、单播的模式。
*/

2、第二种
在这里插入图片描述

/*
每个发到 fanout 类型交换器的消息都会分到所有绑定的队列上去。fanout 交换器不处理路由键,
只是简单的将队列绑定到交换器上,每个发送到交换器的消息都会被转发到与该交换器绑定的所有队
列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。fanout 类型转发消息是最快的。
*/

3、第三种
在这里插入图片描述

/*
topic 交换器通过模式匹配分配消息的路由键属性,将路由键和某个模式进行匹配,此时队列需要绑定到
一个模式上。它将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开。它同样也会识别两个通配
符:符号“#”和符号“*”。#匹配0个或多个单词,* 匹配一个单词。
*/

2.6、RabbitMQ安装与测试

RabbitMQ的安装,这里我们使用docker进行安装与测试:

# 1.检查RabbitMQ容器镜像
[root@laizhenghua /]# docker images
REPOSITORY      TAG                 IMAGE ID       CREATED         SIZE
rabbitmq        3.8.14-management   ee045987e252   5 months ago    187MB # RabbitMQ容器镜像
mysql           5.7                 f07dfa83b528   8 months ago    448MB
redis           latest              ef47f3b6dc11   8 months ago    104MB
kibana          7.4.2               230d3ded1abc   22 months ago   1.1GB
elasticsearch   7.4.2               b1179d41a7b4   22 months ago   855MB
nginx           1.10                0346349a1a64   4 years ago     182MB
# 如果没有镜像,使用docker pull xxx下一个即可,注意一定要下标签带类似 3.8.14-management 有 management(管理界面)的,方便我们从web端查看

# 2.启动 RabbitMQ 容器
[root@laizhenghua /]# docker run -d -p 5672:5672 -p 15672:15672 --name rabbitmq ee045987e252
32b2eb19574db54e977254665b78f2ab769e5fb594a395253bb96c4547a99b22

# 3.此时就可以通过 15672 端口访问 rabbitMQ 的管理界面

在这里插入图片描述
注意:账号密码都是guest

关于测试(在管理界面完成联动调试)可以按照视频上的操作:https://www.bilibili.com/video/BV1KW411F7oX?p=16&spm_id_from=pageDriver

视频中测试的消息架构图:
在这里插入图片描述

2.7、SpringBoot整合RabbitMQ

整合之前我们先了解一下Spring对对消息队列的支持情况:

1、 spring-jms提供了对JMS的支持

2、spring-rabbit提供了对AMQP的支持

3、需要ConnectionFactory的实现来连接消息代理

4、提供JmsTemplate、RabbitTemplate来发送消息

5、@JmsListener(JMS)@RabbitListener(AMQP)注解在方法上监听消息代理发
布的消息

6、@EnableJms@EnableRabbit开启支持


开始我们的整合与了解原理:

1、引入RabbitMQ的场景启动器

<!--spring-boot-starter-amqp-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

2、自动配置原理

首先引入场景启动器后,自动配置类RabbitAutoConfiguration就生效了。那么RabbitAutoConfiguration到底为我们做了那些事情呢?

// 1.帮我们自动配置了连接工厂 ConnectionFactory
@Bean
public CachingConnectionFactory rabbitConnectionFactory(RabbitProperties properties, ...) throws Exception {...}

// 2.RabbitProperties封装了 RabbitMQ 的配置

// 3.提供了给RabbitMQ发送和接收消息的 RabbitTemplate
@Bean
@ConditionalOnSingleCandidate(ConnectionFactory.class)
@ConditionalOnMissingBean(RabbitOperations.class)
public RabbitTemplate rabbitTemplate(RabbitTemplateConfigurer configurer, ConnectionFactory connectionFactory) {
	RabbitTemplate template = new RabbitTemplate();
	configurer.configure(template, connectionFactory);
	return template;
}

// 4.提供了RabbitMQ系统管理功能组件AmqpAdmin,在程序中管理交换器、队列、绑定关系等
@Bean
@ConditionalOnSingleCandidate(ConnectionFactory.class)
@ConditionalOnProperty(prefix = "spring.rabbitmq", name = "dynamic", matchIfMissing = true)
@ConditionalOnMissingBean
public AmqpAdmin amqpAdmin(ConnectionFactory connectionFactory) {
	return new RabbitAdmin(connectionFactory);
}

测试RabbitTemplate:测试单播(点对点)

@Autowired
private RabbitTemplate rabbitTemplate;

@Test
public void sendMessage() {
	// rabbitTemplate.send(exchange, routeKey, Object); 需要自己构造Message,定义消息体内容和消息头
	// rabbitTemplate.convertAndSend(exchange, routeKey, Object); 默认当成消息体,只需要传入要发送的对象,自动序列化发送给RabbitMQ
	Map<String, Object> map = new HashMap<>();
	map.put("message", "success");
	map.put("data", "潇潇雨歇");
	map.put("status", 200);
	rabbitTemplate.convertAndSend("exchange.direct", "howie.news", map);
	// 可以去管理界面查看消息,序列化采用的JDK的序列化方式
	System.out.println("发送成功!");
}
@Test
public void receivedMessage() {
	Object message = rabbitTemplate.receiveAndConvert("howie.news"); // 每执行一次相应的队列中会根据先进先出的规则减少消息
	System.out.println(message.getClass()); // class java.util.HashMap
	System.out.println(message); // {data=潇潇雨歇, message=success, status=200}
}

经过上面的测试,我们发现RabbitTemplate,使用自动转换的方式(convertAndSend())给队列发送消息时,默认采用的序列化机制是JDK,这是因为MessageConverter采用的是SimpleMessageConverter

private MessageConverter messageConverter = new SimpleMessageConverter();

如果消息转换时,需要支持json格式,我们需要自定义MessageConverter

@Configuration
public class AMQPConfig {
    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }
}

再次发送消息:
在这里插入图片描述


2.8、RabbitMQ监听消息

上面例子我们使用RabbitTemplate测试了点对点式的消息发送与接收,但是真实开发场景中,我们一般不会手动给队列发送消息,也不会手动从队列消费消息。

我们往往引入监听场景,去完成我们的业务。如通过消息队列解耦的订单服务与库存服务,当订单服务生成订单时向消息服务发送消息,库存服务时时监听消息,一旦监听到就减少相应的库存。

Spring为了简化开发,引入了相应的监听注解:@EnableRabbit + @RabbitListener 监听消息队列的内容

@RabbitListener(queues = {"howie.news"})
public void receive1(EmployeeEntity employeeEntity) {
    System.out.println("监听到消息:" + employeeEntity);
}

@RabbitListener(queues = {"howie"})
public void receive2(Message message) {
    // 可以获取消息头等
    System.out.println(message.getBody());
    System.out.println(message.getMessageProperties());
}

// 编写测试方法,发送消息
@Test
public void sendEmployeeTest() {
	EmployeeEntity employee = new EmployeeEntity(1, "岳父", "123@qq.com", 1, 1);
	rabbitTemplate.convertAndSend("exchange.direct","howie", employee);
	System.out.println("send success !");
}

至此,我们学会了RabbitMQ的基本使用,所有的测试都是基于手动创建好的交换器队列路由键以及手动绑定好的关系。那么我们可不可以使用程序创建我们需要的这些东西呢?


2.9、AmqpAdmin管理组件

我们需要经常使用程序创建和删除QueueExchangeBinding等,而RabbitMQ系统管理功能组件AmqpAdmin就可以帮助我们管理这些核心功能。

@Autowired
private AmqpAdmin amqpAdmin;

@Test
public void createExchange() {
	Exchange exchange = new DirectExchange("employee.exchange");
	amqpAdmin.declareExchange(exchange);
	System.out.println("create success");
}

@Test
public void createQueue() {
	String queue = amqpAdmin.declareQueue(new Queue("employee.queue", true));
	System.out.println(queue);
}

@Test
public void createBinding() {
	amqpAdmin.declareBinding(new Binding("employee.queue", Binding.DestinationType.QUEUE, "employee.exchange", "employee.route", null));
	System.out.println("binding success");
}

更多AmqpAdmin的方法,可自行学习。


3、SpringBoot与ElasticSearch

3.1、初识ElasticSearch

我们的应用经常需要添加检索功能,开源的 ElasticSearch 是目前全文搜索引擎的首选。他可以快速的存储、搜索和分析海量数据。Spring Boot通过整合SpringData ElasticSearch为我们提供了非常便捷的检索功能支持。

Elasticsearch是一个分布式搜索服务,提供Restful API,底层基于Lucene,采用多shard(分片)的方式保证数据安全,并且提供自动resharding的功能,github等大型的站点也是采用了ElasticSearch作为其搜索服务。

官方原话:

Elasticsearch 是一个分布式的开源搜索和分析引擎,适用于所有类型的数据,包括文本、数字、地理空间、结构化和非结构化数据。Elasticsearch 在 Apache Lucene 的基础上开发而成,由 Elasticsearch N.V.(即现在的 Elastic)于 2010 年首次发布。Elasticsearch 以其简单的 REST 风格 API、分布式特性、速度和可扩展性而闻名,是 Elastic Stack 的核心组件;Elastic Stack 是适用于数据采集、充实、存储、分析和可视化的一组开源工具。人们通常将 Elastic Stack 称为 ELK Stack(代指 Elasticsearch、Logstash 和 Kibana),目前 Elastic Stack 包括一系列丰富的轻量型数据采集代理,这些代理统称为 Beats,可用来向 Elasticsearch 发送数据。

3.2、ElasticSearch基本概念

1、Index (索引)

  • 动词:相当于MySQL中的insert
  • 名词:相当于MySQL中的Database

2、Type (类型)

  • 在 Index (索引) 中,可以定义一个或多个类型,类似于MySQL中的Table,每一种类型的数据放在一起。

3、Document (文档)

  • 保存在某个索引 (index) 下,某种类型 (Type) 的一个数据 (Document)。文档是JSON格式的,Document就像是MySQL中的某个table里面的内容。

4、概念关系图一览
在这里插入图片描述
5、倒排索引机制
!

3.3、ElasticSearch与kibana安装

为了安装方便,我们还是以容器化技术Docker进行安装:

1、下载镜像文件

[howie@laizhenghua /]$ sudo docker search elasticsearch
...

docker pull elasticsearch:7.4.2 # 存储和检索数据
dicker pull kibana:7.4.2 # 可视化检索数据

[howie@laizhenghua /]$ sudo docker images
REPOSITORY      TAG       IMAGE ID       CREATED         SIZE
mysql           5.7       f07dfa83b528   3 weeks ago     448MB
redis           latest    ef47f3b6dc11   5 weeks ago     104MB
kibana          7.4.2     230d3ded1abc   14 months ago   1.1GB
elasticsearch   7.4.2     b1179d41a7b4   14 months ago   855MB

2、创建挂载目录

sudo mkdir -p /mydata/elasticsearch/config
sudo mkdir -p /mydata/elasticsearch/data
sudo chmod -R 777 /mydata/elasticsearch/
sudo echo "http.host: 0.0.0.0" >/mydata/elasticsearch/config/elasticsearch.yml

chmod:change mode -R(Recursion 递归)

3、启动elasticsearch

sudo docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e  "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xmx512m" \
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
-v  /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.4.2

4、启动 kibana

sudo docker run --name kibana -e ELASTICSEARCH_HOSTS=http://49.234.92.64:9200 -p 5601:5601 -d kibana:7.4.2

# 启动
[howie@laizhenghua /]$ sudo docker run --name kibana -e ELASTICSEARCH_HOSTS=http://49.234.92.64:9200 -p 5601:5601 -d kibana:7.4.2
9b88afc2f1c00891125eeb9610bb149a023b8044bb887568219f7f160059005d

访问服务器的5601端口!

3.4、基础入门

前面呢,我们不仅安装好了es,还认识了es的基本概念!然而es功能非常丰富,无论是被用作全文检索、结构化搜索还是分析与可视化它都是一款非常优秀的开源产品!我们学好它也并非一朝一夕,需要根据文档慢慢磨练与实践~

官方中文文档地址:https://www.elastic.co/guide/cn/elasticsearch/guide/current/index.html
在这里插入图片描述

3.5、初步检索

1、_cat
  • GET/_cat/nodes查看所有节点
  • GET/_cat/health查看es健康状态 http://49.234.92.64:9200/_cat/health
  • GET/_cat/master查看主节点
  • GET/_cat/indices查看所有索引 --> show databases;
2、索引一个文档 (保存)

保存一个数据,保存在哪个索引的哪个类型下,指定用哪个唯一标识。

PUT customer/external/1在customer索引下的 external 类型下保存 1号数据为

// http://49.234.92.64:9200/customer/external/1

// 请求体内容,发送put请求
{
    "name": "howie"
}
// 回显数据(元数据),同一个请求发送多次是一个更新操作
{
    "_index": "customer",
    "_type": "external",
    "_id": "1",
    "_version": 1,
    "result": "created",
    "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
    },
    "_seq_no": 0,
    "_primary_term": 1
}

小结:

  • 发送请求时,put和post都可以。
  • post新增、如果不指定id,会自动生成id。指定id就会修改这个数据,并新增版本号。
  • put可以新增可以修改,put必须指定id,由于put需要指定id,我们一般都来做修改操作,不指定id会报错。
3、查询文档 (指定索引、指定类型、指定id、发送get请求)
GET customer/external/1
// http://49.234.92.64:9200/customer/external/1

// 回显数据
{
    "_index": "customer",
    "_type": "external",
    "_id": "1",
    "_version": 1,
    "_seq_no": 0, // 并发控制字段,每次更新就会 +1,用来做乐观锁
    "_primary_term": 1, // 同上,主分片重新分配,如重启,就会变化
    "found": true,
    "_source": {
        "name": "howie"
    }
}

// 触发乐观锁(更新发起请求时携带:?if_seq_no=0&if_primary_term=1)
// http://49.234.92.64:9200/customer/external/1?if_seq_no=0&if_primary_term=1
4、更新文档
POST customer/external/1/_update

POST http://49.234.92.64:9200/customer/external/1/_update
// 会对比原来的数据,如果和原来的一样就什么都不做!
{
    "doc":{
        "name": "newName"
    }
}
或者
POST http://49.234.92.64:9200/customer/external/1
{
    "name": "newName"
}
或者
PUT customer/external/1
{
    "name": "newName"
}

// 更新的同时,增加属性
POST http://49.234.92.64:9200/customer/external/1/_update
{
    "doc":{
        "name": "newName",
        "age": "ageNumber"
    }
}
// put和post不带_update也是可以的

小结:

  • put和post如果不带_update,都会直接更新数据
5、删除
DELETE customer/external/1 // 删除文档
DELETE customer // 删除索引(清空数据)

DELETE http://49.234.92.64:9200/customer/external/1/

// 回显数据
{
    "_index": "customer",
    "_type": "external",
    "_id": "1",
    "_version": 4,
    "result": "deleted",
    "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
    },
    "_seq_no": 3,
    "_primary_term": 1
}

// 以get请求再次请求
GET http://49.234.92.64:9200/customer/external/1/
// 回显数据
{
    "_index": "customer",
    "_type": "external",
    "_id": "1",
    "found": false
}
6、bulk 批量 API

语法格式:

{action:{metadata}}\n
{request body}\n

{action:{metadata}}\n
{request body}\n

示例1:

// POST http://49.234.92.64:9200/customer/external/_bulk

POST customer/external/_bulk
{"index":{"_id":"1"}}
{"name":"alex"}
{"index":{"_id":"2"}}
{"name":"howie"}

// 回显数据
#! Deprecation: [types removal] Specifying types in bulk requests is deprecated.
{
  "took" : 12,
  "errors" : false,
  "items" : [
    {
      "index" : {
        "_index" : "customer",
        "_type" : "external",
        "_id" : "1",
        "_version" : 1,
        "result" : "created",
        "_shards" : {
          "total" : 2,
          "successful" : 1,
          "failed" : 0
        },
        "_seq_no" : 4,
        "_primary_term" : 1,
        "status" : 201
      }
    },
    {
      "index" : {
        "_index" : "customer",
        "_type" : "external",
        "_id" : "2",
        "_version" : 1,
        "result" : "created",
        "_shards" : {
          "total" : 2,
          "successful" : 1,
          "failed" : 0
        },
        "_seq_no" : 5,
        "_primary_term" : 1,
        "status" : 201
      }
    }
  ]
}

示例2:对于整个索引执行批量操作

POST /_bulk
{"delete":{"_index":"website","_type":"blog","_id":"123"}}
{"create":{"_index":"website","_type":"blog","_id":"123"}}
{"title":"my first blog post"}
{"index":{"_index":"website","_type":"blog"}}
{"title":"my second blog post"}
{"update":{"_index":"website","_type":"blog","_id":"123"}}
{"doc":{"title":"my updated blog post"}}

示例3:bulk批量操作,导入样板测试数据。

准备了一份顾客银行账户信息的虚构的JSON文档样本。每个文档都有下列的schema(模式)

{
	"account_number": 1,
	"balance": 39225,
	"firstname": "Amber",
	"lastname": "Duke",
	"age": 32,
	"gender": "M",
	"address": "880 Holmes Lane",
	"employer": "Pyrami",
	"email": "amberduke@pyrami.com",
	"city": "Brogan",
	"state": "IL"
}

数据地址:https://github.com/elastic/elasticsearch/blob/master/docs/src/test/resources/accounts.json

POST /bank/account/_bulk
{"index":{"_id":"1"}}
{"account_number":1,"balance":39225,"firstname":"Amber","lastname":"Duke","age":32,"gender":"M","address":"880 Holmes Lane","employer":"Pyrami","email":"amberduke@pyrami.com","city":"Brogan","state":"IL"}
{"index":{"_id":"6"}}
{"account_number":6,"balance":5686,"firstname":"Hattie","lastname":"Bond","age":36,"gender":"M","address":"671 Bristol Street","employer":"Netagy","email":"hattiebond@netagy.com","city":"Dante","state":"TN"}
{"index":{"_id":"13"}}
{"account_number":13,"balance":32838,"firstname":"Nanette","lastname":"Bates","age":28,"gender":"F","address":"789 Madison Street","employer":"Quility","email":"nanettebates@quility.com","city":"Nogal","state":"VA"}
{"index":{"_id":"18"}}
{"account_number":18,"balance":4180,"firstname":"Dale","lastname":"Adams","age":33,"gender":"M","address":"467 Hutchinson Court","employer":"Boink","email":"daleadams@boink.com","city":"Orick","state":"MD"}

3.6、进阶检索

1、search API

ES 支持两种基本方式检索:

  • 一个是通过使用 REST request URI 发送搜索参数 (uri + 检索参数)
  GET /bank/_search?q=*&sort=account_number:asc
  • 另一个是通过使用 REST request body 来发送它们 (uri + 请求体)
  GET /bank/_search
  {
    "query": { "match_all": {} }, // 查询条件是匹配所有
    "sort": [
      { "account_number": "asc" } // 排序方式
    ],
    "from": 10, // 从10-19的数据
    "size": 10
  }
2、Query DSL (查询表达式)

ElasticSearch提供一个可以执行查询的json风格的DSL(domain specific language 领域特定语言)。这个被称为 Query DSL。该查询语言非常全面。

官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started-search.html

(1) 基本语法格式

一个查询语句的典型结构:

QUERY_NAME:{
   ARGUMENT:VALUE,
   ARGUMENT:VALUE,...
}

如果针对于某个字段,那么它的结构如下:

{
  QUERY_NAME:{
     FIELD_NAME:{
       ARGUMENT:VALUE,
       ARGUMENT:VALUE,...
      }   
   }
}
    
// 示例
GET /bank/_search
{
  "query":{
    "match_all":{}
  },
  "sort":{
    "balance": "desc"
  },
  "from":0,
  "size": 3
}

query定义如何查询:

  • match_all查询类型【代表查询所有的所有】,es中可以在query中组合非常多的查询类型完成复杂查询;
  • 除了query参数之外,我们可也传递其他的参数以改变查询结果,如sort,size;
  • from+size限定,完成分页功能;
  • sort排序,多字段排序,会在前序字段相等时后续字段内部排序,否则以前序为准;

(2) 查询部分字段

GET /bank/_search
{
  "query":{
    "match_all":{}
  },
  "sort":{
    "balance": "desc"
  },
  "from":0,
  "size": 3,
  "_source": "email" // 只返回email字段、["firstname","email"]
}

(3) match 匹配查询

  • 数字类型,精确匹配!
GET /bank/_search
{
  "query": {
    "match": {
      "account_number": 1 // "1" 效果是一样的,如果可以转成数字,就精确匹配
    }
  }
}

// 返回account_number=1的数据
  • 字符串类型,全文检索(分词匹配)
GET /bank/_search
{
  "query": {
    "match": {
      "address": "Holmes"
    }
  }
}

// 倒排索引:对address字段进行模糊匹配,类似于where address like '%Holmes%',但是在es中还要分词匹配

(4) match_phrase [短句匹配]

将需要匹配的值当成一整个单词(不分词)进行检索

GET bank/_search
{
  "query": {
    "match_phrase": {
      "address": "mill road"
    }
  }
}
// 查处address中包含mill_road的所有记录,并给出相关性得分

// 使用match的keyword属性
GET bank/_search
{
  "query": {
    "match": {
      "address.keyword": "990 Mill Road" // 与match_phrase的区别就是,如果条件为 Mill Road,keyword就会匹配不到而match_phrase可以
    }
  }
}
// 文本字段的匹配,使用keyword,匹配的条件就是要显示字段的全部值,要进行精确匹配的

(5) multi_match [多字段匹配]

GET /bank/_search
{
  "query": {
    "multi_match": {
      "query": "Lane",
      "fields": ["address","firstname"]
    }
  }
}
// 检索 address和firstname字段中包含Lane的数据,并且在查询过程中,会对于查询条件进行分词

(6) bool [复合查询]

复合语句可以合并,任何其他查询语句,包括符合语句。这也就意味着,复合语句之间 可以互相嵌套,可以表达非常复杂的逻辑。

must:必须达到must所列举的所有条件

GET /bank/_search
{
  "query": {
    "bool": {
      "must": [
        {"match": {
          "age": "32"
        }},
        {"match": {
          "gender": "M"
        }}
      ]
    }
  }
}
// 检索 age 字段为 32 并且 gender 字段为 M 

must_not,必须不匹配must_not所列举的所有条件。

should,应该满足should所列举的条件(满足了增加评分,不满足也行)。

GET bank/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "gender": "M"
          }
        },
        {
          "match": {
            "address": "mill"
          }
        }
      ],
      "must_not": [
        {
          "match": {
            "age": "18"
          }
        }
      ],
      "should": [
        {
          "match": {
            "lastname": "Wallace"
          }
        }
      ]
    }
  }
}

(7) filter [过滤]

并不是所有的查询都需要产生分数,特别是哪些仅用于filtering过滤的文档,并不计算相关性得分,elasticsearch会自动检查场景并且优化查询的执行。

GET /bank/_search
{
  "query": {
    "bool": {
      "must": [
        {"match": {
          "address": "Lane"
        }}
      ],
      "filter": {
        "range": {
          "balance": {
            "gte": 20000,
            "lte": 100000
          }
        }
      }
    }
  }
}
// 全文检索address字段包含Lane。并且过滤掉余额小于20000和大于100000的数据

Each must, should, and must_not element in a Boolean query is referred to as a query clause. How well a document meets the criteria in each must or should clause contributes to the document’s relevance score. The higher the score, the better the document matches your search criteria. By default, Elasticsearch returns documents ranked by these relevance scores.

在boolean查询中,must, shouldmust_not 元素都被称为查询子句 。 文档是否符合每个“must”或“should”子句中的标准,决定了文档的“相关性得分”。 得分越高,文档越符合您的搜索条件。 默认情况下,Elasticsearch返回根据这些相关性得分排序的文档。

The criteria in a must_not clause is treated as a filter. It affects whether or not the document is included in the results, but does not contribute to how documents are scored. You can also explicitly specify arbitrary filters to include or exclude documents based on structured data.

“must_not”子句中的条件被视为“过滤器”。 它影响文档是否包含在结果中, 但不影响文档的评分方式。 还可以显式地指定任意过滤器来包含或排除基于结构化数据的文档。

(8) term 也是全文检索,但是更适用于非text字段与精确检索

和match一样。匹配某个属性的值。全文检索字段用match,其他非text字段匹配用term。

GET /bank/_search
{
  "query": {
    "term": {
      "age": 32
    }
  }
}
// 检索成功

GET /bank/_search
{
  "query": {
    "term": {
      "address": "880 Holmes Lane"
    }
  }
}
// 检索失败,失败原因:
/*
By default, Elasticsearch changes the values of text fields as part of analysis. This can make finding exact matches for text field values difficult.

默认情况下,Elasticsearch作为analysis的一部分更改' text '字段的值。这使得为“text”字段值寻找精确匹配变得困难。
*/

小结:

全文检索text

字段用match,其他非text字段匹配用term

3、Aggregations (执行聚合)

聚合提供了从数据中分组和提取数据的能力。最简单的聚合方法大致等于SQL GROUP BYSQL的聚合函数。在ElasticSearce中,您有执行搜索返回hits(命中结果),并且同时返回聚合结果,把一个响应中的所有hits(命中结果)分隔开的能力,这是非常强大且有效的。您可以执行查询和多个聚合,并且在一次使用中得到各自的(任何一个的)返回结果,使用一次简洁和简化的API来避免网络往返 。

聚合语法如下:

"aggs":{
    "aggs_name这次聚合的名字,方便展示在结果集中":{
        "AGG_TYPE聚合的类型(avg,term,terms)":{}
     }
}

示例1:

搜索address中包含mill的所有人的年龄分布以及平均年龄,但不显示这些人的详情。

GET /bank/_search
{
  "query": {
    "match": {
      "address": "mill"
    }
  },
  "aggs": {
    "ageAggs": {
      "terms": {
        "field": "age",
        "size": 10
      }
    },
    "ageAvg": {
      "avg": {
        "field": "age"
      }
    }
  },
  "size": 0 // 只看聚合结果
}

示例2:

按照年龄聚合,并且求这些年龄段的的平均工资。

GET /bank/_search
{
  "query": {
    "match_all": {} // 检索所有
  },
  "aggs": {
    "aggAge": {
      "terms": {
        "field": "age",
        "size": 100
      },
      "aggs":{
        "ageAvg": {
          "avg": {
            "field": "balance"
          }
        }
      }
    }
  },
  "size": 0
}

示例3:

查出所有年龄分布,并且这些年龄段中 M 的平均薪资和 F 的平均薪资以及这个年龄段的总体平均薪资。

GET /bank/_search
{
  "query": {
    "match_all": {}
  },
  "aggs": {
    "aggAge": {
      "terms": {
        "field": "age",
        "size": 100
      },
      "aggs": {
        "genderAgg": {
          "terms": {
            "field": "gender.keyword"
          },
          "aggs": {
            "balanceAgg": {
              "avg": {
                "field": "balance"
              }
            }
          }
        },
        "ageBlalanceAgg": {
          "avg": {
            "field": "balance"
          }
        }
      }
    }
  },
  "size": 0
}

3.7、Mapping

1、字段类型映射

类型映射在6.0版本中已经移除。

详情文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/removal-of-types.html

2、映射

Mapping(映射) Maping是用来定义一个文档(document),以及它所包含的属性(field)是如何存储和索引的。比如:使用maping来定义:

  • 哪些字符串属性应该被看做全文本属性(full text fields)
  • 哪些属性包含数字,日期或地理位置
  • 文档中的所有属性是否都嫩被索引(all 配置)
  • 日期的格式
  • 自定义映射规则来执行动态添加属性
  • 查看mapping信息 GET bank/_mapping
GET /bank/_mapping

// 回显数据
{
  "bank" : {
    "mappings" : {
      "properties" : {
        "account_number" : {
          "type" : "long"
        },
        "address" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "age" : {
          "type" : "long"
        },
        "balance" : {
          "type" : "long"
        },
        "city" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "email" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "employer" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "firstname" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "gender" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "lastname" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "state" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        }
      }
    }
  }
}

新版本改变:

ElasticSearch7-去掉type概念

  1. 关系型数据库中两个数据表示是独立的,即使他们里面有相同名称的列也不影响使用,但ES中不是这样的。elasticsearch是基于Lucene开发的搜索引擎,而ES中不同type下名称相同的filed最终在Lucene中的处理方式是一样的。

    • 两个不同type下的两个user_name,在ES同一个索引下其实被认为是同一个filed,你必须在两个不同的type中定义相同的filed映射。否则,不同type中的相同字段名称就会在处理中出现冲突的情况,导致Lucene处理效率下降。
    • 去掉type就是为了提高ES处理数据的效率。
  2. Elasticsearch 7.x URL中的type参数为可选。比如,索引一个文档不再要求提供文档类型。

  3. Elasticsearch 8.x 不再支持URL中的type参数。

  4. 解决: 将索引从多类型迁移到单类型,每种类型文档一个独立索引

    将已存在的索引下的类型数据,全部迁移到指定位置即可。详见数据迁移

2.1、创建映射

字段可设置数据类型:

官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html

PUT /my_index
{
  "mappings": {
    "properties": {
      "id": {
        "type": "long"
      },
      "age": {
        "type": "integer"
      },
      "name": {
        "type": "text" // 全文检索(分词)
      },
      "email": {
        "type": "keyword" // 精确匹配
      }
    }
  }
}

// 查看映射信息
GET /my_index/_mapping
2.2、添加新的字段映射

前面我们已经创建好我们自己的字段映射my_index,现在我们想给my_index添加新的字段映射employeeId,那该怎么做呢?

PUT /my_index/_mapping
{
  "properties": {
    "employeeId": {
      "type": "long",
      "index": false // 是否参与检索,默认是true,是一个冗余字段
    }
  }
}

GET /my_index/_mapping

映射参数除了type、index等,官方还提供了丰富的参数设置!

详情请参考:https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-params.html

2.3、更新字段映射

对于已经存在的字段映射,我们已经保存了数据,如果更新某字段,数据就会产生矛盾!所以es规定,不能更新已存在的字段映射。更新必须创建新的索引,进行数据迁移。

2.4、数据迁移

官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/7.10/docs-reindex.html

1、先创建新的索引

PUT /new_bank
{
  "mappings": {
    "properties": {
      "account_number": {
        "type": "long"
      },
      "address": {
        "type": "text"
      },
      "age": {
        "type": "integer"
      },
      "balance": {
        "type": "long"
      },
      "city": {
        "type": "keyword"
      },
      "email": {
        "type": "text"
      },
      "employer": {
        "type": "text"
      },
      "firstname": {
        "type": "text"
      },
      "gender": {
        "type": "keyword"
      },
      "lastname": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "state": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      }
    }
  }
}

// 回显数据
{
  "acknowledged" : true,
  "shards_acknowledged" : true,
  "index" : "new_bank"
}

// 查看映射信息
GET /new_bank/_mapping

2、使用如下方式进行数据迁移(6.0以后没有类型)

语法:

POST _reindex // 固定写法
{
  "source":{
      "index":"old_index" // bank
   },
  "dest":{
      "index":"new_index" // new_bank
   }
}

将旧索引的type下的数据进行迁移:

我们之前保存的测试数据是有类型的(account),所以我们在迁移数据时,需要指明类型!

POST _reindex
{
  "source": {
    "index": "bank",
    "type": "account"
  },
  "dest": {
    "index": "new_bank"
  }
}

// 再次查看
GET /new_bank/_search

3.8、分词

1、分词器介绍(tokenizer)

一个tokenizer(分词器)接收一个字符流,将之分割为独立的tokens(词元,通常是独立的单词),然后输出tokens流。

例如:whitespace tokenizer遇到空白字符时分割文本。它会将文本“Quick brown fox!”分割为[Quick,brown,fox!]。

该tokenizer(分词器)还负责记录各个terms(词条)的顺序或position位置(用于phrase短语和word proximity词近邻查询),以及term(词条)所代表的原始word(单词)的start(起始)和end(结束)的character offsets(字符串偏移量)(用于高亮显示搜索的内容)。

elasticsearch提供了很多内置的分词器,可以用来构建custom analyzers(自定义分词器)。

关于更多 详细分词器:https://www.elastic.co/guide/en/elasticsearch/reference/7.10/analysis-analyzers.html

standard分词器:

POST _analyze
{
  "analyzer": "standard",
  "text": "我爱中国!"
}

// 执行结果
{
  "tokens" : [
    {
      "token" : "我",
      "start_offset" : 0,
      "end_offset" : 1,
      "type" : "<IDEOGRAPHIC>",
      "position" : 0
    },
    {
      "token" : "爱",
      "start_offset" : 1,
      "end_offset" : 2,
      "type" : "<IDEOGRAPHIC>",
      "position" : 1
    },
    {
      "token" : "中",
      "start_offset" : 2,
      "end_offset" : 3,
      "type" : "<IDEOGRAPHIC>",
      "position" : 2
    },
    {
      "token" : "国",
      "start_offset" : 3,
      "end_offset" : 4,
      "type" : "<IDEOGRAPHIC>",
      "position" : 3
    }
  ]
}

我们发现standard分词器,对中文的分词结果并不正确!所有的语言分词,默认使用的都是Standard Analyzer,但是这些分词器针对于中文的分词,并不友好。为此需要安装中文的分词器(ik)。

2、安装ik分词器

注意:不能用默认elasticsearch-plugin install xxx.zip 进行自动安装 https://github.com/medcl/elasticsearch-analysis-ik/对应es版本安装。

在前面安装的elasticsearch时,我们已经将elasticsearch容器的/usr/share/elasticsearch/plugins目录,映射到宿主机的/mydata/elasticsearch/plugins目录下,所以比较方便的做法就是下载/elasticsearch-analysis-ik-7.4.2.zip文件,然后解压到该文件夹下即可。安装完毕后,需要重启elasticsearch容器。

[howie@laizhenghua plugins]$ pwd
/mydata/elasticsearch/plugins
[howie@laizhenghua plugins]$ ls
elasticsearch-analysis-ik-7.4.2.zip
# 已下载好文件

1、解压文件

[howie@laizhenghua plugins]$ sudo unzip elasticsearch-analysis-ik-7.4.2.zip -d ik

2、修改权限

[howie@laizhenghua plugins]$ sudo chmod -R 777 ik/
[howie@laizhenghua ik]$ ll
total 1432
-rwxrwxrwx 1 root root 263965 May  6  2018 commons-codec-1.9.jar
-rwxrwxrwx 1 root root  61829 May  6  2018 commons-logging-1.2.jar
drwxrwxrwx 2 root root   4096 Oct  7  2019 config
-rwxrwxrwx 1 root root  54643 Nov  4  2019 elasticsearch-analysis-ik-7.4.2.jar
-rwxrwxrwx 1 root root 736658 May  6  2018 httpclient-4.5.2.jar
-rwxrwxrwx 1 root root 326724 May  6  2018 httpcore-4.4.4.jar
-rwxrwxrwx 1 root root   1805 Nov  4  2019 plugin-descriptor.properties
-rwxrwxrwx 1 root root    125 Nov  4  2019 plugin-security.policy

3、测试是否安装成功

[howie@laizhenghua plugins]$ sudo docker exec -it 68a /bin/bash[root@68a92965eb76 elasticsearch]# cd bin[root@68a92965eb76 bin]# elasticsearch-plugin listik# 打印出了ik表示安装成功

4、重启并测试分词效果

[howie@laizhenghua plugins]$ sudo docker restart elasticsearch
elasticsearch
POST _analyze
{
  "analyzer": "ik_smart", // 粗粒度分词
  "text": "我爱中国共产党"
}
// 回显数据
{
  "tokens" : [
    {
      "token" : "我",
      "start_offset" : 0,
      "end_offset" : 1,
      "type" : "CN_CHAR",
      "position" : 0
    },
    {
      "token" : "爱",
      "start_offset" : 1,
      "end_offset" : 2,
      "type" : "CN_CHAR",
      "position" : 1
    },
    {
      "token" : "中国共产党",
      "start_offset" : 2,
      "end_offset" : 7,
      "type" : "CN_WORD",
      "position" : 2
    }
  ]
}

分词器种类:

  • ik_smart 粗粒度分词
  • ik_max_word细粒度分词
3、自定义词库

1、安装nginx并启动(只是为了复制出配置)

我们把所有关于nginx的东西,都放在一个文件夹里(mydata/nginx):

[howie@laizhenghua mydata]$ sudo mkdir nginx
[howie@laizhenghua mydata]$ ls
elasticsearch  mysql  nginx  redis

docker下载nginx:

[howie@laizhenghua mydata]$ sudo docker pull nginx:1.10

启动nginx:

[howie@laizhenghua mydata]$ sudo docker run -p 80:80 --name nginx -d nginx:1.10

2、将nginx容器内的配置文件拷贝到当前目录

[howie@laizhenghua mydata]$ sudo docker container cp nginx:/etc/nginx .
[howie@laizhenghua mydata]$ cd nginx
[howie@laizhenghua nginx]$ ls
conf.d  fastcgi_params  koi-utf  koi-win  mime.types  modules  nginx.conf  scgi_params  uwsgi_params  win-utf

终止原容器:

[howie@laizhenghua nginx]$ sudo docker stop nginx

执行命令删除原容器:

[howie@laizhenghua nginx]$ sudo docker rm nginx
nginx

修改文件名称:

[howie@laizhenghua mydata]$ sudo mv nginx conf

把这个conf移动到/mydata/nginx

[howie@laizhenghua mydata]$ sudo mkdir nginx
[howie@laizhenghua mydata]$ sudo mv conf nginx/
[howie@laizhenghua mydata]$ ls
elasticsearch  mysql  nginx  redis
[howie@laizhenghua mydata]$ cd nginx
[howie@laizhenghua nginx]$ ls
conf

创建新的nginx:

sudo docker run -p 80:80 --name nginx \
-v /mydata/nginx/html:/usr/share/nginx/html \
-v /mydata/nginx/logs:/var/log/nginx \
-v /mydata/nginx/conf:/etc/nginx \
-d nginx:1.10

创建远程扩展字典:

[howie@laizhenghua es]$ pwd
/mydata/nginx/html/es
[howie@laizhenghua es]$ sudo vi cut_word.txt
# 输入自己的词库如'尚硅谷'、'乔碧萝'等

此时我们就可以在浏览器上访问到我们自己的远程词库!

换成自己的ip地址即可:http://49.234.92.64/es/cut_word.txt

修改配置文件,配置远程扩展字典:

[howie@laizhenghua config]$ pwd
/mydata/elasticsearch/plugins/ik/config
[howie@laizhenghua config]$ sudo vi IKAnalyzer.cfg.xml

修改为自己的远程扩展字典地址:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
        <comment>IK Analyzer 扩展配置</comment>
        <!--用户可以在这里配置自己的扩展字典 -->
        <entry key="ext_dict"></entry>
         <!--用户可以在这里配置自己的扩展停止词字典-->
        <entry key="ext_stopwords"></entry>
        <!--用户可以在这里配置远程扩展字典 -->
        <entry key="remote_ext_dict">http://49.234.92.64/es/cut_word.txt</entry>
        <!--用户可以在这里配置远程扩展停止词字典-->
        <!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>

最后重启elasticsearch并测试:

[howie@laizhenghua es]$ sudo docker restart elasticsearch

来到kibana测试:

POST _analyze
{
  "analyzer": "ik_smart",
  "text": "尚硅谷永远滴神,乔碧萝殿下"
}

查看回显数据:

{
  "tokens" : [
    {
      "token" : "尚硅谷",
      "start_offset" : 0,
      "end_offset" : 3,
      "type" : "CN_WORD",
      "position" : 0
    },
    {
      "token" : "永远",
      "start_offset" : 3,
      "end_offset" : 5,
      "type" : "CN_WORD",
      "position" : 1
    },
    {
      "token" : "滴",
      "start_offset" : 5,
      "end_offset" : 6,
      "type" : "CN_CHAR",
      "position" : 2
    },
    {
      "token" : "神",
      "start_offset" : 6,
      "end_offset" : 7,
      "type" : "CN_CHAR",
      "position" : 3
    },
    {
      "token" : "乔碧萝",
      "start_offset" : 8,
      "end_offset" : 11,
      "type" : "CN_WORD",
      "position" : 4
    },
    {
      "token" : "殿下",
      "start_offset" : 11,
      "end_offset" : 13,
      "type" : "CN_WORD",
      "position" : 5
    }
  ]
}

我们发现,尚硅谷与乔碧萝已成功分离出来!以后我们想分离什么词组,就往远程词库cut_word.txt里添加对应的词组就可以了!

3.9、SpringBoot整合ElasticSearch

1、两种整合方案

1、TCP协议访问9300端口

spring-data-elasticsearch:transport-api.jar

  • SpringBoot版本不同,transport-api.jar不同,不能适配es版本
  • 7.x已经不建议使用,8以后就要废弃

2、HTTP协议访问9200端口

  • JestClient:非官方,更新慢
  • RestTemplate:模拟发送HTTP请求。ES很多操作需要自己封装,比较麻烦!
  • HttpClient:同上
  • Elasticsearch-Rest-Client:官方封装了ES操作,API层次分明,上手简单。

综上所述,在我们的项目中选择使用Elasticsearch-Rest-Client(elastisearch-rest-high-level-client)

官方文档:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/index.html


2、导入maven依赖

<!--ElasticSearch-->
<dependency>
	<groupId>org.elasticsearch.client</groupId>
	<artifactId>elasticsearch-rest-high-level-client</artifactId>
	<version>7.4.2</version>
</dependency>

一定要导入与es对应的版本,导入后我们还有一个问题,可查看maven的依赖关系Dependencies!发现spring-boot-dependencies中所依赖的ELK版本与我们的不一致!这是因为SpringData默认整合es导致,所以我们还需修改es的默认版本!

<properties>
    <java.version>1.8</java.version>
    <!--加上这一行即可-->
    <elasticsearch.version>7.4.2</elasticsearch.version>
</properties>

一定要保证,所有版本都一致!
在这里插入图片描述


3、编写es的配置

参照官网文档:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high-getting-started-initialization.html

也就是配置客户端实例(新建config配置包,给容器中注入RestHighLevelClient对象):

@Configuration
public class ElasticSearchConfig {
    @Bean
    public RestHighLevelClient restHighLevelClient(){
        RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
                new HttpHost("49.234.92.64",9200,"http")));
        return client;
    }
}
4、测试
@Autowired
private RestHighLevelClient restHighLevelClient;

@Test
public void elasticSearchTest() {
	System.out.println(restHighLevelClient);
}

关于RestHighLevelClient的使用是重难点,这里主要介绍如何整合SrpingBoot,更多RestHighLevelClient的使用方法,今后再出文章。

4、SpringBoot与任务(异步/定时/邮件)

4.1、异步任务

在Java应用中,绝大多数情况下都是通过同步的方式来实现交互处理的;但是在处理与第三方系统交互的时候,容易造成响应迟缓的情况,之前大部分都是使用多线程来完成此类任务,其实,在Spring 3.x之后,就已经内置了@Async来完美解决这个问题。

两个注解:@EnableAysnc、@Aysnc

SpringBoot应用中,我们使用传统方式编写多线程任务是麻烦的,例如有这样一个方法:

@RequestMapping(path = "/hello", method = RequestMethod.GET)
public R hello() {
    return R.ok().put("data", asyncService.hello());
}

@Override
public String hello() {
    try {
        Thread.sleep(3000); // 3s
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "hello world";
}

如果我们不希望这个方法在3秒后在响应,就可以采用@EnableAysnc+@Aysnc完成此场景。

@Override
@Async // 标注此方法是异步方法
public String hello() {
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "hello world";
}

4.2、定时任务

项目开发中经常需要编写一些定时任务,比如需要在每天凌晨时候,分析一次前一天的日志信息。Spring为我们提供了异步执行任务调度的方式,提供了TaskExecutorTasckScheduler接口。

两个注解:EnableScheduling@Scheduled

cron表达式:second, minute, hour, day of month, month, and day of week

示例:"0 * * * * MON-FRI"

使用示例(注意启动类必须加上@EnableScheduling // 开启基于注解的定时任务):

@RequestMapping(value = "/scheduled", method = {RequestMethod.GET, RequestMethod.POST})
@Scheduled(cron = "0 * * * * MON-TUE")
public R scheduledTest() {
    System.out.println(scheduledService.getNowTime());
    return R.ok().put("time", scheduledService.getNowTime());
}

cron表达式:

字段允许值允许的特殊字符
0-59, - * /
0-59, - * /
小时0-23, - * /
日期1-31, - ? / L W C
月份1-12, - * /
星期0-7或SUN-SAT 0,7是SUN, - * ? / L C #

特殊字符代表的含义:

特殊字符代表含义示例
,枚举@Scheduled(cron = "0,1,2,3,4 * * * * MON-SAT")
-区间@Scheduled(cron = "0-4 * * * * MON-SAT")
*任意@Scheduled(cron = "0 * * * * MON-SAT")
/步长@Scheduled(cron = "0/4 * * * * MON-SAT")每4s执行一次
?日/星期冲突匹配
L最后
W工作日
C和calendar联系后计算过的值
#星期,4#2表示第2 哥星期4

常见表达式:

[0 0/5 14,18 * * ?] // 每天14点整,和18点整,每隔5分钟执行一次
[0 15 10 ? * 1-6] // 每个月的周一至周六10:15分执行一次
[0 0 2 ? * 6L] // 每个月的最后一个周六凌晨2点执行一次
[0 0 2 LW * ?] // 每个月的最后一个工作日凌晨2点执行一次
[0 0 2-4 ? * 1#1] // 每个月的第一个周一凌晨2点到4点期间,每个整点都执行一次

4.3、邮件任务

对于发送邮件,在我们开发中使用的也非常多,以前我们使用原始的写法,需要编写大量的代码!然而在SpringBoot中我们只需要简单的几步就能完成邮件的发送!

首先需要清楚邮件发送过程:
在这里插入图片描述

1、邮件发送需要引入spring-boot-starter-mail

<!--spring-boot-starter-mail-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-mail</artifactId>
</dependency>

2、Spring Boot 自动配置MailSenderAutoConfiguration,从mail模块上入手

...
@Import({ MailSenderJndiConfiguration.class, MailSenderPropertiesConfiguration.class }) // 导入邮件发送器与配置
public class MailSenderAutoConfiguration {
	...
}

3、定义MailProperties内容,配置在application.yml中

spring:
  mail:
    username: 3299447929@qq.com
    password: trpnspwuwlwychjj
    host: smtp.qq.com

4、自动装配JavaMailSender

@Bean
@ConditionalOnMissingBean(JavaMailSender.class)
JavaMailSenderImpl mailSender(MailProperties properties) {
	JavaMailSenderImpl sender = new JavaMailSenderImpl();
	applyProperties(properties, sender);
	return sender;
}

5、测试邮件发送

@Autowired
private JavaMailSenderImpl mailSender;
@Test
public void mailSendTest() {
	// 注意连接QQ邮箱需要安全的连接
	SimpleMailMessage message = new SimpleMailMessage();
	message.setSubject("开会通知");
	message.setText("晚上8点部门例会");
	message.setTo("2671208935@qq.com");
	message.setFrom(Objects.requireNonNull(mailSender.getUsername()));
	mailSender.send(message);
	System.out.println("send success");
}

5、SpringBoot与安全

5.1、SpringSecurity简介

安全是每个应用系统都要考虑的问题,如用户的省份认证、权限控制、预防漏洞攻击等等。市面上有两个常用的安全框架一个是Apache 的shiro、另一个是SpringSecurity

Spring Security是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型。他可以实现强大的web安全控制。对于安全控制,我们仅需引入spring-boot-starter-security模块,进行少量的配置,即可实现强大的安全管理。

1、应用程序的两个主要区域是认证授权(或者访问控制)。这两个主要区域是Spring Security 的两个目标。

2、认证(Authentication),是建立一个他声明的主体的过程(一个“主体”一般是指用户,设备或一些可以在你的应程序中执行动作的其他系统)。

3、授权(Authorization)指确定一个主体是否允许在你的应用程序执行一个动作的过程。为了抵达需要授权的店,主体的身份已经有认。
证过程建立。

4、这个概念是通用的而不只在Spring Security中。

5.2、Web&安全

1、引入SpringSecurity的场景启动器

<!--spring-boot-starter-security-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>

2、编写SpringSecurity的配置

基本功能配置如下这些功能都需要配合前端页面才能完成,然而SpringSecurity功能强大而且使用起来比较复杂,需要根据文档或观看其他视频讲解慢慢积累。

/**
 * @description: SpringSecurity的配置类
 * @author: laizhenghua
 * @date: 2021/9/1 21:24
 * 文档地址:https://docs.spring.io/spring-security-kerberos/docs/1.0.2.BUILD-SNAPSHOT/reference/htmlsingle/
 */
@EnableWebSecurity // 这个注解包含@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

   //定制请求的授权规则
   @Override
   protected void configure(HttpSecurity http) throws Exception {

       http.authorizeRequests().antMatchers("/").permitAll()
      .antMatchers("/level1/**").hasRole("vip1")
      .antMatchers("/level2/**").hasRole("vip2")
      .antMatchers("/level3/**").hasRole("vip3");


       //开启自动配置的登录功能:如果没有权限,就会跳转到登录页面!
           // /login 请求来到登录页
           // /login?error 重定向到这里表示登录失败
       http.formLogin()
          .usernameParameter("username")
          .passwordParameter("password")
          .loginPage("/toLogin")
          .loginProcessingUrl("/login"); // 登陆表单提交请求

       //开启自动配置的注销的功能
           // /logout 注销请求
           // .logoutSuccessUrl("/"); 注销成功来到首页

       http.csrf().disable();//关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
       http.logout().logoutSuccessUrl("/");

       // 记住我
       http.rememberMe().rememberMeParameter("remember"); // 请求参数含有 remember
  }

   //定义认证规则
   @Override
   protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       //在内存中定义,也可以在jdbc中去拿....
       //Spring security 5.0中新增了多种加密方式,也改变了密码的格式。
       //要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密
       //spring security 官方推荐的是使用bcrypt加密方式。

       auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
              .withUser("kuangshen").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
              .and()
              .withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
              .and()
              .withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2");
  }
}

6、SpringBoot与分布式

6.1、分布式应用

分布式应用是指:应用程序分布在不同计算机上,通过网络来共同完成一项任务。通常为服务器/客户端模式。我们可以对网站的功能进行拆分如电商系统可以抽离出商品、订单、库存等服务。

在分布式系统中,国内常用zookeeper+dubbo组合,而SpringBoot推荐使用全栈的Spring,Spring Boot+Spring Cloud。对于分布式任有很多需要探索与进阶的地方,这里先简单了解下即可。

分布式应用架构图:
在这里插入图片描述
1、单一应用架构

当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。


2、垂直应用架构

当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键。


3、分布式服务架构

当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架(RPC)是关键。


4、流动计算架构

当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键。


6.2、Zookeeper和Dubbo

1、Zookeeper(动物管理员)

ZooKeeper是一个分布式的,开放源码(Apache开源)的分布式应用程序协调服务。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组织服务等。
在这里插入图片描述
官方文档地址:https://zookeeper.apache.org/index.html


2、Dubbo

Dubbo是Alibaba开源的分布式服务框架,它最大的特点是按照分层的方式来架构,使用这种方式可以使各个层之间解耦合(或者最大限度地松耦合)。从服务模型的角度来看,Dubbo采用的是一种非常简单的模型,要么是提供方提供服务,要么是消费方消费服务,所以基于这一点可以抽象出服务提供方(Provider)和服务消费方(Consumer)两个角色。

Dubbo原理图:
在这里插入图片描述
官方文档地址:https://dubbo.apache.org/zh/


6.3、SpringBoot整合Zookeeper和Dubbo

我们已基本了解了ZookeeperDubbo,现在就使用SpringBoot搭建一个简单的分布式应用环境。

1、docker安装Zookeeper

docker pull zookeeper:3.6

# 运行zookeeper
docker run --name zookeeper -p 2181:2181 --restart always -d zookeeper:3.6

2、创建两个服务用于测试

过程梳理不便,可根据视频教学内容进行搭建

视频地址:https://www.bilibili.com/video/BV1KW411F7oX?p=33&spm_id_from=pageDriver

完整代码已提交至码云也可自行clone,码云地址:https://gitee.com/laizhenghua/zookeeper-and-dubbo

3、将服务提供者注册到注册中心(provider-ticket)

1.引入dubbo和zkClient

<!--dubbo-spring-boot-starter-->
<dependency>
	<groupId>com.alibaba.boot</groupId>
	<artifactId>dubbo-spring-boot-starter</artifactId>
	<version>0.2.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.101tec/zkclient -->
<dependency>
	<groupId>com.101tec</groupId>
	<artifactId>zkclient</artifactId>
	<version>0.10</version>
</dependency>

2.配置dubbo的扫描包和注册中心地址

dubbo:
  application:
    name: provider-ticket # 应用名字
  registry:
    address: zookeeper://49.234.92.64:2181
  scan:
    base-packages: com.laizhenghua.ticket.service

3.使用@Service发布服务,注意是com.alibaba.dubbo.config.annotation.Service

@Service
@Component("ticketService")
public class TicketServiceImpl implements TicketService {

    @Override
    public String getTicket() {
        return "《蜘蛛侠3-英雄无归》";
    }
}

4、消费&引用服务(consumer-user)

1.引入相关依赖

<!--dubbo-spring-boot-starter-->
<dependency>
	<groupId>com.alibaba.boot</groupId>
	<artifactId>dubbo-spring-boot-starter</artifactId>
	<version>0.2.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.101tec/zkclient -->
<dependency>
	<groupId>com.101tec</groupId>
	<artifactId>zkclient</artifactId>
	<version>0.10</version>
</dependency>

2.配置应用名和注册中心地址

dubbo:
  application:
    name: consumer-user
  registry:
    address: zookeeper://49.234.92.64:2181

3.我们想在consumer-user应用中使用provider-ticket应用的API,也就是进行RPC通信。必须在consumer-user中创建与provider-ticket相同的Service接口,包括包名如:

public interface TicketService {
    String getTicket();
}
// 创建接口即可,无需实现类,但是要与远程服务提供的一样

注意:ticket包是服务提供者包,user包就是服务消费者包!
在这里插入图片描述
4.编写测试接口

@Service("userService")
public class UserServiceImpl implements UserService {
    @Reference
    private TicketService ticketService;

    @Override
    public String getTicket() {
        System.out.println("买到票了");
        return ticketService.getTicket();
    }
}

5.测试是否调用成功,所有代码已发布值码云,地址在步骤2。

7、SpringCloud Alibaba

7.1、SpringCloud Alibaba简介

1、在此项目中,我们选择SpringCloud Alibaba 的一些开源组件去完成我们的业务!

SpringCloud的几大痛点

  • SpringCloud 部分组件停止维护和更新,给开发带来不便。
  • SpringCloud 部分环境搭建复杂,没有完善的可视化界面,我们需要大量的二次开发和定制SpringCloud配置复杂,难以上手,部分配置差别难以区分和合理应用。

Springcloud Alibaba的优势:

  • 阿里使用过的组件经历了考验,性能强悍,设计合理,现在开源出来大家用成套的产品搭配完善的可视化界面给开发运维带来极大的便利搭建简单,学习曲线低。

结合SpringCloud Alibaba我们最终的技术搭配方案:

  • SpringCloud Alibaba - Nacos :注册中心(服务发现/注册)
  • SpringCloud Alibaba - Nacos:配置中心(动态配置管理)
  • SpringCloud - Ribbon:负载均衡
  • Springcloud- Feign:声明式HTTP客户端(调用远程服务)
  • SpringCloud Alibaba-Sentinel:服务容错(限流、降级、熔断)
  • SpringCloud-Gateway:API网关(webflux,编程模式)
  • SpringCloud -Sleuth:调用链监控
  • SpringCloud Alibaba - Seata:原Fescar,即分布式事务解决方案

2、版本规范

  • 1.5.x 版本适用于 Spring Boot 1.5.x
  • 2.0.x 版本适用于 Spring Boot 2.0.x
  • 2.1.x 版本适用于 Spring Boot 2.1.x
  • 2.2.x 版本适用于 Spring Boot 2.2.x

7.2、SpringCloud Alibaba-Nacos[作为注册中心]

官方使用文档:https://github.com/alibaba/spring-cloud-alibaba/blob/master/spring-cloud-alibaba-examples/nacos-example/nacos-discovery-example/readme-zh.md

1、导入依赖,因为每个微服务都需要把服务注册到注册中心Nacos里,所以我们把依赖放在公共模块里即可。

<!--修改 pom.xml 文件,引入 Nacos Discovery Starter-->
<dependency>
     <groupId>com.alibaba.cloud</groupId>
     <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

2、构建Nacos Server

下载地址:https://github.com/alibaba/nacos/releases

尽量下载1.1.X版本的,版本过高时,会出现各种问题!

在应用的 /src/main/resources/application.yml 配置文件中配置 Nacos Server 地址与服务名

# nacos注册中心的地址,我们还要注册服务名称,才能注入到服务注册中心
cloud:
  nacos:
    discovery:
      server-addr: 127.0.0.1:8848
# 服务名称
application:
  name: gulimall-coupon

注意每个微服务都要配(注册中心地址地址不变,服务名称改为对应的即可)

3、使用 @EnableDiscoveryClient 注解为每个微服务开启服务注册与发现功能

@SpringBootApplication
@EnableDiscoveryClient // 开启服务注册与发现功能
public class GulimallCouponApplication {

    public static void main(String[] args) {
        SpringApplication.run(GulimallCouponApplication.class, args);
    }

}

4、启动nacos server

搭建nacos集群,然后分别启动各个微服务,将它们注册到Nacos中。

访问:127.0.0.1:8848/nacos

账号密码都是nacos

查看注册情况!这里只启动了一个微服务(gulimall-coupon)!!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QbWOfjcg-1630806989342)(C:\Users\laizhenghua\AppData\Roaming\Typora\typora-user-images\image-20201207175328736.png)]

7.3、SpringCloud Alibaba-Nacos[作为配置中心]

官方使用文档:https://github.com/alibaba/spring-cloud-alibaba/blob/master/spring-cloud-alibaba-examples/nacos-example/nacos-config-example/readme-zh.md

为什么要使用配置中心,因为我们每次修改配置文件时,都需要服务重新启动,配置文件才会生效,如果我们有很多个服务需要同时配置一些东西,我们不可能逐一打开每个服务的配置文件进行修改,这时我们希望有一个统一管理、统一修改所有服务的配置文件组件,这个组件就是使用nacos作为配置中心。

1、首先,修改 pom.xml 文件,引入 Nacos Config Starter。

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

2、在每个应用的 src/main/resources/bootstrap.properties 配置文件中配置 Nacos Config 元数据。

说明:bootstrap.properties配置文件。该配置文件会优先于“application.yml”加载被读取

# 服务名
spring.application.name=gulimall-coupon
# 配置中心地址
spring.cloud.nacos.config.server-addr=127.0.0.1:8848

此时当我们配置好配置中心地址后,服务启动默认会先去保留空间寻找服务名.properties的配置文件!

3、完成上面两步后,我们编写测试代码,注意添加@RefreshScope注解和配合@Value注解

@RestController
@RequestMapping("coupon/coupon")
@RefreshScope // 动态获取并刷新配置
public class CouponController {
    @Autowired
    private CouponService couponService;
    
    @Value(value = "${coupon.user.name}")
    private String username; // 测试属性
    @Value(value = "${coupon.user.password}")
    private String password; // 测试属性
    
    @RequestMapping(path = "/test")
    public R test(){
        return R.ok().put("username",username).put("password",password);
    }
    ...
}

4、去注册中心,配置列表添加配置。默认Data ID就是服务名.properties

此配置叫数据集,可添加任何配置,实现动态获取配置!!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LXO959AA-1630807124947)(C:\Users\laizhenghua\AppData\Roaming\Typora\typora-user-images\image-20201207221056983.png)]

5、重启启动服务,访问:http://localhost:7000/coupon/coupon/test

我们发现可以正常返回我们在配置中心配置的内容,再次修改配置内容,然后再次访问:http://localhost:7000/coupon/coupon/test

此时就可以实现动态修改配置文件!

6、注解小结:

@RefreshScope动态获取并刷新配置

@Value(value = "${配置项的名}")获取到配置

如果配置中心和当前应用的配置文件中都配置了相同的项,会优先使用配置中心的配置。

7、Nacos配置中心使用细节

1.命名空间:作用是配置隔离。

/* 第一个使用场景(环境隔离) */

// 默认是public(保留空间),默认新增的所有配置都在public空间里。
// 命名空间->新建命令空间(dev、test、prop)
// 开发(dev)、测试(test)、生产(prop):利用命名空间来做环境隔离
// 注意:在bootstrap.properties配置文件里配置需要使用哪个命名空间下的配置
spring.cloud.nacos.config.namespace=aec2029f-1604-493e-841f-314e2e227d33
    
/* 第二个使用场景(微服务隔离) */

// 每一个微服务之间互相隔离配置,每一个微服务都创建自己的命名空间,只加载自己命名下所有的配置
// 命名空间->新建命令空间(coupon、member、product、order、ware)
// 让微服务启动时,读自己的配置文件
// 修改bootstrap.properties配置文件,指定微服务启动时读取的配置文件(coupon)
spring.cloud.nacos.config.namespace=b30efa5c-6321-43c8-8e69-ebe026fecb75

2.配置集

所有配置的集合就叫配置集。例如,一个配置集可能包含了数据源、线程池、日志级别等配置项。

3.配置集ID:类似文件名(Data Id)

4.配置分组:默认所有的配置集都属于DEFAULT_GROUP,比如我们可以为获得进行分组,双十一、618、双十二等

bootstrap.properties

# 指定使用的组
spring.cloud.nacos.config.group=组名

总结:

在我们这个项目中,我们为每个微服务创建自己的命名空间,利用配置分组区分环境(dev、test、prop)

bootstrap.properties:

# 服务名
spring.application.name=gulimall-coupon
# 配置中心地址
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
# coupon微服务自己的命名空间
spring.cloud.nacos.config.namespace=b30efa5c-6321-43c8-8e69-ebe026fecb75
# 开发环境
spring.cloud.nacos.config.group=dev

8、同时加载多个配置集

在实际开发中,我们不可能所有的配置都写在一个配置文件里,那样的话不便于维护并且比较繁琐。所以我们要拆分配置文件,比如数据库连接信息我们写在datasource.yml,mybatis配置相关写在mybatis.yml配置文件里等等。配置中心Nacos也支持拆分操作!!

我们在配置中为coupon新建多个配置文件(datasource.yml、mybatis.yml、other.yml)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5SrnoAI6-1630807124967)(C:\Users\laizhenghua\AppData\Roaming\Typora\typora-user-images\image-20201208201249936.png)]

最后在bootstrap.properties中加载多个配置集

# 服务名
spring.application.name=gulimall-coupon
# 配置中心地址
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
# coupon微服务自己的命名空间
spring.cloud.nacos.config.namespace=b30efa5c-6321-43c8-8e69-ebe026fecb75
# spring.cloud.nacos.config.group=dev

spring.cloud.nacos.config.extension-configs[0].data-id=datasource.yml
spring.cloud.nacos.config.extension-configs[0].group=dev
spring.cloud.nacos.config.extension-configs[0].refresh=true

spring.cloud.nacos.config.extension-configs[1].data-id=mybatis.yml
spring.cloud.nacos.config.extension-configs[1].group=dev
spring.cloud.nacos.config.extension-configs[1].refresh=true

spring.cloud.nacos.config.extension-configs[2].data-id=other.yml
spring.cloud.nacos.config.extension-configs[2].group=dev
spring.cloud.nacos.config.extension-configs[2].refresh=true

我们把application.yml注释掉,进行测试!!

访问:http://localhost:7000/coupon/coupon/list

经测试发现,OKOK。

9、配置中心Nacos总结

  1. 任何配置文件,都可以放在配置中心中,只需要在bootstrap.properties中说明加载配置中心哪些配置文件即可。
  2. 可以使用@Value@ConfigurationProperties注解获取配置文件的信息。
  3. 需要添加@RefreshScope动态获取并刷新配置,并且配置中心的配置文件,会优先于写在当前应用的配置文件
  4. 更多关于Nacos的使用可查看官方文档:https://nacos.io/zh-cn/docs/what-is-nacos.html

End

Thank you for watching

End

  • 3
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lambda.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值