大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。
Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺
架构图及微服务划分
环境配置
Linux 环境
JDK
yum install java-1.8.0-openjdk.x86_64
java -version
vi /etc/profile
#set java environment
JAVA_HOME=/usr/lib/jvm/jre-1.8.0-openjdk
PATH=$PATH:$JAVA_HOME/bin
CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
export JAVA_HOME CLASSPATH PATH
Docker
- 卸载系统之前的docker
sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine
- 设置存储库
sudo yum install -y yum-utils
sudo yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo
- 安装DOCKER引擎
sudo yum install docker-ce docker-ce-cli containerd.io
- 启动Docker.
sudo systemctl start docker
- 配置镜像加速
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF' { "registry-mirrors": ["https://ozz4irqv.mirror.aliyuncs.com"] } EOF
sudo systemctl daemon-reload
sudo systemctl restart docker
- 开机自启
sudo docker update redis --restart=always
MySQL (Docker)
- 拉取 mysql镜像
sudo docker pull mysql:8.0
- 启动mysql容器
# --name指定容器名字 -v目录挂载(左主右从) -p指定端口映射 -e设置mysql参数 -d后台运行
sudo docker run --name mysql -v /usr/local/mysql/data:/var/lib/mysql -v /usr/local/mysql:/etc/mysql/conf.d -v /usr/local/mysql/log:/var/log/mysql -e MYSQL_ROOT_PASSWORD=root -p 3306:3306 -d mysql:8.0
- 进入mysql容器
docker exec -it a4435a23e7a470297fded7fbdeb1a06045530e1631517585f490c680e4039891 bin/bash
- 开机自启
sudo docker update mysql --restart=always
Redis(Docker)
- 拉取redis镜像到本地
docker pull redis
- 修改需要自定义的配置(docker-redis默认没有配置文件,自己在宿主机建立后挂载映射)
位置在 /usr/local/redis/redis.conf
#开启远程权限
bind 0.0.0.0
#开启aof持久化
appendonly yes
- 启动redis服务运行容器
docker run --name redis -v /usr/local/redis/data:/data -v /usr/local/redis/redis.conf:/usr/local/etc/redis/redis.conf -p 6379:6379 -d redis redis-server /usr/local/etc/redis/redis.conf
- 连接 redis
docker exec -it redis redis-cli
Nginx(Docker)
# 终极版!
docker run -p 80:80 --name nginx \
-v /usr/local/nginx/html:/usr/share/nginx/html \
-v /usr/local/nginx/conf/nginx.conf/:/etc/nginx/nginx.conf \
-v /usr/local/nginx/logs:/var/log/nginx \
-d nginx
docker run -p 80:80 --name nginx \
-v /Users/june/AServerMiddleware/nginx-docker/html:/usr/share/nginx/html \
-v /Users/june/AServerMiddleware/nginx-docker/conf/nginx.conf:/etc/nginx/nginx.conf \
-v /Users/june/AServerMiddleware/nginx-docker/conf/default.conf:/etc/nginx/conf.d/default.conf \
-v /Users/june/AServerMiddleware/nginx-docker/logs:/var/log/nginx -d nginx
RabbitMQ(Docker)
Docker系列之RabbitMQ安装部署教程 – 云+社区 – 腾讯云 (tencent.com)
docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 25672:25672 -p 15671:15671 -p 15672:15672 -e RABBITMQ_DEFAULT_USER=june -e RABBITMQ_DEFAULT_PASS=L200107208017./@ rabbitmq:management
# 4369 25672 Erlang发现&集群端口
# 5671 5672 AMQP 端口
# 15672 web管理后台端口
# 61613 61614 STOMP协议端口
# 1883 8883 MQTT 协议端口
Nacos(Docker) Linux 部署 Mac 不支持
https://www.jianshu.com/p/3d3e17bc629f
# 1.创建本地配置文件
mkdir -p /home/nacos/logs/ #新建logs目录
mkdir -p /home/nacos/init.d/
vim /home/nacos/init.d/custom.properties #修改配置文件
# 2.添加如下内容
server.contextPath=/nacos
server.servlet.contextPath=/nacos
server.port=8848
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://sh-cdb-0ej7ogfe.sql.tencentcdb.com:58887/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=L200107208017@
nacos.cmdb.dumpTaskInterval=3600
nacos.cmdb.eventTaskInterval=10
nacos.cmdb.labelTaskInterval=300
nacos.cmdb.loadDataAtStart=false
management.metrics.export.elastic.enabled=false
management.metrics.export.influx.enabled=false
server.tomcat.accesslog.enabled=true
server.tomcat.accesslog.pattern=%h %l %u %t "%r" %s %b %D %{
User-Agent}i
nacos.security.ignore.urls=/,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/v1/auth/login,/v1/console/health/**,/v1/cs/**,/v1/ns/**,/v1/cmdb/**,/actuator/**,/v1/console/server/**
nacos.naming.distro.taskDispatchThreadCount=1
nacos.naming.distro.taskDispatchPeriod=200
nacos.naming.distro.batchSyncKeyCount=1000
nacos.naming.distro.initDataRatio=0.9
nacos.naming.distro.syncRetryDelay=5000
nacos.naming.data.warmup=true
nacos.naming.expireInstance=true
# 3.启动容器
docker run \
--name nacos -d \
-p 8848:8848 \
--privileged=true \
--restart=always \
-e JVM_XMS=256m \
-e JVM_XMX=256m \
-e MODE=standalone \
-e PREFER_HOST_MODE=hostname \
-v /home/nacos/logs:/home/nacos/logs \
-v /home/nacos/init.d/custom.properties:/home/nacos/init.d/custom.properties \
nacos/nacos-server:1.4.2
Sentinel
Linux直接部署
# 注意,不要同时在开发机器部署微服务,云服务器部署sentinel,因为sentinel也是需要访问本机的!
java -Dserver.port=8858 -Dcsp.sentinel.dashboard.server=localhost:8858 -Dproject.name=sentinel-dashboard -Dsentinel.dashboard.auth.username=sentinel-qs -Dsentinel.dashboard.auth.password=L200107208017@ -jar sentinel-dashboard-1.8.1.jar
本机部署
nohup java -server -Xms64m -Xmx256m -Dserver.port=8858 -Dcsp.sentinel.dashboard.server=localhost:8858 -Dproject.name=sentinel-dashboard -jar /Users/june/AServerMiddleware/sentinel/sentinel-dashboard-1.8.1.jar
Zipkin(Docker)
zipkin 使用外部 MySQL 持久化存储 – 简书 (jianshu.com)
docker run -d -p 9411:9411 openzipkin/zipkin:latest
-e JAVA_OPTS=-Xmx128m \
docker run \
--name zipkin-server -d \
--restart=always \
-p 9411:9411 \
-e MYSQL_USER=root \
-e MYSQL_PASS=L200107208017@ \
-e MYSQL_HOST=sh-cdb-0ej7ogfe.sql.tencentcdb.com \
-e STORAGE_TYPE=mysql \
-e MYSQL_DB=zipkin \
-e MYSQL_TCP_PORT=58887 \
openzipkin/zipkin
开发机器环境
git
git config --global user.name "June"
git config --global user.email "1243134432@qq.com"
ssh-keygen -t rsa -C "1243134432@qq.com"
# 查看生成的密钥内容
cat ~/.ssh/id_rsa.pub
# 复制密钥内容到 gitee,以后该机器就推送内容不用输入密码
# 测试
ssh -T git@gitee.com
# gitignore 中添加以下内容
**/mvnw
**/mvnw.cmd
**/.mvn
**/target
.idea
**/.gitignore
**/README.md
Node.js
- 官网下载 node.js 附带有 npm
- 配置 npm 镜像
npm config set registry http://registry.npm.taobao.org/
项目构架
renren-generator
利用这个模块给每一个业务模块生成代码,以 product 模块为例
选用技术
SpringCloud Alibaba - Nacos (服务发现/注册)
SpringCloud Alibaba - Nacos (动态配置管理)
SpringCloud Alibaba - Seata (分布式事务解决方案)
SpringCloud Alibaba - Sentinel (限流、降级、熔断等)
SpringCloud - Ribbon (负载均衡)
SpringCloud - Feign 远程调用服务
SpringCloud - Gateway (API网关)
SpringCloud - Sleuth (调用链监控)
核心依赖
这块配不好,踩坑少不了
<!--聚合服务-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!--子服务-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/>
</parent>
<!--子服务-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
技术要点
Nacos 注册中心
- 引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
- 指定配置中心位置
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
- 添加注解
@EnableDiscoveryClient
OpenFeign 远程调用
- 引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
- 指定配置中心位置
- 创建接口,指定方法
- 添加注解,上图中一个,启动类一个
@EnableFeignClients(basePackages = "org.june.member.feign")
Nacos 配置中心
- 引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-nacos-config</artifactId>
</dependency>
- 创建
bootstrap.properties
指定配置中心位置以及自己的服务名称
spring.application.name=mall-coupon
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
- Nacos 配置中心添加
服务名.properties
配置文件,这个文件可以动态读取 - 需要使用 动态配置的代码类头添加
@RefreshScope
,使用@Value("${name.age}")
(在成员变量处)方式获取
注:
- 配置中心配置项优先级较本地文件优先级高
- 经测试,第三步中 yaml yml 都无法识别,只有properties可以识别
命名空间的说明
# 追加在 bootstrap.properties 中
spring.cloud.nacos.config.namespace=3ce35e9e-4e10-44df-b1a4-8fd753c3e4ea
配置中心的说明
# 追加在 bootstrap.properties 中
spring.cloud.nacos.config.group=ddd
类似于上面的命名空间,都是用来隔离文件的
最终实践
spring.application.name=mall-coupon
# 服务注册/发现
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
# 配置中心 这两项可以只配 第二个
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=2863031e-ae6a-4bff-903e-48d27201e1ab
spring.cloud.nacos.config.ext-config[0].data-id=datasource.yml
spring.cloud.nacos.config.ext-config[0].group=dev
spring.cloud.nacos.config.ext-config[0].refresh=true
spring.cloud.nacos.config.ext-config[1].data-id=mybatis.yml
spring.cloud.nacos.config.ext-config[1].group=dev
spring.cloud.nacos.config.ext-config[1].refresh=true
spring.cloud.nacos.config.ext-config[2].data-id=others.yml
spring.cloud.nacos.config.ext-config[2].group=dev
spring.cloud.nacos.config.ext-config[2].refresh=true
Gateway 网关
- 添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
- 指定配置中心(配置文件、启动类注解)
- 配置文件写路由
注:SpringBoot 2.3.1.RELEASE
对应 SpringCloud Hoxton.SR6
不会报错
Spring Cloud Gateway 2.1.0 中文官网文档 – 云+社区 – 腾讯云 (tencent.com)
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(类型)
在Index中,可以定义一个或多个类型,类似于MySQL中的表;每一种类型的数据放在一起;
- Document(文档)
保存在某个索引下,某种类型的一个数据,文档是JSON格式的,Document就像是MySQL中某个Table里面的内容
- 倒排索引
创建实例
- elasticsearch
# Docker !8版本需要额外配置东西
docker pull elasticsearch:7.17.0
docker pull kibana:7.17.0
# 创建
mkdir -p /Users/june/AServerMiddleware/elsatic-docker/plugins
mkdir -p /Users/june/AServerMiddleware/elsatic-docker/config
mkdir -p /Users/june/AServerMiddleware/elsatic-docker/data
#
echo "http.host: 0.0.0.0" >/Users/june/AServerMiddleware/elsatic-docker/config/elasticsearch.yml
#
chmod -R 777 /Users/june/AServerMiddleware/elsatic-docker
# 启动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 /Users/june/AServerMiddleware/elsatic-docker/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /Users/june/AServerMiddleware/elsatic-docker/data:/usr/share/elasticsearch/data \
-v /Users/june/AServerMiddleware/elsatic-docker/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.17.0
- kibana
sudo docker run --name kibana -e ELASTICSEARCH_HOSTS=http://124.222.22.217: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.分词
添加自定义词汇
- 配置 nginx 作为远程词库
- 在 html/es/fenci.txt 中填入新词
- elasticsearch/plugs/… 中配置远程词库为上面的地址
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 RedissonConfig {
@Bean
RedissonClient redisson(){
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setPassword("L200107208017@./");
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
注解说明
@Cacheable
代表当前方法的结果需要缓存,如果缓存中有,方法不用调用。如果缓存中没有,会调用方法,最后将方法结果放入缓存
默认行为:
- 如果缓存中有,则方法不调用
- key 默认自动生成,{指定名称}::SimpleKey [] (默认名称)
- 缓存的 value 值,默认使用jdk序列化机制,保存的是序列化结果
- 默认TTL=-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: /dev
password: ***
username: june
@RabbitListener
类+方法上(监听哪些队列);@RabbitHandler
标在方法上(重载区分不同的消息)
RabbitMQ消息确认机制
spring:
rabbit:
listener:
simple:
acknowledge-mode: manual # 手动收货模式
延时队列
接口幂等性
谷粒商城-接口幂等性文档_明快de玄米61的博客-程序员秘密_谷粒商城接口文档 – 程序员秘密 (cxymm.net)
分布式事务【重点】
谷粒商城—本地事务和分布式事务_明快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 实现
- 每个微服务创建 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
文件上传
接入阿里云OSS步骤
- 引入依赖
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.5.0</version>
</dependency>
- 测试代码
@Test
void testOss() {
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。
// 强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
String accessKeyId = "***";
String accessKeySecret = "***";
String bucketName = "mall-project-february";
String objectName = "application.yml";
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
String content = "Hello OSS";
ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(content.getBytes()));
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
}
以上是简单接入方法,还可以使用阿里云整合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
优雅的校验参数-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);
}
}
定时任务
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
# 查看
kubectl get pod
kubectl get all
kubectl get svc
# 测试部署 tomcat
kubectl expose deployment tomcat6 --port=80 --target-port=8080 --type=NodePort
# 扩容
kubectl scale --replicas=1 deployment tomcat6
# 删除
kubectl delete deployment.apps/tomcat6
# yaml 文件
kubectl create deployment tomcat6 --image=tomcat:6.0.53-jre8 --dry-run -o yaml
kubectl get pod podId -o yaml
概念
Kubesphere
业务实现
三层菜单
后端核心步骤
三层菜单数据
@Override
public List<CategoryEntity> listWithTree() {
//1.查出所有分类
List<CategoryEntity> entities = baseMapper.selectList(null);
//2.组装成三级结构
//2.1 找到所有一级分类
List<CategoryEntity> levelOne = entities.stream()
.filter(categoryEntity -> categoryEntity.getParentCid() == 0)
.peek((menu) -> menu.setChildren(getChildrens(menu, entities)))
.sorted(Comparator.comparingInt(o -> (o.getSort() == null ? 0 : o.getSort())))
.collect(Collectors.toList());
return levelOne;
}
/** * 递归查找参数一的子菜单 * * @param root * @param total * @return */
private List<CategoryEntity> getChildrens(CategoryEntity root, List<CategoryEntity> total) {
List<CategoryEntity> children = total.stream()
.filter(categoryEntity -> categoryEntity.getParentCid() == root.getCatId())
.peek(categoryEntity -> categoryEntity.setChildren(getChildrens(categoryEntity, total)))
.sorted(Comparator.comparingInt(o -> (o.getSort() == null ? 0 : o.getSort())))
.collect(Collectors.toList());
return children;
}
网关改造
- 路径重写
- 配置网关跨域
package com.example.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.cors.reactive.CorsWebFilter;
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsWebFilter(){
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedHeader("*"); // 任意请求头
corsConfiguration.addAllowedMethod("*"); // 任意请求方式
corsConfiguration.addAllowedOrigin("*"); // 任意请求来源
corsConfiguration.setAllowCredentials(true); // 可以携带 cookie
// 对所有请求执行以上配置
source.registerCorsConfiguration("/**",corsConfiguration);
return new CorsWebFilter(source);
}
}
vue路径改造
static/config/index.js 修改项目路径前缀使其访问网关
状态码规范 & 统一异常处理
@Slf4j
@RestControllerAdvice(basePackages = "org.june.product.controller")
public class MallExceptionControllerAdvice {
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handleValidException(MethodArgumentNotValidException e) {
// log.error("数据校验出现问题:{},异常类型:{},异常原因:{}", e.getMessage(), e.getClass(), e.getCause());
BindingResult bindingResult = e.getBindingResult();
Map<String, String> errorMap = new HashMap<>();
bindingResult.getFieldErrors().forEach(result -> {
errorMap.put(result.getField(), result.getDefaultMessage());
});
return R.error(StatusCode.VALID_EXCEPTION.getCode(), StatusCode.VALID_EXCEPTION.getMsg()).put("data", errorMap);
}
@ExceptionHandler(value = Throwable.class)
public R handleException(Throwable e) {
return R.error(StatusCode.UNKNOW_EXCEPTION.getCode(),StatusCode.UNKNOW_EXCEPTION.getMsg());
}
}
SPU&SKU&基本属性与销售属性
SPU: Standard Product Unit(标准化产品单元)
SPU是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性;通俗点讲,属性值、特性相同的商品就可以称为一个SPU。
SKU: Stock Keeping Unit(库存量单位)
SKU即库存进出计量的单位, 可以是以件、盒、托盘等为单位。
SKU是物理上不可分割的最小存货单元。在使用时要根据不同业态,不同管理模式来处理。在服装、鞋类商品中使用最多最普遍。
基本属性(规格参数)与销售属性:每个分类下的商品共享规格参数与销售属性,只是有些商品不一定要用这个分类下的全部属性;
属性分组
父子组件消息传递
Json日期数据格式转换
spring:
jackson:
date-format: yyyy年MM月dd日 HH时mm秒
time-zone: Asia/Shanghai
商品上架
依托于elasticsearch实现
es 数据分析
前者空间耗费大,但属性值完全,利于条件查询
java 实体类
@Data
public class SkuEsModel {
private Long skuId;
private Long spuId;
private String skuTitle;
private BigDecimal skuPrice;
private String skuImg;
private Boolean hasStock;
private Long hotScore;
private Long catalogId;
private String brandName;
private String brandImg;
private String catalogName;
private List<Attrs> attrs;
@Data
public static class Attrs{
private Long attrId;
private String attrName;
private String attrValue;
}
}
首页显示
引入 thymeleaf
- 项目结构
- 命名空间
<html lang="en" xmlns:th="http://www.thymeleaf.org">
- 配置静态资源位置(如可用则不用配置)
- 页面热刷新
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
- thymeleaf 语法引入
thymeleaf 语法简介
- 简单遍历
- 值替换
三级分类显示
查询思路
首先查出所有分类,再从中找出一级分类,对其进行遍历,根据其id找出所有对应的二级分类,再根据二级分类id查出所有三级分类,封装在 vo 中
nginx 代理
linux 域名映射文件在 /etc/hosts
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KD0xOJZx-1658978519938)(http://markdown-pic-june.oss-cn-beijing.aliyuncs.com/uPic/image%20(1)].png)
反向代理
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
upstream gulimall {
server 192.168.163.1:88;
}
server {
listen 80;
server_name gulimall.com;
#charset koi8-r;
#access_log /var/log/nginx/log/host.access.log main;
location / {
proxy_set_header Host $host;
proxy_pass http://gulimall;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
include /etc/nginx/conf.d/*.conf;
}
搜索页
ES版本选择
- 2022.3.1日
high-level-client
已经不再维护,新版本是elasticsearch-java-api-client
elasticsearch-java:7.16.3
版本有bug,在Nested Aggregation
查询时无法返回buckets
- 通过抓包查看,发送的数据正确,接收的数据也正确,但似乎这个版本的api无法解析结果
- 最终解决方案是升级到
8.0.0
,如下图所示,出现了聚合结果,点下去会出现目标buckets
- 另外要说的一点是,这个官方给的依赖非常烂,以下是不报错的依赖引入
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>8.0.0</version>
</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>
ES-Mapping
PUT product
{
"mappings": {
"properties": {
"skuId": {
"type": "long" },
"spuId": {
"type": "keyword" },
"skuTitle": {
"type": "text",
"analyzer": "ik_smart"
},
"skuPrice": {
"type": "double" },
"skuImg": {
"type": "keyword"
},
"saleCount":{
"type":"long" },
"hasStock": {
"type": "boolean" },
"hotScore": {
"type": "long" },
"brandId": {
"type": "long" },
"catalogId": {
"type": "long" },
"brandName": {
"type": "keyword"
},
"brandImg":{
"type": "keyword"
},
"catalogName": {
"type": "keyword"
},
"attrs": {
"type": "nested",
"properties": {
"attrId": {
"type": "long" },
"attrName": {
"type": "keyword"
},
"attrValue": {
"type": "keyword" }
}
}
}
}
}
ES-DSL
{
"query": {
"bool": {
"must": [
{
"match": {
"skuTitle": "Apple"
}
}
],
"filter": [
{
"term": {
"catalogId": "225"
}
},
{
"terms": {
"brandId": [
"2",
"3",
"9"
]
}
},
{
"nested": {
"path": "attrs",
"query": {
"bool": {
"must": [
{
"term": {
"attrs.attrId": {
"value": "11"
}
}
},
{
"terms": {
"attrs.attrValue": [
"1月",
"7月"
]
}
}
]
}
}
}
},
{
"term": {
"hasStock": false
}
},
{
"range": {
"skuPrice": {
"gte": 0,
"lte": 6000
}
}
}
]
}
},
"aggs": {
"brand_agg": {
"terms": {
"field": "brandId",
"size": 1
},
"aggs": {
"brand_img_agg": {
"terms": {
"field": "brandImg",
"size": 10
}
}
}
},
"catalog_agg": {
"terms": {
"field": "catalogId",
"size": 10
}
},
"attr_agg": {
"nested": {
"path": "attrs"
},
"aggs": {
"attr_id_agg": {
"terms": {
"field": "attrs.attrId",
"size": 10
},
"aggs": {
"attr_name_agg": {
"terms": {
"field": "attrs.attrName",
"size": 10
}
},
"attr_value_agg": {
"terms": {
"field": "attrs.attrValue",
"size": 10
}
}
}
}
}
}
},
"sort": [
{
"skuPrice": {
"order": "desc"
}
}
],
"from": 17,
"size": 5,
"highlight": {
"fields": {
"skuTitle": {
}
},
"pre_tags": "<b style='color:red'>",
"post_tags": "</b>"
}
}
面包屑导航
商品详情
自定义线程池
@ConfigurationProperties(prefix = "mall.thread")
@Component
@Data
public class ThreadPoolConfigProperties {
private Integer coreSize;
private Integer maxSize;
private Integer keepAliveTime;
}
@Configuration
public class MyThreadConfig {
@Bean
public ThreadPoolExecutor threadPoolExecutor(MyThreadPoolConfigProperties pool) {
return new ThreadPoolExecutor(pool.getCoreSize(),
pool.getMaxSize(),
pool.getKeepAliveTime(),
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(100000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
}
}
异步编排商品查询
public SkuItemVo item(Long skuId) {
SkuItemVo skuItemVo = new SkuItemVo();
CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
// 1.sku基本信息获取 pms_sku_info
SkuInfoEntity info = getById(skuId);
skuItemVo.setInfo(info);
return info;
}, executor);
CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync(info -> {
// 2.获取 spu 销售属性组合
skuItemVo.setSaleAttrs(skuSaleAttrValueService.getSaleAttrsBySpuId(info.getSpuId()));
}, executor);
CompletableFuture<Void> spuDescFuture = infoFuture.thenAcceptAsync(info -> {
// 2.获取spu介绍
Long spuId = info.getSpuId();
skuItemVo.setDesp(spuInfoDescService.getById(spuId));
}, executor);
CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync(info -> {
// 2.获取spu规格参数信息
skuItemVo.setBaseAttrs(attrGroupService.getAttrGroupWithAttrsBySpuId(info.getSpuId(), info.getCatalogId()));
}, executor);
CompletableFuture<Void> imgFuture = CompletableFuture.runAsync(() -> {
// 1.sku图片信息 pms_sku_images
skuItemVo.setImages(skuImagesService.getImagesBySkuId(skuId));
}, executor);
try {
// 等待所有任务完成
CompletableFuture.allOf(infoFuture, saleAttrFuture, spuDescFuture, baseAttrFuture, imgFuture).get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
return skuItemVo;
}
登录&注册
验证码组件
@Data
@Component
@ConfigurationProperties(prefix = "spring.cloud.alicloud.sms")
public class SmsComponent {
private String host;
private String path;
private String template;
private String sign;
private String appcode;
public HttpResponse sendSmsCode(String phone,String code){
String host = this.host;
String path = this.path;
String method = "POST";
String appcode = this.appcode;
Map<String, String> headers = new HashMap<String, String>();
headers.put("Authorization", "APPCODE " + appcode);
Map<String, String> querys = new HashMap<String, String>();
querys.put("mobile", phone);
querys.put("param", "**code**:"+code+",**minute**:1");
querys.put("smsSignId", this.sign);
querys.put("templateId", this.template);
Map<String, String> bodys = new HashMap<String, String>();
try {
return HttpUtils.doPost(host, path, method, headers, querys, bodys);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
密码加密
new BCryptPasswordEncoder().encode(password)
社交登录
OAuth
是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户密码提供给第三方网站或分享他们数据的所有内容OAuth2.0
对于用户相关的OpenAPI(例如获取用户信息,动态同步,照片,日志,分享等),为了保护用户数据的安全和隐私,第三方网站访问用户数据前都需要显式的向用户征求授权
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D2dFOhoQ-1658978519939)(http://markdown-pic-june.oss-cn-beijing.aliyuncs.com/uPic/OIP-C.VaNd_GxD29x5oLDd8rNg6AHaE1)]
微信
如图所示
注:微信登录需要获取微信平台的 appid和appsecret,同时指定回调地址(这个回调地址是自己调用的,不需要内网穿透,代码中的回调地址必须与微信平台设置的相同)
/** * 准备必要的参数,再重定向到微信平台获取登录二维码 * wx.open.app_id=xxx * wx.open.app_secret=bbb * wx.open.redirect_url=http://localhost:8160/oauth2/wx/callback * 固定值 */
@GetMapping("/login")
public String getWxCode(HttpSession session) {
if(session.getAttribute(AuthenticCommonConstant.LOGIN_USER)!=null){
// 已经有用户登录
return "redirect:http://";
}
// 微信开放平台授权baseUrl
String baseUrl = "https://open.weixin.qq.com/connect/qrconnect" +
"?appid=%s" +
"&redirect_uri=%s" +
"&response_type=code" +
"&scope=snsapi_login" +
"&state=%s" +
"#wechat_redirect";
// 回调地址
String redirectUrl = AuthenticationConstant.WX_OPEN_REDIRECT_URL; //获取业务服务器重定向地址
try {
redirectUrl = URLEncoder.encode(redirectUrl, "UTF-8"); // url编码
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String state = UUID.randomUUID().toString().replaceAll("-", "");
String qrcodeUrl = String.format(
baseUrl,
AuthenticationConstant.WX_OPEN_APP_ID,
redirectUrl,
state);
return "redirect:" + qrcodeUrl;
}
/** * 微信平台检测到用户登录且确认,调用以下方法(本机调用) * 方法逻辑简单来说就是通过两次get请求微信API获取用户信息 * 第一次:code(由上一步用户扫码后平台返回,封装在请求参数中)、appid、secret、 * 第二次:微信返回 access_token、openid(用户id),拿着这两个参数去最终获取用户信息 */
@GetMapping("/callback")
public String callback(String code, String state) {
...
}
单点登录
[单点登录(SSO)解决方案介绍 – 冰湖一角 – 博客园 (cnblogs.com)](https://www.cnblogs.com/bingyimeiling/p/11698468.html#:~:text=单点登录
(Single Sign On),简称为,SSO,是目前比较流行的企业业务整合的解决方案之一。. SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。. 例如:百度旗下有很多的产品,比如百度贴吧、百度知道、百度文库等,只要登录百度账号,在任何一个地方都是已登录状态,不需要重新登录。. 当用户第一次访问应用系统的时候,因为还没有登录,会被引导到认证系统中进行登录;根据用户提供的登录信息,认证系统进行身份校验,如果通过校验,应该返回给用户一个认证的凭据--ticket;用户再访问别的应用的时候,就会将这个ticket带上,作为自己认证的凭据,应用系统接受到请求之后会把ticket送到认证系统进行校验,检查ticket的合法性。.)
同域下的单点登录
- session广播机制,缺点是资源消耗大,tomcat原生支持
- 使用 cookie + redis
- redis:key(唯一随机值),value(用户数据)
- cookie:把redis里面生成的key值放到cookie里面
- 访问项目其他模块,发送请求带着cookie进行发送,根据cookie(与key值相关)查询value,存在则为登录
- springsession 就是这种实现
- 使用 token
- 前端请求发出->后端验证成功后,按照规则生成字符串,把登陆之后的用户信息保存到其中(token)并返回(JWT工具类)
- 前端保存请求返回的值(token),存入cookie。
- 接着添加前端拦截器,如果cookie存在第二步的cookie,则在请求头添加header,值为cookie中的对应字符串
- 后端能从请求头中获取这个token且有效就可以判定为已登录
- 其实2、3两种都是后端校验成功后给了前端一个登录凭证,这个凭证可以使cookie,也可以是token(放在请求参数位置),然后后端将登录信息存到共享的数据库中,一般是redis。这里有一个问题,如果用户的cookie或者token是伪造的怎么办?这个伪造的cookie或token值一般需要大量尝试才能得出真正的有用值,后台可以增加对于重试的监测机制
不同域名下的单点登录
如图所示,这是CAS(统一身份认证)流程
解释如下:
- 用户访问app系统,app系统是需要登录的,但用户现在没有登录。
- 跳转到CAS server,即SSO登录系统,以后图中的CAS Server我们统一叫做SSO系统。 SSO系统也没有登录,弹出用户登录页。
- 用户填写用户名、密码,SSO系统进行认证后,将登录状态写入SSO的session,浏览器(Browser)中写入SSO域下的Cookie。
- SSO系统登录完成后会生成一个ST(Service Ticket),然后跳转到app系统,同时将ST作为参数传递给app系统。
- app系统拿到ST后,从后台向SSO发送请求,验证ST是否有效。
- 验证通过后,app系统将登录状态写入session并设置app域下的Cookie。
至此,跨域单点登录就完成了。以后我们再访问app系统时,app就是登录的。接下来,我们再看看访问app2系统时的流程。
- 用户访问app2系统,app2系统没有登录,跳转到SSO。
- 由于SSO已经登录了[有相应的cookie],不需要重新登录认证。
- SSO生成ST,浏览器跳转到app2系统,并将ST作为参数传递给app2。
- app2拿到ST,后台访问SSO,验证ST是否有效。
- 验证成功后,app2将登录状态写入session,并在app2域下写入Cookie。
这样,app2系统不需要走登录流程,就已经是登录了。SSO,app和app2在不同的域,它们之间的session不共享也是没问题的。
有的同学问我,SSO系统登录后,跳回原业务系统时,带了个参数ST,业务系统还要拿ST再次访问SSO进行验证,觉得这个步骤有点多余。他想SSO登录认证通过后,通过回调地址将用户信息返回给原业务系统,原业务系统直接设置登录状态,这样流程简单,也完成了登录,不是很好吗?
其实这样问题时很严重的,如果我在SSO没有登录,而是直接在浏览器中敲入回调的地址,并带上伪造的用户信息,是不是业务系统也认为登录了呢?这是很可怕的。
总结
单点登录,资源都在各个业务系统这边,不在SSO那一方。 用户在给SSO服务器提供了用户名密码后,作为业务系统并不知道这件事。 SSO随便给业务系统一个ST,那么业务系统是不能确定这个ST是用户伪造的,还是真的有效,所以要拿着这个ST去SSO服务器再问一下,这个用户给我的ST是否有效,是有效的我才能让这个用户访问。
购物车
ThreadLocal
Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(它的key实际上是一个弱引用)。每个线程在往某个ThreadLocal里塞值的时候,都会往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
弱引用
代码逻辑
/** * Controller * auth:login * 添加购物车 */
@GetMapping("/addToCart")
public String addToCart(@RequestParam("skuId") Long skuId, @RequestParam("num") Integer num) {
cartService.addToCart(skuId, num, CartInterceptor.loginUser.get());
return "redirect:http://cart.projectdemo.top/cart/getItem.html?skuId=" + skuId;
}
/** * Service * 添加到购物车,但实际可能是修改操作 */
@Override
public CartItem addToCart(Long skuId, Integer num, Long memberId) {
// key prefix-memberId -> cart:8 | hash-key -> skuId hash-value -> cartItem-Json
BoundHashOperations<String, String, String> bound =
redisTemplate.boundHashOps(CartConstant.CART_MEMBER_PREFIX + memberId);
CartItem cartItem = new CartItem();
String o = bound.get(skuId.toString());
if (StringUtils.isEmpty(o)) {
// 添加,fillItem方法就是查询数据库并封装 cartItem(需要数据库的商品info等信息)
fillItem(skuId, num, cartItem);
bound.put(skuId.toString(), JSON.toJSONString(cartItem));
} else {
// 修改,把redis中的json取出来改一遍就行了
cartItem = JSON.parseObject(o, CartItem.class);
int i = cartItem.getCount() + num;
cartItem.setCount(i);
bound.put(skuId.toString(), JSON.toJSONString(cartItem));
}
return cartItem;
}
订单
订单
RabbitMQ-概念设计
RabbitMQ-代码设计
/** * Order */
@Bean
public Queue orderDelayQueue() {
Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", "order-event-exchange");
args.put("x-dead-letter-routing-key", "order.release.order");
args.put("x-message-ttl", 60000);
return new Queue("order.delay.queue",
true,
false,
false,
args);
}
@Bean
public Queue orderReleaseQueue() {
return new Queue("order.release.order.queue",
true,
false,
false
);
}
@Bean
public Exchange orderEventExchange() {
return new TopicExchange("order-event-exchange",
true,
false
);
}
@Bean
public Binding orderCreateOrder() {
// 交换机根据routing-key绑定队列
return new Binding("order.delay.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.create.order",null);
}
@Bean
public Binding orderReleaseOrder() {
return new Binding("order.release.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.order",null);
}
/** * Ware(Stock) */
@Configuration
public class MyRabbitConfig {
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
@Bean
public Exchange stockEventExchange(){
return new TopicExchange("stock-event-exchange",
true,false);
}
@Bean
public Queue stockReleaseStockQueue(){
return new Queue("stock.release.stock.queue",
true,false,false);
}
@Bean
public Queue stockDelayQueue(){
Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", "stock-event-exchange");
args.put("x-dead-letter-routing-key", "stock.release.stock");
args.put("x-message-ttl", 120000);
return new Queue("stock.delay.queue",
true,
false,
false,
args);
}
@Bean
public Binding stockLockBinding(){
return new Binding("stock.delay.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.lock.stock",
null);
}
@Bean
public Binding stockReleaseBinding(){
return new Binding("stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.release.#",
null);
}
}
/** * Listener1(Controller) */
@RabbitListener(queues = "order.release.order.queue")
public void releaseOrder(OrderEntity o, Channel channel, Message message) throws IOException {
try {
orderService.closeOrder(o);
// 手动调用支付宝收单,防止用户再去支付 TODO
//https://opendocs.alipay.com/apis/api_1/alipay.trade.close
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
e.printStackTrace();
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
/** * Service * Order服务收到MQ关闭订单消息 */
@Override
public void closeOrder(OrderEntity o) {
log.error("MQ消息-尝试关闭订单!");
OrderEntity byId = this.getById(o.getId());
// 只有新建状态的订单才能关闭
if(Objects.equals(byId.getStatus(), OrderStatusEnum.CREATE_NEW.getCode())){
OrderEntity orderEntity = new OrderEntity();
orderEntity.setId(o.getId());
orderEntity.setStatus(OrderStatusEnum.CANCELED.getCode());
// 订单服务数据库更改订单数据
this.updateById(orderEntity);
OrderTo order = new OrderTo();
BeanUtils.copyProperties(byId,order);
// 主动发消息给库存使其库存回滚
try {
// 订单服务给 ware 服务发送rabbitmq消息,使其回滚库存
rabbitTemplate.convertAndSend("order-event-exchange",
"order.release.other",order);
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Component
@Slf4j
@RabbitListener(queues = "stock.release.stock.queue")
public class StockReleaseListener {
@Autowired
WareOrderTaskDetailService wareOrderTaskDetailService;
@Autowired
WareOrderTaskService wareOrderTaskService;
@Autowired
OrderFeignService orderFeignService;
@Autowired
WareSkuDao wareSkuDao;
@Autowired
WareSkuService wareSkuService;
/** * Listener2(Controller) * 库存服务解锁库存 */
@RabbitHandler
public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
log.error("库存服务解锁库存!");
try {
StockDetailTo detail = to.getDetail();
Long detailId = detail.getId();
WareOrderTaskDetailEntity byId = wareOrderTaskDetailService.getById(detailId);
// WareOrderTaskDetailEntity 不为空-说明库存已经扣减,
// 需要检查订单是否有效,否则回滚订单锁定的库存
if (byId != null) {
// 检查确认订单状态
Long id = to.getId();
WareOrderTaskEntity taskEntity = wareOrderTaskService.getById(id);
String orderSn = taskEntity.getOrderSn();
R r1 = orderFeignService.getOrderStatus(orderSn);
if (r1.getCode() == 0) {
OrderVo data = r1.getData(new TypeReference<OrderVo>() {
});
// 订单已经【取消】或【不存在】,必须解锁库存
if (data == null || Objects.equals(data.getStatus(), OrderStatusEnum.CANCELED.getCode())) {
if (byId.getLockStatus() == 1) {
// 锁定状态必须是1-锁定
wareSkuService.unlockStock(detail.getSkuId(), detail.getWareId(), detail.getSkuNum(), detailId);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
} else {
// 远程调用出问题,拒收消息,重新放回队列
channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
}
} else {
// mq中有消息,但数据库中没有记录,可能是锁单有异常发生,数据库数据回滚,无需解锁
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
}
// 查不到库存工作单,无需解锁
} catch (IOException e) {
// 未知异常,消息重新入队
e.printStackTrace();
channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
}
}
/** * Listener1(Controller) * 来源于order.release.order.queue->listener1->order.release.other(OrderTo) */
@RabbitHandler
@Transactional
public void handleOrderCloseRelease(OrderTo to, Message message, Channel channel){
log.error("订单主动回滚库存!");
// 查询订单状态
WareOrderTaskEntity w = wareOrderTaskService.getOrderTaskByOrderSn(to.getOrderSn());
Long id = w.getId();
List<WareOrderTaskDetailEntity> list = wareOrderTaskDetailService.list(new QueryWrapper<WareOrderTaskDetailEntity>().
eq("task_id", id).eq("lock_status", 1));
for (WareOrderTaskDetailEntity entity : list) {
wareSkuService.unlockStock(entity.getSkuId(), entity.getWareId(),
entity.getSkuNum(),entity.getId());
}
try {
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (IOException e) {
e.printStackTrace();
}
}
}
/** * 提交订单 */
@Override
@Transactional
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
orderSubmitVoThreadLocal.set(vo);
SubmitOrderResponseVo response = new SubmitOrderResponseVo();
// token是做幂等性处理的参数,由 confirmOrder 接口生成(上一步的接口)
String voToken = vo.getOrderToken();
Long userId = LoginUserInterceptor.loginUser.get();
// 查-比-删,全部成功返回1,这里是有并发问题的,必须要原子操作
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
Collections.singletonList(OrderConstant.USER_ORDER_TOKEN_PREFIX + userId), voToken);
if (result == 0L) {
// 失败,幂等性对比失败,用户可能修改了token或者点太快
response.setCode(1);
return response;
} else {
// 生成订单基础信息,如订单号、收货人、价格、积分等,这个数据是可信赖的
OrderCreateTo order = createOrder();
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal payPrice = vo.getPayPrice();
// 对比前后端的价格,以后端为准
if (payAmount.subtract(payPrice).abs().doubleValue() < 0.01) {
// 金额对比容错成功 ...
// 保存订单且锁定库存,有异常就回滚 Transactional
WareSkuLockVo wareSkuLockVo = new WareSkuLockVo();
wareSkuLockVo.setOrderSn(order.getOrder().getOrderSn());
wareSkuLockVo.setLocks(order.getItems().stream().map(item -> {
OrderItemVo orderItemVo = new OrderItemVo();
orderItemVo.setSkuId(item.getSkuId());
orderItemVo.setCount(item.getSkuQuantity());
return orderItemVo;
}).collect(Collectors.toList()));
// 远程锁库存(创建库存工作单,发送rabbitmq死信队列消息)
R r = wareFeignService.lockStock(wareSkuLockVo);
if (r.getCode() == 0) {
// ?成功
// 1.保存订单数据
saveOrder(order);
// 2.锁库存
response.setOrder(order.getOrder());
// 发送rabbitmq消息(1m后关闭订单,回滚库存)
rabbitTemplate.convertAndSend("order-event-exchange",
"order.create.order",order.getOrder());
return response;
} else {
// 有异常-无库存
response.setCode(3);
return response;
}
} else {
// 价格校验失败
response.setCode(2);
return response;
}
}
}
@Override
@Transactional(rollbackFor = NoStockException.class)
public boolean lockStock(WareSkuLockVo vo) {
// 保存库存工作单 WareOrderTaskEntity
WareOrderTaskEntity wareOrderTaskEntity = new WareOrderTaskEntity();
wareOrderTaskEntity.setOrderSn(vo.getOrderSn());
wareOrderTaskService.save(wareOrderTaskEntity);
List<OrderItemVo> locks = vo.getLocks();
List<SkuWareHasStock> collect = locks.stream().map(item -> {
SkuWareHasStock stock = new SkuWareHasStock();
Long skuId = item.getSkuId();
stock.setSkuId(skuId);
// 查询并设置有库存的仓库id
stock.setWareId(wareSkuDao.listWareIdHasSkuStock(skuId));
stock.setNum(item.getCount());
return stock;
}).collect(Collectors.toList());
// 如果所有个商品都锁定成功,将当前商品锁定了几件的工作单记录发给MQ
// 有商品锁定失败,前面保存的工作单信息就回滚了
// 发送出去的消息,即使想要解锁记录,由于去数据库查不到id,所以就不用解锁
for (SkuWareHasStock s : collect) {
boolean skuStocked = false;
Long skuId = s.getSkuId();
Integer num = s.getNum();
List<Long> wareIds = s.getWareId();
if (CollectionUtils.isNotEmpty(wareIds)) {
for (Long wareId : wareIds) {
// 当前遍历的仓库尝试锁单
Long success = wareSkuDao.lockSkuStock(skuId, wareId, num);
if (success == 1) {
// 当前仓库锁定成功
skuStocked = true;
WareOrderTaskDetailEntity detail =
new WareOrderTaskDetailEntity(null, skuId, "", s.getNum(),
wareOrderTaskEntity.getId(), wareId, 1);
wareOrderTaskDetailService.save(detail);
StockLockedTo stockLockedTo = new StockLockedTo();
stockLockedTo.setId(wareOrderTaskEntity.getId());
StockDetailTo stockDetailTo = new StockDetailTo();
BeanUtils.copyProperties(detail, stockDetailTo);
stockLockedTo.setDetail(stockDetailTo);
// 锁单成功,添加锁单死信队列消息
rabbitTemplate.convertAndSend("stock-event-exchange",
"stock.lock.stock", stockLockedTo);
break;
}
// 失败,重试下一个仓库
}
if (!skuStocked) {
// 这个商品没有库存 - 报异常
throw new NoStockException(skuId);
}
} else {
throw new NoStockException(skuId);
}
}
return true;
}
支付
支付宝
有几个点需要注意:
- 公钥加密,私钥解密;私钥加签,公钥验签
- 同步回调地址
return_url
不需要内网穿透,异步回调地址notify_url
需要指定为 外网地址 - 本项目设计为异步回调至本机80端口,nginx根据其Host进行匹配转发到网关,同时修改掉原本的Host值为
order.projectdemo.top
- 由于内网穿透软件特性,每次启动更改内网穿透的对应外网地址,故每次启动需要更改
- order项目配置文件
notify_url
- nginx
server_name
匹配路径
- order项目配置文件
收单
- 订单在支付页,不支付,一直刷新,订单过期了才支付,订单状态已经改为已支付了,但是库存解锁了
- 设置支付宝支付超时时间解决,过期无法支付
- 由于时延问题,订单已支付完成,但解锁库存之后,支付宝异步通知才到
- 订单解锁,手动收单
- 网络阻塞问题,订单支付成功的异步通知一直不到达
- 查询订单列表时,ajax获取当前未支付的订单状态,查询订单状态时,再获取支付宝此订单状态
- 其他各种问题
- 每天服务器闲时下载支付宝账单,一一对账
秒杀
要点:限流+异步+缓存(页面静态化)+独立部署
限流方式:
- 前端限流,一些高并发的网站直接在前端页面开始限流
- nginx 限流,直接负载部分请求到错误的静态页面
- 网关限流,限流的过滤器
- 代码中使用分布式信号量
- rabbitmq 限流( 能者多劳:channel.basicQos(1) ),保证发挥所有服务器的性能
思路设计
1. 秒杀流程一(加入购物车秒杀-弃用)
优点:加入购物车实现天然的流量错峰,与正常购物流程一致只是价格为秒杀价格,数据模型与正常下单兼容性好
缺点:秒杀服务与其他服务关联性提高,比如这里秒杀服务会与购物车服务关联,秒杀服务高并发情况下,可能会把购物车服务连同压垮,导致正常商品,正常购物也无法加入购物车下单
2. 秒杀流程二(独立秒杀业务来处理)
优点:从用户下单到返回没有对数据库进行任何操作,只是做了一些条件校验,校验通过后也只是生成一个单号,再发送一条消息
缺点:如果订单服务全挂掉了,没有服务来处理消息,就会导致用户一直不能付款
解决方案:不使用订单服务处理秒杀消息,需要一套独立的业务来处理
3. 创建秒杀队列
项目设计
Redis 设计
上架&查询
/** * 找出start_time在今天0:00到三天后0:00区间内的session(秒杀场次) * 并将上架秒杀商品信息和库存数放入redis * 此方法采用redisson分布式锁,固定加锁时间为20s */
@Async
// 秒 分 时 日 月 周 年
// @Scheduled(cron = "0 0 3 * * ?") // 每天3:00AM上架秒杀商品
@Scheduled(cron = "*/10 * * * * ?")
public void uploadSeckillSkuLatest3Days() {
log.info("上架秒杀!");
RLock lock = redissonClient.getLock(upload_lock);
lock.lock(20, TimeUnit.SECONDS);
try {
seckillService.uploadSeckillSkuLatest3Days();
}finally {
lock.unlock();
}
}
/** * 扫描需要参与秒杀的活动,并缓存商品信息 */
@Override
public void uploadSeckillSkuLatest3Days() {
R r = couponFeignService.getLatestNDaySession(3);
if (r.getCode() == 0) {
// 上架商品
List<SeckillSessionWithSkus> data = r.getData(new TypeReference<List<SeckillSessionWithSkus>>() {
});
// 缓存活动信息和关联商品信息
saveSessionInfos(data);
}
}
/** * 该方法缓存商品信息 * 有两个要点:随机码、信号量(库存数) * 随机码是用于商品加密,只有在秒杀时间段才会返回给前台,该值作为库存的key * 使用redisson将库存数作为信号量 */
private void saveSessionInfos(List<SeckillSessionWithSkus> data) {
BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SeckillConstant.SECKILL_SKUS_PREFIX);
data.forEach(session -> {
// 秒杀场次信息缓存,采用list数据结构存储,可以有相同的键,不能有相同的值
long start = session.getStartTime().getTime();
long end = session.getEndTime().getTime();
String redisKey = SeckillConstant.SECKILL_SESSION_PREFIX + start + "-" + end + "-" + session.getId();
if (Boolean.FALSE.equals(redisTemplate.hasKey(redisKey))) {
List<String> ids = session.getRelationSkus().stream().map(i ->
i.getSkuId().toString()).collect(Collectors.toList());
redisTemplate.opsForList().leftPushAll(redisKey, ids);
}
// 每一场秒杀商品缓存如下
session.getRelationSkus().forEach(item -> {
// 使用 sessionId-skuId 为SeckillConstant.SECKILL_SKUS_PREFIX Hash下的键,值为商品信息
String skuKey = item.getSkuId().toString() + "-" + session.getId();
if (Boolean.FALSE.equals(hashOps.hasKey(skuKey))) {
SeckillSkuRedisTo to = new SeckillSkuRedisTo();
// 1.SKUs基本信息
R r = productFeignService.skuInfo(item.getSkuId());
if (r.getCode() == 0) {
SkuInfoVo skuInfo = r.getData("skuInfo", new TypeReference<SkuInfoVo>() {
});
to.setSkuInfoVo(skuInfo);
}
// 2.SKUs秒杀信息,当场商品秒杀信息是一样的
BeanUtils.copyProperties(item, to);
to.setSeckillLimit(item.getSeckillLimit());
// 3.设置当前商品的秒杀时间信息
to.setStartTime(session.getStartTime().getTime());
to.setEndTime(session.getEndTime().getTime());
// 4.随机码
String token = UUID.randomUUID().toString().replace("-", "");
to.setRandomCode(token);
hashOps.put(skuKey, JSON.toJSONString(to));
// 5.引入redisson分布式信号量作为库存——起限流作用
RSemaphore semaphore = redissonClient.getSemaphore(SeckillConstant.SECKILL_SKU_STOCK_PREFIX + token);
// 商品可以秒杀的数量作为信号量
semaphore.trySetPermits(item.getSeckillCount());
}
});
});
}
秒杀逻辑
/** * 秒杀方法 url:kill?killId=31-2&randomCode=bc8114eff5f64c1cba576f6ae2e649fd&num=2 */
@GetMapping("/kill")
public String seckill(@RequestParam("killId")String killId,
@RequestParam("randomCode")String randomCode,
@RequestParam("num")Integer num,
Model model){
String orderSn = seckillService.kill(killId,randomCode,num);
model.addAttribute("orderSn",orderSn);
return "success";
}
/** * 秒杀逻辑 */
@Override
public String kill(String killId, String randomCode, Integer num) {
BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SeckillConstant.SECKILL_SKUS_PREFIX);
String s = hashOps.get(killId); // killId:31-2
if (StringUtils.isNotEmpty(s)) {
// 校验参数
SeckillSkuRedisTo redisTo = JSON.parseObject(s, SeckillSkuRedisTo.class);
Long startTime = redisTo.getStartTime();
Long endTime = redisTo.getEndTime();
long now = new Date().getTime();
// 1.校验时间
if (now >= startTime && now <= endTime) {
// 2.校验随机码和商品ID
String redisToRandomCode = redisTo.getRandomCode();
String redisKey = redisTo.getSkuId() + "-" + redisTo.getPromotionSessionId();
if (redisToRandomCode.equals(randomCode) && killId.equals(redisKey)) {
// 3.校验购物数量
if (num <= redisTo.getSeckillLimit()) {
// 4.校验是否购买过
Long userID = LoginUserInterceptor.loginUser.get();
// 格式:prefix-userId-skuId-sessionId
String redisUserKey = SeckillConstant.SECKILL_USER_MARK_PREFIX + redisToRandomCode +
userID + "-" + redisTo.getSkuId() + "-" + redisTo.getPromotionSessionId();
Boolean mark = redisTemplate.opsForValue().setIfAbsent(redisUserKey, String.valueOf(num),
Duration.ofMillis(endTime - now));
if (Boolean.TRUE.equals(mark)) {
// 占位成功,未购买过
RSemaphore semaphore = redissonClient.getSemaphore(SeckillConstant.SECKILL_SKU_STOCK_PREFIX + randomCode);
// semaphore.acquire(num); // 该方法阻塞
try {
if (semaphore.tryAcquire(num, 500, TimeUnit.MILLISECONDS)) {
// 秒杀成功
String timeId = IdWorker.getTimeId();
// 对象构造
QuickOrderTo quickOrderTo = new QuickOrderTo();
quickOrderTo.setOrderSn(timeId);
quickOrderTo.setNum(num);
quickOrderTo.setMemberId(userID);
quickOrderTo.setPromotionSessionId(redisTo.getPromotionSessionId());
quickOrderTo.setSkuId(redisTo.getSkuId());
quickOrderTo.setSeckillPrice(redisTo.getSeckillPrice());
// 秒杀成功,发送mq订单消息,并返回订单号通知前台秒杀成功
rabbitTemplate.convertAndSend("order-event-exchange",
"order.seckill.order", quickOrderTo);
return timeId;
}
} catch (InterruptedException e) {
return null;
}
}
}
}
}
}
return null;
}
/** * 监听已经秒杀成功的订单,削峰创建订单 */
@RabbitListener(queues = "order.seckill.order.queue")
public void seckillOrder(QuickOrderTo to, Channel channel, Message message) throws IOException {
try {
log.info("创建秒杀订单!");
orderService.createSeckillOrder(to);
// 手动调用支付宝收单,防止用户再去支付 TODO
// https://opendocs.alipay.com/apis/api_1/alipay.trade.close
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
e.printStackTrace();
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
/** * 削峰建单 */
@Override
public void createSeckillOrder(QuickOrderTo to) {
OrderEntity orderEntity = new OrderEntity();
orderEntity.setOrderSn(to.getOrderSn());
orderEntity.setMemberId(to.getMemberId());
orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
orderEntity.setPayAmount(to.getSeckillPrice().multiply(BigDecimal.valueOf(to.getNum())));
this.save(orderEntity);
OrderItemEntity orderItemEntity = new OrderItemEntity();
orderItemEntity.setOrderSn(to.getOrderSn());
orderItemEntity.setRealAmount(orderEntity.getPayAmount());
orderItemEntity.setSkuQuantity(to.getNum());
orderItemEntity.setSkuId(to.getSkuId());
// TODO 查询SKU详细信息并保存
orderItemService.save(orderItemEntity);
}
项目BUG记录
微信登录-session 作用域
微信登录调用回调函数时,由于谷粒学院APP写死回调地址,必须是 localhost:8160/… ,导致产生了额外的会话,而domain都是demoproject.top,回调的cookie(MALLSESSION)则直接失效,也就没有传递到项目主地址,就没了session,这个问题叫做跨域名cookie失效,引出一个单点登录的问题
远程调用Session丢失
// Openfeign 调用配置拦截器加上cookie
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor(){
return template -> {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String cookie = request.getHeader("Cookie");
template.header("Cookie",cookie);
};
}
Feign远程调用丢失上下文
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/179235.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...