Nacos 注册中心
- 引入依赖
<!--服务注册/发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--配置中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
-
指定配置中心位置
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
-
添加注解
@EnableDiscoveryClient
@RefreshScope 搭配 @Value 可以实现动态配置项
OpenFeign 远程调用
- 引入依赖(前置依赖Nacos)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
- 创建接口,指定方法
- 启动类注解`@EnableFeignClients(basePackages = "org.june.member.feign")
Gateway 网关
- 添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
- 指定配置中心(配置文件、启动类注解)
- 配置文件写路由
Spring Cloud Gateway 2.1.0 中文官网文档 - 云+社区 - 腾讯云 (tencent.com)
- 简单Demo
spring:
cloud:
gateway:
routes:
- id: qq_route
uri: https://www.qq.com
predicates:
- Query=url,qq # 网关地址:端口?url=qq -> qq.com
跨域
// 网关统一跨域
@Configuration
public class CorsConfig {
//添加跨域过滤器
@Bean
public CorsWebFilter corsWebFilter() {
// 基于url跨域
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// 跨域配置信息
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.setAllowCredentials(true);
// 向source中注册new出来的配置,并设置任意url都要进行跨域配置
source.registerCorsConfiguration("/**", corsConfiguration);
return new CorsWebFilter(source);
}
}
路径重写
// 根据请求路径
- RewritePath=/api(?<segment>/?.*), /renren-fast/$\{segment}
干掉api 需要保留的部分 替换的目标逻辑
// 根据域名
- Host=projectdemo.top,item.projectdemo.top,www.projectdemo.top
MybatisPlus
// 逻辑删除
@TableLogic(value = "1", delval = "0")
private Integer showStatus;
// 分页
@Configuration
@EnableTransactionManagement
@MapperScan("org.june.product.dao")
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加【分页】插件
PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor();
// 设置请求的页面大于最大页后操作,true调回到首页,false继续请求。默认false
pageInterceptor.setOverflow(true);
// 单页分页条数限制,默认无限制
pageInterceptor.setMaxLimit(1000L);
// 设置数据库类型
pageInterceptor.setDbType(DbType.MYSQL);
interceptor.addInnerInterceptor(pageInterceptor);
return interceptor;
}
}
ES6
ECMAScript 是浏览器脚本语言的规范,JavaScript 是该规范的具体实现。以下示例以 JavaScript 为例
变量
- var 变量声明变量往往会越狱,只能声明一次
- let 声明变量有严格作用域,可以声明多次
- const 声明变量不允许改变
解构表达式
let arr = [1,2,3]
// ↓
let [a,b,c] = arr
/////////////////
const person = {
name: "June",
age: 21
}
// ↓
const {name:var1,age:var2} = person;
console.log(var1,var2)
字符串扩展
let str = "helloworld"
str.startsWith()
str.endsWith()
str.includes()
模板字符串
let var1 = "June"
let var2 = "March"
let ss = `${var1}
this is a test
${var2}`
console.log(ss)
函数参数
function test1(a,b){
...
}
test(var1) // 只传一个
///////////////////////
function test2(...vars){
...
}
test(var1,var2) // 传多个
箭头函数
var print = function(obj){
console.log(obj)
}
// ↓
var print = obj => console.log(obj)
///////////////// 对象解构
var person = {
name:"jack",
age:21
}
var hello = ({name}) => console.log("hello," + name)
对象优化
var person = {
name:"jack",
age:21
}
Object.keys(person) -> ["name","age"]
Object.values(person) -> ["jack",21]
Object.entries(person) -> [Array(2),Array(2),Array(2)]
////////////////////////////// 追加
const target = {a:1}
const source1 = {b:2}
const source2 = {c:3}
Object.assign(target, source1, source2)
target -> {a:1, b:2, c:3}
////////////////////////////// 对象简写1
const age = 21
const name = "张三"
const person = {age,name}
////////////////////////////// 对象简写2
let person = {
name: "jack",
eat: function(food){
console.log(this.name + "在吃" + food)
},
eat2: food => console.log(person.name + "在吃" + food)
}
/////////////////////////////// 拷贝对象(深拷贝)
let p1 = {name: "Any", age:15}
let p2 = {...p1}
/////////////////////////////// 对象合并(会覆盖)
let age = {age:15}
let name = {name: "Amy"}
let person = {...age,...name}
数组增强
let arr = ['1', '2', '3', '4']
arr.map(item)=>{
return item*2
} // => [2,4,6,8]
// ↓
arr = arr.map(item=>item*2)
////////////////////////////////
let result = arr.reduce((a,b)=>{
return a+b
},100) // => 110
模块化
///////// js1.js
var name = "jack"
var age = 21
function add(a,b){
return a + b
}
export{name,age,add}
////////// js2.js
import {name,age,add} from "./js1.js"
add(1,2)
Vue
v-text v-html
文本值绑定
<div id="app">
<h1>{{ name }}</h1>
<h1 v-text="name1"></h1>
<span v-html="name2"></span>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
let vm = new Vue({
el: '#app',
data: {
name: '张三d',
name1: '张三2',
name2: '<h1>张三3</h1>'
}
})
</script>
v-bind
属性值绑定,一般用于 href、class、style
<a v-bind:href:"link">gogogo</a>
<div id="app">
<span v-bind:class="{active:isActive,'text-danger':hasError}"
v-bind:style="{color:color1,frontSize:size}">你好</span>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
let vm = new Vue({
el: '#app',
data: {
isActive:true,
hasError:true,
color1:'red',
size:'36px'
}
})
</script>
v-model
双向绑定,不同于以上两者
v-on
事件绑定
v-for
遍历
v-if v-show
v-if 条件为 true,元素才会被渲染;v-show 条件为true,元素才会显示
前者是注释掉了相关代码,后者把样式改变了
计算属性、侦听器和过滤器
<h1>
{{totalPrice}}
</h1>
<script>
new Vue({
...
data:{
a: 5,
b: 6
},
<!--计算属性-->
computed:{
totalPrice(){
return a+b
}
},
<!--侦听器-->
watch:{
a: function(newVal,oldVal){
...
}
}
<!--过滤器-->
filters:{
genderFilter(val){
if(val ==1){
return '男'
} else{
return '女'
}
}
<!--
调用
{{user.gender | genderFilter}}
-->
}
})
</script>
<script>
<!--全局过滤器-->
Vue.filter("gFilter",function(val)){
if(val ==1){
return '男'
} else{
return '女'
}
}
</script>
组件化
生命周期和钩子函数
vue 模块化开发
sudo npm install webpack -g
npm install -g @vue/cli-init
sudo npm install --global vue-cli
# 再创建项目文件夹
vue init webpack vue-demo
cd vue-demo
npm run dev
Elasticsearch
基本概念
- Index(索引)
MySQL的库
- Type(类型)(deprecated in 6.0.0)
在Index中,可以定义一个或多个类型,类似于MySQL中的表;每一种类型的数据放在一起;
- Document(文档)
保存在某个索引下,某种类型的一个数据,文档是JSON格式的,Document就像是MySQL中某个Table里面的内容
- 倒排索引
创建实例
- elasticsearch
# Docker !8版本需要额外配置东西
docker pull elasticsearch:7.17.0
docker pull kibana:7.17.0
# 创建
mkdir -p /opt/elasticsearch/plugins
mkdir -p /opt/elasticsearch/config
mkdir -p /opt/elasticsearch/data
#
echo "http.host: 0.0.0.0" >/opt/elasticsearch/config/elasticsearch.yml
#
chmod -R 777 /opt/elasticsearch
# 启动Elastic search
# 9200是用户交互端口 9300是集群心跳端口
# -e指定是单阶段运行
# -e指定占用的内存大小,生产时可以设置32G
sudo docker run --name elasticsearch \
-p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xmx512m" \
-v /opt/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /opt/elasticsearch/data:/usr/share/elasticsearch/data \
-v /opt/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.17.0
- kibana
sudo docker run --name kibana \
-e ELASTICSEARCH_HOSTS=http://localhost:9200 \
-p 5601:5601 -d kibana:7.17.0
# 配置中文
docker cp 源 目的 # 容器写法 容器ID:路径 主机直接写路径
docker exec -it ID /bin/bash
kibana.yml 中添加 i18n.locale: "zh-CN"
初步检索
1._cat
GET /_cat/nodes 查看所有节点
GET /_cat/health 查看es健康状况
GET /_cat/master 查看主节点
GET /_cat/indices 查看所有索引 show databases
2.添加数据
注:http://124.222.22.217:9200/customer/external/1
其中的 '1' 指定了id,PUT 请求必须携带id;而 POST 可以不指定 id,不指定id,会自动生成id,指定id会对其进行修改(不存在则新增)
3.查询数据
GET customer/external/1
精确根据ID查找
GET customer/_search
查询所有
GET customer/_search 条件查询
{
"query":{"match_all":{}},
"sort":[
{"account_number":"asc"}
],
"from":10,
"size":10
}
http://124.222.22.217:9200/customer/external/1?if_seq_no=0&if_primary_term=1 # 修改配合并发使用
4.修改数据
POST携带JSON(带上doc) http://124.222.22.217:9200/customer/external/1/_update # 会检查前后更新内容是否一致,其余方式如PUT、POST(不带_update)都不会对比内容
5.删除数据
DELETE http://124.222.22.217:9200/customer/external/1
6.批量API
POST custmoer/external/_bulk
{"index":{"_id":1}}
{"name":"Jone"}
{"index":{"_id":"2"}}
{"name":"Jane"}
# 回车必要
进阶检索
1.SearchAPI
ES支持两种基本方式减缩:
- 一个是通过使用 REST request URI 发送搜索参数( uri + 检索参数)
- 另一个是通过使用 REST request body 来发送他们( uri + 请求体)
# 样例
GET bank/_search?q=*&sort=account_number:asc
-------
GET bank/_search
{
"query":{
"match_all":{}
},
"sort":[{
"account_number":"asc"
},{
"balance":"desc"
}
]
}
请求体语法格式
{
QUERY_NAME{
ARGUMENT:VALUE,
ARGUMENT:VALUE
}
}
参数说明
# 一级参数
query 指定查询操作
sort 指定排序字段
from 分页操作
size 分页操作
_source 指定查询字段
# 二级参数
query:match { key:value } 非字符串值模糊查询(按相关度-score排序);数字则精确匹配
query:match_phrase 类似于前者,但不会对字符串进行分词,而是当做一条短语进行匹配
query:multi_match 分词 + 多字段匹配
query:bool 构造复杂查询 must must_not should(可以提高得分)
query:filter 不计算相关性得分,直接过滤
query:term term是代表完全匹配,即不进行分词器分析,文档中必须包含整个搜索的词汇;全文检索字段用 match ,其他 text 字段用term
query:aggregations 字段聚合处理
2.Mapping
指定索引下的属性类型
添加映射
那么如何修改?
数据迁移
elastic已经不推荐使用 type
3.分词
安装ik分词器
添加自定义词汇
- 配置 nginx 作为远程词库
- 在 html/es/fenci.txt 中填入新词
- elasticsearch/plugs/... 中配置远程词库为上面的地址
项目应用
- 依赖
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>8.0.0</version>
<!--docker elasticsearch 7.17-->
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.13.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.13.0</version>
</dependency>
<dependency>
<groupId>jakarta.json</groupId>
<artifactId>jakarta.json-api</artifactId>
<version>2.0.1</version>
</dependency>
- ik分词器
ik7.17.0下载链接
# 拷贝并解压到 /opt/elasticsearch/plugins/ik 目录下
docker exec -it elasticsearch bash
elasticsearch-plugin list
# 配置自定义词库,需要nginx 略
vim /opt/elasticsearch/plugins/ik/config/IKAnalyzer.cfg.xml
<entry key="remote_ext_dict">http://xxx</entry>
Redis 缓存
整合 Redis
- 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 调用API RedisTemplate StringRedisTemplate
- 代码整合示例
public Map<String, List<Catalog2Vo>> getCatalogJson(){
String catalogJSON = redis.opsForValue().get("catalogJson");
if(StringUtils.isEmpty(catalogJSON)){
Map<String, List<Catalog2Vo>> catalogJsonFromDB = getCatalogJsonFromDB();
redis.opsForValue().set("catalogJson",
JSON.toJSONString(catalogJsonFromDB));
}
Map<String, List<Catalog2Vo>> list = JSON.parseObject(catalogJSON,
new TypeReference<Map<String, List<Catalog2Vo>>>(){});
return list;
}
高并发下的缓存问题
缓存穿透
- 查询一个一定不存在的数据,由于缓存一定不命中,将去查询数据库,但数据库也无记录,这就失去了缓存的意义
- 利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃
- 解决
- null 结果缓存,并加入短暂过期时间
- 布隆过滤器
缓存雪崩
- 设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到了数据库,压力瞬时过大
- 解决
- 过期时间采用随机数
- 加锁
缓存击穿
- 对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发的访问,是一种非常【热点】的数据
- 如果这个 key 在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,称为缓存击穿
- 解决
- 加锁;大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取锁,先查缓存,就会有数据,不用去db
加锁解决【缓存击穿】
该段代码存在分布式锁的问题
分布式锁简单实现
依托于redis的 set catalog_lock lockId [ex seconds][px millseconds] nx
命令实现
public Map<String, List<Catalog2Vo>> getCatalogJson() {
// double check
String catalogJSON = redis.opsForValue().get("catalogJson");
if (StringUtils.isEmpty(catalogJSON)) {
// 分布式加锁 ↓
String lockId = UUID.randomUUID().toString();
// set catalog_lock lockId [ex seconds][px millseconds] nx
if (Boolean.TRUE.equals(redis.opsForValue().
setIfAbsent("catalog_lock", lockId, 300L, TimeUnit.SECONDS))) {
// log.error("redis成功加锁!!!");
// 分布式加锁 ↑
//////业务执行开始//////
try {
Map<String, List<Catalog2Vo>> catalogJsonFromDB = getCatalogJsonFromDB();
redis.opsForValue().set("catalogJson",
JSON.toJSONString(catalogJsonFromDB),
1, TimeUnit.DAYS);
} finally {
//////业务执行结束,勿忘删除??//////
// 防止业务执行时间过长,导致删除操作实际上删除的是别人的锁
// 但是这两步骤并不是原子操作,获取值进行比较的时候可能锁已经过期
// 所以需要采用 lua 脚本来保证原子性
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
redis.execute(new DefaultRedisScript<>(script, Long.class), Collections.singletonList("lock"), lockId);
// log.error("redis成功删锁!!!");
}
} else {
// log.error("等待锁!!");
try {
// 防止空转
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJson();
}
}
return JSON.parseObject(catalogJSON,
new TypeReference<Map<String, List<Catalog2Vo>>>() {
});
}
Redisson框架解决分布式锁
- 引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.8</version>
</dependency>
- 配置类
@Configuration
public class MyRedissonConfig {
@Value("${spring.redis.host}")
String host;
@Value("${spring.redis.port}")
String port;
@Bean
RedissonClient redisson() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://"+host+":"+port);
return Redisson.create(config);
}
}
Redisson 说明
- ReentrantLock
- ReadWriteLock
加锁示例
- Semaphore
计数器,正计时
- CountDownLatch
计数器,倒计时
分布式锁 Redisson 实现
/**
* 普通锁实现
*/
public Map<String, List<Catalog2Vo>> getCatalogJson() {
// 双重检查
String catalogJson = redis.opsForValue().get("catalogJson");
Map<String, List<Catalog2Vo>> catalogJsonFromDB;
if (StringUtils.isEmpty(catalogJson)) {
// lock
RLock catalogLock = redisson.getLock("catalogJsonLock");
catalogLock.lock();
try {
// 双重检查
catalogJson = redis.opsForValue().get("catalogJson");
if (StringUtils.isNotEmpty(catalogJson)) {
return JSON.parseObject(catalogJson,
new TypeReference<Map<String, List<Catalog2Vo>>>() {
});
}
catalogJsonFromDB = getCatalogJsonFromDB();
} finally {
catalogLock.unlock();
}
return catalogJsonFromDB;
}else{
return JSON.parseObject(catalogJson,
new TypeReference<Map<String, List<Catalog2Vo>>>() {
});
}
}
/**
* 读写锁实现
*/
public Map<String, List<Catalog2Vo>> getCatalogJson() {
RReadWriteLock readWriteLock = redisson.getReadWriteLock("catalogJsonLock");
RLock rLock = readWriteLock.readLock();
Map<String, List<Catalog2Vo>> catalogJsonFromDB;
try {
rLock.lock();
// 业务中注意仍然要双重检查
catalogJsonFromDB = getCatalogJsonFromDB();
} finally {
rLock.unlock();
}
return catalogJsonFromDB;
}
【缓存数据一致性】问题及解决方案
SpringCache
- 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
- 写配置
spring:
cache:
type: redis
redis:
time-to-live: 3600000 # 单位|毫秒
key-prefix: CACHE_
cache-null-values: true
- 启动类或配置类
@EnableCaching
- 配置类
@EnableConfigurationProperties(CacheProperties.class)
@Configuration
@EnableCaching
public class MyRedisCacheConfig {
private Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer() {
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer =
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.configure(MapperFeature.USE_ANNOTATIONS, false);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
// 此项必须配置,否则会报java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to XXX
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
return jackson2JsonRedisSerializer;
}
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory){
//初始化一个RedisCacheWriter输出流
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory);
//采用Jackson2JsonRedisSerializer序列化机制
// Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
//创建一个RedisSerializationContext.SerializationPair给定的适配器pair
RedisSerializationContext.SerializationPair<Object> pair = RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer());
//创建CacheConfig
RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(pair);
return new RedisCacheManager(redisCacheWriter, defaultCacheConfig);
}
// @Bean
// RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
// RedisCacheConfiguration config = RedisCacheConfiguration.
// defaultCacheConfig().
// serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())).
// serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new Jackson2JsonRedisSerializer<>(Object.class)));
// CacheProperties.Redis redisProperties = cacheProperties.getRedis();
// 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;
// }
}
注解说明
@Cacheable
代表当前方法的结果需要缓存,如果缓存中有,方法不用调用。如果缓存中没有,会调用方法,最后将方法结果放入缓存
默认行为:
- 如果缓存中有,则方法不调用
- key 默认自动生成,{指定名称}::SimpleKey [] (默认名称)
- 缓存的 value 值,默认使用jdk序列化机制,保存的是序列化结果,一定要实现
Serializable
接口! - 默认TTL=-1
- 动态取值
@Cacheable(value = "sku",key = "#root.args[1]")
(可以点进源码看更详细的)
改动:
- 指定redis key名称:
key = "#root.methodName"
- 更改序列化器
@Configuration
@EnableCaching
public class MyCacheConfig {
@Bean
RedisCacheConfiguration redisCacheConfiguration(){
return RedisCacheConfiguration.
defaultCacheConfig().
serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())).
serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
}
}
@CacheaEvict
方法执行后删除指定的缓存,一般加在更新操作上(失效模式)
@Override
@Transactional
@Caching(evict = {
@CacheEvict(value = "category",key = "'getLevelOneCategories'" ),
@CacheEvict(value = "category",key = "'getCatalogJson'" ),
})
// @CacheEvict(value = "{category}",allEntries = true) // 第二种方式
public void updateCascade(CategoryEntity category){...}
@CachePut
方法执行后将方法的返回值放入缓存,一般加在更新操作上(双写模式)
流程说明
- 业务代码执行前先检查缓存,有就返回,没有就执行业务代码
- 业务代码执行完后,返回值被SpringCache接收,并将其添加入缓存
- 如果
sync = true
那么其查询缓存的操作会变成加锁方式,这是一个本地锁,虽然不能保证一次的数据库查询,但也能保证个位数的查询,性能完全够用,而且操作简单
SpringCache && Redisson 的实现
/**
* SpringCache + Redisson
* 经测试,该段代码在大量并发下仍然不能保证1次数据库查询
* 但查询次数在 200 并发下数据库查询已经降到了个位数
*/
@Override
@Cacheable(value = "category", key = "'getCatalogJson'")
public Map<String, List<Catalog2Vo>> getCatalogJson() {
// 如果能进入这里,那redis中必然没有缓存
RLock catalogLock = redisson.getLock("catalogJsonLock");
try {
catalogLock.lock();
// 双重检查
String catalogJson = redis.opsForValue().get("CACHE_{category}::getCatalogJson");
if (StringUtils.isNotEmpty(catalogJson)) {
return JSON.parseObject(catalogJson,
new TypeReference<Map<String, List<Catalog2Vo>>>() {
});
}
// log.error("查询数据库!!!");
/////////业务开始//////////
List<CategoryEntity> selectList = baseMapper.selectList(null);
List<CategoryEntity> level1Categories = getParentCid(selectList, 0L);
Map<String, List<Catalog2Vo>> result = level1Categories.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
// 找到一级分类
List<CategoryEntity> catalog1Vos =
getParentCid(selectList, v.getCatId());
// 封装二级、三级分类开始
List<Catalog2Vo> catalog2Vos = null;
if (CollectionUtils.isNotEmpty(catalog1Vos)) {
catalog2Vos = catalog1Vos.stream().map(l2 -> {
/////////////// 组装开始 /////////////////
Catalog2Vo catalog2Vo = new Catalog2Vo(
v.getCatId().toString(),
null,
l2.getCatId().toString(),
l2.getName()
);
List<CategoryEntity> level3 = getParentCid(selectList, l2.getCatId());
if (CollectionUtils.isNotEmpty(level3)) {
List<Catalog2Vo.Catalog3Vo> catalog3Vos = level3.stream().map(l3 ->
new Catalog2Vo.Catalog3Vo(
l2.getCatId().toString(),
l3.getCatId().toString(),
l3.getName()))
.collect(Collectors.toList());
catalog2Vo.setCatalog3List(catalog3Vos);
}
/////////////// 组装结束 /////////////////
return catalog2Vo;
}).collect(Collectors.toList());
}
return catalog2Vos;
}));
/////////业务结束//////////
return result;
}finally {
catalogLock.unlock();
}
}
/**
* SpringCache
*/
@Override
@Cacheable(value = "category", key = "'getCatalogJson'",sync = true)
public Map<String, List<Catalog2Vo>> getCatalogJson() {
// 如果能进入这里,那redis中必然没有缓存
/////////业务开始//////////
List<CategoryEntity> selectList = baseMapper.selectList(null);
List<CategoryEntity> level1Categories = getParentCid(selectList, 0L);
Map<String, List<Catalog2Vo>> result = level1Categories.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
// 找到一级分类
List<CategoryEntity> catalog1Vos =
getParentCid(selectList, v.getCatId());
// 封装二级、三级分类开始
List<Catalog2Vo> catalog2Vos = null;
if (CollectionUtils.isNotEmpty(catalog1Vos)) {
catalog2Vos = catalog1Vos.stream().map(l2 -> {
/////////////// 组装开始 /////////////////
Catalog2Vo catalog2Vo = new Catalog2Vo(
v.getCatId().toString(),
null,
l2.getCatId().toString(),
l2.getName()
);
List<CategoryEntity> level3 = getParentCid(selectList, l2.getCatId());
if (CollectionUtils.isNotEmpty(level3)) {
List<Catalog2Vo.Catalog3Vo> catalog3Vos = level3.stream().map(l3 ->
new Catalog2Vo.Catalog3Vo(
l2.getCatId().toString(),
l3.getCatId().toString(),
l3.getName()))
.collect(Collectors.toList());
catalog2Vo.setCatalog3List(catalog3Vos);
}
/////////////// 组装结束 /////////////////
return catalog2Vo;
}).collect(Collectors.toList());
}
return catalog2Vos;
}));
/////////业务结束//////////
return result;
}
异步&线程池
线程回顾
- 继承Thread
- 主线程无法获取运算结果
- 实现Runnable 接口
- 主线程无法获取运算结果
- 其实与上面是一种形势,都实现的是 Runnable 接口的 run 方法
- 实现 Callable 接口 + FutureTask(可拿到返回结果,处理异常)
- 可以获取运算结果
- 以上三种方式都不能控制资源,易导致资源耗尽而系统崩溃
- 线程池
// 1.创建固定数量的线程池(自带工具类快速创建)
ExecutorService executor = Executors.newFixedThreadPool(5);
// 2. 原生方式创建
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
java并发编程-线程池(二)ThreadPoolExecutor参数详解 - 知乎 (zhihu.com)
CompletableFuture 异步编排
提供了四个静态方法来创建异步操作:
- runXxx 都是没有返回结果的,supplyXxx 都是可以获取返回结果的
- 可以传入自定义线程池,否则就用默认线程池
简单Demo
↑ 简单写法 handle() ↓
线程串行化方法
thenRun
不能获取到上一步执行结果thenAcceptAsync
接收上一步返回结果,无返回值thenApplyAsync
接收上一步结果,有返回值
两组任务组合
都要完成
runAfterBoth
组合两 future ,不需要获取 future 结果,只需两个 future 处理完后,处理该任务thenAcceptBoth
组合两个 future,获取两个 future 返回结果,然后处理任务,没有返回值thenCombine
组合两个 future,获取两个 future 的返回结果,并返回当前任务的返回值
CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(()->{
System.out.println("1开始");
System.out.println("1结束");
return 1;
},executor);
CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(()->{
System.out.println("2开始");
System.out.println("2结束");
return 2;
},executor);
// runAfterBothxxxb 无返回值
f1.runAfterBothAsync(f2,()-> System.out.println("3"));
// thenAcceptBothAsync 可以获取前两步的返回结果
f1.thenAcceptBothAsync(f2,(a,b)-> System.out.println("a:"+a+";b:"+b),executor);
// thenCombineAsync 可以获取前两步返回结果,可以后自己的返回值
CompletableFuture<String> f3 = f1.thenCombineAsync(f2, (a, b) -> {
return a + " " + b + " 3333";
}, executor);
一个完成
applyToEither
两任务有一个执行完成就获取返回值,处理任务并有返回值acceptEither
两任务有一个执行完成就获取返回值,处理任务但没有返回值runAfterEither
两任务有一个执行完成,不需要获取 future 结果,处理任务,也没有返回值
多任务组合
Session共享【重点】
问题描述
解决方案
session复制(session广播)
客户端存储
这种方式类似于 token 方式,但不如 token 优雅;总体来说安全性不高
哈希一致性
统一存储
Spring-Session(统一存储方案)
- 依赖
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
- 配置项
spring.session.store-type=redis
server.servlet.session.timeout=30m
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=L200107208017@./
- 配置类个性化配置
@Configuration
public class SessionConfig {
/**
* 指定session作用域
*/
@Bean
public CookieSerializer cookieSerializer(){
DefaultCookieSerializer defaultCookieSerializer = new DefaultCookieSerializer();
// defaultCookieSerializer.setDomainName("mall.com"); //TODO 项目上线后需要将session作用域放大!
defaultCookieSerializer.setCookieName("MALLSESSION");
return defaultCookieSerializer;
}
/**
* 指定序列化器
*/
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
return new GenericFastJsonRedisSerializer();
}
}
原理分析
- 第一次访问服务器,服务器都会设置这个cookie:session的id,默认名为
JSESSIONID
,这个值可以修改
登陆拦截
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<Long> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
MemberRespVo attribute = (MemberRespVo) session.getAttribute(AuthenticCommonConstant.LOGIN_USER);
if (attribute != null) {
loginUser.set(attribute.getId());
return true;
}
else {
Map<String,String> list = new HashMap<>();
list.put("msg","请登录!");
session.setAttribute("errors",list);
response.sendRedirect("http://auth.projectdemo.top/login.html");
return false;
}
}
}
性能压测
- 响应时间(Response Time:RT)
响应时间指从客户端发起某一个请求开始,到客户端接收到服务器端返回的响应结束,整个过程所耗费的时间
- HPS(Hits Per Second)
每秒点击次数,单位【次/秒】
- TPS(Transaction per Second)
系统每秒处理交易数,单位【次/笔】
- QPS(Query per Second)
系统每秒处理查询次数,单位【次/秒】;对于互联网业务中,如果某些业务有且仅有一个请求连接,那么TPS = QPS = HPS
,一般情况下用TPS来衡量整个业务流程,用QPS来衡量接口查询次数,用HPS来表示对服务器的单机请求
无论TPS、QPS、HPS,此指标是衡量系统处理能力非常重要的指标,越大越好,根据经验,一般情况下:
-
金融行业:1K~5W TPS,不包括互联网化的活动
-
保险行业:100 ~ 10W TPS,不包括互联网化的活动
-
制造行业:10 ~ 5000 TPS
-
互联网电子商务:1W ~ 100W TPS
-
互联网中型网站:1K ~ 5W TPS
-
互联网小型网站:500 ~ 1W TPS
-
最大响应时间(Max Response Time)& 最少响应时间(Mininum Response Time)
-
90%响应时间(90% Response Time)
所有用户的响应时间进行排序,90%用户的响应时间平均值
性能测试主要关注以下三个指标:
- 吞吐量:每秒钟系统能够处理的请求数、任务数
- 响应时间:服务处理一个请求或一个任务的耗时
- 错误率:一批请求中结果出错的请求占比
JMeter Address Already in use 问题解决
Windows 本身提供的端口访问机制的问题。
Windows 提供给 TCP/IP 链接的端口为 1024-5000,并且要四分钟来循环回收他们。就导致我们在短时间内跑大量的请求时将端口占满了。
-
cmd 中,用 regedit 命令打开注册表
-
在
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters
下,- 右击
parameters
,添加一个新的DWORD
,名字为MaxUserPort
- 然后双击 MaxUserPort,输入数值数据为 65534,基数选择十进制(如果是分布式运
行的话,控制机器和负载机器都需要这样操作哦)
- 右击
-
修改配置完毕之后记得重启机器才会生效
https://support.microsoft.com/zh-cn/help/196271/when-you-try-to-connect-from-tcp-ports-greater-than-5000-you-receive-t
TCPTimedWaitDelay
:30
压测结论:中间件越多,性能损失越大,大多损失在网络交互
压测结论
优化一:Nginx 动静分离
- 静态资源全部放在 nginx 目录下 nginx/html/static/index 中
- 给模板中所有静态资源的请求路径前都加上 /static;
- 添加 nginx 配置文件如下
# /static/ 下所有的请求都转给 nginx
location /static/ {
root /user/share/nginx/html/;
}
优化二:循环查库
一次查库,java封装
优化三:缓存
哪些数据适合放入缓存?
- 即时性、数据一致性要求不高
- 访问量大而且更新频率不高的数据(读多,写少)
// 逻辑伪代码
data = cache.load(d);
if(data == null){
data = db.load(id);
cache.put(id,data);
}
Nginx 动静分离
- 配置文件
- 静态文件放在指定目录下
RabbitMQ
MQ详解及四大MQ比较 - duanxz - 博客园 (cnblogs.com)
应用场景
异步处理,应用解耦,流量削峰
两种消息标准
RabbitMQ概念
RabbitMQ是一个由erlang开发的AMQP(Advanved Message Queue Protocol)的开源实现
- Message
消息,消息是不具名的,它由消息头和消息体组成。消息是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等
- Publisher
消息的生产者,也是一个向交换器发布消息的客户端应用程序
- Exchange
交机机,用来接收生产者发送的消息并将这些消息路由给服务器中的队列;Exchange有四种类型:direct(默认)、fanout、topic和headers,不同类型的Exchange转发消息的策略有所区别
- Queue
消息队列,用来保存消息直到发送给消费者。是消息的容器,也是消息的重点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走
- Binding
绑定,用于消息队列和交换机之间的关联,一个绑定就是基于路由键将交换机和消息队列连接起来的路由规则,所以可以将交换机理解成一个绑定构成的路由表;Exchange 和 Queue 的绑定可以是多对多的关系
- Connection
网络连接,比如一个TCP连接
- Channel
信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内的虚拟连接,AMQP命令都是通过信道发送出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁TCP连接都是非常昂贵的开销,所以引入了信道的概念,用来复用TCP连接
SpringBoot整合RabbitMQ
- 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
- 注解
@EnableRabbit
- 配置类
spring:
rabbitmq:
host: ***
port: 5672
virtual-host: /prod
password: ***
username: june
@RabbitListener
类+方法上(监听哪些队列);@RabbitHandler
标在方法上(重载区分不同的消息)
RabbitMQ消息确认机制
spring:
rabbit:
listener:
simple:
acknowledge-mode: manual # 手动收货模式
延时队列
接口幂等性
谷粒商城-接口幂等性文档_明快de玄米61的博客-程序员秘密_谷粒商城接口文档 - 程序员秘密 (cxymm.net)
分布式事务【重点】
Spring 本地事务 | 七墨博客 (qimok.cn)
谷粒商城---本地事务和分布式事务_明快de玄米61的博客-CSDN博客
CAP定理
CAP定理又称为CAP原则,指的是在一个分布式系统中
- 一致性(Consistency)
- 在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)
- 可用性(Availability)
- 在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)
- 分区容错性(Partition tolerance)
- 大多数分布式系统都分布在多个子网络,每一个子网络就叫做一个区(partition)。分区容错的意思是,区间通信可能失败。比如,中国和美国的服务器通信可能失败
CAP原则指的是,这三个要素最多只能同时实现两点,不可兼得
一般来说,分区容错无法避免,因此可以认为CAP中的P总是成立。
面临的问题
对于大型互联网应用的场景,主机众多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可用性达到99.999... ,即保证PA舍弃C。
BASE理论
BASE理论是对CAP理论的延伸,思想是即使无法做到强一致性(CAP的一致性就是强一致性),但可以采用适当的弱一致性,即最终一致性
BASE是指
- 基本可用(Basically Available)
- 基本可用是指分布式系统在出现故障时,允许损失部分可用性(例如响应时间、功能上的可用性),允许损失部分可用性。需要注意的是,基本可用绝不等于系统不可用
- 响应时间上的缺失:正常情况下搜索引擎需要在0.5s内返回给用户相应的查询结果,但由于出现了故障,查询结果的响应时间增加到了1~2s
- 功能上的缺失:购物网站在购物高峰(如11.11时,为了保护系统的稳定性,部分消费者可能会被引导到一个降级页面)
- 软状态(Soft State)
- 软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中数据一般由很多个副本,允许不同副本同步的延时就是软状态的体现。mysql replication的异步复制也是一种体现
- 最终一致性(Eventual Consistency)
- 最终一致性指系统中的所有数据副本经过一定时间后,最终能够达到一致性的状态。弱一致性和强一致性相反,最终一致性是弱一致性的特殊情况。
强一致性、弱一致性、最终一致性
分布式事务几种解决方案
2PC模式
数据库支持的2PC【2 phase commit 二阶段提交】,又叫做XA Transactions。MySQL从5.5版本开始支持,SQL Server 2005开始支持,Oracle 7 开始支持。其中,XA是一个两阶段提交协议,该协议分为以下两个阶段:
第一阶段:事务协调器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交。
第二阶段:事务协调器要求每个数据库提交数据
其中,如果有任何一个数据库否决此次提交,那么所有数据库都会要求回滚它们在此事务中的那部分信息。
柔性事务-TCC事务补偿型方案
刚性事务:遵循ACID原则,强一致性;
柔性事务:遵循BASE理论,最终一致性;
与刚性事务不同,柔性事务允许在一定时间内,不同节点的数据不一致,但要求最终一致
一阶段 prepare 行为:调用自定义的 prepare 逻辑;
二阶段 commit 行为:调用自定义的 commit 逻辑;
二阶段 rollback 行为:调用自定义的 rollback 逻辑;
所谓TCC模式,是指支持把自定义的分支事务纳入到全局事务的管理中。
柔性事务-最大努力通知型方案
柔性事务-可靠消息+最终一致性方案(异步确保型)
项目最终采用这种方案,用RabbitMQ来传递可靠消息
Seata 实现
Seata 是什么
- 每个微服务创建 undo_log 表
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
- 引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
- 复制 registry.conf & file.conf 两个文件到参与分布式事务的微服务resource目录下,详细修改如下:
- nacos + seata 报错 endpoint format should like ip:port - C,python,linux,java - 博客园 (cnblogs.com)
- seata-server 的 file.conf也要修改!
- 添加
@GlobalTransactional
- 代理数据源
@Configuration
public class SeataConfig {
@Autowired
DataSourceProperties dataSourceProperties;
@Bean
public DataSource dataSource(DataSourceProperties dataSourceProperties){
HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder()
.type(HikariDataSource.class).build();
if (StringUtils.hasText(dataSourceProperties.getName())) {
dataSource.setPoolName(dataSourceProperties.getName());
}
return new DataSourceProxy(dataSource);
}
}
文件上传
服务端签名后直传 (aliyun.com)
接入阿里云OSS步骤
- 引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alicloud-oss</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
- 测试代码
@Autowired
OSS ossClient;
@Value("${spring.cloud.alicloud.oss.endpoint}")
private String endpoint;
@Value("${spring.cloud.alicloud.access-key}")
private String accessId;
@Value("${spring.cloud.alicloud.secret-key}")
private String accessKey;
@Value("${spring.cloud.alicloud.bucket}")
private String bucket;
@Autowired
private SmsComponent smsComponent;
@Test
void contextLoads() {
HttpResponse httpResponse = smsComponent.sendSmsCode("17513324841", "222");
}
@Test
void testOss() throws FileNotFoundException {
log.info("测试用例:上传application.yml文件到oss");
ossClient.putObject("mall-project-february", "application.yml",
new FileInputStream("/Users/june/IdeaProjects/mall-project/mall-third-party/src/main/resources/application.yml"));
ossClient.shutdown();
log.info("over!");
}
以上是简单接入方法,还可以使用阿里云整合SpringBoot方式接入
- 引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alicloud-oss</artifactId>
</dependency>
- 配置yml
spring:
cloud:
alicloud:
access-key: **
secret-key: **
oss:
endpoint: oss-cn-hangzhou.aliyuncs.com
bucket: mall-project-february
- 配置Controller
@RestController
@RequestMapping("oss")
public class OssController {
@Autowired
OSS ossClient;
@Value("${spring.cloud.alicloud.oss.endpoint}")
private String endpoint;
@Value("${spring.cloud.alicloud.access-key}")
private String accessId;
@Value("${spring.cloud.alicloud.secret-key}")
private String accessKey;
@Value("${spring.cloud.alicloud.bucket}")
private String bucket;
@RequestMapping("/policy")
public R policy(){
String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint
String format = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
String dir = format+"/"; // 用户上传文件时指定的前缀。
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessId, accessKey);
try {
long expireTime = 30;
long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
Date expiration = new Date(expireEndTime);
// PostObject请求最大可支持的文件大小为5 GB,即CONTENT_LENGTH_RANGE为5*1024*1024*1024。
PolicyConditions policyConds = new PolicyConditions();
policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);
String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
byte[] binaryData = postPolicy.getBytes("utf-8");
String encodedPolicy = BinaryUtil.toBase64String(binaryData);
String postSignature = ossClient.calculatePostSignature(postPolicy);
Map<String, String> respMap = new LinkedHashMap<String, String>();
respMap.put("accessid", accessId);
respMap.put("policy", encodedPolicy);
respMap.put("signature", postSignature);
respMap.put("dir", dir);
respMap.put("host", host);
respMap.put("expire", String.valueOf(expireEndTime / 1000));
// respMap.put("expire", formatISO8601Date(expiration));
return R.ok().put("data",respMap);
} catch (Exception e) {
// Assert.fail(e.getMessage());
System.out.println(e.getMessage());
} finally {
ossClient.shutdown();
}
return null;
}
}
前端的上传组件
multiUpload.vue
<template>
<div>
<el-upload
action="http://mall-project-february.oss-cn-hangzhou.aliyuncs.com"
:data="dataObj"
list-type="picture-card"
:file-list="fileList"
:before-upload="beforeUpload"
:on-remove="handleRemove"
:on-success="handleUploadSuccess"
:on-preview="handlePreview"
:limit="maxCount"
:on-exceed="handleExceed"
>
<i class="el-icon-plus"></i>
</el-upload>
<el-dialog :visible.sync="dialogVisible">
<img width="100%" :src="dialogImageUrl" alt />
</el-dialog>
</div>
</template>
<script>
import { policy } from "./policy";
import { getUUID } from '@/utils'
export default {
name: "multiUpload",
props: {
//图片属性数组
value: Array,
//最大上传图片数量
maxCount: {
type: Number,
default: 30
}
},
data() {
return {
dataObj: {
policy: "",
signature: "",
key: "",
ossaccessKeyId: "",
dir: "",
host: "",
uuid: ""
},
dialogVisible: false,
dialogImageUrl: null
};
},
computed: {
fileList() {
let fileList = [];
for (let i = 0; i < this.value.length; i++) {
fileList.push({ url: this.value[i] });
}
return fileList;
}
},
mounted() {},
methods: {
emitInput(fileList) {
let value = [];
for (let i = 0; i < fileList.length; i++) {
value.push(fileList[i].url);
}
this.$emit("input", value);
},
handleRemove(file, fileList) {
this.emitInput(fileList);
},
handlePreview(file) {
this.dialogVisible = true;
this.dialogImageUrl = file.url;
},
beforeUpload(file) {
let _self = this;
return new Promise((resolve, reject) => {
policy()
.then(response => {
console.log("这是什么${filename}");
_self.dataObj.policy = response.data.policy;
_self.dataObj.signature = response.data.signature;
_self.dataObj.ossaccessKeyId = response.data.accessid;
_self.dataObj.key = response.data.dir + "/"+getUUID()+"_${filename}";
_self.dataObj.dir = response.data.dir;
_self.dataObj.host = response.data.host;
resolve(true);
})
.catch(err => {
console.log("出错了...",err)
reject(false);
});
});
},
handleUploadSuccess(res, file) {
this.fileList.push({
name: file.name,
// url: this.dataObj.host + "/" + this.dataObj.dir + "/" + file.name; 替换${filename}为真正的文件名
url: this.dataObj.host + "/" + this.dataObj.key.replace("${filename}",file.name)
});
this.emitInput(this.fileList);
},
handleExceed(files, fileList) {
this.$message({
message: "最多只能上传" + this.maxCount + "张图片",
type: "warning",
duration: 1000
});
}
}
};
</script>
<style>
</style>
singleUpload.vue
<template>
<div>
<el-upload
action="http://mall-project-february.oss-cn-hangzhou.aliyuncs.com"
:data="dataObj"
list-type="picture"
:multiple="false" :show-file-list="showFileList"
:file-list="fileList"
:before-upload="beforeUpload"
:on-remove="handleRemove"
:on-success="handleUploadSuccess"
:on-preview="handlePreview">
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过10MB</div>
</el-upload>
<el-dialog :visible.sync="dialogVisible">
<img width="100%" :src="fileList[0].url" alt="">
</el-dialog>
</div>
</template>
<script>
import {policy} from './policy'
import { getUUID } from '@/utils'
export default {
name: 'singleUpload',
props: {
value: String
},
computed: {
imageUrl() {
return this.value;
},
imageName() {
if (this.value != null && this.value !== '') {
return this.value.substr(this.value.lastIndexOf("/") + 1);
} else {
return null;
}
},
fileList() {
return [{
name: this.imageName,
url: this.imageUrl
}]
},
showFileList: {
get: function () {
return this.value !== null && this.value !== ''&& this.value!==undefined;
},
set: function (newValue) {
}
}
},
data() {
return {
dataObj: {
policy: '',
signature: '',
key: '',
ossaccessKeyId: '',
dir: '',
host: '',
// callback:'',
},
dialogVisible: false
};
},
methods: {
emitInput(val) {
this.$emit('input', val)
},
handleRemove(file, fileList) {
this.emitInput('');
},
handlePreview(file) {
this.dialogVisible = true;
},
beforeUpload(file) {
let _self = this;
return new Promise((resolve, reject) => {
policy().then(response => {
console.log("响应的数据:",response)
_self.dataObj.policy = response.data.policy;
_self.dataObj.signature = response.data.signature;
_self.dataObj.ossaccessKeyId = response.data.accessid;
_self.dataObj.key = response.data.dir +getUUID()+'_${filename}';
_self.dataObj.dir = response.data.dir;
_self.dataObj.host = response.data.host;
resolve(true)
}).catch(err => {
reject(false)
})
})
},
handleUploadSuccess(res, file) {
console.log("上传成功...")
this.showFileList = true;
this.fileList.pop();
this.fileList.push({name: file.name, url: this.dataObj.host + '/' + this.dataObj.key.replace("${filename}",file.name) });
this.emitInput(this.fileList[0].url);
}
}
}
</script>
<style>
</style>
policy.js
import http from '@/utils/httpRequest.js'
export function policy() {
return new Promise((resolve,reject)=>{
http({
url: http.adornUrl("/third-party/oss/policy"),
method: "post",
params: http.adornParams({})
}).then(({ data }) => {
resolve(data);
})
});
}
模块导入
JSR303 后端自定义校验
- 对于错误参数,后端会报错
MethodArgumentNotValidException
,应该定义统一异常处理 - 标注了分组的字段/方法合成一组校验逻辑
优雅的校验参数-javax.validation - 简书 (jianshu.com)
// 示例代码
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
@TableId
@NotNull(message = "修改必须指定品牌id",groups ={UpdateGroup.class})
@Null(message = "新增不能指定id",groups = {AddGroup.class})
private Long brandId;
@NotBlank(message = "品牌名必须提交",groups = {AddGroup.class,UpdateGroup.class})
private String name;
@NotBlank(groups = {AddGroup.class})
@URL(message = "logo必须是一个合法的URL地址",groups = {AddGroup.class,UpdateGroup.class})
private String logo;
@NotBlank(groups = AddGroup.class)
private String descript;
@Range(min = 0,max = 1,message = "显示状态必须在0-1之间",groups = {AddGroup.class,UpdateGroup.class})
private Integer showStatus;
@NotBlank(groups = AddGroup.class)
@Pattern(regexp = "^[a-zA-Z]$",message = "检索首字母必须有且只有一个字母",groups = {AddGroup.class,UpdateGroup.class})
private String firstLetter;
@Min(value = 0,message = "排序字段必须大于等于0",groups = {AddGroup.class,UpdateGroup.class})
private Integer sort;
}
自定义校验注解
ListValue
@Documented
@Constraint(validatedBy = {ListValueConstraintValidator.class}) //指定自定义校验器
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
String message() default "{org.june.common.valid.ListValue}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
int[] values() default {};
}
ListValueConstraintValidator
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
private final Set<Integer> set = new HashSet<>();
// 初始化方法
@Override
public void initialize(ListValue constraintAnnotation) {
int[] values = constraintAnnotation.values();
for (int value : values) {
set.add(value);
}
}
// 判断是否校验成功
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return set.contains(value);
}
}
Json日期数据格式转换
spring:
jackson:
date-format: yyyy年MM月dd日 HH时mm秒
time-zone: Asia/Shanghai
定时任务
cron 表达式
语法: 秒 分 时 日 月 周 年 (Spring不支持年)
SpringBoot整合
@Slf4j
@Component
@EnableScheduling
@EnableAsync
public class HelloSchedule {
/**
* 1. Spring中没有年,仅6位表达式
* 2. 周一到周日:1-7
* 3. 默认定时任务为阻塞的,想要不阻塞可以创建新线程开任务
* 1. CompletableFuture.runAsync(...
* 2. 定时任务异步执行 @EnableAsync @Async
*/
@Scheduled(cron="* * * ? * 4")
@Async
public void hell(){
try {
Thread.sleep(3000);
log.info("hello");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Sentinel 熔断降级流控
什么是熔断
A服务调用B服务的某个功能,由于网络不稳定,或者B服务卡顿,导致功能时间超长,如果这样次数太多,我们就可以直接将B服务熔断了(A不再请求B接口),凡是调用B的直接返回降级数据,不必等待B的超长执行,这样不会产生级联问题
什么是降级
整个网站处于流量高峰期,根据当前业务情况及流量,对一些服务和页面进行有策略的降级【停止服务,所有的调用直接返回降级数据】。以此环节服务器资源的压力。
异同点
- 为了保证集群的大部分服务可用性和可靠性,防止崩溃,牺牲部分
- 用户最终都是体验到某个功能不可用
- 熔断是被调用方故障,触发的系统主动规则
- 降级是基于全局考虑,停止一些正常服务,释放资源
什么是限流
对打入服务的请求流量进行控制,使服务能够承担不超过自己能力的流量压力
SpringBoot整合
- 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
- 启动 sentinel 控制台
- 配置文件
spring:
cloud:
sentinel:
transport:
# clientIp: localhost
port: 8719
dashboard: localhost:8858
# 暴露SpringBoot-Endpoint 仅支持 properties
management.endpoints.web.exposure.include=*
# 启用 feign 调用失败的熔断策略
feign.sentinel.enabled=true
- 默认地址 localhost:8858 控制台调整参数【默认所有配置保存在内存中,重启失效】
降级策略
[Sentinel隔离和降级-熔断策略 - Ruthless - 博客园 (cnblogs.com)](https://www.cnblogs.com/linjiqin/p/15374998.html#:~:text=Sentinel熔断降级的策略有哪些?,1.慢调用比例:超过指定时长的调用为慢调用,统计单位时长内慢调用的比例,超过阈值则熔断 2.异常比例:统计单位时长内异常调用的比例,超过阈值则熔断)
自定义受保护的资源
// 1. 代码方式
try(Entry entry = SphU.entry("seckillSkus")){
// 业务逻辑
}catch (BlockException e){
log.error("seckill资源控制");
e.printStackTrace();
}
// 2. 注解方式
public List<SeckillSkuRedisTo> blockHandler(BlockException e){
log.error("getCurrentSeckillSkus 被限流!");
return null;
}
public List<SeckillSkuRedisTo> fallbackHandler(){
log.error("异常发生!");
return null;
}
/**
* blockHandler 针对原方法被限流/降级/系统保护的时候调用
* fallback 函数针对所有类型的异常调用
* @return
*/
@Override
@SentinelResource(value = "seckillSkus",fallback = "fallbackHandler",blockHandler = "blockHandler"){
// 业务逻辑
}
网关限流
- 依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
<version>1.8.1</version>
</dependency>
- 启动类
public static void main(String[] args) {
// 这句话识别网关
System.setProperty("csp.sentinel.app.type", "1");
SpringApplication.run(GatewayApplication.class, args);
}
- 重启sentinel
问题点这
Sleuth + Zipkin 服务链路追踪
基本术语
- Span(跨度):基本工作单元,发送一个远程调度任务,就会产生一个Span,Span是一个64位ID唯一标识的,Trace是用另一个64位ID唯一标识的,Span还有其他数据信息,比如摘要、时间戳时间、Span的ID以及进度ID
- Trace(追踪):用来及时记录一个事件的,一些核心注解用来定义一个请求的开始和结束。这些注解包括以下:
- cs - Client Sent 客户端发送一个请求,这个注解描述了这个 Span 的开始
- sr - Server Received 服务端获得请求并准备开始处理它,如果将其 sr 减去 cs 时间戳便可得到网络传输时间
- ss - Server Sent(服务端发送响应) 该注解表明请求处理的完成(当请求返回客户端),如果 ss 的时间戳减去 sr 时间戳,就可以得到服务器请求的时间
- cr - Client Received(客户端接收时间)此时 Span 结束,cr 的时间戳减去 cs 的时间戳便可以得到整个请求所消耗的时间
整合Zipkin(集成了sleuth)
- 依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
- 配置文件
spring:
zipkin:
base-url: http://***:9411 # zipkin服务地址
sender:
type: web # 数据收集方式:web、kafka、rabbit,我使用的是最简单的web,别的方式请自行学习
discovery-client-enabled: false #关闭服务发现 否则springcloud会把zipkin的url当作服务名称
sleuth:
redis:
enabled: false # 关闭redis链路追踪,否则会产生死锁,这是官方的一个BUG
sampler:
probability: 1 # sleuth 日志记录采样率,1为100%,默认为0.1即10%,正式环境视情况修改该配置。
Kubernetes
测试 - 集群部署
目标
- 所有节点上安装 Docker 和 kubeadm
- 部署 Kubernetes Master
- 部署容器网络插件
- 部署 Kubernetes Node,将节点加入 Kubernetes 集群中
- 部署 Dashboard Web 页面,可视化查看 Kubernetes 资源
入门
Kubernetes kubectl get 命令详解 _ Kubernetes(K8S)中文文档_Kubernetes中文社区
# 关闭 swap
sed -ri 's/.*swap.*/#&/' /etc/fstab
free -g # 验证 swap必须为0
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: tomcat6
name: tomcat6
spec:
replicas: 3
selector:
matchLabels:
app: tomcat6
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: tomcat6
spec:
containers:
- image: tomcat:6.0.53-jre8
name: tomcat
---
apiVersion: apps/v1
kind: Service
metadata:
creationTimestamp: null
labels:
app: tomcat6
name: tomcat6
spec:
selector:
app: tomcat6
type: NodePort
ports:
- port: 80
protocol: TCP
targetPort: 8080
ThreadLocal
Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(它的key实际上是一个弱引用)。每个线程在往某个ThreadLocal里塞值的时候,都会往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
弱引用
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<Long> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
boolean match = new AntPathMatcher().match("/member/**", uri);
if (match) {
return true;
}
HttpSession session = request.getSession();
MemberRespVo attribute = (MemberRespVo) session.getAttribute(AuthenticCommonConstant.LOGIN_USER);
if (attribute != null) {
loginUser.set(attribute.getId());
return true;
} else {
Map<String, String> list = new HashMap<>();
list.put("msg", "请登录!");
session.setAttribute("errors", list);
response.sendRedirect("http://auth.projectdemo.top/login.html");
return false;
}
}
}
项目索引优化
- 分组关联表建立索引
pms_attr_attrgroup_relation
alter table pms_attr_attrgroup_relation modify attr_id bigint not null comment 'not null!';
alter table pms_attr_attrgroup_relation modify attr_group_id bigint not null comment 'not null!';
create index idx_attrGroupId_attrId on pms_attr_attrgroup_relation(attr_group_id,attr_id);
create index idx_attrId_attrGroupId on pms_attr_attrgroup_relation(attr_id,attr_group_id);
show index from pms_attr_attrgroup_relation;
explain SELECT attr_id,attr_group_id FROM pms_attr_attrgroup_relation WHERE (attr_id = 1)
- 【规格参数】的索引
pms_attr
alter table pms_attr modify attr_type tinyint default 0 not null comment '属性类型[0-销售属性,1-基本属性,2-既是销售属性又是基本属性]'; # 非null
create index idx_attrType on pms_attr(attr_type);
show index from pms_attr;
-
【item.html】查询优化
create index idx_skuId_attrName_attrValue on pms_sku_sale_attr_value(sku_id,attr_name,attr_value);
create index idx_spuId on pms_sku_info(spu_id);
explain SELECT ssa.attr_id,
ssa.attr_name,
ssa.attr_value,
GROUP_CONCAT(DISTINCT si.sku_id) sku_ids
FROM pms_sku_info si
LEFT JOIN pms_sku_sale_attr_value ssa ON si.sku_id = ssa.sku_id
WHERE si.spu_id = 4
GROUP BY ssa.attr_id,
ssa.attr_name,
ssa.attr_value
- 【pms_sku_images】查询优化
create index idx_skuId_imgUrl_imgSort_defaultImg on pms_sku_images(sku_id,img_url,img_sort,default_img);
explain SELECT id,sku_id,img_url,img_sort,default_img FROM pms_sku_images WHERE (sku_id = 30);
- 三表join查询优化
explain
SELECT ag.attr_group_name, ag.catalog_id catalogId, a.attr_name, pav.attr_value
FROM `pms_attr_group` ag
LEFT JOIN `pms_attr_attrgroup_relation` agr ON agr.attr_group_id = ag.attr_group_id
LEFT JOIN `pms_attr` a ON a.attr_id = agr.attr_id
LEFT JOIN `pms_product_attr_value` pav ON a.attr_id = pav.attr_id
WHERE pav.spu_id = 4
AND ag.catalog_id = 225;
# 有一些索引是前面步骤已经建立好的
show index from pms_product_attr_value;
create index idx_spuId_attrValue on pms_product_attr_value(spu_id,attr_value);
show index from pms_attr_group;
create index idx_catalogId_attrGroupName on pms_attr_group(catalog_id,attr_group_name);
6.【ums_ware_sku】库存查询简单优化
explain
select sum(stock - stock_locked)
from `wms_ware_sku`
where sku_id = 45;
create index idx_skuId_stock_stockLocked on wms_ware_sku (sku_id, stock, stock_locked);
show index from wms_ware_sku;
7.订单查询
explain
SELECT id,
order_id,
order_sn,
consignee,
consignee_tel,
delivery_address,
order_comment,
payment_way,
task_status,
order_body,
tracking_no,
create_time,
ware_id,
task_comment
FROM wms_ware_order_task
WHERE (order_sn = '202207221244263431550340972083552257');
create index idx_orderSn on wms_ware_order_task (order_sn);
Mybatis 的一些语法
<delete id="deleteBatchRelation">
delete from `pms_attr_attrgroup_relation` where
<foreach collection="collect" item="item" separator="or">
(attr_id=#{item.attrId} and attr_group_id=#{item.attrGroupId})
</foreach>
</delete>
数据库 Emoji 表情支持
ALTER TABLE xxx CONVERT TO CHARACTER SET utf8mb4;
JVM相关优化
# 每个服务
-Xmx512M -Xms512M -Xmn128M -Xss256k -XX:SurvivorRatio=2 -XX:MaxPermSize=256m -XX:ParallelGCThreads=10-XX:ParallelCMSThreads=16 -XX:+CMSParallelRemarkEnabled-XX:MaxTenuringThreshold=15-XX:+UseCMSCompactAtFullCollection-XX:+UseCMSInitiatingOccupancyOnly-XX:CMSInitiatingOccupancyFraction=70-XX:+CMSClassUnloadingEnabled-XX:-DisableExplicitGC-XX:+HeapDumpOnOutOfMemoryError-verbose:gc-XX:+PrintGCDetails-XX:+PrintGCTimeStamps-XX:+PrintGCDateStamps-Xloggc:/app/log/hbase/gc-hbase-REGIONSERVER-`hostname`-`date +%Y%m%d`.log
-Xmx256M -Xms256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
本文暂时没有评论,来添加一个吧(●'◡'●)