程序员的资源宝库

网站首页 > gitee 正文

商城技术要点

sanyeah 2024-04-13 16:19:03 gitee 4 ℃ 0 评论

Nacos 注册中心

  1. 引入依赖
<!--服务注册/发现-->
<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>
  1. 指定配置中心位置 spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

  2. 添加注解 @EnableDiscoveryClient

@RefreshScope 搭配 @Value 可以实现动态配置项

OpenFeign 远程调用

  1. 引入依赖(前置依赖Nacos)
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  1. 创建接口,指定方法
image-20220207203846109
  1. 启动类注解`@EnableFeignClients(basePackages = "org.june.member.feign")

Gateway 网关

  1. 添加依赖
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
  1. 指定配置中心(配置文件、启动类注解)
  2. 配置文件写路由

Spring Cloud Gateway 2.1.0 中文官网文档 - 云+社区 - 腾讯云 (tencent.com)

  1. 简单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

双向绑定,不同于以上两者

image-20220208104318428

v-on

事件绑定

image-20220208110004487

v-for

遍历

image-20220208110424357

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>

组件化

image-20220208112839999

image-20220208112913291

生命周期和钩子函数

11370083-f279314aef6741db

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

基本概念

  1. Index(索引)

MySQL的库

  1. Type(类型)(deprecated in 6.0.0)

在Index中,可以定义一个或多个类型,类似于MySQL中的表;每一种类型的数据放在一起;

  1. Document(文档)

保存在某个索引下,某种类型的一个数据,文档是JSON格式的,Document就像是MySQL中某个Table里面的内容

image-20220221085326429
  1. 倒排索引

image-20220221085521456

创建实例

  1. 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
  1. 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.添加数据
image-20220221103048603

注: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
}

image-20220221104512956

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				不计算相关性得分,直接过滤
image-20220221150205112 image-20220221150418453
query:term		term是代表完全匹配,即不进行分词器分析,文档中必须包含整个搜索的词汇;全文检索字段用 match ,其他 text 字段用term
query:aggregations  字段聚合处理
image-20220221151250680 image-20220221152825993
2.Mapping

指定索引下的属性类型

image-20220221153935500

添加映射

image-20220221154303463

那么如何修改?

数据迁移

elastic已经不推荐使用 type

image-20220221154908391

3.分词

安装ik分词器

添加自定义词汇
  1. 配置 nginx 作为远程词库
  2. 在 html/es/fenci.txt 中填入新词
  3. elasticsearch/plugs/... 中配置远程词库为上面的地址

项目应用

  1. 依赖
<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>
  1. 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

  1. 引入依赖
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  1. 调用API RedisTemplate StringRedisTemplate
  2. 代码整合示例
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
加锁解决【缓存击穿】

image-20220225140213893

该段代码存在分布式锁的问题

该段代码存在分布式锁的问题

分布式锁简单实现

依托于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框架解决分布式锁
  1. 引入依赖
<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson</artifactId>
  <version>3.16.8</version>
</dependency>
  1. 配置类
@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 说明
  1. ReentrantLock

image-20220225170236075

image-20220225172533478

image-20220225172729487

image-20220225172944846

image-20220225173148538

  1. ReadWriteLock

image-20220225195809795

加锁示例

image-20220225195838791

  1. Semaphore

计数器,正计时

  1. CountDownLatch

image-20220226093038532

计数器,倒计时

分布式锁 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;
    }
【缓存数据一致性】问题及解决方案

image-20220226100959051

image-20220226103821451

image-20220226104750333

image-20220226105018038

SpringCache
  1. 引入依赖
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
  1. 写配置
spring:
  cache:
    type: redis
    redis:
    time-to-live: 3600000  # 单位|毫秒
    key-prefix: CACHE_
    cache-null-values: true
  1. 启动类或配置类 @EnableCaching
  2. 配置类
@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 代表当前方法的结果需要缓存,如果缓存中有,方法不用调用。如果缓存中没有,会调用方法,最后将方法结果放入缓存

image-20220226184519052

默认行为:

  1. 如果缓存中有,则方法不调用
  2. key 默认自动生成,{指定名称}::SimpleKey [] (默认名称)
  3. 缓存的 value 值,默认使用jdk序列化机制,保存的是序列化结果,一定要实现Serializable接口!
  4. 默认TTL=-1
  5. 动态取值 @Cacheable(value = "sku",key = "#root.args[1]")(可以点进源码看更详细的)

改动:

  1. 指定redis key名称:key = "#root.methodName"
  2. 更改序列化器
@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 方法执行后将方法的返回值放入缓存,一般加在更新操作上(双写模式

流程说明
  1. 业务代码执行前先检查缓存,有就返回,没有就执行业务代码
  2. 业务代码执行完后,返回值被SpringCache接收,并将其添加入缓存
  3. 如果 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();
        }
    }

image-20220226192643479

/**
* 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;
}

image-20220226192816095

异步&线程池

线程回顾

  1. 继承Thread
    • 主线程无法获取运算结果
  2. 实现Runnable 接口
    • 主线程无法获取运算结果
    • 其实与上面是一种形势,都实现的是 Runnable 接口的 run 方法
  3. 实现 Callable 接口 + FutureTask(可拿到返回结果,处理异常)
    • 可以获取运算结果
    • 以上三种方式都不能控制资源,易导致资源耗尽而系统崩溃
  4. 线程池
// 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)

image-20220303091153951

image-20220303090351627

CompletableFuture 异步编排

提供了四个静态方法来创建异步操作:

image-20220303092637598

  1. runXxx 都是没有返回结果的,supplyXxx 都是可以获取返回结果的
  2. 可以传入自定义线程池,否则就用默认线程池
简单Demo

image-20220303093752828

↑ 简单写法 handle() ↓

image-20220303101144479

线程串行化方法
image-20220303102452990
  1. thenRun 不能获取到上一步执行结果
  2. thenAcceptAsync 接收上一步返回结果,无返回值
  3. thenApplyAsync 接收上一步结果,有返回值
两组任务组合
都要完成
  1. runAfterBoth 组合两 future ,不需要获取 future 结果,只需两个 future 处理完后,处理该任务
  2. thenAcceptBoth 组合两个 future,获取两个 future 返回结果,然后处理任务,没有返回值
  3. 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);
一个完成
  1. applyToEither 两任务有一个执行完成就获取返回值,处理任务并有返回值
  2. acceptEither 两任务有一个执行完成就获取返回值,处理任务但没有返回值
  3. runAfterEither 两任务有一个执行完成,不需要获取 future 结果,处理任务,也没有返回值
多任务组合

image-20220303110335562

image-20220303110805435

Session共享【重点】

问题描述

image-20220306210324728

image-20220306210305357

解决方案

session复制(session广播)

image-20220306210431111

客户端存储

image-20220306210526251

这种方式类似于 token 方式,但不如 token 优雅;总体来说安全性不高

哈希一致性

image-20220306210742136

统一存储

image-20220306211135918

Spring-Session(统一存储方案)

  1. 依赖
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
  1. 配置项
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@./
  1. 配置类个性化配置
@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();
    }
}
原理分析

image-20220307105607605

  • 第一次访问服务器,服务器都会设置这个cookie:session的id,默认名为JSESSIONID,这个值可以修改

核心代码-SessionRepositoryFilter

image-20220307112603267

登陆拦截

@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%用户的响应时间平均值

性能测试主要关注以下三个指标:

  • 吞吐量:每秒钟系统能够处理的请求数、任务数
  • 响应时间:服务处理一个请求或一个任务的耗时
  • 错误率:一批请求中结果出错的请求占比

image-20220224103731298

image-20220224103742398

JMeter Address Already in use 问题解决

Windows 本身提供的端口访问机制的问题。

Windows 提供给 TCP/IP 链接的端口为 1024-5000,并且要四分钟来循环回收他们。就导致我们在短时间内跑大量的请求时将端口占满了。

  1. cmd 中,用 regedit 命令打开注册表

  2. HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters 下,

    1. 右击 parameters,添加一个新的 DWORD,名字为 MaxUserPort
    2. 然后双击 MaxUserPort,输入数值数据为 65534,基数选择十进制(如果是分布式运
      行的话,控制机器和负载机器都需要这样操作哦)
  3. 修改配置完毕之后记得重启机器才会生效
    https://support.microsoft.com/zh-cn/help/196271/when-you-try-to-connect-from-tcp-ports-greater-than-5000-you-receive-t
    TCPTimedWaitDelay30

压测结论:中间件越多,性能损失越大,大多损失在网络交互

压测结论

image-20220224204150429

优化一:Nginx 动静分离

image-20220224200804664
  • 静态资源全部放在 nginx 目录下 nginx/html/static/index 中
  • 给模板中所有静态资源的请求路径前都加上 /static;
  • 添加 nginx 配置文件如下
# /static/ 下所有的请求都转给 nginx
location /static/ {
	root /user/share/nginx/html/;
}
image-20220224201256837

优化二:循环查库

image-20220224203949999

image-20220224204018070

一次查库,java封装

优化三:缓存

哪些数据适合放入缓存?

  • 即时性、数据一致性要求不高
  • 访问量大而且更新频率不高的数据(读多,写少)
image-20220224205729002
// 逻辑伪代码
data = cache.load(d);
if(data == null){
  data = db.load(id);
  cache.put(id,data);
}

Nginx 动静分离

  1. 配置文件

d'd

  1. 静态文件放在指定目录下

image-20220308155026450

RabbitMQ

MQ详解及四大MQ比较 - duanxz - 博客园 (cnblogs.com)

img

应用场景

异步处理,应用解耦,流量削峰

image-20220310083624409

image-20220310083954870

两种消息标准

image-20220310085558857

RabbitMQ概念

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

  1. Message

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

  1. Publisher

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

  1. Exchange

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

image-20220310101202959

image-20220310101230475

  1. Queue

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

  1. Binding

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

  1. Connection

网络连接,比如一个TCP连接

  1. Channel

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

image-20220310091311258

SpringBoot整合RabbitMQ

  1. 依赖
<dependency>     				
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
  1. 注解 @EnableRabbit
  2. 配置类
spring:
  rabbitmq:
    host: ***
    port: 5672
    virtual-host: /prod
    password: ***
    username: june
  1. @RabbitListener 类+方法上(监听哪些队列);@RabbitHandler 标在方法上(重载区分不同的消息)

RabbitMQ消息确认机制

image-20220311083436951

image-20220311083521047

image-20220311083625962

image-20220311083941643

image-20220311090235529

spring:
  rabbit:
    listener:
    simple:
      acknowledge-mode: manual # 手动收货模式

延时队列

image-20220315100534895

image-20220315101042776

image-20220315101413752

image-20220315101625550

image-20220315101751979

接口幂等性

谷粒商城-接口幂等性文档_明快de玄米61的博客-程序员秘密_谷粒商城接口文档 - 程序员秘密 (cxymm.net)

分布式事务【重点】

Spring 本地事务 | 七墨博客 (qimok.cn)

谷粒商城---本地事务和分布式事务_明快de玄米61的博客-CSDN博客

image-20220314150755116

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)
    • 最终一致性指系统中的所有数据副本经过一定时间后,最终能够达到一致性的状态。弱一致性和强一致性相反,最终一致性是弱一致性的特殊情况。

强一致性、弱一致性、最终一致性

image-20220314160341656

分布式事务几种解决方案

2PC模式

数据库支持的2PC【2 phase commit 二阶段提交】,又叫做XA Transactions。MySQL从5.5版本开始支持,SQL Server 2005开始支持,Oracle 7 开始支持。其中,XA是一个两阶段提交协议,该协议分为以下两个阶段:

第一阶段:事务协调器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交。

第二阶段:事务协调器要求每个数据库提交数据

其中,如果有任何一个数据库否决此次提交,那么所有数据库都会要求回滚它们在此事务中的那部分信息。

image-20220314163422945

image-20220314163558139

柔性事务-TCC事务补偿型方案

刚性事务:遵循ACID原则,强一致性;
柔性事务:遵循BASE理论,最终一致性;

与刚性事务不同,柔性事务允许在一定时间内,不同节点的数据不一致,但要求最终一致

image-20220314163813428

一阶段 prepare 行为:调用自定义的 prepare 逻辑;
二阶段 commit 行为:调用自定义的 commit 逻辑;
二阶段 rollback 行为:调用自定义的 rollback 逻辑;

所谓TCC模式,是指支持把自定义的分支事务纳入到全局事务的管理中。

柔性事务-最大努力通知型方案

image-20220314164315294

柔性事务-可靠消息+最终一致性方案(异步确保型)

项目最终采用这种方案,用RabbitMQ来传递可靠消息

image-20220314164711125

Seata 实现

Seata 是什么

  1. 每个微服务创建 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;
  1. 引入依赖
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
  1. 复制 registry.conf & file.conf 两个文件到参与分布式事务的微服务resource目录下,详细修改如下:
    • nacos + seata 报错 endpoint format should like ip:port - C,python,linux,java - 博客园 (cnblogs.com)
    • seata-server 的 file.conf也要修改!
  2. 添加 @GlobalTransactional
  3. 代理数据源
@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);
    }
}

文件上传

image-20220211130000529 image-20220211130043800

服务端签名后直传 (aliyun.com)

接入阿里云OSS步骤

  1. 引入依赖
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alicloud-oss</artifactId>
    <version>2.2.0.RELEASE</version>
</dependency>
  1. 测试代码
@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方式接入

  1. 引入依赖
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alicloud-oss</artifactId>
</dependency>
  1. 配置yml
spring:
  cloud:
    alicloud:
      access-key: **
      secret-key: **
      oss:
        endpoint: oss-cn-hangzhou.aliyuncs.com
      bucket: mall-project-february
  1. 配置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);
        })
    });
}
模块导入

image-20220211213708574

image-20220211213718389

JSR303 后端自定义校验

  • 对于错误参数,后端会报错MethodArgumentNotValidException,应该定义统一异常处理
  • 标注了分组的字段/方法合成一组校验逻辑

优雅的校验参数-javax.validation - 简书 (jianshu.com)

image-20220212110023237

// 示例代码
@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不支持年)

image-20220317105627180

image-20220317105644627

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的超长执行,这样不会产生级联问题

什么是降级

整个网站处于流量高峰期,根据当前业务情况及流量,对一些服务和页面进行有策略的降级【停止服务,所有的调用直接返回降级数据】。以此环节服务器资源的压力。

异同点

  1. 为了保证集群的大部分服务可用性和可靠性,防止崩溃,牺牲部分
  2. 用户最终都是体验到某个功能不可用
  3. 熔断是被调用方故障,触发的系统主动规则
  4. 降级是基于全局考虑,停止一些正常服务,释放资源

什么是限流

对打入服务的请求流量进行控制,使服务能够承担不超过自己能力的流量压力

image-20220110165114710

Sentinel-features-overview

SpringBoot整合

  1. 依赖
<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>
  1. 启动 sentinel 控制台
  2. 配置文件
spring:
  cloud:
    sentinel:
      transport:
#        clientIp: localhost
        port: 8719
        dashboard: localhost:8858
# 暴露SpringBoot-Endpoint 仅支持 properties
management.endpoints.web.exposure.include=*
# 启用 feign 调用失败的熔断策略
feign.sentinel.enabled=true 
  1. 默认地址 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"){
      // 业务逻辑
    }

网关限流

  1. 依赖
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
    <version>1.8.1</version>
</dependency>
  1. 启动类
public static void main(String[] args) {
  // 这句话识别网关
        System.setProperty("csp.sentinel.app.type", "1");
        SpringApplication.run(GatewayApplication.class, args);
    }
  1. 重启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)

  1. 依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
  1. 配置文件
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

image-20220320174702089

image-20220320175508744

测试 - 集群部署

目标
  1. 所有节点上安装 Docker 和 kubeadm
  2. 部署 Kubernetes Master
  3. 部署容器网络插件
  4. 部署 Kubernetes Node,将节点加入 Kubernetes 集群中
  5. 部署 Dashboard Web 页面,可视化查看 Kubernetes 资源

image-20220320181142486

入门

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;
        }
    }
}

项目索引优化

  1. 分组关联表建立索引 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)
  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;
  1. 【item.html】查询优化

    image-20220720111427403

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
  1. 【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);
  1. 三表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);

image-20220722114648775

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

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表