全文检索工具:快速储存、搜索和分析海量数据。
词 | 记录 |
---|---|
红海 | 1,2,3,4,5 |
行动 | 1,2,3 |
探索 | 2,5 |
特别 | 3,5 |
记录篇 | 4 |
特工 | 5 |
分词:将整句分拆为单词
保存的记录
1 - 红海行动
2 - 探索红海行动
3 - 红海特别行动
4 - 红海纪录篇
5 - 特工红海特别探索
检索
红海特工行动?
红海行动?
相关性得分
比如搜索“红海特别行动”,找到词“红海”,“特工”和“行动”,共涉及到记录1, 2, 3, 4, 5。
对于记录1和5,都命中了两个词。但是记录1只拆分出了2个词,记录5拆分出了4个词,所以记录1的相关性得分会更高。
docker pull elasticsearch:7.4.2 # 存储和检索数据
docker pull kibana:7.4.2 # 可视化检索数据
mkdir -p /mydata/elasticsearch/config # 用于挂载
mkdir -p /mydata/elasticsearch/data # 用于挂载
echo "http.host: 0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml # 允许任何IP访问
docker run命令:
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \ # 9200-API端口 9300-集群通信端口
-e "discovery.type=single-node" \ # 指定参数
-e ES_JAVA_OPTS="-Xms64m -Xmx128m" \ # 指定参数
-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 # 指定后台启动镜像
下面供复制使用:
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -e ES_JAVA_OPTS="-Xms64m -Xmx128m" -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
问题1 运行后
docker ps
没有运行,使用docker logs elasticsearch
查看报错原因如下:... "org.elasticsearch.bootstrap.StartupException: ElasticsearchException[failed to bind service]; nested: AccessDeniedException[/usr/share/elasticsearch/data/nodes];" ... "Caused by: java.nio.file.AccessDeniedException: /usr/share/elasticsearch/data/nodes"
解决 宿主机挂载目录权限问题,给要被挂载的目录所有用户的读写权限:
chmod -R 777 /mydata
-R是给目录下所有文件赋予权限
docker run --name kibana -e ELASTICSEARCH_HOSTS=http://172.16.212.10:9200 -p 5601:5601 -d kibana:7.4.2
请求 | 说明 |
---|---|
GET /_cat/nodes | 查看所有节点 |
GET /_cat/health | 查看ES健康状况 |
GET /_cat/master | 查看主节点 |
GET /_cat/indices | 查看所有索引 → show databases; |
_
开头的是元数据。现在type已不推荐使用,使用 _doc
请求 | 说明 |
---|---|
PUT customer/external/1 body {“name”: “John”} |
在customer索引下的external类型下保存1号数据为请求体中的JSON内容。如果之前存在对应ID的数据,则会进行更新;否则新增。必须携带ID。 |
POST customer/external/1body {“name”: “John”} |
同上,但是可以不带ID,表示新增。 |
GET customer/external/1 |
查询一条数据。返回信息中:_seq_no: 6 , 并发控制字段,每次更新就会+1,用来做乐观锁_primary_term: 1 , 同上,主分片重新分配、重启就会变化更新时携带下面的参数: ?if_seq_no=0&if_primary_term=1 ,比如只想修改序列号为1时的数据,别人如果中途修改过就不再修改,返回409错误并提供新的版本号。 |
POST customer/external/1/_updatebody {“doc”: {“name”: “John”}} |
更新文档。会对比原来数据,如果更新前后没有变化,那么序列号和版本都不会增加。前面两种更新都会修改。 |
DELETE customer/external/1 |
删除一条数据。 |
DELETE customer |
删除索引。ES不支持删除类型。 |
# 在Kibana中执行POST customer/external/_bulk{“index”:{“_id”:“1”}} {“name”:{“name”:“John”}} {“index”:{“_id”:“2”}} {“name”:{“name”:“Jack”}} |
批量保存。上一条失败不会影响下一条。语法格式: {action: {metadata}} {request body} actions: delete create title index |
select * from bank order by account_number asc
GET bank/_search?q=*&sort=account_number:asc
select balance, firstname from bank order by account_number asc limit 0, 5
GET bank/_search
{
"query": {
"match_all": {}
},
"sort": [{
"account_number": "asc"
},
{
"balance": "desc"
}],
"from": 0,
"size": 5,
"_source": ["balance", "firstname"]
}
select * from bank where balance = 16418
GET bank/_search
{
"query": {
"match": {
"balance": 16418
}
}
}
select * from bank where address like '%mill lane%'
GET bank/_search
{
"query": {
"match_phrase": {
"address": "mill lane"
}
}
}
select * from bank where address like '%mill%' or city like '%mill%' or address like '%movico%' or city like '%movico%'
GET bank/_search
{
"query": {
"multi_match": {
"query": "mill movico",
"fields": ["address", "city"]
}
}
}
must
must not
should
,均为字面意思,符合查询值的会贡献相关性得分。filter
进行过滤,但不会计算相关性得分。GET bank/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"gender": "F"
}
}
]}}}
text
字段由于存在分词分析的原因,会查询不到,还是需要使用match
。GET bank/_search
{
"query": {
"term": {
"age": "28"
}
}
}
实践match
,其他非text
字段匹配用term
。select * from bank
+ select age, count(age) from age group by age
GET bank/_search
{
"query": {
"match_all": {}
},
"aggs": {
"ageAgg": {
"terms": {
"field": "age",
"size": 10 # 只看10条聚合结果
}
}
},
"size": 0 # 不看具体记录只看聚合结果
}
.keyword
。with (select age, count(age) from bank group by age) as t1
select avg(balance) from bank, t1 on bank.age = t1.age group by balance
GET bank/_search
{
"query": {
"match_all": {}
},
"aggs": {
"ageAgg": {
"terms": {
"field": "age",
"size": 10 # 只看10条聚合结果
},
"aggs": {
"balanceAvg": {
"avg": {
"field": "balance"
}
}
}
}
},
"size": 0 # 不看具体记录只看聚合结果
}
select avg(age) from bank
GET bank/_search
{
"query": {
"match_all": {}
},
"aggs": {
"ageAvg": {
"avg": {
"field": "age",
}
}
},
"size": 0 # 不看具体记录只看聚合结果
}
其他聚合类型参考官方文档。
PUT /my_index
{
"mappings": {
"properties": {
"age": {"type": "integer"},
"email": {"type": "keyword"},
"name": {"type": "text"} # text类型会进行分词分析
}
}
}
employee-id
:PUT /my_index/_mapping
{
"properties": {
"employee-id": {
"type": "keyword",
"index": false # 控制这个字段是否可以被查询(false为冗余字段)
}
}
}
POST _reindex
{
"source": {
"index": "bank",
"type": "account" # 过时,迁移后默认变成_doc
},
"dest": {
"index": "newbank"
}
}
默认按空格进行分词,忽略句尾句号。但是这样无法对中文进行分词(被拆分为单字)。
POST _analyze
{
"analyzer": "standard",
"text": "中文之间是没有空格的"
}
在GitHub上下载对应ES版本的ik分词器https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v7.4.2,使用wget下载到挂载插件目录下
cd /mydata/elasticsearch/plugins
wget https://www.notion.so/1-ElasticSearch-a3b1bbe49f404ee8898d1b99b76d812a#061e5af6f9f940509ca3e89197754abc
unzip -d ik elasticsearch-analysis-ik-7.4.2.zip
rm elasticsearch-analysis-ik-7.4.2.zip
然后进入docker容器使用elasticsearch-plugin list
查看安装好的插件。
使用ik分词器
POST _analyze
{
"analyzer": "ik_max_word",
"text": "我是中国人"
}
我们经常需要自定义词库,可以将自定义词库部署到nginx让ES来访问。
附录——调整ES占用内存最大为512M
删除原来的容器,新建一个。
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
附录——安装Nginx
- 随便启动一个nginx实例,为了复制其中配置文件
docker run -p 80:80 --name nginx -d nginx:1.10
- 将容器内的配置文件拷贝到当前目录
docker container cp nginx:/etc/nginx .
- 修改文件名,并移动到/mydata/nginx下
mv nginx conf
mkdir /mydata/nginx
mv conf /mydata/nginx
- 终止原容器并删除
docker stop nginx
docker rm nginx
- 创建新的nginx
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
Nginx可以直接通过路径访问html目录下的文件。
在nginx的html目录下创建一个txt文件存储自定义词库
cd /mydata/nginx/html
mkdir es
cd es
vim segmentation.txt # 词汇换行存储
然后修改ES中ik插件的配置文件
cd /mydata/elasticsearch/plugins/ik/config
vim IKAnalyzer.cfg.xml
# 将下面这条配置取消注释,并配置为自己的自定义词库文件
<!--用户可以在这里配置远程扩展字典 -->
<entry key="remote_ext_dict">http://172.16.212.10/es/segmentation.txt</entry>
重启ES就可以生效了
docker restart elasticsearch
使用官方提供的Java High Level REST Client
https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.9/java-rest-high-getting-started-maven.html
Maven导入:
<properties>
<java.version>1.8java.version>
<elasticsearch.version>7.4.2elasticsearch.version>
properties>
<dependency>
<groupId>org.elasticsearch.clientgroupId>
<artifactId>elasticsearch-rest-high-level-clientartifactId>
<version>7.4.2version>
dependency>
问题1 按照视频中导入上方陪配置后报错
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'configurationPropertiesBeans' defined in class path resource
解决 nacos和springboot版本冲突,引入依赖管理:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloudgroupId> <artifactId>spring-cloud-dependenciesartifactId> <version>${spring-cloud.version}version> <type>pomtype> <scope>importscope> dependency> dependencies> dependencyManagement>
@Test
void indexData() throws IOException {
IndexRequest indexRequest = new IndexRequest("users");
indexRequest.id("1");
// indexRequest.source("userName", "zhangsan", "age", 18, "gender", "男");
User user = new User();
String jsonStr = JSON.toJSONString(user);
indexRequest.source(jsonStr, XContentType.JSON);
IndexResponse index = client.index(indexRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
System.out.println(index);
}
@Test
void searchData() throws IOException {
SearchRequest searchRequest = new SearchRequest();
// 指定在哪里检索
searchRequest.indices("bank");
// 指定DSL,检索条件
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 构造检索条件
sourceBuilder.query(QueryBuilders.matchQuery("address", "mill"));
sourceBuilder.aggregation(AggregationBuilders.terms("ageAgg").field("age").size(10));
sourceBuilder.aggregation(AggregationBuilders.avg("balanceAvg").field("balance"));
searchRequest.source(sourceBuilder);
// 执行检索
SearchResponse response = client.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
// 分析结果
SearchHits hits = response.getHits();
SearchHit[] hits1 = hits.getHits();
for (SearchHit hit : hits) {
String hitStr = hit.getSourceAsString();
System.out.println("hit: " + hitStr);
}
Aggregations aggregations = response.getAggregations();
Terms ageAgg = aggregations.get("ageAgg");
for (Terms.Bucket bucket : ageAgg.getBuckets()) {
String keyStr = bucket.getKeyAsString();
System.out.println("age: " + keyStr);
}
Avg balanceAvg = aggregations.get("balanceAvg");
System.out.println("balance avg: " + balanceAvg.getValue());
}
关于nested字段类型
让Nginx帮我们进行反向代理,所有来自gulimall.com的请求,都转到商品服务。
nginx.conf
|— 全局块
配置影响nginx全局的指令。如:用户组,nginx进程pid存放路径,日志存放路径。配置文件引入,允许生成worker process数等
|— events块
配置影响nginx服务器或与用户的网络连接。如:每个进程的最大连接数,选取哪种事件驱动模型处理连接请求,是否允许同时接受多个网路连接,开启多个网络连接序列化等
|— http块
可以嵌套多个server,配置代理,缓存,日志定义等绝大多数功能和第三方模块的配置,如文件引入,mime-type定义,日志自定义,是否使用sendile传掩文件,连接超时时间,单连接请求数等
|— http全局块
如upstream,错误页面,连接超时等
|— server块
配置虚拟主机的相关参数。—个http中可以有多个server
|— location
配置请求的路由,以及各种页面的处理情况
|— location
|— ...
include /etc/nginx/conf.d/*.conf;
表示会读取这conf.d目录下所有的conf配置文件listen 80;
server_name gulimall.com;
#charset koi8-r;
#access_log /var/log/nginx/log/host.access.log main;
location / {
proxy_pass http://172.16.212.1:10000;
}
缺点 如果后期微服务实例增多,需要再进入配置文件进行修改。
解决方式 给Nginx的上游服务器配置为网关,将所有匹配请求转发到网关,由网关再进行转发。
# 配置上游服务器
upstream gulimall {
server 172.16.212.1:88; # 这个是网关URL
}
proxy_set_header Host $host
location / {
proxy_set_header Host $host; # 添加header Host,内容为原来的host内容
proxy_pass http://gulimall;
}
- id: host_route
uri: lb://product
predicates:
- Host=**.gulimall.com
将静态资源如js, css和图片放到Nginx来处理,减轻Tomcat的负担。
/mydata/nginx/html/static
location /static/ {
root /usr/share/nginx/html;
}
brew install jmeter
在Options中可以将语言设置为简体中文。
数据库、应用程序、中间件(Tomcat、Nginx)、网络和操作系统等方面
首先考虑自己的应用属于CPU密集型还是IO密集型
升级版的jconsole
问题1
在终端中执行jvisualvm
后报错如下:The operation couldn’t be completed. Unable to locate a Java Runtime that supports jvisualvm. Please visit http://www.java.com for information on installing Java.
并且环境变量中已有$JAVA_HOME
解决
高版本JDK不再自带jvisualvm。从官网 https://visualvm.github.io/download.html 下载VisualVM使用。下载后打开又报错需要JDK来运行而不是JRE,修改程序目录下
/etc/visualvm.conf
添加javahome的配置:
visualvm_jdkhome="/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home”
插件中心不需要修改,是用默认的就可以安装。按照JDK修改后出现安装插件缺失依赖的问题。
VisualVM中可以看到的线程状态
运行Running | 正在运行的 |
---|---|
休眠Sleeping | sleep |
等待Wait | wait |
驻留Park | 线程池里面的空闲线程 |
监视Monitor | 阻塞的线程,正在等待锁 |
docker stats
查看容器的CPU和内存占用压测内容 | 压测线程数 | 吞吐量/s (耗时原因) | 90%响应时间 | 99%响应时间 |
---|---|---|---|---|
Nginx | 50 | 5165.8 | 13 | 52 |
Gateway | 50 | 20500.2 | 4 | 16 |
简单服务 | 50 | 24898.7 | 3 | 5 |
首级一级菜单渲染 | 50 | 634.9 (db, thymeleaf) | 119 | 200 |
首级渲染(开缓存) | 50 | 761.9 | 94 | 151 |
首级渲染(数据库加索引,关日志) | 50 | 1856.6 | 44 | 81 |
三级分类数据获取 | 50 | 7.7 (db) | 6680 | 7625 |
三级分类数据获取(数据库加索引) | 50 | 13.7 | 4223 | 4910 |
三级分类数据获取(关日志) | 50 | 23.7 | 2670 | 2802 |
三级分类数据获取(优化业务减少DB查询次数) | 50 | 126.8 | 338 | 599 |
三级分类数据获取(使用Redis缓存) | 50 | 511.3 | 124 | 254 |
首页全量数据获取 | 50 | 360.4 (静态资源) | 0 | 701 |
Nginx+Gateway | ||||
Gateway+简单服务 | 50 | 7267.1 | 12 | 49 |
全链路 | 50 | 354.9 | 40 | 61 |
结论
为了系统性能的提升,我们一般都会将部分的数据放入缓存中,加速访问。而DB承担数据落盘工作。
举例
电商类应用,商品分类,商品列表等适合缓存并加一个失效时间(根据更新频率来定),后台如果发布一个商品,买家需要5分钟才能看到新的商品一般还是可以接受的。
spring-boot-data-redis-starter
spring:
redis:
host: 172.16.212.10
port: 6379
Redis客户端可能会出现堆外内存溢出OutOfDirectMemoryError问题的原因
-Xmx300m
最大堆内存,如果netty没有指定堆外内存,默认使用这个配置解决方案
首先,不能使用 -Dio.netty.maxDirectMemory
去调大堆外内存
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
<exclusions>
<exclusion>
<groupId>io.lettucegroupId>
<artifactId>lettuce-coreartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
dependency>
lettuce 和 jedis是什么关系
lettuce和Jedis都是操作Redis的底层客户端,Spring再次封装成RedisTemplate,同时引入了两者可以根据需要选用
(查询不存在数据)
出现原因
查询一个一定不存在的数据,由于缓存没命中,去查数据库,数据库也不存在这条记录,不会写入缓存,之后再请求这个不存在数据都会到数据库去查询。利用不存在的数据进行攻击,数据库瞬时压力增大,导致崩溃
解决
null结果缓存,并加入短暂过期时间
(大面积key同时失效)
出现原因
设置的key采用了相同的过期时间,导致某一时刻同时失效,请求全部转发到数据库,数据库瞬时压力过重雪崩
解决
原有的失效时间基础上增就一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件
(某一个高频热点key失效)
出现原因
对于一些设置了过期时间的key,可能会在某些时间点被超高并发访问,是一种热点数据。如果这个key再大量请求同时进来之前正好失效,所有查询进入到数据库,数据库崩溃
解决
加锁:大量并发只让一个人去查,其他人等待。查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去数据库
数据更新时同时修改数据库和缓存。
数据更新
→ 写数据库
→ 写缓存
读到的最新数据有延迟:最终一致性。
由于卡顿等原因,导致写缓存2在最前,写缓存1在后面就出现了不一致。
这是暂时的脏数据问题,但是在数据稳定,缓存过期以后,又能得到最新的正确数据。
数据更新时更新数据库,删掉缓存中的旧数据。
数据更新
→ 写数据库
→ 删缓存
问题
我们系统的一致性解决方案
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-cacheartifactId>
dependency>
spring:
cache:
type: redis
@Cacheable
触发将数据保存到缓存的操作@CacheEvict
触发将数据从缓存删除的操作@CachePut
不影响方法执行更新缓存@Caching
组合以上多个操作@CacheConfig
在类级别共享缓存的相同配置@EnableCaching
开启缓存功能,能够扫描到这个注解就能生效默认行为
自定义
// key识别为表达式,字符串注意加单引号
@Cacheable(value = {"category"}, key = "'level1Categories'")
// 使用方法名作为key
@Cacheable(value = {"category"}, key = "#root.method.name")
spring:
cache:
type: redis
redis:
time-to-live: 3600000 #一小时
@Configuration
@EnableCaching
// 视频中在这启用cache有关的配置类,在容器中注入CacheProperties
// 实测不用,因为原来也会注入到容器中
// @EnableConfigurationProperties(CacheProperties.class)
public class MyCacheConfig {
@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericFastJsonRedisSerializer()));
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
// 将配置文件中的所有配置都生效(因为如果使用自己的配置,在org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration中读取配置文件设置的config就会被取代,所以在这里重新执行读取配置的操作)
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}
原理
CacheAutoConfiguration
→ RedisCacheConfiguration
→ 自动配置了 RedisCacheManager
→ 初始化所有的缓存
→ 每个缓存决定是用什么配置
→ 如果 redisCacheConfiguration
有就用已有的,没有就使用默认配置
→ 想改缓存的配置,只需要给容器中放一个 RedisCacheConfiguration
即可
→ 就会应用到当前 RedisCacheManager
管理的所有缓存分区中
其他的配置
spring:
cache:
type: redis
redis:
time-to-live: 3600000
# 如果指定了前缀,就用我们指定的前缀加上缓存名字作为前缀(视频中的版本为替换掉缓存名字)
key-prefix: cache_
use-key-prefix: true
# 是否缓存空值,防止缓存穿透
cache-null-values: true
可以用来实现失效模式。
在更新方法上添加这个注解,调用时就会删除掉指定的缓存。
@Override
@Transactional
// 一定要记得加单引号
@CacheEvict(value = "category", key = "'getLevel1Categories'")
public void updateCascade(CategoryEntity category) ...
批量删除
同一个注解不能重复添加,想要批量删除可以使用 @Caching
@Caching(evict = {
@CacheEvict(value = "category", key = "'getLevel1Categories'"),
@CacheEvict(value = "category", key = "'getCatalogJson'")
})
或者
// 这样会删除category分区下的所有key
@Caching(value = "category", allEntries = true)
实践
存储同一类型的数据,都可以指定成同一个分区。分区名默认就是缓存的前缀。
可以用来实现双写模式,方法执行后会将结果放入缓存,方法需要有返回值。
缓存问题 | 描述 | 通用解决 | SpringCache解决 |
---|---|---|---|
缓存穿透 | 查询一个null数据 | 缓存空数据 | 配置文件中设置缓存空数据 |
缓存击穿 | 大量并发进来同时查询一个正好过期的数据 | 加锁 | 在 @Cacheable 参数中添加 sync = true |
缓存雪崩 | 大量的key同时过期 | 加随机时间 | 配置文件中设置过期时间 |
总结
本地锁如synchronized和ReentrantLock不适用于分布式微服务的情况,所以要引入分布式锁。
private Map<String, List<Catalog2Vo>> getCatalogJsonWithRedisLock() {
// 获取UUID用于删除锁时的验证
String uuid = UUID.randomUUID().toString();
while (true) {
// 加锁(要设定一个锁的有效时间,防止设置锁所在的机器断电不能释放锁)
Boolean locked = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
// 判断是否加锁成功
if (Boolean.TRUE.equals(locked)) {
try {
// 加锁成功
} finally {
// lua脚本解锁
String script = "if redis.call(\"get\", KEYS[1]) == ARGV[1]" +
"\nthen" +
"\n return redis.call(\"del\", KEYS[1])" +
"\nelse" +
"\n return 0" +
"\nend";
redisTemplate.execute(new DefaultRedisScript<>(script, Integer.class), Collections.singletonList("lock"), uuid);
}
}
}
}
使用lua脚本保证 【获取UUID和删除锁】 操作原子性的意义
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.12.0version>
dependency>
@Configuration
public class RedissonConfig {
@Bean(destroyMethod = "shutdown")
public RedissonClient redisson() {
// 1. 创建配置
Config config = new Config();
config.useSingleServer().setAddress("redis://172.16.212.10:6379");
// 2. 根据Config创建出RedissonClient实例
return Redisson.create(config);
}
}
@ResponseBody
@GetMapping("/hello")
public String hello() {
// 1. 获取一把锁,只要锁的名字一样,就是同一把锁
RLock lock = redisson.getLock("my-lock");
// 2. 加锁
lock.lock();
或者lock.lock(10, TimeUnit.SECONDS); // 10s自动解锁,但是一定要大于业务的执行时间
// 1) 锁的自动续期,如果业务超长,运行期间自动给锁续到30s。不用担心业务时间长,锁自动过期被删掉
// 2) 加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s以后自动删除
try {
Thread.sleep(30000);
} catch (InterruptedException ignored) {
} finally {
lock.unlock();
}
return "hello";
}
lock.lock()与自动续期
30 * 1000
( LockWatchdogTimeout
看门狗的默认时间)。LockWatchdogTimeout / 3
(10s)重新执行一遍续期tryAquire
方法得到当前被占用锁的剩余有效时间 ttl
,然后通过 future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS)
阻塞 ttl
这么长的时间,之后会重新尝试获取锁;如果 ttl
为null说明获取到了锁就不需要阻塞了RedissonLock
的 unlockAsync
方法中,调用了 cancelExpirationRenewal(threadId)
来结束续期最佳实战
建议使用 lock.lock(10, TimeUnit.SECONDS)
省掉了整个续期操作。设置一个较大的过期时间比如30s,即使是业务超时了也说明这个业务出现问题了。
问题
使用Redisson分布式锁时出现这个报错:
... Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.redisson.api.RedissonClient] ... Caused by: org.redisson.client.RedisConnectionException: Unable to init enough connections amount! Only 10 of 24 were initialized. ...
解决
最低连接数要求过高,在Redisson配置类中添加
setConnectionMinimumIdleSize(1)
(数值根据情况设置)@Bean(destroyMethod = "shutdown") public RedissonClient redisson() { // 1. 创建配置 Config config = new Config(); config.useSingleServer().setAddress("redis://172.16.212.10:6379").setConnectionMinimumIdleSize(1); // 2. 根据Config创建出RedissonClient实例 return Redisson.create(config); }
锁的粒度越细越快
具体缓存的是某个数据,如11号商品: product-11-lock
RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
// 获取写锁
RLock wLock = lock.writeLock();
// 获取读锁 RLock rLock = lock.readLock();
lock.lock();
lock.unlock();
后 先 |
读 | 写 |
---|---|---|
读 | 相当于无锁,并发读,只会在redis中记录好,所有当前的读锁,他们都会同时加锁成功 | 有读锁,写也需要等待 |
写 | 等待写锁释放 | 阻塞 |
注意要在使用之前在Redis中添加这个key和数值。
set park 3
@GetMapping("/park")
@ResponseBody
public String park() throws InterruptedException {
RSemaphore park = redisson.getSemaphore("park");
park.acquire(); // 阻塞式获取一个信号,获取一个值,占一个车位
park.tryAcquire(); // 非阻塞式
return "ok";
}
@GetMapping("/go")
@ResponseBody
public String go() {
RSemaphore park = redisson.getSemaphore("park");
park.release(); // 释放一个车位
return "ok";
}
1 创建一个异步操作
runAsync
get()
阻塞等待执行完成。public static CompletableFuture<Void> runAsync(Runnable runnable)
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
supplyAsync
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)
2 计算完成回调
whenComplete
public CompletableFuture<T> whenComplete(
BiConsumer<? super T, ? super Throwable> action)
public CompletableFuture<T> whenCompleteAsync(
BiConsumer<? super T, ? super Throwable> action)
public CompletableFuture<T> whenCompleteAsync(
BiConsumer<? super T, ? super Throwable> action, Executor executor)
exceptionally
public CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)
示例CompletableFuture<Integer> done = CompletableFuture.supplyAsync(() -> {
System.out.println("bef"); // 会打印
int a = 1 / 0;
System.out.println("aft"); // 不会打印
return a;
}, executor).whenComplete((result, exception) -> {
System.out.println("done");
}).exceptionally((exception) -> {
return 1;
});
System.out.println(done.get()); // 打印1
3 方法执行完成后的处理
handle
二合一。
CompletableFuture<Integer> done = CompletableFuture.supplyAsync(() -> {
return 1 / 0;
}, executor).handle((result, exception) -> {
System.out.println("done");
return 1;
});
System.out.println(done.get()); // 打印1
4 线程串行化方法
方法 | 感知上一步结果 | 有返回值 |
---|---|---|
thenApply | ✓ | ✓ |
thenAccept | ✓ | ✗ |
thenRun | ✗ | ✗ |
带有Async默认是异步执行的。同之前。 | ||
以上都要前置任务成功完成。 |
5 两个任务都必须完成再执行
future1.combine(future2, (result1, result2) -> …)
方法 | 感知上一步结果 | 有返回值 |
---|---|---|
thenCombine | ✓ | ✓ |
thenAcceptBoth | ✓ | ✗ |
runAfterBoth | ✗ | ✗ |
6 一个完成就执行
方法 | 感知上一步结果 | 有返回值 |
---|---|---|
applyToEither | ✓ | ✓ |
acceptEither | ✓ | ✗ |
runAfterEither | ✗ | ✗ |
7 多任务组合
allOf
所有的事都做完再执行
anyOf
有一个成功就执行
但是本来session就是有有效期的,所以两种反向代理的方式可以使用
可以配置session在浏览器上的cookie存储的Domain属性,从而让多个子域名共享session id。
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-data-redisartifactId>
dependency>
spring:
session:
store-type: redis
timeout: PT30M
@Configuration
public class MySessionConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setDomainName(".gulimall.com");
cookieSerializer.setCookieName("GULISESSION");
return cookieSerializer;
}
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 25672:25672 -p 15671:15671 -p 15672:15672 rabbitmq:management
docker update rabbitmq --restart=always
4369, 25672 (Erlang发现&集群端口)
5672, 5671 (AMQP端口)
15672 (web管理后台端口)
61613, 61614(STOMP协议端口)
1883, 8883 (MQTT协议端口)
https://www.rabbitmq.com/networking.html
#
(匹配0个或多个单词)和 *
(匹配一个单词)。主题<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
引入生效大致原理
RabbitAutoConfiguration
就会自动生效RabbitTemplate
AmqpAdmin
CachingConnectionFactory
RabbitMessagingTemplate
Spring配置
spring:
rabbitmq:
host: 172.16.212.10
port: 5672
virtual-host: /
@EnableRabbit
开启rabbit@Autowired
private AmqpAdmin amqpAdmin
public void createExchange() {
DirectExchange directExchange = new DirectExchange(
name: "hello-java-exchange",
durable: true,
autoDelete: false);
amqpAdmin.declareExchange(directExchange);
}
public void createQueue() {
Queue queue = new Queue(
name: "hello-java-queue",
durable: true,
autoDelete: false
);
amqpAdmin.declareQueue(queue);
}
public void createBinding() {
// 将exchange指定的交换机和destination目的地进行绑定,使用routingKey作为指定的路由键
Binding binding = new Binding(
destination: "hello-java-queue",
Binding.DestinationType.QUEUE,
exchange: "hello-java-exchange",
routingKey: "hello.java"
);
amqpAdmin.declareBinding(binding);
}
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendMessage() {
rabbitTemplate.convertAndSend(
exchange: "hello-java-exchange",
routingKey: "hello.java",
new Object() // 如果发送的消息是一个对象,就会使用序列化机制,对象必须实现Serializable
);
}
不使用序列化而使用JSON格式:
@Configuration
public class MyRabbitConfig {
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
}
@RabbitListener(queues = {"hello-java-queue"})
public void receiveMessage(
Message message,
T content // 当时发送的消息类型T在接收时Spring会自动转化
Channel channel // 当前传输数据的通道
) {
byte[] body = message.getBody();
}
@RabbitListener 和 @RabbitHandler
@RabbitListener
可以标注在方法或类上
@RabbitHandler
可以标注在方法上
二者配合使用实现对重载区分不同的消息
@RabbitListener(queues = ...)
public class RabbitTest {
@RabbitHandler
public void handle(Car car) {
...
}
@RabbitHandler
public void handle(Plane plane) {
...
}
}
保证消息不丢失,可靠抵达,可以使用事务消息,性能下降250倍,为此引入确认机制
在application.properties中设置
spring.rabbitmq.publisher-confirms=true
PublisherConfirms(true)
选项, 开启 confirmCallback。returnCallback
。@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct // @Configuration对象创建完成以后执行的方法
public void initRabbitTemplate() {
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(
CorrelationData correlationData, // 当前消息的唯一关联数据(这个是消息的唯一id)
boolean ack, // 消息是否成功收到
String cause // 失败的原因
) {
// callback
}
});
}
在application.properties中配置
spring.rabbitmq.publisher-returns=true
spring.rabbitmq.template.mandatory=true
@PostConstruct // @Configuration对象创建完成以后执行的方法
public void initRabbitTemplate() {
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void confirm(
Message message, // 投递失败的消息详细信息
int replyCode, // 回复的状态码
String replyText, // 回复的文本内容
String exchange, // 当时这个消息发送给哪个交换机
String routingKey // 当时这个消息用哪个路由键
) {
// callback
}
});
}
默认是自动确认的,只要消息收到,客户端会自动确认,服务端会移除这个消息。
问题
我们收到很多消息,自动回复给服务器ack,只有一个消息处理成功,宕机了,发生消息丢失。
开启手动确认模式
spring.rabbitmq.listener.simple.acknowledge-mode=manual
只要没明确告诉mq货物被签收(没有ack),消息一直是unacked状态。即使Consumer宕机,消息也不会丢失,会重新变为Ready,下一次有新的Consumer连接进来就发给他。
@RabbitListener(queues = {"hello-java-queue"})
public void receiveMessage(
Message message,
T content, // 当时发送的消息类型T在接收时Spring会自动转化
Channel channel // 当前传输数据的通道
) {
// deliveryTag是channel内按顺序自增的
channel.basicAck(
message.getMessageProperties().getDeliveryTag(),
multiple: false
requeue: true
);
}
Ack消息确认机制总结
ack()
,接受下一个消息,此消息broker就会移除nack()
/reject()
,重新发送给其他人进行处理,或者容错处理后ackack()
/nack()
方法,broker认为此消息正在被处理,不会投递给别人。此时客户端断开,消息不会被broker移除,会投递给别人