一、初识
接口文档:https://easydoc.net/doc/75716633/ZUqEdvA4/E97bmStq
最后要将同类的笔记整理到一起
【值得学习】
简介
springboot v2.3.7.RELEASE
高级篇
springcloud组件:nacos注册中心、配置中心、sentinel、seata、oss、feign、gateway、sleuth +zipkin
webflux、 shardingsphere【值得学习】
接口幂等性、本地事务,分布式事务、jmeter等性能压力测试工具、ElasticSearch、异步线程池、单点登录、社交登陆、商城业务(购物车、订单、秒杀,商品详情:redis、springcache,数据不一致问题,击穿穿透)
rabbitmq(关单死信队列、最终一致、消峰解耦),支付宝沙箱、定时任务
准备
项目接口文档:https://easydoc.xyz/doc/75716633/ZUqEdvA4/hKJTcbfd
openfeign
虚拟机
使用vagrant和virtualBox可以快速创建虚拟机
$ vagrant init centos/7
执行完上面的命令后,会在用户的家目录下生成Vagrantfile文件。
$ vagrant up
下载镜像过程比较漫长,也可以采用先用下载工具下载到本地后,然后使用“ vagrant box add ”添加,再“vagrant up”即可
#将下载的镜像添加到virtualBox中
$ vagrant box add centos/7 E:\迅雷下载\CentOS-7-x86_64-Vagrant-1905_01.VirtualBox.box
#启动
$ vagrant up
缺点:端口映射(如Docker)
vagrant ssh
或者vagrantFile添加网卡信息
C:\Users\Administrator>ipconfig
Windows IP 配置
以太网适配器 VirtualBox Host-Only Network:
连接特定的 DNS 后缀 . . . . . . . :
本地链接 IPv6 地址. . . . . . . . : fe80::a00c:1ffa:a39a:c8c2%16
IPv4 地址 . . . . . . . . . . . . : 192.168.56.1
子网掩码 . . . . . . . . . . . . : 255.255.255.0
默认网关. . . . . . . . . . . . . :
配置网络信息,打开”Vagrantfile”文件:
config.vm.network "private_network", ip: "192.168.56.10"
修改完成后,重启启动vagrant
vagrant reload
检查宿主机和virtualBox之间的通信是否正常
[vagrant@localhost ~]$ ping 192.168.43.43 PING 192.168.43.43 (192.168.43.43) 56(84) bytes of data.
64 bytes from 192.168.43.43: icmp_seq=1 ttl=127 time=0.533 ms
64 bytes from 192.168.43.43: icmp_seq=2 ttl=127 time=0.659 ms
--- 192.168.43.43 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.533/0.596/0.659/0.063 ms
[vagrant@localhost ~]$
[vagrant@localhost ~]$
[vagrant@localhost ~]$ ping www.baidu.com
PING www.a.shifen.com (112.80.248.76) 56(84) bytes of data.
64 bytes from 112.80.248.76 (112.80.248.76): icmp_seq=1 ttl=53 time=56.1 ms
64 bytes from 112.80.248.76 (112.80.248.76): icmp_seq=2 ttl=53 time=58.5 ms
64 bytes from 112.80.248.76 (112.80.248.76): icmp_seq=3 ttl=53 time=53.4 ms
开启远程登陆,修改“/etc/ssh/sshd_config”
PermitRootLogin yes
PasswordAuthentication yes
然后重启SSHD
systemctl restart sshd
docker
systemctl enable docker # 自启动
启动系统即运行docker容器
docker update mysql --restart=always
docker update redis --restart=always
mysql
docker run -p 3306:3306 --name mysql \
-v /mydata/mysql/master/log:/var/log/mysql \
-v /mydata/mysql/master/data:/var/lib/mysql \
-v /mydata/mysql/master/conf:/etc/mysql \
-e MYSQL_ROOT_PASSWORD=root \
-d mysql:5.7
# pwd
/mydata/mysql/conf
# cat my.cnf
[client]
default-character-set=utf8
[mysql]
default-character-set=utf8
[mysqld]
init_connect='SET collation_connection = utf8_unicode_ci'
init_connect='SET NAMES utf8'
character-set-server=utf8
collation-server=utf8_unicode_ci
skip-character-set-client-handshake
skip-name-resolve
(1 )打开cmd,登录到mysql
mysql -u root -p
(2) 输入授权语句:
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%'IDENTIFIED BY 'root' WITH GRANT OPTION;
# 赋予所用权限给root账户从任何iP以mypassword为密码登录
GRANT ALL PRIVILEGES ON *.* TO 'myuser'@'192.168.1.3'IDENTIFIED BY '123' WITH GRANT OPTION;
# 赋予所用权限给myuser账户从任何192.168.1.3以123为密码登录
(3) FLUSH PRIVILEGES;
redis
docker pull redis
启动
mkdir -p /mydata/redis/conf
touch /mydata/redis/conf/redis.conf
echo "appendonly yes" >> /mydata/redis/conf/redis.conf # 持久化
docker run -p 6379:6379 --name redis -v /mydata/redis/data:/data \
-v /mydata/redis/conf/redis.conf:/etc/redis/redis.conf \
-d redis redis-server /etc/redis/redis.conf
连接到docker的redis
docker exec -it redis redis-cli
set key1 v1
get key1
设置redis容器在docker启动的时候启动
docker update redis --restart=always
人人开源
值得研究!
代码生成器
- 修改application.yaml与generator.properties,配置数据库相关信息
- 运行查看界面下载
前端
值得研究:
- 添加和修改抽离出组件
ui库比较低,修改src/element-ui/index.js
后端
值得研究:
- 各种工具类,如:分页等
mp
将项目中的各种依赖包放入common,需要配置mp分页插件,否则分页有误
@Configuration
@EnableTransactionManagement // 开启事务
@MapperScan("com.ming.gulimall.product.dao")
public class MybatisConfig {
// 分页插件
@Bean
public PaginationInterceptor paginationInterceptor() {
// .setOverflow(false),超过最后一页,true第一页,false继续请求
// .setLimit(500),最大单页限制数量,默认500条,-1不
return new PaginationInterceptor();
}
}
项目初始化
powerdesign
创建
初始化网址:https://start.aliyun.com
操作:
- 父maven,pom并modules
- 子module,通过common聚合
视频:
empty父项目
子springboot项目(web、openfeign)
父项目添加pom,并添加到maven
<packaging>pom</packaging>
<modules>
<module>gulimall-coupon</module>
<module>gulimall-member</module>
<module>gulimall-order</module>
<module>gulimall-product</module>
<module>gulimall-ware</module>
</modules>
gitignore
下面是一些.gitignore文件忽略的匹配规则:
*.a # 忽略所有 .a 结尾的文件
!lib.a # 但 lib.a 除外
/TODO # 仅仅忽略项目根目录下的 TODO 文件,不包括 subdir/TODO
build/ # 忽略 build/ 目录下的所有文件
doc/*.txt # 会忽略 doc/notes.txt 但不包括 doc/server/arch.txt
**/.mvn # 忽略所有下的.mvn
**/target/ # target下的所有东西
.idea
下载gitee插件
maven
下载太慢
setting.xml
<mirror>
<id>nexus-aliyun</id>
<mirrorOf>*</mirrorOf>
<name>Nexus aliyun</name>
<url>http://maven.aliyun.com/nexus/content/groups/public</url>
</mirror>
java版本
D:\Environment\apache-maven-3.6.1\conf\setting.xml
<profile>
<id>development</id>
<activation>
<jdk>1.8</jdk>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
</properties>
</profile>
pom
maven中三种classpath
编译,测试,运行
- compile:默认范围,编译测试运行都有效
- provided:在编译和测试时有效
- runtime:在测试和运行时有效
- test:只在测试时有效
- system:在编译和测试时有效,与本机系统关联,可移植性差
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
Error
Diamond types are not supported at this language level
版本老是不一致
正确:https://blog.csdn.net/isea533/article/details/48575983
pom添加
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
https://blog.csdn.net/liu16659/article/details/80230164 修改idea配置
https://blog.csdn.net/weixin_36210698/article/details/72085710 修改pom
java.lang.IllegalStateException: Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or @SpringBootTest(classes=…) with your test
测试类与启动类不再同一包下
字符集UTF-8
https://blog.csdn.net/m0_38132361/article/details/80628203
Too many connections
java.sql.SQLNonTransientConnectionException: Data source rejected establishment of connection, message from server: “Too many connections”
一直运行不起来
将依赖放入一个pom,规定springboot相关版本?
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
数据库连接池
idleTimeout is close to or more than maxLifetime
当连接池的参数idleTimeout
或maxLifetime
大于数据库的interactive_timeout
或wait_timeout
时,连接池里的连接没过期,但数据库那边已经过期了,就会出现上面的错误。
spring:
datasource:
hikari:
connection-timeout: 10000
validation-timeout: 3000
idle-timeout: 60000
login-timeout: 5
max-lifetime: 60000
maximum-pool-size: 10
minimum-idle: 3
read-only: false
Feign连接超时
# feign调用超时时间配置
feign:
client:
config:
default:
connectTimeout: 10000
readTimeout: 600000
整合
数据库
- 导入依赖
- 配置
- 数据源
- 数据库驱动
- 数据源配置信息
- @Mapperscan,dao
- 映射文件路径,mapper
- 数据源
1)、导入依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.26</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
2)、配置
1、配置数据源;
1)、导入数据库的驱动。https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-versions.html
2)、在application.yml配置数据源相关信息
spring:
datasource:
username: root
password: root
url: jdbc:mysql://#:3306/gulimall_pms
driver-class-name: com.mysql.cj.jdbc.Driver
2、配置MyBatis-Plus;
1)、使用@MapperScan
,如果使用了@Mapper就不需要
2)、告诉MyBatis-Plus,sql映射文件位置
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
#主键自增
id-type: auto
测试
注意些
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
@RunWith(SpringRunner.class)
@SpringBootTest(classes = StudyApplication.class) // 可以不写()后面的
public class Test {
@Autowired
private UserService userService;
@org.junit.Test
public void test() {
System.out.println(userService.loginOrRegister(new UserLoginVo("1", "2")));
}
}
二、分布式
nacos
阿里巴巴cloud:https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md
添加微服务
- 添加pom,服务发现依赖
- 配置nacos.discovery.server-addr,application.name,server.port
- 添加@EnableDiscoveryClient
- 网关添加路由
docker
docker pull nacos/nacos-server:1.4.1
docker run --env MODE=standalone --name nacos -d -p 8848:8848 nacos/nacos-server:1.4.1
docker run -d \
-e MODE=standalone \
-e SPRING_DATASOURCE_PLATFORM=mysql \
-e MYSQL_SERVICE_HOST=xxx \
-e MYSQL_SERVICE_PORT=3306 \
-e MYSQL_SERVICE_USER=root \
-e MYSQL_SERVICE_PASSWORD=xxx \
-e MYSQL_SERVICE_DB_NAME=nacos \
-p 8848:8848 \
--restart=always \
--name mynacos \
nacos/nacos-server
注册发现
1.依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
2.配置(nacos位置和应用名称)
spring.application.name=gulimall-coupon
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
# or
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
application:
name: gulimall-coupon
可以存入bootstrap.properties
spring.application.name=gulimall-coupon
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
3.启动
@EnableDiscoveryClient
配置中心
应用
优先使用配置中心的配置
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
bootstrap.properties,会先于application.properties
spring.application.name=gulimall-coupon
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
nacos配置中心添加数据集(Data Id)gulimall-coupon.properties,应用名.properties
需要使用的地方类上添加@RefreshScope,使用@Value动态获取配置
error
就是不改变!!!!
config版本?
命名空间
#命名空间id
spring.cloud.nacos.config.namespace=6399421b-1c23-45bb-817d-79b9928348db
配置分组
group: dev
spring.cloud.nacos.config.group=dev
配置拆分
spring.cloud.nacos.config.ext-config[0].data-id=redis.properties
# 开启动态刷新配置,否则配置文件修改,工程无法感知
spring.cloud.nacos.config.ext-config[0].refresh=true
spring.cloud.nacos.config.ext-config[1].data-id=jdbc.properties
spring.cloud.nacos.config.ext-config[1].refresh=true
openfeign
- 创建时就引入了依赖
- 要调用的服务创建接口
依赖
<properties>
<spring-cloud.version>Hoxton.SR9</spring-cloud.version>
</properties>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
# feign调用超时时间配置
feign:
client:
config:
default:
connectTimeout: 10000
readTimeout: 600000
@FeignClient("gulimall-coupon")
public interface CouponFeignService {
@RequestMapping("/coupon/coupon/member/list")
public R memberCoupons();
}
源码:
先判断是不是toString hashCode这些方法,如果不是就调用原方法,
Feign将参数转为json,封装到template中,里面有url,编码模式等,负载均衡执行请求并解码
异常则重试器会重复(有default实现本接口,重复一定次数),也可以用neverTry,直接抛异常,重试器默认关闭
头丢失问题
默认不携带头信息,添加requestInterceptor,通过RequestContextHolder获取线程request属性
@Configuration
public class GuliFeignConfig {
@Bean
public RequestInterceptor requestInterceptor() {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
//1. 使用RequestContextHolder拿到老请求的请求数据
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
HttpServletRequest request = requestAttributes.getRequest();
if (request != null) {
//2. 将老请求得到cookie信息放到feign请求上
String cookie = request.getHeader("Cookie");
template.header("Cookie", cookie);
}
}
}
};
}
}
线程下执行还是丢失,已经不是原来的线程,无法获取到threadLocal中的requestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
CompletableFuture.supplyAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> checkedItems = cartFeignService.getCheckedItems();
return checkedItems;
}, executor)
GateWay
官网:https://docs.spring.io/spring-cloud-gateway/docs/2.2.9.RELEASE/reference/html/
还要学习:https://blog.csdn.net/qq_38380025/article/details/102968559
路由转发、快速转发到后台服务上
- 统一的熔断、限流、认证、日志监控
- 与服务注册中心完美整合,Eureka、Consul、Nacos
springCloud GateWay特征
- 动态路由,匹配任何请求属性
- SpringCloud服务发现功能
- 路由指定Predicate(断言)、Filter(过滤器)
- 集成Hystrix断路器
- 请求限流功能
- 路径重写
过程:
接受请求,GateWay Handler Mapping找到匹配的路由,发送到Web Handler,通过指定的过滤器链将请求发送到实际的服务执行业务逻辑,过滤器会代理请求之前和请求之后业务逻辑
pre:参数校验、权限校验、流量监控、日志输出、协议转换
post:响应内容、响应头修改、日志输出、流量监控
依赖
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
<spring-cloud.version>Hoxton.SR9</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>com.ming.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Yaml配置
spring:
cloud:
gateway:
routes:
- id: test_route
uri: https://www.baidu.com
predicates:
- Query=url,baidu
# 1. 管理后台,通过路径转发
- id: admin_route
# 负载均衡
uri: lb://renren-fast
# 匹配路径
predicates:
- Path=/api/**
filters:
# 替换路径
- RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}
# 2. 首页,通过域名转发
- id: index
uri: lb://index
# 匹配路径
predicates:
- Host=**.mingyuefusu.cn
访问
predicate
Host
predicates:
- Host=**.somehost.org,**.anotherhost.org
Http Method
predicates:
- Method=GET
Path
# /foo/.* 或/bar/.*
predicates:
- Path=/foo/{segment},/bar/{segment}
获取segment
PathMatchInfo variables = exchange.getAttribute( URI_TEMPLATE_VARIABLES_ATTRIBUTE );
Map<String, String> uriVariables = variables.getUriVariables();
String segment = uriVariables.get("segment");
Query
# 查询参数foo的内容ba.正则规则
predicates:
- Query=foo, ba.
RemoteAddr
predicates:
- RemoteAddr=192.168.1.1/24
时间
# 时间之后匹配
predicates:
- After=2018-12-25T14:33:47.789+08:00
# 时间之前匹配
predicates:
- Before=2018-12-25T14:33:47.789+08:00
# 时间之间
- Between=2018-12-25T14:33:47.789+08:00, 2018-12-26T14:33:47.789+08:00
cookie
# 存在名为cookiename,内容匹配value的
predicates:
- Cookie=cookiename, cookievalue
Header
# 头部存在X-Request-Id,内容为数字的header的请求
predicates:
- Header=X-Request-Id, \d+
Filter
rewritePath
# 路径是/foo/bar,改为/bar
# YAML 的格式中使用$\来代替$。
filters:
- RewritePath=/foo/(?<segment>.*), /$\{segment}
header
filters:
- AddRequestHeader=X-Request-Foo, Bar
parameter
filters:
- AddRequestParameter=foo, bar
responseHeader
filters:
- AddResponseHeader=X-Response-Foo, Bar
熔断器
引入spring-cloud-starter-netflix-hystrix
依赖,并提供HystrixCommand
的名字,即可生效Hystrix GatewayFilter
filters:
- Hystrix=myCommandName
fallbackUri
,来支持路由熔断后的降级处理,降级后,请求会跳到fallbackUri
配置的路径
filters:
- name: Hystrix
args:
name: fallbackcmd
fallbackUri: forward:/incaseoffailureusethis
降级跳到外部的服务
spring:
cloud:
gateway:
routes:
- id: ingredients
uri: lb://ingredients
predicates:
- Path=//ingredients/**
filters:
- name: Hystrix
args:
name: fetchIngredients
fallbackUri: forward:/fallback
- id: ingredients-fallback
uri: http://localhost:9994
predicates:
- Path=/fallback
跨域问题
原本
@Configuration
public class CorsConfig implements WebMvcConfigurer {
// @Override
// public void addCorsMappings(CorsRegistry registry) {
// registry.addMapping("/**")
// .allowedOrigins("*")
// .allowCredentials(true)
// .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
// .maxAge(3600);
// }
}
网关中统一解决
注意与第一个不要重复,否则会重复响应头,如果失败,查看是否启动了两个服务?
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
@Configuration
public class GulimallCorsConfiguration {
@Bean
public CorsWebFilter corsWebFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 1、配置跨域
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.setAllowCredentials(true);
source.registerCorsConfiguration("/**", corsConfiguration);
return new CorsWebFilter(source);
}
}
Boot
JSR303
依赖
2.3之后默认移除了
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.17.Final</version>
<scope>compile</scope>
</dependency>
普通使用
实体类顺序添加@NotBlank等注解,提示默认为\org\hibernate\validator\ValidationMessages_zh_CN.properties中的提示语
@NotBlank(message = "")
@Pattern(regexp = "^[a-zA-Z]$", message = "检索首字母必须是一个字母")
@NotEmpty //不能为null或""
@NotBlank //不能为null,并且至少包含一个非空白字符
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@TableId
@NotNull(
message = "修改需要指定id",
groups = { UpdateGroup.class }
)
@Null(
message = "添加不能指定id",
groups = { AddGroup.class }
)
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "品牌名必须提交", groups = { AddGroup.class})
private String name;
/**
* 品牌logo地址
*/
@NotBlank(groups = {AddGroup.class})
@URL(message = "logo需要是合理的url地址", groups = {AddGroup.class, UpdateGroup.class}) // 修改带上了就要符合
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
@NotNull( groups = {AddGroup.class})
@ListValue(vals={0, 1}, groups = {AddGroup.class, UpdateGroup.class})
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty( groups = {AddGroup.class})
@Pattern(regexp = "^[a-zA-Z]$", message = "检索首字母必须是一个字母", groups = {AddGroup.class, UpdateGroup.class})
private String firstLetter;
/**
* 排序
*/
@NotNull( groups = {AddGroup.class})
@Min(value=0, message = "排序必须大于零", groups = {AddGroup.class, UpdateGroup.class})
private Integer sort;
}
接收数据前@Valid
,或者@Validated
进行指定分组
分组校验
使用分组校验时,没加分组的字段将不进行校验
public R update( @Validated({UpdateGroup.class} ) @RequestBody BrandEntity brand){}
接收异常
@RequestMapping("/update")
// @RequiresPermissions("product:brand:update")
public R update(@Valid @RequestBody BrandEntity brand, BindingResult result){
if(result.hasErrors()) {
Map<String, String> map = new HashMap<>();
result.getFieldErrors().forEach((item) -> {
map.put(
item.getField(),
item.getDefaultMessage()
);
});
return R.error(400, "数据不合法").put("data", map);
}
brandService.updateById(brand);
return R.ok();
}
规则汇总
javax.validation.constraints.AssertFalse.message = 只能为false
javax.validation.constraints.AssertTrue.message = 只能为true
javax.validation.constraints.DecimalMax.message = 必须小于或等于{value}
javax.validation.constraints.DecimalMin.message = 必须大于或等于{value}
javax.validation.constraints.Digits.message = 数字的值超出了允许范围(只允许在{integer}位整数和{fraction}位小数范围内)
javax.validation.constraints.Email.message = 不是一个合法的电子邮件地址
javax.validation.constraints.Future.message = 需要是一个将来的时间
javax.validation.constraints.FutureOrPresent.message = 需要是一个将来或现在的时间
javax.validation.constraints.Max.message = 最大不能超过{value}
javax.validation.constraints.Min.message = 最小不能小于{value}
javax.validation.constraints.Negative.message = 必须是负数
javax.validation.constraints.NegativeOrZero.message = 必须是负数或零
javax.validation.constraints.NotBlank.message = 不能为空
javax.validation.constraints.NotEmpty.message = 不能为空
javax.validation.constraints.NotNull.message = 不能为null
javax.validation.constraints.Null.message = 必须为null
javax.validation.constraints.Past.message = 需要是一个过去的时间
javax.validation.constraints.PastOrPresent.message = 需要是一个过去或现在的时间
javax.validation.constraints.Pattern.message = 需要匹配正则表达式"{regexp}"
javax.validation.constraints.Positive.message = 必须是正数
javax.validation.constraints.PositiveOrZero.message = 必须是正数或零
javax.validation.constraints.Size.message = 个数必须在{min}和{max}之间
org.hibernate.validator.constraints.CreditCardNumber.message = 不合法的信用卡号码
org.hibernate.validator.constraints.Currency.message = 不合法的货币 (必须是{value}其中之一)
org.hibernate.validator.constraints.EAN.message = 不合法的{type}条形码
org.hibernate.validator.constraints.Email.message = 不是一个合法的电子邮件地址
org.hibernate.validator.constraints.Length.message = 长度需要在{min}和{max}之间
org.hibernate.validator.constraints.CodePointLength.message = 长度需要在{min}和{max}之间
org.hibernate.validator.constraints.LuhnCheck.message = ${validatedValue}的校验码不合法, Luhn模10校验和不匹配
org.hibernate.validator.constraints.Mod10Check.message = ${validatedValue}的校验码不合法, 模10校验和不匹配
org.hibernate.validator.constraints.Mod11Check.message = ${validatedValue}的校验码不合法, 模11校验和不匹配
org.hibernate.validator.constraints.ModCheck.message = ${validatedValue}的校验码不合法, ${modType}校验和不匹配
org.hibernate.validator.constraints.NotBlank.message = 不能为空
org.hibernate.validator.constraints.NotEmpty.message = 不能为空
org.hibernate.validator.constraints.ParametersScriptAssert.message = 执行脚本表达式"{script}"没有返回期望结果
org.hibernate.validator.constraints.Range.message = 需要在{min}和{max}之间
org.hibernate.validator.constraints.SafeHtml.message = 可能有不安全的HTML内容
org.hibernate.validator.constraints.ScriptAssert.message = 执行脚本表达式"{script}"没有返回期望结果
org.hibernate.validator.constraints.URL.message = 需要是一个合法的URL
org.hibernate.validator.constraints.time.DurationMax.message = 必须小于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'}
org.hibernate.validator.constraints.time.DurationMin.message = 必须大于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'}
自定义校验规则
- 校验注解
- 校验器
- 关联以上两个
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
- 校验注解
@ListValue(vals={0, 1}, groups = {AddGroup.class, UpdateGroup.class})
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {ListValueConstraintValidator.class}) // 关联,将该注解交给校验器校验,多个,不用类型使用,连接
public @interface ListValue {
String message() default "{com.ming.common.valid.ListValue.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
int[] vals() default {};
}
- 校验器
public class ListValueConstraintValidator implements ConstraintValidator<ListValue, Integer> {
private Set<Integer> set = new HashSet<>();
@Override
public void initialize(ListValue constraintAnnotation) {
int[] vals = constraintAnnotation.vals();
for(int val : vals) {
set.add(val);
}
}
/**
*
* @param value 需要校验的值
* @param context
* @return
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return set.contains(value);
}
}
返回码
日志
@Slf4j
log.error("问题{},{}", e.getMessage(), e.getClass());
事务
配置类(mybatis)上@EnableTransactionManagement // 开启事务
方法上@Transactional
报错
Possibly consider using a shorter maxLifetime value
猜测也有可能是数据库的原因,没给对应的数据库配置
2021-02-03 23:48:40.633 WARN 9800 — [nio-8090-exec-4] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 - Failed to validate connection com.mysql.cj.jdbc.ConnectionImpl@3a6d1822 (No operations allowed after connection closed.). Possibly consider using a shorter maxLifetime value.
spring:
datasource:
username: root
password: mingyuefusu!
url: jdbc:mysql://106.75.103.68:3306/gulimall_pms?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
max-lifetime: 1800000
配置
日期返回
#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
日志开启
logging:
level:
com.ming.gulimall: debug
ElasticSearch
商品属性
思考
分开还是合并
- spu和spu一起存,100万*2KB = 2GB
- 分开存
- 搜索小米:粮食、手机、电器,4000个spu对应属性,4000*8=32000B=32KB,10000人一起查=320000KB=320MB
库存数量
只用存是否存在,不需要存具体数量,不然每次都要更新索引
{
"gulimall_product": {
"mappings": {
"properties": {
"attrs": {
"type": "nested",
"properties": {
"attrId": {
"type": "long"
},
"attrName": {
"type": "keyword"
},
"attrValue": {
"type": "keyword"
}
}
},
"brandId": {
"type": "long"
},
"brandImg": {
"type": "keyword"
},
"brandName": {
"type": "keyword"
},
"catalogId": {
"type": "long"
},
"catalogName": {
"type": "keyword"
},
"hasStock": {
"type": "boolean"
},
"hotScore": {
"type": "long"
},
"saleCount": {
"type": "long"
},
"skuId": {
"type": "long"
},
"skuImg": {
"type": "keyword"
},
"skuPrice": {
"type": "double"
},
"skuTitle": {
"type": "text",
"analyzer": "ik_smart"
},
"spuId": {
"type": "keyword"
}
}
}
}
}
PUT product
{
"mappings":{
"properties": {
"skuId":{
"type": "long"
},
"spuId":{
"type": "keyword"
},
"skuTitle": {
"type": "text",
"analyzer": "ik_smart"
},
"skuPrice": {
"type": "double"
},
"skuImg":{
"type": "keyword",
"index": false,
"doc_values": false
},
"saleCount":{
"type":"long"
},
"hasStock": {
"type": "boolean"
},
"hotScore": {
"type": "long"
},
"brandId": {
"type": "long"
},
"catalogId": {
"type": "long"
},
"brandName": {
"type": "keyword",
"index": false,
"doc_values": false
},
"brandImg":{
"type": "keyword",
"index": false,
"doc_values": false
},
"catalogName": {
"type": "keyword",
"index": false,
"doc_values": false
},
"attrs": {
"type": "nested",
"properties": {
"attrId": {
"type": "long"
},
"attrName": {
"type": "keyword",
"index": false,
"doc_values": false
},
"attrValue": {
"type": "keyword"
}
}
}
}
}
}
压力测试
概念
压力测试考察当前软硬件环境下系统所能承受的最大负荷并帮助找岀系统瓶颈所在。
压测都是为了系统在线上的处理能力和稳定性维持在一个标准范围内,做到心中有数。
使用压力测试,我们有希望找到很多种用其他测试方法更难发现的错误。有两种错误类型是内存泄漏,并发与同步。
有效的压力测试系统将应用以下这些关键条件:重复,并发,量级,随机变化。
概念
- 响应时间RT:发请求开始,得到响应的整个时间
- HPS(Hit per Second):每秒点击次数
- TPS(Transaction per Second):每秒交易交易数
- QPS(Query per Second):每秒查询处理次数
金融:1000TPS~5000TPS
保险:100TPS~100000TPS
制造:10TPS~5000TPS
互联网电子商务:10000~1000000TPS
中型网站:1000~50000TPS
小型:500~10000TPS
最大响应时间:发出到响应最大时间
最少响应时间:发出的响应的最少时间
90%响应时间:响应时间排序,第90%名的响应时间
主要关注:
- 吞吐量:每秒系统能处理的请求数,任务数
- 响应时间:服务处理一个请求,一个任务的耗时
- 错误率:一批请求结果出错的比例
影响性能的考虑点:
数据库、应用程序、中间件(tomcat、Nginx)、网络和操作系统等
是CPU密集型还是IO密集型
Jmeter
解压运行bin/jmeter.bat
使用
修改语言
新建线程
线程数、线程启动时间s,循环次数 /finite永远
高级设置:从html获取包含的资源,并行线程6
取样器
监听器
监听结果数(每一次的结果)、汇总报告(异常%,吞吐量)、聚合报告(平均响应时间、90%、最大最小)
JVM
调整堆,对象实例和数组都在堆分配,垃圾回收的朱啊哟区域,GC堆
堆:
新生代
- Eden
- From Survivor(与To来回交换)
- To Survivor
老年代
永久代(java8之前),java8后元空间,直接操作物理内存
过程
创建对象,进入新生代,看是否放得下,放不下就yGc(将eden的对象放到survivor,放不下或者存活了15岁就放到Old)中,如果还不行就放到老年代,老年代放不下,就Full GC(性能慢10倍),将新老都清理,还不行就内存溢出
Jconsole
cmd jconsole,连接对应的应用
jvisualvm
jvisualvm(9后已经移除)监控,jconsole直接运行
下载:https://visualvm.github.io/download.html
运行
修改conf 添加 visualvm_jdkhome=”E:\eclipse”,运行即可
作用:监控内存泄露,跟踪垃圾回收,执行时内存、cpu分析,线程分析
状态
运行:正在运行的
休眠: sleep
等待:wait
驻留:线程池里面的空闲线程
监视:等待锁
安装visual GC插件方便查看gc、安装插件可能存在更新问题,设置修改对应的版本,java -version,根据版本号https://visualvm.github.io/pluginscenters.html在里面换对应的地址,如下:
主要看Monitor、visual GC
可以用 -Xmx1024m -Xms1024m -Xmn512m
增大内存
-Xmx最大 -Xms最小 -Xmn 新生代+幸存者区
-Xmx100m
优化步骤
- 页面缓存(thymeleaf)
- 增加内存(vm optiion: -Xmx512m)
- 业务优化(数据库查找、加普通索引)
- 关闭日志输出
- 动静分离
- 去掉中间层(nginx+gateway多层会变慢)
测试结果
中间件越多,性能损失越大,网络交互
压测内容 | 压测线程数 | 吞吐量/s | 90%响应时间 | 99%响应时间 |
---|---|---|---|---|
Nginx | 50 | 3606 | 19 | 146 |
Gateway | 50 | 5751 | 18 | 38 |
简单服务 | 50 | 10000 | 5 | 15 |
首页一级菜单渲染 | 50 | 350(db,thymeleaf) | 270 | 540 |
首页渲染(开缓存) | 50 | 400 | 185 | 350 |
首页渲染(开缓存,优化数据库,关日志) | 50 | 750 | 157 | 333 |
三级分类数据获取 | 50 | 3(db)/ 8(加索引) | … | … |
三级分类(优化业务) | 50 | 94(db)/ 8(加索引) | 1000 | 1600 |
首页全量数据获取 | 50 | 12(静态资源) | ||
Nginx+Gateway | 50 | |||
Gateway+简单服务 | 50 | 2340 | 44 | 64 |
全链路 | 50 | 600 | 129 | 395 |
error
socket close: 手动关闭
Address already in use: connect
原因: windows提供TCP/IP端口为1024-50000,十分钟循环回收,短时间内大量请求将端口占满
1. 打开注册表:regedit
2. HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\TCP\Parameters
3. 新建 DWORD值,name:TcpTimedWaitDelay,value:30(十进制) –> 设置为30秒,默认是240
4. 新建 DWORD值,name:MaxUserPort,value:65534(十进制) –> 设置最大连接数65534
5. 重启系统
Redis
为了系统性能的提升,我们一般都会将部分数据放入缓存中,加入访问,而db承担数据持久化工作
适合放入缓存的数据
- 对及时性、数据一致性要求不高的
- 访问量大且更新频率不高的数据
1) 使用hashmap本地缓存
缺点:重启就没了,分布式下存在多份
//测试本地缓存,通过hashmap
private Map<String,Object> cache = new HashMap<>();
public Map<String, List<Catalog2Vo>> getCategoryMap() {
Map<String, List<Catalog2Vo>> catalogMap =
(Map<String, List<Catalog2Vo>>) cache.get("catalogMap");
//如果没有缓存,则从数据库中查询并放入缓存中
if (catalogMap == null) {
catalogMap = getCategoriesDb();
cache.put("catalogMap",catalogMap);
}
return catalogMap;
}
初始化
内存泄露在2.3版本修复
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置
spring:
redis:
host: #
port: 6379
password:
简单使用
@Autowired
StringRedisTemplate stringRedisTemplate;
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
String catalogJson = ops.get("catalogJson", catalogJson, 1, TimeUnit.DAYS);
// json序列化
ops.set("catalogJson", JSON.toJSONString(categoriesDb););
// 反序列化
Map<String, List<Catalog2Vo>> listMap = JSON.parseObject(
catalogJson,
new TypeReference< Map< String, List<Catalog2Vo> > >() {}
);
OutOfDirectMemoryError
当进行压力测试时后期后出现堆外内存溢出OutOfDirectMemoryError,堆外内存溢出
产生原因:
1)、springboot2.0以后默认使用 lettuce 操作redis的客户端,5.2没事,它使用 netty 进行网络通信
2)、lettuce的bug导致netty堆外内存溢出,默认-Xmx300m
解决方案:由于是lettuce的bug造成,可以使用-Dio.netty.maxDirectMemory去调大虚拟机堆外内存,但是还是会出现
1)、升级lettuce客户端,引入5.2.*,5.2.0.RELEASE
2)、切换使用jedis,去掉lettuce-core
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
基本操作
// RedisTemplate、StringRedisTemplate
// 1.opsFor
StringRedisTemplate redisTemplate;
redisTemplate.opsForValue();
get().set();
opsForHash();
opsForList();
opsForSet();
opsForZSet();
opsForGeo();
opsForHyperLogLog();
// 2.bound直接操作 举例,这是操作Hash类型数据的,也就是键key对应的value值是
BoundHashOperations<String, Object, Object> operation = redisTemplate.boundHashOps(cartKey);
// fastJSON
Map<String, List<Catalog2Vo>> categoriesDb;
// 序列化
String toJSONString = JSON.toJSONString(categoriesDb);
// 反序列化
Map<String, List<Catalog2Vo>> listMap = JSON.parseObject(
catalogJson,
new TypeReference< Map<String, List<Catalog2Vo>> >() {}
);
缓存穿透
指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义
风险:
利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃
解决:
null结果缓存,并加入短暂过期时间
缓存雪崩
缓存雪崩是指在我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
解决:
原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
缓存击穿与最佳逻辑
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。
如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。
解决:
加锁。大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去db
// 本地锁:最佳逻辑,虽然分布式下每个服务都会查询DB
if( !getCache() ) { // 没有缓存
synchronized( this ) { // 加锁查数据库
if( !getCache() ) { // 再次确认没有缓存,避免释放锁后,还未来得及放入缓存,多次查询数据库
getDb();
setCache();
}
return
}
}
return
// 0、通常, 存在缓存击穿问题
getCache();
if( !getCache() ) { // 没缓存
getDb();
setCache();
}
return data;
// 1、本地加锁
getCache();
if( !getCache() ) { // 没缓存
synchronized( this ) { // 同时都在排队等待查询数据库,存在问题
getDb();
}
setCache();
}
// 1.1 加锁、查询前再进行一次cache查找
getCache();
if( !getCache() ) { // 没缓存
synchronized( this ) { // 同时都在排队等待查询数据库
if( !getCache() ) { // 每次只进来一个,所以理论上只会查询一次数据库,实际上可能存在cache未存入就进来的情况
getDb();
setCache();
return ;
}
}
}
return ;
// springboot 中所有组件在容器中都是单例的
synchronized( this ) { // 单体有效,分布式无效,还会有多个线程进入查询数据库
// 得到锁应该去缓存中确定一次,因为没有得到锁的 还在是排队查数据库
}
// 这种情况下可能存在释放锁还没放缓存前多次查数据库
private synchronized Map<String, List<Catalog2Vo>> getCategoriesDb() {
return listMap;
}
分布式下idea:program arguments: --server.port=10001
setnx
分布式锁
当有多个服务存在时,每个服务的缓存仅能够为本服务使用,这样每个服务都要查询一次数据库,并且当数据更新时只会更新单个服务的缓存数据,就会造成数据不一致的问题,所有的服务都到同一个redis进行获取数据,就可以避免这个问题
setIfAbsent(“lock-name”, “value”, 5, TimeUnit.SECONDS)
stringRedisTemplate.opsForValue().setIfAbsent("lock", "1111",5, TimeUnit.SECONDS);
// 确保redis无数据,从数据库查询数据并保存到redis
public Map<String, List<Catalog2Vo>> getCategoryMap() {
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
String catalogJson = ops.get("catalogJson");
if (StringUtils.isEmpty(catalogJson)) {
System.out.println("缓存不命中,准备查询数据库。。。");
Map<String, List<Catalog2Vo>> categoriesDb= getCategoriesDb();
String toJSONString = JSON.toJSONString(categoriesDb);
ops.set("catalogJson", toJSONString);
return categoriesDb;
}
System.out.println("缓存命中。。。。");
Map<String, List<Catalog2Vo>> listMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() {});
return listMap;
}
// 分布式锁情况下,查询数据
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {
// 1.
// Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");
// 2. set lock 1111 EX 300 NX 设置锁的同时设置过期时间
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "1111",5, TimeUnit.SECONDS);
if (lock) {
// 1.设置过期时间,但是如果断电就死锁
// stringRedisTemplate.expire("lock", 30, TimeUnit.SECONDS);
Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();
stringRedisTemplate.delete("lock");
return categoriesDb;
}else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJsonDbWithRedisLock();
}
}
删除锁直接删除???
如果由于业务时间很长,锁自己过期了,我们直接删除,有可能把别人正在持有的锁删除了。
解决:
占锁的时候,值指定为uuid,每个人匹配是自己的锁才删除。
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {
String uuid = UUID.randomUUID().toString();
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
// 为当前锁设置唯一的uuid,只有当uuid相同时才会进行删除锁的操作
Boolean lock = ops.setIfAbsent("lock", uuid,5, TimeUnit.SECONDS);
if (lock) {
Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();
String lockValue = ops.get("lock");
if (lockValue.equals(uuid)) { // 注意:存在问题,如果传输回来的过程中过期了就还会删完
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
stringRedisTemplate.delete("lock");
}
return categoriesDb;
}else {
}
如果传输回来的过程中过期了就还会删完,因为查找和删除不是原子操作
解决:
删除锁必须保证原子性。使用redis+Lua脚本完成
最佳实践
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {
String uuid = UUID.randomUUID().toString();
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
Boolean lock = ops.setIfAbsent("lock", uuid, 50, TimeUnit.SECONDS);
if (lock) {
Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();
try{
getDataFromDb();
} finally {
// 避免自己的锁过期了,误删别人的锁
// 如果使用java,判断是自己的没过期,但是删除的时候已经过期了,还会删除别人的,所以查询和删除要原子性
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
stringRedisTemplate.execute(
new DefaultRedisScript<Long>( // 返回值类型
script,
Long.class
),
Arrays.asList("lock"), // keys[1]
uuid // argv[1]
);
}
return categoriesDb;
}else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJsonDbWithRedisLock(); // 注意;递归容易栈溢出
}
}
保证加锁【占位+过期时间】和删除锁【判断+删除】的原子性。更难的事情,锁的自动续期
Redssion
Redisson
与GUC类似
简介
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet
, Set
, Multimap
, SortedSet
, Map
, List
, Queue
, BlockingQueue
, Deque
, BlockingDeque
, Semaphore
, Lock
, AtomicLong
, CountDownLatch
, Publish / Subscribe
, Bloom filter
, Remote service
, Spring cache
, Executor service
, Live Object service
, Scheduler service
)
Redisson提供了使用Redis的最简单和最便捷的方法。
Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
本文我们仅关注分布式锁的实现,更多请参考官方文档
可重入锁
(Reentrant Lock),A方法中调用B方法,A、B都要抢同一个锁,这个锁必须是同重入锁
RLock lock = redissonClient.getLock("CatalogJson-Lock");
lock.lock();
lock.unlock();
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisson() {
Map<String, List<Catalog2Vo>> categoryMap=null;
RLock lock = redissonClient.getLock("CatalogJson-Lock");
// lock.lock(10, TimeUnit.SECONDS); // 没有自动续期,一定要大于业务的时间,并且unlock可能无法删除
// 1. 如果传递超时时间,就给redis发送执行脚本,进行占锁,默认超时就是我们的时间
// 2. 未指定时间,默认使用30秒时间,占锁成功启动定时任务(重新给锁设定行的过期时间),【看门狗时间】 / 3 一次续期,续成满时间
lock.lock();
// boolean res = lock.tryLock(100, 10, TimeUnit.SECODS); // 最多等待100秒,上锁10秒后自动解锁
//if(res)
try {
Thread.sleep(30000);
categoryMap = getCategoryMap();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 默认30秒过期,业务时间长,会检查服务是否运行,自动续时
return categoryMap;
}
}
公平锁
getFairLock(“”)
读写锁
(ReadWriteLock),写锁会阻塞读锁,但是读锁不会阻塞读锁,但读锁会阻塞写锁
可多读、但仅一写且无读
RReadWriteLock lock = redissonClient.getReadWriteLock("ReadWrite-Lock");
lock.readLock().lock();
lock.writeLock().lock();
@GetMapping("/read")
@ResponseBody
public String read() {
RReadWriteLock lock = redissonClient.getReadWriteLock("ReadWrite-Lock");
RLock rLock = lock.readLock();
String s = "";
try {
rLock.lock();
System.out.println("读锁加锁"+Thread.currentThread().getId());
Thread.sleep(5000);
s= redisTemplate.opsForValue().get("lock-value");
}finally {
rLock.unlock();
return "读取完成:"+s;
}
}
@GetMapping("/write")
@ResponseBody
public String write() {
RReadWriteLock lock = redissonClient.getReadWriteLock("ReadWrite-Lock");
RLock wLock = lock.writeLock();
String s = UUID.randomUUID().toString();
try {
wLock.lock();
System.out.println("写锁加锁"+Thread.currentThread().getId());
Thread.sleep(10000);
redisTemplate.opsForValue().set("lock-value",s);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
wLock.unlock();
return "写入完成:"+s;
}
}
信号量
Semaphore
信号量为存储在redis中的一个数字,当这个数字大于0时,即可以调用acquire()
方法增加数量,也可以调用release()
方法减少数量,但是当调用release()
之后小于0的话方法就会阻塞,直到数字大于0
RSemaphore park = redissonClient.getSemaphore("park");
park.acquire(2);
park.release(2);
boolean flag = park.tryAcquire(2); // 尝试获取
闭锁
(CountDownLatch)
可以理解为门栓,使用若干个门栓将当前方法阻塞,只有当全部门栓都被放开时,当前方法才能继续执行。
以下代码只有offLatch()
被调用5次后 setLatch()
才能继续执行
RCountDownLatch latch = redissonClient.getCountDownLatch("CountDownLatch");
latch.trySetCount(5); // 5个都有
latch.await(); // 进行等待
latch.countDown(); // 释放
加锁最好细粒度,名字为product-11-lock
分布式锁,微服务架构下
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.56.102:6379");
RedissonClient redisson = Redisson.create(config);
return redisson;
}
}
依赖、配置
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
@Configuration
public class RedissonConfig {
@Bean(destroyMethod="shutdown")
RedissonClient redisson() throws IOException {
Config config = new Config();
// Redis url should start with redis:// or rediss://(ssl)
config.useClusterServers()
.addNodeAddress("redis:127.0.0.1:6379"");
return Redisson.create(config);
}
}
命名
细粒度锁:
product-id-lock
基本方法
RLock lock = redission.getLock("lockname");
lock.lock(); // 阻塞,能够自动续期,默认30秒,启动定时任务,每10s续期
// 推荐,省去续期
lock.lock(10, TimeUnit.SECONDS); // 自动解锁,要保证业务时间在范围内,否则可能解开别人的锁
try {} catch() {} finally{ }
lock.unLock();
// 尝试加锁,最多等待100s,10秒后解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
// 公平
redission.getFairLock("");
// 读写锁
rwLock = redission.getReadWriteLock();
rLock = rwLock.readLock();
wLock = rwLock.writeLock();
lock.lock();
// 信号量
RSemaphore semaphore = redission.getSemaphore("lock");
semaphore.acquire();
semaphore.tryAcquire(); // 限流
semaphore.release();
// 闭锁,所有都做完,班级走完才关大门
RCountDownLatch latch = redission.getCountDownLatch("lock");
latch.trySetCount(5); // 有5个班级
latch.await(); // 等待走完
RCountDownLatch latch = redission.getCountDownLatch("lock");
latch.countDown(); // 走了一个班级
缓存数据的一致性
双写模式@CachePut
简介:当数据更新时,更新数据库时同时更新缓存
存在问题
脏数据,A写db、redis中间包裹另B写db、redis
这是暂时性的脏数据问题,但是在数据稳定,缓存过期以后,又能得到最新的正确数据
失效模式@CacheEvict
简介:数据库更新时将缓存删除
存在问题,脏数据,最终一致
当两个请求同时修改数据库,一个请求已经更新成功并删除缓存时又有读数据的请求进来,这时候发现缓存中无数据就去数据库中查询并放入缓存,在放入缓存前第二个更新数据库的请求成功,这时候留在缓存中的数据依然是第一次数据更新的数据
解决方案
无论是双写模式还是失效模式,都会导致缓存的不一致问题。即多个实例同时更新会出事。怎么办?
- 如果是用户纬度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可
- 如果是菜单,商品介绍等基础数据,也可以去使用canal(大数据量)订阅binlog的方式。
- 缓存数据+过期时间也足够解决大部分业务对于缓存的要求。
- 通过加锁保证并发读写, 写写的时候按顺序排好队。读读无所谓。所以适合使用读写锁。(业务不关心脏数据,允许临时脏数据可忽略);
总结:
- 我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可。
- 我们不应该过度设计,增加系统的复杂性
- 遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。
- 当个用户数据并发几率小,过期时间自动过期即可
- 菜单、商品介绍基础数据(允许不一致性),可以使用canal订阅binlog方式。
- 缓存加过期时间满足大部分业务
- 读写锁保证并发读写
canal
- 更新缓存(cannal订阅binlog,更新redis)
- 数据异构,聚合生成新的数据(推荐商品)
SpringCache
实现:AOP,默认无加锁,sync=true整个过程加本地锁
存在缓存击穿
cacheManager用来定义规则,cache的遵守的规则
初始化
原理:
CacheAutoConfiguration -> RedisCacheConfiguration -> 自动配置了RedisCacheManager - > 初始化所有缓存 -> 每个缓存决定使用什么配置
-> 如果RedisCacheConfiguration有就用已有的,没有就用默认配置
1) 导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
2) 自定义配置
指定缓存类型并在主配置类上加上注解@EnableCaching
spring:
cache:
#指定缓存类型为redis
type: redis
redis:
# 指定redis中的过期时间为1h
time-to-live: 3600000
# 一般不定义,让分区名就是缓存的前缀,这样可以统一操作分区
# key-prefix: CACHE_SWZL
# 是否使用前缀
use-key-prefix: true
# 是否存空值
cache-null-values: true
默认使用jdk进行序列化,默认缓存名字:simplyKey [],默认使用jdk序列化机制,保存到redis,ttl -1,默认永久,自定义序列化方式需要编写配置类
- 指定key
- 序列化为JSON
- 配置过期时间
@EnableConfigurationProperties(CacheProperties.class) // 导入原来的配置属性
@Configuration
@EnableCaching
public class MyCacheConfig {
@Bean
public org.springframework.data.redis.cache.RedisCacheConfiguration redisCacheConfiguration(
CacheProperties cacheProperties) {
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
.defaultCacheConfig();
// 指定缓存序列化方式为json
config = config.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer( new GenericJackson2JsonRedisSerializer() )
);
// 设置配置文件中的各项配置,如过期时间
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixKeysWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}
spel表达式
缓存注解
Cacheable 添加
@Cacheable(value = {"category"}, key = "'cacheKey'") // ''使用字符串
根据方法对其返回结果进行缓存,下次请求时,如果缓存存在,则直接读取缓存数据返回;如果缓存不存在,则执行方法,并把返回的结果存入缓存中。一般用在查询方法上。
查看源码,属性值如下:
属性/方法名 | 解释 |
---|---|
value | 缓存名,必填,它指定了你的缓存存放在哪块命名空间 |
cacheNames | 与 value 差不多,二选一即可 |
key | 可选属性,可以使用 SpEL 标签自定义缓存的key |
sync | true,使用锁 |
CachePut 修改
使用该注解标志的方法,每次都会执行,并将结果存入指定的缓存中。其他方法可以直接从响应的缓存中读取缓存数据,而不需要再去查询数据库。一般用在新增方法上。
查看源码,属性值如下:
属性/方法名 | 解释 |
---|---|
value | 缓存名,必填,它指定了你的缓存存放在哪块命名空间 |
cacheNames | 与 value 差不多,二选一即可 |
key | 可选属性,可以使用 SpEL 标签自定义缓存的key |
CacheEvict 删除
@CacheEvict(value = "category", key = "'methodName'")
@Cacheable(value = {"category"}, key = "#root.method.name")
@CacheEvict(value = "category", allEntries = true) // 删除所有category部分
使用该注解标志的方法,会清空指定的缓存。一般用在更新或者删除方法上
查看源码,属性值如下:
属性/方法名 | 解释 |
---|---|
value | 缓存名,必填,它指定了你的缓存存放在哪块命名空间 |
cacheNames | 与 value 差不多,二选一即可 |
key | 可选属性,可以使用 SpEL 标签自定义缓存的key |
allEntries | 是否清空所有缓存,默认为 false。如果指定为 true,则方法调用后将立即清空value下的所有的缓存 |
beforeInvocation | 是否在方法执行前就清空,默认为 false。如果指定为 true,则在方法执行前就会清空缓存 |
Caching 多操作
进行多种缓存操作
@Caching(evict = {
@CacheEvict( value = "category", key = "'getLevel2'"),
@CacheEvict( value = "category", key = "'getLevel1'" )
})
CacheConfig
不足
常规数据(读多写少,及时性、一致性不高的数据完全可以用spring-cache
读模式
缓存穿透: 查询null数据,解决:spring.cache.redis.cache-null-values=true
缓存击穿:缓存过期后,大量请求突然涌入,解决:加synchronized锁(sync=true,貌似是锁对象),默认无加锁
缓存雪崩:缓存集体失效,解决:加随机时间
写模式
缓存与数据库一致
读写加锁:适合多读少写
引入Canal,感知MySQL的更新去更新
读多写多,直接查DB
RabbitMQ
学习文档:https://niceseason.github.io/2020/04/18/springboot/#%E4%BA%8C%E3%80%81RabbitMQ
官网:https://www.rabbitmq.com/documentation.html
异步、流量控制(消峰)、解耦
RabbitMQ是一个由erlang开发的AMQP(Advanved Message Queue Protocol)的开源实现
rocketmq开源版本功能不够完善
AMQP、rabbitMq高性能、高并发、高可用、数据处理量不错、开源、多语言
kafka性能高、但是可能会重发
下载
官网:
https://github.com/erlang/otp/releases?page=3
https://www.rabbitmq.com/install-windows.html
软件
https://github.com/erlang/otp/releases/download/OTP-23.3.4.14/otp_win64_23.3.4.14.exe
https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.9.19/rabbitmq-server-3.9.19.exe
(太新的erlang也要更新)
安装目录下 E:\environment\rabbitmq\rabbitmq_server-3.9.19\sbin
port: 15672
rabbitmqctl start_app
rabbitmqctl stop
# 可视化插件
rabbitmq-plugins enable rabbitmq_management
guest/guest
# 使用命令添加用户并授权
# 添加用户
rabbitmqctl add_user admin admin
# 设置permissions
rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
# 设置用户角色
rabbitmqctl set_user_tags admin administrator
# 查看新添加的admin
rabbitmqctl list_users
# 查看用于的权限
rabbitmqctl list_permissions -p /
核心概念
- Message
- 消息,消息是不具名的,它由消息头和消息体组成
- 消息头,包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等
- Publisher
- 消息的生产者,也是一个向交换器发布消息的客户端应用程序
- Exchange
- 交换器,将生产者消息路由给服务器中的队列
- 类型有direct(默认),fanout, topic, 和headers,具有不同转发策略
- Queue
- 消息队列,保存消息直到发送给消费者
- Binding
- 绑定,用于消息队列和交换器之间的关联
- Connection
- 网络连接,比如一个TCP连接
- Consumer
- 消息的消费者,表示一个从消息队列中取得消息的客户端应用程序
- Virtual Host
- 虚拟主机,表示一批交换器、消息队列和相关对象。
- vhost 是 AMQP 概念的基础,必须在连接时指定
- RabbitMQ 默认的 vhost 是 /
- Broker
- 消息队列服务器实体
- channel
- 建立一条连接,有很多通道
运行机制
消息路由
AMQP 中增加了Exchange 和 Binding 的角色, Binding 决定交换器的消息应该发送到那个队列
Exchange 类型
direct
点对点模式,消息中的路由键(routing key)如果和 Binding 中的 binding
key 一致, 交换器就将消息发到对应的队列中。fanout
广播模式,每个发到 fanout 类型交换器的消息都会分到所有绑定的队列上去
topic
将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。它将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开。
识别通配符: # 匹配 0 个或多个单词, *匹配一个单词
初始化
amqp:5672、5671
http,web端口:15672
clustering:25672
4369,25672:erlang发现&集群端口
1883,8883:MQTT协议端口
# for RabbitMQ 3.9, the latest series
docker run \
--ip 192.168.0.9 --net mynet \
-it --rm -d \
--name rabbitmq \
-p 5672:5672 \
-p 15672:15672 rabbitmq:3.9-management
docker run \
-it --rm -d \
--name rabbitmq \
-p 5672:5672 \
-p 15672:15672 rabbitmq:3.9-management
# for RabbitMQ 3.8,
# https://www.rabbitmq.com/versions.html
docker run -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3.8-management
docker update rabbitmq --restart=always
队列模式
https://www.rabbitmq.com/getstarted.html
这里写的太烂了
简单队列
点对点,交换机对应一个队列(一个路由)
简单
生产的很多,可能内存溢出
send
public class Send {
private final static String QUEUE_NAME = "hello";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("101.227.11.219");
factory.setUsername("root");
factory.setPassword("");
factory.setVirtualHost("/ming");
factory.setPort(5672);
try (
Connection connection = factory.newConnection();
Channel channel = connection.createChannel()
) {
channel.queueDeclare(
QUEUE_NAME, // 名称
false, // 持久化
false, // 排他队列,基于连接下,可以访问,不可重名,关闭连接,也会删除
/*
1.基于连接可见,同一连接下的不同通道可以访问同一连接下的其他排它队列
2.基于连接创建,连接下排它队列不可同名
3.即使是持久化的,连接关闭则排它队列删除
只限于一个客户端发送读取消息场景
*/
false, // 自动删除,如果没有消费者则删除
null
);
String message = "Hello World!";
// 交换机、路由、是否持久化、消息内容
channel.basicPublish(
"",
QUEUE_NAME,
null,
message.getBytes(StandardCharsets.UTF_8)
);
System.out.println(" [x] Sent '" + message + "'");
}
}
}
recv
public class Recv {
private final static String QUEUE_NAME = "hello";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("101.227.11.219");
factory.setUsername("root");
factory.setPassword("");
factory.setVirtualHost("/ming");
factory.setPort(5672);
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(
QUEUE_NAME,
false,
false,
false,
null
);
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
};
channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
}
}
轮训
启动多个消费者,做到消费一个队列中的消息
一人接收一个
确定:消费者的能力不一样
// 处理完才接受下一条
int prefetchCount = 1;
channel.basicQos(prefetchCount);
public static Connection getConnection() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("101.227.11.219");
factory.setUsername("root");
factory.setPassword("mingyuefusu!");
factory.setVirtualHost("/ming");
factory.setPort(5672);
return factory.newConnection();
}
recv
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
channel.basicAck(
delivery.getEnvelope().getDeliveryTag(),
false // false一条条确认消息
);
};
channel.basicConsume(
QUEUE_NAME,
true, // 手动确认收到消息
deliverCallback,
consumerTag -> { }
);
发布订阅/广播 fanout
消息到达一个交换机后,会给绑定的所有队列都发送一样的消息
多个消费者收到同一条消息
send
try (
Connection connection = RabbitMqUtils.getConnection();
Channel channel = connection.createChannel();
) {
// 声明广播的交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
String message = "Hello World!";
// 只需要放交换机即可
channel.basicPublish(
EXCHANGE_NAME,
"",
null,
message.getBytes(StandardCharsets.UTF_8)
);
System.out.println(" [x] Sent '" + message);
}
recv
// 声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
// 获取广播交换机中的队列
String queue = channel.queueDeclare().getQueue();
// 绑定队列和交换机
channel.queueBind(queue, EXCHANGE_NAME, "");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
};
channel.basicConsume(
queue,// 监听的还是队列
true, // 自动回复
deliverCallback,
consumerTag -> { }
);
路由模式 direct
会员才能看到会员专属的信息
缺点:路由key太多难以管理
send
try (
Connection connection = RabbitMqUtils.getConnection();
Channel channel = connection.createChannel();
) {
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
String[] argv = {"info", "error", "warn", "debug"};
for (String severity : argv) {
String message = "my " + severity;
// 交换机、路由、是否持久化、消息内容
channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + severity + "':'" + message + "'");
}
}
recv
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
String queueName = channel.queueDeclare().getQueue();
String[] argv = {"info", "error", "warn", "debug"};
for (String severity : argv) {
channel.queueBind(queueName, EXCHANGE_NAME, severity);
}
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
主题模式
*唯一单词,#0或多个
发送详细,接受通配
匹配不到默认丢弃
send
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
String routingKey = getRouting(argv);
String message = getMessage(argv);
// 交换机 路由 持久化 内容
channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
recv
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
// 为什么能获取队列名?如果这个交换机有多个队列呢? 但是这里好像是先有的接收
String queueName = channel.queueDeclare().getQueue();
String[] argv = {"com.#"};
for (String bindingKey : argv) {
channel.queueBind(queueName, EXCHANGE_NAME, bindingKey);
}
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
});
RPC
客户端和服务端同步
官网:https://www.rabbitmq.com/tutorials/tutorial-six-java.html
client请求携带reply_to(队列名)和correlation_id发送至队列
server消费队列后,使用reply_to将消息发送至队列,client根据correlation_id取出
confirm
生产者的消息不能知道消息是否到达了队列,可以使用事务
channel.txSelect();
channel.txRollback();
- 普通confirm(同步)
每发送一条消息,调用waitForConfirms()方法,等待服务器端confirm,实际上是一种串行confirm
- 批量confirm(同步)
每发送一条消息后,waitForConfirmOrDie(),等待服务器端confirm
- 异步confirm
提供回调方法,服务端confirm一条或多条消息后,client端会回调这个方法
channel.confirmSelect();
channel.waitForConfirms(); // 普通confirm
waitForConfirmOrDie(); // 批量模式,只要有一条没确认就抛异常
send
public class Send {
private static final String QUEUE_NAME = "async";
public static void main(String[] temp) throws Exception {
Connection connection = RabbitMqUtils.getConnection();
Channel channel = connection.createChannel();
try {
final SortedSet<Long> confirmSet = Collections.synchronizedSortedSet(new TreeSet<Long>());
// 开启确认模式
channel.confirmSelect();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
if( multiple ) {
System.out.println("multiple" + deliveryTag);
// 删除 deliveryTag 项标识id ?
confirmSet.headSet(deliveryTag + 1L).clear();
} else {
System.out.println("single" + deliveryTag);
confirmSet.headSet(deliveryTag).clear();
}
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
if(multiple) {
System.out.println("failed-multiple->" +deliveryTag);
confirmSet.headSet(deliveryTag +1L).clear();
} else {
System.out.println("failed-single->" +deliveryTag);
confirmSet.remove(deliveryTag);
}
}
});
while(true) {
String message = "hello world";
Long seqNo = channel.getNextPublishSeqNo();
channel.basicPublish("", QUEUE_NAME, null, message.getBytes("utf-8"));
confirmSet.add(seqNo);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if(null != channel && channel.isOpen()) {
channel.close();
}
if(null != channel && connection.isOpen()) {
connection.close();
}
}
}
//..
}
recv
public static void main(String[] argv) throws Exception {
Connection connection = RabbitMqUtils.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(
QUEUE_NAME,
false,
false,
false,
null
);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
};
channel.basicConsume(
QUEUE_NAME,
true, // 自动回复
deliverCallback,
consumerTag -> { }
);
}
SpringAMQP
依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>2.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
</dependencies>
配置
开启使用
@EnableRabbit
配合文件
spring:
# 1 broker接受回调,队列错误回调,消费端接受回调
rabbitmq:
host: 101.200.169.229
port: 5672
username: guest
password: guest
virtual-host: /
publisher-confirm-type: correlated
publisher-returns: true
# 抵达队列,异步方式回调
template:
mandatory: true
listener:
simple:
acknowledge-mode: manual
# 2
rabbitmq:
host: 101.227.11.219
port: 5672
username: root
password:
virtual-host: /ming
server:
port: 8081
@Autowired
AmqpAdmin amqpAdmin;
序列化方式
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyRabbitConfig {
// 序列化方式,传输Object
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
}
简单API
@SpringBootTest
@Slf4j
public class Test {
@Autowired
private AmqpAdmin amqpAdmin;
@Autowired
private RabbitTemplate rabbitTemplate;
// 交换机、队列、绑定
@org.junit.jupiter.api.Test
public void test() {
// exchange: durable autoDelete
DirectExchange hello = new DirectExchange("hello-exchange", true, false);
amqpAdmin.declareExchange(hello);
log.info("{} finish", "hello");
// queue: durable exclusive autoDelete
Queue queue = new Queue("hello-queue", true, false, false);
amqpAdmin.declareQueue(queue);
// binding
/* String destination, 目的地
Binding.DestinationType destinationType, 类型: 交换机、队列
String exchange, 交换机
String routingKey, 路由键
@Nullable Map<String, Object> arguments
*/
Binding binding = new Binding("hello-queue", Binding.DestinationType.QUEUE, "hello-exchange", "hello.java", null);
amqpAdmin.declareBinding(binding);
// send Msg
String msg = "hello world!!!";
rabbitTemplate.convertAndSend("hello-exchange", "hello.java", msg);;
}
}
发送
OrderEntity orderEntity = new OrderEntity();
orderEntity.setOrderSn(UUID.randomUUID().toString());
orderEntity.setModifyTime(new Date());
// 反馈可以统一放到configuration中
/*
public MyRabbitConfig(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
initTemplate();
}
*/
rabbitTemplate.setConfirmCallback((data, ack, cause) -> {
if(ack) {
System.out.println("发送成功");
} else {
System.out.println("发送失败");
}
});
rabbitTemplate.setReturnCallback((msg, respCode, respText, exchange, routeingKey) -> {
System.out.println("发送失败" +msg );
});
rabbitTemplate.convertAndSend("order-event-exchange", "order.create.order", orderEntity);
Init资源
有listener才会创建
@Configuration
@EnableRabbit // 需要监听的时候要添加
public class RabbitMQConfig {
@Bean
public Exchange orderEventExchange() {
/**
* String name,
* boolean durable,
* boolean autoDelete,
* Map<String, Object> arguments
*/
return new TopicExchange("order-event-exchange", true, false);
}
@Bean
public Queue orderDelayQueue() {
HashMap<String, Object> paramMap = new HashMap<>();
paramMap.put("x-dead-letter-exchange", "order-event-exchange");
paramMap.put("x-dead-letter-routing-key", "order.release.order");
// 20s
paramMap.put("x-message-ttl", TimeUnit.SECONDS.toMillis(20L));
return new Queue("order.delay.queue", true, false, false, paramMap);
}
/**
* 普通队列
*
* @return
*/
@Bean
public Queue orderReleaseQueue() {
return new Queue("order.release.order.queue", true, false, false);
}
/**
* 创建订单的binding,order-event-exchange 和 order.delay.queue 通过路由 order.create.order绑定
* @return
*/
@Bean
public Binding orderCreateBinding() {
/**
* String destination, 目的地(队列名或者交换机名字)
* DestinationType destinationType, 目的地类型(Queue、Exhcange)
* String exchange,
* String routingKey,
* Map<String, Object> arguments
* */
return new Binding(
"order.delay.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.create.order",
null
);
}
}
样例2
@Configuration
public class DirectRabbitConfig {
@Bean
public Queue rabbitmqDemoDirectQueue() {
/**
* 1、name: 队列名称
* 2、durable: 是否持久化
* 3、exclusive: 是否独享、排外的。如果设置为true,定义为排他队列。则只有创建者可以使用此队列。也就是private私有的。
* 4、autoDelete: 是否自动删除。也就是临时队列。当最后一个消费者断开连接后,会自动删除。
* */
return new Queue(RabbitMQConfig.RABBITMQ_DEMO_TOPIC, true, false, false);
}
@Bean
public DirectExchange rabbitmqDemoDirectExchange() {
//Direct交换机
return new DirectExchange(RabbitMQConfig.RABBITMQ_DEMO_DIRECT_EXCHANGE, true, false);
}
@Bean
public Binding bindDirect() {
//链式写法,绑定交换机和队列,并设置匹配键
return BindingBuilder
//绑定队列
.bind(rabbitmqDemoDirectQueue())
//到交换机
.to(rabbitmqDemoDirectExchange())
//并设置匹配键
.with(RabbitMQConfig.RABBITMQ_DEMO_DIRECT_ROUTING);
}
}
监听接收
如果已经创建好了队列,将不会覆盖,要删除队列
@Component
@RabbitListener(queues = {"order.release.order.queue"})
// 使用queuesToDeclare属性,如果不存在则会创建队列
// @RabbitListener(queuesToDeclare = @Queue(RabbitMQConfig.RABBITMQ_DEMO_TOPIC))
public class OrderCloseListener {
@RabbitHandler
public void getCloseOrderEntity(OrderEntity orderEntity, Channel channel, Message message) throws IOException {
System.out.println("接受到关闭订单" +orderEntity);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
发送模式
from:https://developer.aliyun.com/article/769883#slide-13
直连、主题通过 routerKey 绑定交换机和队列
广播会发送到绑定了交换机的所有队列
header exchange 通过头部的信息和绑定的头部信息进行对比,相当于将路由键放在了头部
直连
@Configuration
public class DirectRabbitConfig {
@Bean
public Queue rabbitmqDemoDirectQueue() {
/**
* 1、name: 队列名称
* 2、durable: 是否持久化
* 3、exclusive: 排他队列,基于连接下,可以访问,不可重名,关闭连接就会删除
* 4、autoDelete: 是否自动删除。也就是临时队列。当最后一个消费者断开连接后,会自动删除。
* */
return new Queue(RabbitMQConfig.RABBITMQ_DEMO_TOPIC, true, false, false);
}
@Bean
public DirectExchange rabbitmqDemoDirectExchange() {
// Direct交换机
return new DirectExchange(RabbitMQConfig.RABBITMQ_DEMO_DIRECT_EXCHANGE, true, false);
}
@Bean
public Binding bindDirect() {
// 链式写法,绑定交换机和队列,并设置匹配键
return BindingBuilder
// 绑定队列
.bind(rabbitmqDemoDirectQueue())
// 到交换机
.to(rabbitmqDemoDirectExchange())
// 并设置匹配键
.with(RabbitMQConfig.RABBITMQ_DEMO_DIRECT_ROUTING);
}
}
发送
rabbitTemplate.convertAndSend(
RabbitMQConfig.RABBITMQ_DEMO_DIRECT_EXCHANGE,
RabbitMQConfig.RABBITMQ_DEMO_DIRECT_ROUTING,
dataHashMap
);
接收
@RabbitListener(
queuesToDeclare =
@Queue(RabbitMQConfig.RABBITMQ_DEMO_TOPIC)
)
广播模式
通过将两个队列绑定到相同的交换机上,启动项目时就创建交换机和队列
不需要路由键
声明
@Component
public class DirectRabbitConfig implements BeanPostProcessor {
@Resource
private RabbitAdmin rabbitAdmin;
@Bean
public Queue fanoutExchangeQueueA() {
// 队列A
return new Queue(RabbitMQConfig.FANOUT_EXCHANGE_QUEUE_TOPIC_A, true, false, false);
}
@Bean
public Queue fanoutExchangeQueueB() {
// 队列B
return new Queue(RabbitMQConfig.FANOUT_EXCHANGE_QUEUE_TOPIC_B, true, false, false);
}
@Bean
public FanoutExchange rabbitmqDemoFanoutExchange() {
// 创建 FanoutExchange 类型交换机
return new FanoutExchange(
RabbitMQConfig.FANOUT_EXCHANGE_DEMO_NAME,
true,
false
);
}
@Bean
public Binding bindFanoutA() {
//队列A绑定到FanoutExchange交换机
return BindingBuilder
.bind(fanoutExchangeQueueA())
.to(rabbitmqDemoFanoutExchange());
}
@Bean
public Binding bindFanoutB() {
//队列B绑定到FanoutExchange交换机
return BindingBuilder.bind(fanoutExchangeQueueB()).to(rabbitmqDemoFanoutExchange());
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
//启动项目即创建交换机和队列
rabbitAdmin.declareExchange(rabbitmqDemoFanoutExchange());
rabbitAdmin.declareQueue(fanoutExchangeQueueB());
rabbitAdmin.declareQueue(fanoutExchangeQueueA());
return null;
}
}
发送
@Resource
private RabbitTemplate rabbitTemplate;
// 发布消息,就可以不指定路由
@Override
public String sendMsgByFanoutExchange(String msg) throws Exception {
Map<String, Object> message = getMessage(msg);
try {
rabbitTemplate.convertAndSend(RabbitMQConfig.FANOUT_EXCHANGE_DEMO_NAME, "", message);
return "ok";
} catch (Exception e) {
e.printStackTrace();
return "error";
}
}
接受
@RabbitListener(
queuesToDeclare =
@Queue(RabbitMQConfig.FANOUT_EXCHANGE_QUEUE_TOPIC_A)
)
@RabbitHandler
主题模式
通过”*” 、 “#”通配符,路由到对应的队列
*
符号:有且只匹配一个词
#
符号:匹配一个或多个词
一个带路由匹配规则的消息到来,交换机通过路由规则,发送到所有对应的队列
可以实现直连direct、广播fanout模式的,感觉主要是看路由键
header exchange
不是通过路由键,而是通过请求头中的键值
队列绑定时,能规定头部信息,x-match中的值为any/all,对消息的头部信息 和 队列绑定的头部信息进行匹配
@Component
public class RabbitConfig implements BeanPostProcessor {
@Bean
public Queue headersQueueA() {
return new Queue(RabbitMQConfig.HEADERS_EXCHANGE_QUEUE_A, true, false, false);
}
@Bean
public Binding bindHeadersA() {
Map<String, Object> map = new HashMap<>();
map.put("key_one", "java");
map.put("key_two", "rabbit");
// 全匹配
return BindingBuilder.bind(headersQueueA())
.to(rabbitmqDemoHeadersExchange())
.whereAll(map).match();
// 部分匹配
return BindingBuilder.bind(headersQueueB())
.to(rabbitmqDemoHeadersExchange())
.whereAny(map).match();
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
rabbitAdmin.declareExchange(rabbitmqDemoHeadersExchange());
rabbitAdmin.declareQueue(headersQueueA());
rabbitAdmin.declareQueue(headersQueueB());
return null;
}
}
发送
MessageProperties messageProperties = new MessageProperties();
// 消息持久化
messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
messageProperties.setContentType("UTF-8");
// 添加消息
messageProperties.getHeaders().putAll(headerDataMap);
Message message = new Message(msg.getBytes(), messageProperties);
rabbitTemplate.convertAndSend(
RabbitMQConfig.HEADERS_EXCHANGE_DEMO_NAME,
null,
message
);
接受
@RabbitListener(queuesToDeclare = @Queue(RabbitMQConfig.HEADERS_EXCHANGE_QUEUE_B))
public void process(Message message) throws Exception {
MessageProperties messageProperties = message.getMessageProperties();
String contentType = messageProperties.getContentType();
System.out.println("队列[" + RabbitMQConfig.HEADERS_EXCHANGE_QUEUE_B + "]收到消息:" + new String(message.getBody(), contentType));
}
邮箱发送
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
<version>2.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>2.1.7.RELEASE</version>
</dependency>
配置
server:
port: 8083
spring:
mail:
host: smtp.qq.com
port: 465
username: 284908631@qq.com
password: #
#test-connection: 是否测试链接
protocol: smtp
default-encoding: UTF-8
properties.mail.smtp.ssl.enable: true
# properties:
# mail.smtp.socketFactory.fallback : true
# mail.smtp.starttls.enable: true
rabbitmq:
host:
username: root
password:
virtual-host: /ming
@Bean
public Queue queue() {
return new Queue("com.ming.mail");
}
模板
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>欢迎</title>
</head>
<body>
欢迎<span style="color:orangered" th:text="${name}"></span>加入
</body>
</html>
接收
@Component
public class MailRecv {
private static Logger logger = LoggerFactory.getLogger(MailRecv.class);
@Autowired
private JavaMailSender javaMailSender;
@Autowired
private MailProperties mailProperties;
@Autowired
private TemplateEngine templateEngine;
@RabbitListener(queues = "com.ming.mail")
public void handle(MyUser myUser) {
MimeMessage msg = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(msg);
try{
helper.setFrom(mailProperties.getUsername());
helper.setTo(myUser.getEmail());
helper.setSubject("入职欢迎");
helper.setSentDate(new Date());
Context context = new Context();
context.setVariable("name", myUser.getName());
// 模板生成文本
String text = templateEngine.process("mail", context);
helper.setText(text, true);
javaMailSender.send(msg);
} catch (MessagingException e) {
e.printStackTrace();
logger.error("email error");
}
}
}
发送
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void sendMail() {
MyUser myUser = new MyUser();
myUser.setAge(20L);
myUser.setName("mingyue");
myUser.setEmail("284908631@qq.com");
rabbitTemplate.convertAndSend("com.ming.mail", myUser);
}
可靠性传递【发送】
# 已经过时
spring.rabbitmq.publisher-confirms=ture
# 高版本,发送端信息到达brock
sprin.rabbitmq.publisher-confirm-type=correlated
# 配置config,以下实现有
# 失败回调,发送端信息抵达队列
spring.rabbitmq.publisher-returns=true
# 抵达队列,异步方式回调
spring.rabbitmq.template.mandatory=true
publisher-confirm-type: correlated
publisher-returns: true
# 抵达队列,异步方式回调
template:
mandatory: true
生产端可靠性投递
成功发出
成功接收
生产者能收到应答
1.消息落库,进行标记,mq回调改变状态为已发送,定时任务将未成功发送的重新发送
2.消费者发送消息到队列,告诉回调服务已经消费完成,生产者延迟再次投递,回调服务进行确认,如果没有id相同的消费完成记录,则发起RPC重新发送
实现
spring:
rabbitmq:
host: 101.227.11.219
username: #
password: #
virtual-host: /ming
# 确认回调
publisher-confirm-type: correlated
# 失败回调
publisher-returns: true
config
设置 rabbitTemplate 成功和失败回调
@Configuration
public class RabbitMQConfig {
public final Logger logger = LoggerFactory.getLogger(RabbitMQConfig.class);
@Autowired
private CachingConnectionFactory cachingConnectionFactory;
@Autowired
private MailLogService mailLogService;
// 也可以使用@PostConstruct,对rabbitTemplate进行改造
@Bean
public RabbitTemplate rabbitTemplate() {
RabbitTemplate rabbitTemplate = new RabbitTemplate(cachingConnectionFactory);
// broker确认接受到信息,回调
rabbitTemplate.setConfirmCallback((data, ack, cause) -> {
String msgId = data.getId();
if(ack) {
logger.info("{}->发送成功", msgId);
MailLog mailLog = new MailLog();
mailLog.setStatus(1);
mailLog.setMsgId(msgId);
mailLogService.updateById(mailLog);
} else {
logger.error("{}->发送失败", msgId);
}
});
/**
* 队列接受消息失败,回调
* msg:消息主题
* respCode:响应码
* respText:响应描述
* exchange:交换机
* routingKey: 路由器
*/
rabbitTemplate.setReturnCallback((msg, respCode, respText, exchange, routeingKey) -> {
logger.error("{}->发送到queue失败", msg.getBody());
});
return rabbitTemplate;
}
@Bean
public Queue queue() {
return new Queue(EmailConstants.MAIL_QUEUE_NAME);
}
@Bean
public TopicExchange topicExchange() {
return new TopicExchange(EmailConstants.MAIL_EXCHANGE_NAME);
}
@Bean
public Binding binding() {
return BindingBuilder
.bind(queue())
.to(topicExchange())
.with(EmailConstants.MAIL_ROUTING_KEY);
}
}
发送
准备 UUID 作为 msgID ,在 converAndSend中 new CorrelationData(msgId) 发送给队列
@Test
public void sendMail() {
System.out.println("???");
String msgID = UUID.randomUUID().toString();
MailLog mailLog = new MailLog();
mailLog.setMsgId(msgID);
mailLog.setEid(0);
mailLog.setStatus(0);
mailLog.setRouteKey(EmailConstants.MAIL_ROUTING_KEY);
mailLog.setExchange(EmailConstants.MAIL_EXCHANGE_NAME);
mailLog.setCount(0);
mailLog.setTryTime(LocalDateTime.now().plusMinutes(EmailConstants.MSG_TIMEOUT));
mailLog.setCreateTime(LocalDateTime.now());
mailLog.setUpdateTime(LocalDateTime.now());
mailLogService.save(mailLog);
MyUser myUser = new MyUser();
myUser.setAge(20L);
myUser.setName("mingyue");
myUser.setEmail("284908631@qq.com");
rabbitTemplate.convertAndSend(
EmailConstants.MAIL_EXCHANGE_NAME,
EmailConstants.MAIL_ROUTING_KEY,
myUser,
new CorrelationData(msgID) // 消息的唯一id
);
}
定时
数据库中查找状态为 未发送0且到了重试时间的,如果其尝试次数小于3则重新发送给mq
@Scheduled(cron = "0/10 * * * * ?")
public void mail() {
List<MailLog> list = mailLogService.list(
new QueryWrapper<MailLog>()
.eq("status", 0)
.lt("tryTime", LocalDateTime.now())
);
System.out.println("????");
list.forEach(System.out::println);
System.out.println("????");
list.forEach(mailLog -> {
if (3 <= mailLog.getCount()) {
MailLog mailLog1 = new MailLog();
mailLog1.setStatus(2);
mailLog1.setMsgId(mailLog.getMsgId());
mailLogService.updateById(
mailLog1
);
}
MailLog mailLog2 = new MailLog();
mailLog2.setTryTime(LocalDateTime.now().plusMinutes(EmailConstants.MSG_TIMEOUT));
mailLog2.setUpdateTime(LocalDateTime.now());
mailLog2.setMsgId(mailLog.getMsgId());
mailLog2.setCount(mailLog.getCount() + 1);
mailLogService.updateById(mailLog2);
System.out.println("eid" + mailLog.getEid());
MyUser myUser = new MyUser();
myUser.setAge(20L);
myUser.setName("mingyue");
myUser.setEmail("284908631@qq.com");
rabbitTemplate.convertAndSend(
EmailConstants.MAIL_EXCHANGE_NAME,
EmailConstants.MAIL_ROUTING_KEY,
myUser,
new CorrelationData(mailLog.getMsgId())
);
});
}
可靠性传递【接受】
队列发送信息到接收端,如果接收端宕机,channel也还是自动回复,消息就全部没有了
# 收到确认收获
spring.rabbitmq.listener.simple.acknowledge-mode=manual
tag自增
@Component
@RabbitListener(queues = EmailConstants.MAIL_QUEUE_NAME)
public void handle(MyUser myUser,
org.springframework.messaging.Message message,
Channel channel
){
// getTag 1
MessageHeaders headers = message.getHeaders();
long tag = (long) headers.get(AmqpHeaders.DELIVERY_TAG);
// or 2
long tag = message.getMessageProperties().getDeliveryTag();
channel.basicAck(tag, false); // 只确认单条,非批量,不回队列
// 异常
catch (Exception e) {
channel.basicNack(tag, false, true); //,非批量, 重回队列
// channel.basicReject(tag, requeue);
e.printStackTrace();
}
}
消息可靠性
消息丢失
执行 convertAndSend 时网络断开
- 进行try catch,可能网络失败,重试发送
- 做好日志就,每个消息状态是否被服务器收到都记录
- 定期重发,扫描数据库
消息到达Broker,Broder没有写入磁盘
- 确认回调,修改服务器状态
自动ack下,消费者收到消息,没来得及处理就宕机
- 开启手动ACK,消费成功才移除,异常就noAck并重新入队
消息重复
消息处理完成,回复时宕机,导致重复处理
消息消费失败,再次发送
- 使用幂等性接口
message.getMessageProperties().getRedelivered()
是否重复投递- 防重表
消息积压
- 消费者宕机积压
- 消费者消费能力不足
- 发送者流量太大
- 上线更多消费者
- 上线队列消费服务,将数据存入数据库再处理
消息幂等性
定时任务刚好把刚刚放入数据库的记录(但是重发时间3分钟,一般不会出现)
正常情况下,消费者在消费消息时候,消费完毕后,会发送一个确认信息给消息队列(RabbitMQ是发送一个ACK确认消息),就从消息队列中删除
因为网络传输等等故障,确认信息没有传送到消息队列,导致消息队列不知道已经消费过该消息了,再次将该消息分发给其他的消费者
接受message,获取消息tag和msgId,判断redis是否存在msgId,开启手动确认
# 收到确认收获
spring.rabbitmq.listener.simple.acknowledge-mode=manual
@RabbitListener(queues = EmailConstants.MAIL_QUEUE_NAME)
public void handle(MyUser myUser,
org.springframework.messaging.Message message,
Channel channel
){
// or
MyUser myUser1 = (MyUser)message.getPayload();
MessageHeaders headers = message.getHeaders();
long tag = (long) headers.get(AmqpHeaders.DELIVERY_TAG);
String msgId = (String) headers.get("spring_returned_message_correlation");
// redis中重复
HashOperations hashOperations = redisTemplate.opsForHash();
if(hashOperations.entries("email_log").containsKey(msgId)) {
channel.basicAck(tag, false); // 只确认单条
System.out.println("消息已经被消费");
return;
}
// 确认消费
hashOperations.put("email_log", msgId, "ok");
channel.basicAck(tag, false); // 只确认单条
// 异常
catch (Exception e) {
channel.basicNack(tag, false, true); // 重回队列
e.printStackTrace();
}
}
延时队列
场景:未付款的订单,超过十佳后,自动取消订单释放占有物品
方案:schedule定时任务轮询数据库
缺点:消耗系统内存,数据库压力,时间误差(必须要30分钟才能解锁,可能要60分钟才能扫描到)
解决:rabbitmq消息ttl和死信Exchange结合
消息的TTL
消息的存活时间,rabbitmq对队列和消息分别设置TTL
- 对队列设置,就是没有消费者连着的保留时间,超过保留时间,消息就死了,成为死信
- 对队列和消息都设置,取小的,消息如果被路由到不同的队列中,这个消息死亡的时间可能不一样,可以通过设置消息的expiration字段或者x-message-ttl属性设置时间
Dead Letter Exchanges
一个消息满足如下条件 ,会进入死信路由,一个路由可以对应很对队列
- 消息被Consumer拒收,reject方法参数requeue为false,不会再次放到队列中
- 消息的TTL到了(所以不设置监听)
- 队列长度满了,排在前面的消息被丢弃或者扔到死信路由上
dead letter exchange就是普通的exchange,只是在某一个设置dead letter exchange中有消息过期了,就自动触发消息的转发,发送到dead letter exchange中
我们既然可以控制消息一段时间后变成死信,又可以变成死信的消息被路由到某一个指定的交换机,结合二者就变成延时队列
实现
推荐给消息队列设置过期时间,消息过期机制惰性检查,必须要第一个过期了才能拿后面的
设置消息队列,消息死了交给指定的死信队列
设置消息过期时间
使用
websocket
HTML5开始提供的,在当个TCP连接上进行全双工通信的协议,允许服务器端主动向客户端推送数据
webSocket API中,浏览器和服务器只需要完成一次握手,两者就直接创建持久性的连接,并进行双向数据传输
- 控制开销小
- 实时性
- 保持连接
- 二进制支持
- 支持扩展
- 压缩效果
依赖
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-websocket
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
/**
* 添加endpoint,网页能够websocket连接到
* 服务地址,指定是否可以使用webSocketJs
* @param registry
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/wb/ep") //
.setAllowedOrigins("*") // 允许跨域
.withSockJS(); // 支持socketJs
}
/**
* 输入通道参数配置
* @param registration
*/
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
// registration.interceptors()
}
/**
* 消息代理
* @param registry
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 配置代理域,可以配置多个,配置代理目的地前缀,在配置域上向客户端推送消息
registry.enableSimpleBroker("/queue");
}
}
RestTemplate
https://blog.csdn.net/jinjiniao1/article/details/100849237
从spring3开始支持的http请求工具,提供常见的rest请求的模板
getForEntity
@Autowired
RestTemplate restTemplate;
// 1 数字占位
String url = "http://" + host + ":" + port + "/hello?name={1}";
ResponseEntity<String> responseEntity = restTemplate.getForEntity(url, String.class, name);
// 2 名称占位2
// ?name={name}
map.put("name", "name");
restTemplate.getForEntity(url, String.class, map);
// 3 Uri对象,参数直接拼接,但是需要URLEncoder.encode对中文编码
String url = "xxx/hello?name="+ URLEncoder.encode(name,"UTF-8");
URI uri = URI.create(url);
ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);
// --- 直接获取值 ---
Person person = restTemplate.getForObject(uri, Person.class);
// ---- 获取值 ------
// 数据
String body = responseEntity.getBody();
// 状态码
HttpStatus statusCode = responseEntity.getStatusCode();
// 头部
HttpHeaders headers = responseEntity.getHeaders();
// headers.keySet(); header.get(key);
getForObject
与get一样
String url = "http://" + host + ":" + port + "/hello?name=" + URLEncoder.encode(name, "UTF-8");
URI uri = URI.create(url);
String s = restTemplate.getForObject(uri, String.class);
PostForEntiry Object
传递map
MultiValueMap dataMap = new LinkedMultiValueMap();
dataMap.add("name", name);
ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, dataMap, String.class);
传递json,需要依赖?
<dependency>
<groupId>com.justdojava</groupId>
<artifactId>commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
// @RequestBody User user
User userData = new User();
userData.setUsername("牧码小子");
userData.setAddress("深圳");
ResponseEntity<User> responseEntity = restTemplate.postForEntity(url, userData, User.class);
postForLocation
用户注册成功之后,自动跳转到登录页面
MultiValueMap map = new LinkedMultiValueMap();
map.add("username", "牧码小子");
map.add("address", "深圳");
// 结果是重定向的地址,没有则null
URI uri = restTemplate.postForLocation(url, map);
String s = restTemplate.getForObject(uri, String.class);
Put
MultiValueMap key/value 的形式传参,也可以用 JSON
没有返回值
restTemplate.put(url1, map);
delete
参数只能放在地址栏,地址栏直接拼接,或者map
cookie
RestTemplate 的 setInterceptors 设置拦截器
restTemplate.setInterceptors( Collections.singletonList(new ClientHttpRequestInterceptor() {
@Override
public ClientHttpResponse intercept( HttpRequest request, byte[] body, ClientHttpRequestExecution execution ) throws IOException {
HttpHeaders headers = request.getHeaders();
headers.add("cookie","justdojava");
return execution.execute(request,body);
}
}));
String s = restTemplate.getForObject(url, String.class);
exchange通用
HttpHeaders headers = new HttpHeaders();
headers.add("cookie","justdojava");
HttpEntity<MultiValueMap<String,String>> request = new HttpEntity<>(mapData,headers);
ResponseEntity<String> responseEntity = restTemplate.exchange(url, HttpMethod.GET, request, String.class);
System.out.println(responseEntity.getBody());
Session
session存在问题
原理:客户端访问服务器,服务器会返回jessionid=xxx的cookie,通过该cookie可以找到服务器对应的session,关闭浏览器清除会话cookie
存在问题
- session不能跨域名共享
- session不能分布式使用(同域名下session复制,session不同步)
解决方案
session复制
优点:web-server tomcat原生支持,只需要修改配置文件
缺点:
- 数据传输占用网络带宽,降低业务处理能力
- 每一台服务器保留所有web-server的session总和,内存要求大,大型分布式下不可采取
客户端存储
不能使用
优点:服务器不需要存储session,将信息存储到cookie中,节省服务器资源
缺点:
- 每次请求,cookie中都是完整信息,浪费带宽
- cookie长度限制4K,不能存大量信息
- cookie泄露,篡改、窃取
hash一致性
hash将session存到某个服务器,iphash 或者 session_id_hash
优点:
- 只需要改nginx
- 负载均衡,hash均匀的话,web-server也均衡
- 支持水平扩展
缺点:
- 重启服务导致部分session丢失
- 水平扩展后,rehash改变
统一存储
存储到redis中
优点:
- 没有安全隐患
- 可以水平扩展
- 重启也不会丢失
缺点:
- 网络调用
- 要修改应用代码,将getSession替换成从redis获取
springSession可以解决
简介
官网:https://docs.spring.io/spring-session/reference/2.6.1/samples.html
https://docs.spring.io/spring-session/reference/2.6.1/guides/boot-redis.html
第一次使用session,浏览器就将cookie保存,访问 *.mingyue.com 都携带,能够自动续期
session.setAttribute("loginUser", userDasta);
servletResponse.addCookie(
new Cookie("JSESSSION", "id")
.setDomain("")
);
使用
@EnableRedisHttpSession
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
spring.session.store-type=redis
spring.redis.host=localhost
spring.redis.password=
spring.redis.port=6379
# Session timeout. If a duration suffix is not specified, seconds is used.
server.servlet.session.timeout=
# Sessions flush mode.
spring.session.redis.flush-mode=on_save
# Namespace for keys used to store sessions.
spring.session.redis.namespace=spring:session
配置类
序列化和生效域名
@Configuration
public class SessionConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("GULISESSION");
serializer.setCookiePath("/");
serializer.setDomainName("gulimall.com");
return serializer;
}
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
原理
@EnableRedisHttpSession 导入RedisHttpSessionConfiguration配置
- 添加SessionRepository -》RedisOperationsRepository组件,redis操作session
- SessionRepositoryFilter
- 创建,从容器获取到SessionRepository
- doFilterInternal中,将原生request、response上下文包装起来,将包装后的request和response
- wrappedRequest中重写了getSession =》 SessionRepository 中获取到
装饰者模式
https://www.cnblogs.com/of-fanruice/p/11565679.html
抽象奶茶(被装饰者)有多种实现,调料(装饰物)继承被装饰者,装饰者可以使用被装饰者的方法并改造
分布式事务
本地事务
本地事务 @Transactional( isolation = Isolation.READ_COMMITTED ) 不能管分布式
@Transactional( isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED )
// propagation 传播行为
REQUIRED; // 公用
REQUIRS_NEW; // 创建新的
// timeout = 30 超时回滚,子事务必须要创建新的propagation
// isolation
事务失效
事务内不能本类的其他事务方法,无效,没有拿到代理对象
// 本类注入本类,循环依赖【值得学习】
@Autowired
MyselfService myselfService;
解决:引入aspectj
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
// Configuration
@EnableAspectJAutoProxy(exposeProxy = true) // 开启动态代理功能,所有的动态代理都由aspectj创建(jdk默认的都要有接口),暴露接口代理对象
MyService service = (MyService)AopContext.currentProxy();
service.a();
分布式事务
分布式系统异常:宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠的TCP、存储数据丢失
CAP定理
一致性Consistency:
- 分布式系统中的所有数据备份,在统一时刻是否是同样的值
可用性(Availability):
- 集群中某一部分节点故障后,集群整体是否还能响应客户端的读写请求(对数据更新有高可用性)
分区容错性(Partition tolerance):P一定要,因为网络很可能有错
- 大多数分布式系统都分布在多个子网络,每个子网络叫做一个区
- 分布容错的意思是,区间通信可能失败,中国和美国的服务器两个不同的区可能无法通信
CAP,只能同时实现两个,不能都兼顾,无法CA,网络错误肯定有
3个数据库,保存8,所有数据备份都一样(一致性),由就只能在AP中选一个,如果分区通信断了,就有一台不能同步,如果要满足可用性(三个条件都满足),可能访问到故障节点,读到的是不一致的数据
所以我们只能让故障节点不能访问,此时可用性不满足
实现一直性算法:raft、paxos
状态:领导、随从、候选人
随从没听到领导,变成候选人,发起投票,同意则变成领导
领导接受客户端命令,领导写入日志(uncommit,给随从发送日志,等待大部分随从都写入后就提交,并通知其他人)
领导选举:
选举超时150ms-300ms,没有命令就自己推荐,让其他节点投票(只能一票)
领导每一段时间会发送心跳包,否则就要自选领导者了
一般舍弃C
BASE
对CAP的延伸,无法做到强一致性,但是可以弱一致性
基本可用(Basically Available):
- 出现故障,允许损失部分可用性(响应那个时间、功能上的可用性)
- 响应时间损失:原在0.5s返回用户,由于故障,响应时间增加到1~2秒
- 功能损失:双十一,部分消费者引导到降级页面
软状态(Soft State)
- 允许系统存在中间状态,这个状态不会影响系统整体可用性,分布式存储中一般一份数据又多个副本,允许副本同步的延时,MySQL replication的异步复制也是
最终一致性(Eventual Consistency)
- 系统中的数据副本经过一定时间后,最终能够达到一致的状态,弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况
弱一致性:容忍后续的部分或者全部访问不到
最终一致性:过一段时间后要求能访问到更新后的数据
解决方案
2PC
数据库二阶段提交
第一阶段:事务协调器要求每个涉及事务的数据库预提交此操作,并反映是否可以提交
第二阶段:要求每个数据库提交数据
如果任何一个数据库否决此次提交,那么所有数据库都会被要求回滚
- 协议比较简单,数据库实现了XA协议,成本低
- 性能不好,并发量高时,无法满足
- 商业数据库支持的比较好,mysql数据库中没有记录prepare阶段日志,主备切换会导致主库和从库数据不一致
- 许多nosql也没有支持XA,应用场景狭隘
- 也有3PC,引入超时机制
柔性事务-TCC事务
刚性事务:ACID,强一致性
柔性事务:遵循BASE,最终一致性,允许一定时间内不同节点的数据不同,但是最终一致
最大努力通知
不保证数据一定通知成功,但是会提供可查询操作接口进行核对,使用MQ发送http请求,设置最大通知次数,达到通知次数后不通知
允许大并发
可靠消息+最终一致性
允许大并发
Seata
github:https://github.com/seata/seata
官网:https://seata.io/zh-cn/docs/ops/deploy-guide-beginner.html
每个数据库添加undo_log SQL表
软件包0.7,修改配置,registry.conf 注册镇中心配置,type=nacos, file.conf
引入依赖
每个要用到的分布式事务的微服务配置数据源,使用seate DataSourceProxy代理自己的数据源,0.9后默认开启,不需要配置
每个微服务都导入registry.conf、file.conf(_mapping.myserver)
主服务@GlobalTransactional @Transactional,其他@Transactional就行
后台添加商品适合seta的AT分布式事务模式,但是高并发不合适
sentinel
github:https://github.com/alibaba/Sentinel/releases
文档:https://sentinelguard.io/zh-cn/docs/basic-api-resource-rule.html
简介
引入 Sentinel 带来的性能损耗非常小。只有在业务单机量级超过 25W QPS 的时候才会有一些显著的影响(5% - 10% 左右),单机 QPS 不太大的时候损耗几乎可以忽略不计。
熔断 降级 限流
熔断:
调用链请求堆积,全部崩溃
直接告诉降级数据,不能长时间等待
降级:
高峰期,对某些服务的调用返回降级数据,保证核心业务正常运行
相同:
- 保障集群大部分服务可以可靠,防止崩溃
- 用户最终都体验到某个功能不可用
不同:
- 熔断是调用方故障,触发的系统规则
- 降级是基于全局考虑,停止一些正常业务
限流:
流量控制,服务承担不超过自己能力的流量压力
产品对比
线程池隔离:每种资源一个线程池,线程很多,优点:隔离彻底,不会影响别的接口,缺点:线程切换消耗大,特别是低延时的调用
信号量:每种资源一份信号量,每个请求拿一个信号量,用完再加回来
hystrix的信号量限制对某个资源调用的并发数,隔离轻量,缺点是无法对调用自动降级,只能等待客户端超时,因此仍然可能级联阻塞
sentinel的信号量通过并发线程数模式的流量提供信号量隔离,基于响应时间的熔断降级
使用
定义资源、定义规则
依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2.2.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.5.6</version>
</dependency>
<!-- 缺少依赖 -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.18.Final</version>
</dependency>
控制台
建议与依赖sentinel版本一致,应该使用sentinel 1.8
源码构建https://github.com/alibaba/Sentinel
java -jar sentinel-dashboard.jar --server.port=8080
应用配置
spring.cloud.sentinel.transport.dashboard=localhost:8080
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080
# 流量实时监控
management:
endpoints:
web:
exposure:
include: '*'
feign:
sentinel:
enabled: true
# spring.cloud.sentinel.scg.fallback.content-type=application
# spring.cloud.sentinel.scg.fallback.response-status=400
修改降级界面 sentinel 1.8后
@Component
public class GulimallSentinelConfig implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException ex) throws Exception {
String msg = null;
if (ex instanceof FlowException) {
msg = "限流了";
} else if (ex instanceof DegradeException) {
msg = "降级了";
} else if (ex instanceof ParamFlowException) {
msg = "热点参数限流";
} else if (ex instanceof SystemBlockException) {
msg = "系统规则(负载/...不满足要求)";
} else if (ex instanceof AuthorityException) {
msg = "授权规则不通过";
}
// http状态码
response.setStatus(500);
response.setCharacterEncoding("utf-8");
response.setHeader("Content-Type", "application/json;charset=utf-8");
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(JSON.toJSONString(R.error(BizCodeEnum.TO_MANY_REQUEST.getCode(), msg)));
}
}
旧版本
@Component
public class GulimallSentinelConfig implements UrlBlockHandler{
@Override
public void blocked(HttpServletRequest request, HttpServletResponse response, BlockException ex) throws IOException {
R r = R.error(BizCodeEnum.SECKILL_EXCEPTION.getCode(),BizCodeEnum.SECKILL_EXCEPTION.getMsg());
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(JSON.toJSONString(r));
}
}
熔断
callback
实现feigin接口
feign:
sentinel:
enabled: true
@FeignClient(value = "gulimall-seckill",fallback = SeckillFallbackService.class)
public interface SeckillFeignService {
}
@Component
public class SeckillFallbackService implements SeckillFeignService {
@Override
public R getSeckillSkuInfo(Long skuId) {
return R.error(BizCodeEnum.READ_TIME_OUT_EXCEPTION);
}
}
调用方熔断保护
调用方手动指定远程服务的降级策略,远程服务被降级处理,触发回调
超大流量时,牺牲远程服务,正在远程服务指定降级策略,提供方是在运行熔断的数据
资源
代码
// 1 -------try -catch
try (Entry entry = SphU.entry("HelloWorld")) {
// Your business logic here.
System.out.println("hello world");
} catch (BlockException e) {
// Handle rejected request.
e.printStackTrace();
}
// try-with-resources auto exit
// 2 ------- boolean -------
// 资源名可使用任意有业务语义的字符串
if (SphO.entry("自定义资源名")) {
// 务必保证finally会被执行
try {
/**
* 被保护的业务逻辑
*/
} finally {
SphO.exit();
}
} else {
// 资源访问阻止,被限流或被降级
// 进行相应的处理操作
}
注解
// 原本的业务方法. 抛出限流/降级/系统保护后,使用自己的方法,fallbacll针对所有异常
@SentinelResource(blockHandler = "blockHandlerForGetUser")
public User getUserById(String id) {
throw new RuntimeException("getUserById command failed");
}
// blockHandler 函数,原方法调用被限流/降级/系统保护的时候调用
public User blockHandlerForGetUser(String id, BlockException ex) {
return new User("admin");
}
以上方法要配置限流后的默认返回,url请求可以配置统一返回(webCallbackManager)
网关限流
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
<version>2.2.4.RELEASE</version>
</dependency>
spring5新特性 响应式编程【值得学习】
@Configuration
public class SentinelGatewayConfig {
public SentinelGatewayConfig() {
GatewayCallbackManager.setBlockHandler(new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
R error = R.error(BizCodeEnum.TOO_MANY_REQUEST);
String errJson = JSON.toJSONString(error);
Mono<ServerResponse> body = ServerResponse.ok().body(Mono.just(errJson), String.class);
return body;
}
});
}
}
sleuth+zipkin
sleuth github:https://github.com/spring-cloud/spring-cloud-sleuth
zipkin可视化界面
概念
span: 基本单元,发送远程调度任务,就产生一个span,64位ID唯一标识,包括摘要、时间戳事件,span的ID,进度ID
Trace:span组成的树状结构,一个微服务的接口,可能调用多个,产生多个span
Annotaion:记录一个事件,一些核心注解定义请求的开始和结束
- cs client-sent 客户端发送一个请求,这个注解描述span的开始
- sr Server Received-服务端获得请求并开始处理它,如果sr-cs时间戳便可以得到网络传输时间
- ss Server Sent,服务端发送响应,表明请求处理的完成,ss-sr时间戳得到服务器请求的时间
- cr client received,客户端接收响应,此时span结束,cr-cs得到请求消耗的时间
sleuth使用
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
日志
logging:
level:
com.ming.gulimall: debug
org.springframework.cloud.openfeign: debug
org.springframework.cloud.sleuth: debug
调用发现出现日志
[gulimall-product,4f8a67328f3c970f,bb5f0e8067cd3489,false]
zipkin使用
https://github.com/openzipkin/zipkin
可以看到完整链路,包括mq回调,是否异步,异常
# Note: this is mirrored as ghcr.io/openzipkin/zipkin
docker run -d -p 9411:9411 --name zipkin openzipkin/zipkin
# or
java -jar zipkin.jar
docker run -d -p 9411:9411 --env STORAGE_TYPE=elasticsearch --ev ES_HOSTS=xxxxx:9200 --name zipkin openzipkin/zipkin-dependencies
<!-- 包含sleuth -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
三、第三方
oss
https://help.aliyun.com/document_detail/32011.html?spm=a2c4g.11186623.6.932.50e626fdt3yrtk
注意需要给oss设置能跨域
- 登录OSS管理控制台。
- 单击Bucket列表,之后单击目标Bucket名称。
- 单击权限管理 > 跨域设置,在跨域设置区域单击设置
原始
依赖
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.10.2</version>
</dependency>
<!--oss附加依赖,java9-->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.3</version>
</dependency>
code
@Test
void upload() throws FileNotFoundException {
// Endpoint以杭州为例,其它Region请按实际情况填写。
String endpoint = "oss-cn-beijing.aliyuncs.com";
// 阿里云主账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM账号进行API访问或日常运维,请登录RAM控制台创建RAM账号。
String accessKeyId = "";
String accessKeySecret = "";
String bucketName = "ming-mall";
// <yourObjectName>上传文件到OSS时需要指定包含文件后缀在内的完整路径,例如abc/efg/123.jpg。
String objectName = "test.jpg";
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 上传文件到指定的存储空间(bucketName)并将其保存为指定的文件名称(objectName)。
// String content = "Hello OSS";
InputStream inputStream = new FileInputStream("F:\\images\\cat.jpg");
ossClient.putObject(bucketName, objectName, inputStream);
// 关闭OSSClient。
ossClient.shutdown();
System.out.println("ok");
}
封装
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>aliyun-oss-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
<!--版本不兼容时,导入新的依赖-->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.4.5</version>
</dependency>
alibaba.cloud.access-key=your-ak
alibaba.cloud.secret-key=your-sk
alibaba.cloud.oss.endpoint=***
alibaba:
cloud:
access-key: LTAI4G3v
secret-key: RkwqM0t8aOdF8E8BcRu
oss:
endpoint: oss-cn-beijing.aliyuncs.com
@Test
void newUpload() throws FileNotFoundException {
InputStream inputStream = new FileInputStream("F:\\images\\cat.jpg");
String bucketName = "ming-mall";
String objectName = "test1.jpg";
ossClient.putObject(bucketName, objectName, inputStream);
// 关闭OSSClient。
ossClient.shutdown();
System.out.println("ok");
}
直传-服务端签名
示范代码:https://help.aliyun.com/document_detail/91868.html?spm=a2c4g.11186623.2.10.110e7d9c9Zyt20
@Configuration
public class OssConfig {
@Value("${spring.cloud.alicloud.oss.endpoint}")
String endpoint ;
@Value("${spring.cloud.alicloud.oss.bucket}")
String bucketName ;
@Value("${spring.cloud.alicloud.access-key}")
String accessKeyId ;
@Value("${spring.cloud.alicloud.secret-key}")
String accessKeySecret ;
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public OSSClient ossClient(){
ClientConfiguration getconfig = getconfig();
return new OSSClient(endpoint,accessKeyId,accessKeySecret,getconfig);
}
ClientConfiguration getconfig(){
ClientConfiguration clientConfiguration = new ClientConfiguration();
clientConfiguration.setConnectionRequestTimeout(1000);
return clientConfiguration;
}
}
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
alibaba:
cloud:
access-key: LTAI4G3
secret-key: RkwqM0t8aOd
oss:
endpoint: oss-cn-beijing.aliyuncs.com
bucket: ming-mall
server:
port: 8095
package com.ming.gulimall.gulimallthirdparty.controller;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClient;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.common.utils.BinaryUtil;
import com.aliyun.oss.model.MatchMode;
import com.aliyun.oss.model.PolicyConditions;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
@RestController
public class OssController {
@Resource
OSS ossClient;
@Value("${alibaba.cloud.access-key}")
private String accessId;
@Value("${alibaba.cloud.oss.endpoint}")
private String endpoint;
@Value("${alibaba.cloud.oss.bucket}")
private String bucket;
@RequestMapping("/oss/policy")
public Map<String, String> policy(){
Map<String, String> respMap = null;
String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint
// callbackUrl为 上传回调服务器的URL,请将下面的IP和Port配置为您自己的真实信息。
// String callbackUrl = "http://88.88.88.88:8888";
String format = new SimpleDateFormat("yyyy/MM/dd").format(new Date());
String dir = format + "/"; // 用户上传文件时指定的前缀。
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);
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));
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
ossClient.shutdown();
}
return respMap;
}
}
直传-前端
需要修改
支付宝
https://opendocs.alipay.com/open/01bxlm
对称加密、非对称加密
easySDK
https://github.com/alipay/alipay-easysdk/blob/master/README.md
apidocs:https://github.com/alipay/alipay-easysdk/blob/master/APIDoc.md
<!-- https://mvnrepository.com/artifact/com.alipay.sdk/alipay-easysdk -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-easysdk</artifactId>
<version>2.2.1</version>
</dependency>
@Configuration
public class AlipayConfig {
public static Config getOptions() {
Config config = new Config();
config.protocol = "https";
config.gatewayHost = "openapi.alipaydev.com";
config.signType = "RSA2";
config.appId = "2021000118657831";
// 为避免私钥随源码泄露,推荐从文件中读取私钥字符串而不是写入源码中
config.merchantPrivateKey = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCKTMhMS4FdnFy3l4RL9iSn9T0ABE/eNgN88gR0sKF5Lmuz8tI6aOPENGofsX76pYtlnAaW4WW9PIP/S19YcKe132UHTzyedCCKSmJ+VVHdkKMlZkAQZ+0ZumEyK9oW7Qms5tro7n0dHnvYVNm8Lzc1ATnTwc4eO+BaLqCURaomTRlgjTMuiGyMSQg3406MCKcTD6p6FrsFMkf0UfTJM1w1L7zCMLK9NnX49SUBbZXUOSf90mvsd5yDa2MShzK1NjeFjiK5lc9YQ1cDHIjAyky4mimAIpIWjqLHLQ5MG5+KaB/VDcg3GzhSTp6IJ6cyN9MKWrPPIeRXHTEAYzmMUivDAgMBAAECggEAdhtccsuIjwkZpTAgKz7pzwYAMiN8kahPEkUcyQqO245pLCQSpQ8udEDO4IIUjrkRcpTsi62x1Qn5L+yOYFjU4N0GyldAzZEeuPsNYhY685yxtx67V0dplK82kkIg3bNQr/f2uzXwYw3FddoAmFU6MGn7mHvKKse3sUxglj9oL4whzVG9RC4gToQ99gCjPOsrtW8hSz/3FPgtmuzMLMj+zOatPDqFpo54LnWDQ4ocxLyu8nJl+mzOa8MhdlWj1VByTimzbMAh02s7AGqC8HYy7Y//T/oUbpqsKe1chq3KtldsINxITPmkACBnd0OuGGGD8Rw2E1YgZXzlqN81KE/2AQKBgQDgq2qSdRT5mXKZjAVBazbwfpL6GN9+AiPWllGDJy7ngGJjgrK4GHF/pxVTbIbQM4LOMmNXFNvPFc1HmKE6z/ks/T6H/VC4lKo1Buo7nUYtRJk3dllU3ek4Utmi9ge60ZRkwn9a7vKOjXu0ZVBvVNlixL8BGkHboov195ViU3/r2QKBgQCdlgP8+7RFJnw2MvkDoBg5SPvtJhOo78/ubPT+na0FOBIFzd++xgywXou4uwrsfn3olPp1rhmjvzYf5aq3Jm0pbDnvO/X7mKb4zVVErBwvY+PCUmpfI6VwlLP+WITPoDotkZEFBkij8UzON7h0xbtmYUV0Ir4h5qNUVHRrBUae+wKBgQC0lzT/6prkkwJ6CAIFdq/fmm58F9g1ynSSPZvhx3I9ZYYwpNMRhZxd1qkeuKKY4n7nTbtqOPsCt2yde0NGKfwJvLoxx13GMMGsBBXtu1q4cmaSHVBrFkEsI/SKuCa4dVRJtcl3B5DzIyjndXS1OMfQS4OY2ElNyZelK9DpC2NM6QKBgQCFCSN+zIPIqQ/NtcSRcfNhUSMVdsK8KRBSSXufBEAQGuDkM4SDirElp/uuzYEQXE0xL6wt8vfETGWGEh7IFdGsWaijNeyZJas4eihVHDODMoISB/+zJ+XAIFnADLy6h5r142EZa8+hT9G2ekXnlxJ5AP9gZwA6oHocdFwACWkwRwKBgGkFks45zRbZOcrE42Z1PKBnzbhIlGIqkM/aU3mzJlWSlYsS+NRlB0dDdQWEu6cPeUCQmpf6mOVK7Ung67tfelSep8BhmH9FkmzZ9WGyGv5Ph34tvsGZvDXoUwIjQwGNxGguPFevFUbhetv/4Y53wRpx4VhAHFmBZevukd83Lc5P";
//注:证书文件路径支持设置为文件系统中的路径或CLASS_PATH中的路径,优先从文件系统中加载,加载失败后会继续尝试从CLASS_PATH中加载
// config.merchantCertPath = "<-- 请填写您的应用公钥证书文件路径,例如:/foo/appCertPublicKey_2019051064521003.crt -->";
// config.alipayCertPath = "<-- 请填写您的支付宝公钥证书文件路径,例如:/foo/alipayCertPublicKey_RSA2.crt -->";
// config.alipayRootCertPath = "<-- 请填写您的支付宝根证书文件路径,例如:/foo/alipayRootCert.crt -->";
//注:如果采用非证书模式,则无需赋值上面的三个证书路径,改为赋值如下的支付宝公钥字符串即可
config.alipayPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgmYsfRVA6PrXMzJWo4B/cV9lfsDoXhDJixd4HidWvYLTbLGWDcXgQ8pIYFrAU5fCBCICg51yy5RPLOHJ+zvtQB/1tHxYNpTG+tGgJk9Io7+QlwGsBbkba4CYWz+z+Q9PvsMjPmjZtF+jyOU58Z34t2tvp0tveBmE/8nC7kmnqryv+U5OiajY+Mmw6ZNCYB0xt4fm7IY7c2iL2soCNkEJl7Q01dfoMwEEZI1GhJj977qJwocqSZ7lzoODaGGXGhkCbzjNRvaUPuNdf4uv0Gkl1mYVY8E3Ib4oK0jn83m594ic4sY2DL6tYf4o3xSOTFLqi11+Jh1Hlpwib1ObC4VCEQIDAQAB";
//可设置异步通知接收服务地址(可选)
config.notifyUrl = "<-- 请填写您的支付类接口异步通知接收服务地址,例如:https://www.test.com/callback -->";
//可设置AES密钥,调用AES加解密相关接口时需要(可选)
config.encryptKey = "<-- 请填写您的AES密钥,例如:aa4BtZ4tspm2wnXLb1ThQA== -->";
return config;
}
@PostConstruct
public void addOption() {
Factory.setOptions(getOptions());
}
}
订单创建
阿里云创建订单会返回from表单,需要直接响应html
@ResponseBody
@GetMapping(value="/payOrder", produces = "text/html");
四、基础
基础概念
SPU、SKU
小米八:SPU(类)
8G、16G:SKU(对象)
Object划分
https://blog.csdn.net/qq_32447321/article/details/53148092
1.entity字段必须和数据库字段一样
2.model,前端需要什么我们就给什么
3.domain很少用,代表一个对象模块
PO
(persistant object) 持久对象
通常对应数据模型 ( 数据库 ), 本身还有部分业务逻辑的处理。与数据库中的表相映射的 java 对象。
最简单的 PO 就是对应数据库中某个表中的一条记录,多个记录可以用 PO 的集合。 PO 中应该不包含任何对数据库的操作。
DO
(Domain Object)领域对象
就是从现实世界中抽象出来的有形或无形的业务实体。
TO
(Transfer Object) ,数据传输对象
在不同应用程序之间传输的对象,比如远程调用
DTO
(Data Transfer Object)数据传输对象
原来的目的是为了EJB的分布式应用提供粗粒度的数据实体,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载,
泛指用于展示层与服务层之间的数据传输对象。
VO
(value object) 值对象
通常用于业务层之间的数据传递,和 PO 一样也是仅仅包含数据而已。但应是抽象出的业务对象 , 可以和表对应 , 也可以不 , 这根据业务的需要
BO
(business object) 业务对象
封装业务逻辑的 java 对象 , 通过调用 DAO 方法 , 结合 PO,VO 进行业务操作。
主要作用是把业务逻辑封装为一个对象。这个对象可以包括一个或多个其它的对象。 比如一个简历,有教育经历、工作经历、社会关系等等。 我们可以把教育经历对应一个 PO ,工作经历对应一个 PO ,社会关系对应一个 PO 。 建立一个对应简历的 BO 对象处理简历,每个 BO 包含这些 PO 。 这样处理业务逻辑时,我们就可以针对 BO 去处理。
POJO
(plain ordinary java object) 简单无规则 java 对象
纯的传统意义的 java 对象。就是说在一些 Object/Relation Mapping 工具中,能够做到维护数据库表记录的 persisent object 完全是一个符合 Java Bean 规范的纯 Java 对象,没有增加别的属性和方法
DAO
(data access object) 数据访问对象
负责持久层的操作。为业务层提供接口。此对象用于访问数据库。通常和 PO 结合使用, DAO 中包含了各种数据库的操作方法。通过它的方法 , 结合 PO 对数据库进行相关的操作。夹在业务逻辑与数据库资源中间。配合 VO, 提供数据库的 CRUD 操作.
业务
树形
流操作、递归、@JsonInclude(JsonInclude.Include.NON_EMPTY)
不返回某些属性
/**
* 子分类
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@TableField(exist = false)
private List<CategoryEntity> childCategoryEntity;
// -------------controller
@RequestMapping("/list/tree")
// @RequiresPermissions("product:category:list")
public R list(){
List<CategoryEntity> entityList = categoryService.listWithTree();
return R.ok().put("data", entityList);
}
// -------------service
/**
* 获取tree分类
* @return
*/
@Override
public List<CategoryEntity> listWithTree() {
List<CategoryEntity> allEntities = this.list();
List<CategoryEntity> rootCategories = allEntities.stream() // 获取所有root
.filter( categoryEntity -> categoryEntity.getParentCid() == 0 )
.sorted( Comparator.comparingInt( CategoryEntity::getSort ) )
.collect( Collectors.toList() );
List<CategoryEntity> treeEntity = rootCategories.stream() // 所有root添加子节点
.map(rootEntity -> { // 遍历root设置子节点
List<CategoryEntity> children = getChildren(allEntities, rootEntity);
rootEntity.setChildCategoryEntity(children);
System.out.println(children);
return rootEntity;
})
.collect( Collectors.toList() );
return treeEntity;
}
/**
* 通过遍历所有节点,获取当前节点的所有子节点
* @param allEntities
* @param rootEntity
* @return
*/
private List<CategoryEntity> getChildren(List<CategoryEntity> allEntities, CategoryEntity rootEntity) {
List<CategoryEntity> children = allEntities.stream()
.filter( item -> rootEntity.getCatId().equals( item.getParentCid()) ) // 取出子节点
// 从大到小
.sorted( (item1, item2) -> ( item1.getSort() == null ? 0 : item1.getSort() ) - (item2.getSort() == null ? 0 : item2.getSort() ) )
.map(item -> {
item.setChildCategoryEntity( getChildren( allEntities, item ) );
return item;
})
.collect( Collectors.toList() );
return children;
}
处理流程
- controller:处理请求,接受和校验数据
- service接受controller数据,业务处理
- controller接受service数据,封装vo
小问题
- 冗余的字段,修改原数据要记得修改
商品保存
设计太多的数据库表,不够理解,未完成
物品检索
需要后台上架商品到es
1. 检索条件分析
全文检索:skuTitle-》keyword
排序:saleCount(销量)、hotScore(热度分)、skuPrice(价格)
过滤:hasStock、skuPrice区间、brandId、catalog3Id、attrs
聚合:attrs
完整查询参数
难点:多属性(尺寸、系统)多参数筛选
keyword = 小米
& sort = saleCount_desc/asc // 排序
& hasStock = 0/1
& skuPrice = 400_1900 // 最高最低
& brandId = 1& brandId =2 // 数组
& catalog3Id=1
& attrs = 1_3G:4G:5G & attrs = 2_骁龙845 & attrs = 4_高清屏 // 数组
& pageNum = 1
2. DSL分析
GET gulimall_product/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"skuTitle": "华为"
}
}
],
"filter": [
{
"term": {
"catalogId": "225"
}
},
{
"terms": {
"brandId": [
"2"
]
}
},
{
"term": {
"hasStock": "false"
}
},
{
"range": {
"skuPrice": {
"gte": 1000,
"lte": 7000
}
}
},
{
"nested": {
"path": "attrs",
"query": {
"bool": {
"must": [
{
"term": {
"attrs.attrId": {
"value": "6"
}
}
}
]
}
}
}
}
]
}
},
"sort": [
{
"skuPrice": {
"order": "desc"
}
}
],
"from": 0,
"size": 5,
"highlight": {
"fields": {"skuTitle": {}},
"pre_tags": "<b style='color:red'>",
"post_tags": "</b>"
},
"aggs": {
"brandAgg": {
"terms": {
"field": "brandId",
"size": 10
},
"aggs": {
"brandNameAgg": {
"terms": {
"field": "brandName",
"size": 10
}
},
"brandImgAgg": {
"terms": {
"field": "brandImg",
"size": 10
}
}
}
},
"catalogAgg":{
"terms": {
"field": "catalogId",
"size": 10
},
"aggs": {
"catalogNameAgg": {
"terms": {
"field": "catalogName",
"size": 10
}
}
}
},
"attrs":{
"nested": {
"path": "attrs"
},
"aggs": {
"attrIdAgg": {
"terms": {
"field": "attrs.attrId",
"size": 10
},
"aggs": {
"attrNameAgg": {
"terms": {
"field": "attrs.attrName",
"size": 10
}
}
}
}
}
}
}
}
3. 检索代码编写
1) 请求参数和返回结果
请求参数的封装
@Data
public class SearchParam {
//页面传递过来的全文匹配关键字
private String keyword;
//品牌id,可以多选
private List<Long> brandId;
//三级分类id
private Long catalog3Id;
//排序条件:sort=price/salecount/hotscore_desc/asc
private String sort;
//是否显示有货
private Integer hasStock;
//价格区间查询
private String skuPrice;
//按照属性进行筛选
private List<String> attrs;
//页码
private Integer pageNum = 1;
//原生的所有查询条件
private String _queryString;
}
返回结果
@Data
public class SearchResult {
//查询到的所有商品信息
private List<SkuEsModel> product;
//当前页码
private Integer pageNum;
//总记录数
private Long total;
//总页码
private Integer totalPages;
//页码遍历结果集(分页)
private List<Integer> pageNavs;
//当前查询到的结果,所有涉及到的品牌
private List<BrandVo> brands;
//当前查询到的结果,所有涉及到的所有属性
private List<AttrVo> attrs;
//当前查询到的结果,所有涉及到的所有分类
private List<CatalogVo> catalogs;
//===========================以上是返回给页面的所有信息============================//
/* 面包屑导航数据 */
private List<NavVo> navs;
@Data
public static class NavVo {
private String navName;
private String navValue;
private String link;
}
@Data
@AllArgsConstructor
public static class BrandVo {
private Long brandId;
private String brandName;
private String brandImg;
}
@Data
@AllArgsConstructor
public static class AttrVo {
private Long attrId;
private String attrName;
private List<String> attrValue;
}
@Data
@AllArgsConstructor
public static class CatalogVo {
private Long catalogId;
private String catalogName;
}
}
2) 主体逻辑
主要逻辑在service层进行,service层将封装好的SearchParam
组建查询条件,再将返回后的结果封装成SearchResult
@GetMapping(value = {"/search.html","/"})
public String getSearchPage(SearchParam searchParam, Model model, HttpServletRequest request) {
searchParam.set_queryString(request.getQueryString());
SearchResult result=searchService.getSearchResult(searchParam);
model.addAttribute("result", result);
return "search";
}
public SearchResult getSearchResult(SearchParam searchParam) {
SearchResult searchResult= null;
//通过请求参数构建查询请求
SearchRequest request = bulidSearchRequest(searchParam);
try {
SearchResponse searchResponse = restHighLevelClient.search(request, GulimallElasticSearchConfig.COMMON_OPTIONS);
//将es响应数据封装成结果
searchResult = bulidSearchResult(searchParam,searchResponse);
} catch (IOException e) {
e.printStackTrace();
}
return searchResult;
}
3) 构建查询条件
这一部分就是对着前面分析的DSL,将每个条件封装进请求中
private SearchRequest bulidSearchRequest(SearchParam searchParam) {
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//1. 构建bool query
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
//1.1 bool must
if (!StringUtils.isEmpty(searchParam.getKeyword())) {
boolQueryBuilder.must(QueryBuilders.matchQuery("skuTitle", searchParam.getKeyword()));
}
//1.2 bool filter
//1.2.1 catalog
if (searchParam.getCatalog3Id()!=null){
boolQueryBuilder.filter(QueryBuilders.termQuery("catalogId", searchParam.getCatalog3Id()));
}
//1.2.2 brand
if (searchParam.getBrandId()!=null&&searchParam.getBrandId().size()>0) {
boolQueryBuilder.filter(QueryBuilders.termsQuery("brandId",searchParam.getBrandId()));
}
//1.2.3 hasStock
if (searchParam.getHasStock() != null) {
boolQueryBuilder.filter(QueryBuilders.termQuery("hasStock", searchParam.getHasStock() == 1));
}
//1.2.4 priceRange
RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("skuPrice");
if (!StringUtils.isEmpty(searchParam.getSkuPrice())) {
String[] prices = searchParam.getSkuPrice().split("_");
if (prices.length == 1) {
if (searchParam.getSkuPrice().startsWith("_")) {
rangeQueryBuilder.lte(Integer.parseInt(prices[0]));
}else {
rangeQueryBuilder.gte(Integer.parseInt(prices[0]));
}
} else if (prices.length == 2) {
//_6000会截取成["","6000"]
if (!prices[0].isEmpty()) {
rangeQueryBuilder.gte(Integer.parseInt(prices[0]));
}
rangeQueryBuilder.lte(Integer.parseInt(prices[1]));
}
boolQueryBuilder.filter(rangeQueryBuilder);
}
//1.2.5 attrs-nested
//attrs=1_5寸:8寸&2_16G:8G
List<String> attrs = searchParam.getAttrs();
BoolQueryBuilder queryBuilder = new BoolQueryBuilder();
if (attrs!=null&&attrs.size() > 0) {
attrs.forEach(attr->{
String[] attrSplit = attr.split("_");
queryBuilder.must(QueryBuilders.termQuery("attrs.attrId", attrSplit[0]));
String[] attrValues = attrSplit[1].split(":");
queryBuilder.must(QueryBuilders.termsQuery("attrs.attrValue", attrValues));
});
}
NestedQueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery("attrs", queryBuilder, ScoreMode.None);
boolQueryBuilder.filter(nestedQueryBuilder);
//1. bool query构建完成
searchSourceBuilder.query(boolQueryBuilder);
//2. sort eg:sort=saleCount_desc/asc
if (!StringUtils.isEmpty(searchParam.getSort())) {
String[] sortSplit = searchParam.getSort().split("_");
searchSourceBuilder.sort(sortSplit[0], sortSplit[1].equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC);
}
//3. 分页
searchSourceBuilder.from((searchParam.getPageNum() - 1) * EsConstant.PRODUCT_PAGESIZE);
searchSourceBuilder.size(EsConstant.PRODUCT_PAGESIZE);
//4. 高亮highlight
if (!StringUtils.isEmpty(searchParam.getKeyword())) {
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("skuTitle");
highlightBuilder.preTags("<b style='color:red'>");
highlightBuilder.postTags("</b>");
searchSourceBuilder.highlighter(highlightBuilder);
}
//5. 聚合
//5.1 按照brand聚合
TermsAggregationBuilder brandAgg = AggregationBuilders.terms("brandAgg").field("brandId");
TermsAggregationBuilder brandNameAgg = AggregationBuilders.terms("brandNameAgg").field("brandName");
TermsAggregationBuilder brandImgAgg = AggregationBuilders.terms("brandImgAgg").field("brandImg");
brandAgg.subAggregation(brandNameAgg);
brandAgg.subAggregation(brandImgAgg);
searchSourceBuilder.aggregation(brandAgg);
//5.2 按照catalog聚合
TermsAggregationBuilder catalogAgg = AggregationBuilders.terms("catalogAgg").field("catalogId");
TermsAggregationBuilder catalogNameAgg = AggregationBuilders.terms("catalogNameAgg").field("catalogName");
catalogAgg.subAggregation(catalogNameAgg);
searchSourceBuilder.aggregation(catalogAgg);
//5.3 按照attrs聚合
NestedAggregationBuilder nestedAggregationBuilder = new NestedAggregationBuilder("attrs", "attrs");
//按照attrId聚合
TermsAggregationBuilder attrIdAgg = AggregationBuilders.terms("attrIdAgg").field("attrs.attrId");
//按照attrId聚合之后再按照attrName和attrValue聚合
TermsAggregationBuilder attrNameAgg = AggregationBuilders.terms("attrNameAgg").field("attrs.attrName");
TermsAggregationBuilder attrValueAgg = AggregationBuilders.terms("attrValueAgg").field("attrs.attrValue");
attrIdAgg.subAggregation(attrNameAgg);
attrIdAgg.subAggregation(attrValueAgg);
nestedAggregationBuilder.subAggregation(attrIdAgg);
searchSourceBuilder.aggregation(nestedAggregationBuilder);
log.debug("构建的DSL语句 {}",searchSourceBuilder.toString());
SearchRequest request = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX}, searchSourceBuilder);
return request;
}
4) 封装响应结果
private SearchResult bulidSearchResult(SearchParam searchParam, SearchResponse searchResponse) {
SearchResult result = new SearchResult();
SearchHits hits = searchResponse.getHits();
//1. 封装查询到的商品信息
if (hits.getHits()!=null&&hits.getHits().length>0){
List<SkuEsModel> skuEsModels = new ArrayList<>();
for (SearchHit hit : hits) {
String sourceAsString = hit.getSourceAsString();
SkuEsModel skuEsModel = JSON.parseObject(sourceAsString, SkuEsModel.class);
//设置高亮属性
if (!StringUtils.isEmpty(searchParam.getKeyword())) {
HighlightField skuTitle = hit.getHighlightFields().get("skuTitle");
String highLight = skuTitle.getFragments()[0].string();
skuEsModel.setSkuTitle(highLight);
}
skuEsModels.add(skuEsModel);
}
result.setProduct(skuEsModels);
}
//2. 封装分页信息
//2.1 当前页码
result.setPageNum(searchParam.getPageNum());
//2.2 总记录数
long total = hits.getTotalHits().value;
result.setTotal(total);
//2.3 总页码
Integer totalPages = (int)total % EsConstant.PRODUCT_PAGESIZE == 0 ?
(int)total / EsConstant.PRODUCT_PAGESIZE : (int)total / EsConstant.PRODUCT_PAGESIZE + 1;
result.setTotalPages(totalPages);
List<Integer> pageNavs = new ArrayList<>();
for (int i = 1; i <= totalPages; i++) {
pageNavs.add(i);
}
result.setPageNavs(pageNavs);
//3. 查询结果涉及到的品牌
List<SearchResult.BrandVo> brandVos = new ArrayList<>();
Aggregations aggregations = searchResponse.getAggregations();
//ParsedLongTerms用于接收terms聚合的结果,并且可以把key转化为Long类型的数据
ParsedLongTerms brandAgg = aggregations.get("brandAgg");
for (Terms.Bucket bucket : brandAgg.getBuckets()) {
//3.1 得到品牌id
Long brandId = bucket.getKeyAsNumber().longValue();
Aggregations subBrandAggs = bucket.getAggregations();
//3.2 得到品牌图片
ParsedStringTerms brandImgAgg=subBrandAggs.get("brandImgAgg");
String brandImg = brandImgAgg.getBuckets().get(0).getKeyAsString();
//3.3 得到品牌名字
Terms brandNameAgg=subBrandAggs.get("brandNameAgg");
String brandName = brandNameAgg.getBuckets().get(0).getKeyAsString();
SearchResult.BrandVo brandVo = new SearchResult.BrandVo(brandId, brandName, brandImg);
brandVos.add(brandVo);
}
result.setBrands(brandVos);
//4. 查询涉及到的所有分类
List<SearchResult.CatalogVo> catalogVos = new ArrayList<>();
ParsedLongTerms catalogAgg = aggregations.get("catalogAgg");
for (Terms.Bucket bucket : catalogAgg.getBuckets()) {
//4.1 获取分类id
Long catalogId = bucket.getKeyAsNumber().longValue();
Aggregations subcatalogAggs = bucket.getAggregations();
//4.2 获取分类名
ParsedStringTerms catalogNameAgg=subcatalogAggs.get("catalogNameAgg");
String catalogName = catalogNameAgg.getBuckets().get(0).getKeyAsString();
SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo(catalogId, catalogName);
catalogVos.add(catalogVo);
}
result.setCatalogs(catalogVos);
//5 查询涉及到的所有属性
List<SearchResult.AttrVo> attrVos = new ArrayList<>();
//ParsedNested用于接收内置属性的聚合
ParsedNested parsedNested=aggregations.get("attrs");
ParsedLongTerms attrIdAgg=parsedNested.getAggregations().get("attrIdAgg");
for (Terms.Bucket bucket : attrIdAgg.getBuckets()) {
//5.1 查询属性id
Long attrId = bucket.getKeyAsNumber().longValue();
Aggregations subAttrAgg = bucket.getAggregations();
//5.2 查询属性名
ParsedStringTerms attrNameAgg=subAttrAgg.get("attrNameAgg");
String attrName = attrNameAgg.getBuckets().get(0).getKeyAsString();
//5.3 查询属性值
ParsedStringTerms attrValueAgg = subAttrAgg.get("attrValueAgg");
List<String> attrValues = new ArrayList<>();
for (Terms.Bucket attrValueAggBucket : attrValueAgg.getBuckets()) {
String attrValue = attrValueAggBucket.getKeyAsString();
attrValues.add(attrValue);
List<SearchResult.NavVo> navVos = new ArrayList<>();
}
SearchResult.AttrVo attrVo = new SearchResult.AttrVo(attrId, attrName, attrValues);
attrVos.add(attrVo);
}
result.setAttrs(attrVos);
// 6. 构建面包屑导航
List<String> attrs = searchParam.getAttrs();
if (attrs != null && attrs.size() > 0) {
List<SearchResult.NavVo> navVos = attrs.stream().map(attr -> {
String[] split = attr.split("_");
SearchResult.NavVo navVo = new SearchResult.NavVo();
//6.1 设置属性值
navVo.setNavValue(split[1]);
//6.2 查询并设置属性名
try {
R r = productFeignService.info(Long.parseLong(split[0]));
if (r.getCode() == 0) {
AttrResponseVo attrResponseVo = JSON.parseObject(JSON.toJSONString(r.get("attr")), new TypeReference<AttrResponseVo>() {
});
navVo.setNavName(attrResponseVo.getAttrName());
}
} catch (Exception e) {
log.error("远程调用商品服务查询属性失败", e);
}
//6.3 设置面包屑跳转链接
String queryString = searchParam.get_queryString();
String replace = queryString.replace("&attrs=" + attr, "").replace("attrs=" + attr+"&", "").replace("attrs=" + attr, "");
navVo.setLink("http://search.gulimall.com/search.html" + (replace.isEmpty()?"":"?"+replace));
return navVo;
}).collect(Collectors.toList());
result.setNavs(navVos);
}
return result;
}
4.返回内容
{
"took" : 2,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : 0.0,
"hits" : [
{
"_index" : "gulimall_product",
"_type" : "_doc",
"_id" : "82",
"_score" : 0.0,
"_source" : {
"attrs" : [
{
"attrId" : 7,
"attrName" : "机身长度",
"attrValue" : "158.3mm"
},
{
"attrId" : 8,
"attrName" : "CPU品牌",
"attrValue" : "英特尔"
}
],
"brandId" : 6,
"brandImg" : "https://ming-mall.oss-cn-beijing.aliyuncs.com/2021/10/31/8f88b9d0-1c5c-4b3b-8b8e-096b37bc4fc1_key_login.png",
"brandName" : "苹果",
"catalogId" : 225,
"catalogName" : "手机",
"hasStock" : false,
"hotScore" : 0,
"saleCount" : 0,
"skuId" : 82,
"skuImg" : "https://ming-mall.oss-cn-beijing.aliyuncs.com/2021/10/31/a4a6e721-7de0-4eb6-8490-add13221a7b7_key_login.png",
"skuPrice" : 4444.0,
"skuTitle" : "iphone11 aaa aaa 8G 16",
"spuId" : 39
}
}
]
},
"aggregations" : {
"brandAgg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : 6,
"doc_count" : 2,
"brandImgAgg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "https://ming-mall.oss-cn-beijing.aliyuncs.com/2021/10/31/8f88b9d0-1c5c-4b3b-8b8e-096b37bc4fc1_key_login.png",
"doc_count" : 2
}
]
},
"brandNameAgg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "苹果",
"doc_count" : 2
}
]
}
}
]
},
"catalogAgg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : 225,
"doc_count" : 2,
"catalogNameAgg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "手机",
"doc_count" : 2
}
]
}
}
]
},
"attrs" : {
"doc_count" : 4,
"attrIdAgg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : 7,
"doc_count" : 2,
"attrNameAgg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "机身长度",
"doc_count" : 2
}
]
},
"attrValueAgg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "158.3mm",
"doc_count" : 2
}
]
}
},
{
"key" : 8,
"doc_count" : 2,
"attrNameAgg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "CPU品牌",
"doc_count" : 2
}
]
},
"attrValueAgg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "英特尔",
"doc_count" : 2
}
]
}
}
]
}
}
}
}
物品详细
@GetMapping({"/{skuId}.html"})
使用线程池ThreadPoolExecutor,通过CompletableFuture.supplyAsync,runAsync,先获取Sku基本信息,再通过SpuId获取spu销售属性、介绍、规格参数、优惠信息,使用CompletableFuture.thenAcceptAsync串行处理,CompletableFuture.allOf并行
登录
session子域名下不可共享,使用springsession
多域名下不能共享,jwt,单点登录
/xxl-sso-server 登录服务器8080 ssoserver.com
client1.com 8081
client2.com 8082
dns
核心:
三个系统域名不一样,给三个系统同一个用户的凭证
中央认证服务器: ssoserver.com
其他系统想要登录(无token session),去ssoserver.com登录(携带回调地址),登录成功跳转回来(并携带token令牌,添加sso域名下的cookie,当下次再来的时候,@CookieValue(“sso-token”) !null 就知道已经登录成功了,看redis正不正确,返回token)
只要一个登录过,其他都不用登录
全系统统一sso-sessionid(token:UUID,token伪造),所有系统域名都不相同
用户信息通过token获取
购物车
在线购物车
- mongodb
- reds
离线购物车
- localstorage(缺点:后台难数据分析,
- websql(前端)
- redis
list,无法定位,使用hash,能通过skuid定位,cart:userid > skuId > cartItemInfo
第一次进入,要创建临时用户,cookie标识用户身份(一个月),登录后也是同一个
游客intercepter
@Configuration
public class GuliWebConfig implements WebMvcConfigurer() {
@Override
public void addInterceptor(InterceptorRegistry registry) {
registry
.addInterceptor(new CartInterceptor())
.addPathPatterns("/**"); // TODO 排除静态资源
}
}
public Class CarInterceptor implements HandlerIntercepter {
// 线程共享
public static ThreadLocal<UserInfoTo> = new ThreadLocal<>();
@Override
public boolean preHandle() {
threadLocal.set(userInfo);
// 创建临时用户
if(nonUser()) {
// uuid
}
}
@Override
public void postHandle(HttpServletRequest, HttpServletResponse, Object handler) {
if(nonTempUserInfo())
Threadlocal.get();
new Cookie(cookie_name, userInfo.getUserKey());
cookie.setDomain("gulimall.com");
cookie.setMaxAge(30 * 24* 60 *60);
response.addCookies();
}
}
ThreadLocal
同一个线程共享数据,intercepter、controller、service、dao
Map<Thread, Object> threadLocal
// intercepter中
// 线程内共享
// static本来是类共享的,但是ThreadLocal根除了对变量的共享,使用static避免每new一个实例就创建一个ThreadLocal,造成浪费,避免重复创建TSO(与线程相关的变量)
public static ThreadLocal<UserInfoTo> = new ThreadLocal<>(); // 此处强引用
// = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));
// controller中
UserInfoTo userInfoTo = CartInterceptor.threadLoacl.get();
// TODO ThreadLocal用完需要remove,否则如果是线程池里的线程,线程没有回收,即使key是强引用也会有问题,如果key是弱引用,map里的value就一直无法回收,所以会被GC
订单
电商涉及信息流、资金流、物流
【值得学习】
订单状态
等待付款: 库存锁定,超时取消
已付款/代发货: 支付订单、支付流水单号,仓库调拨、配货、分拣、出库
待收货、已发货:同步物流信息
已完成: 订单完成、售后
取消:超时或取消
售后中:申请退款,申请退换货
登录拦截
获取购物车
注意:调用购物车接口时,根据cookie或者session获取用户信息,但是feign没携带,feign在远程调用时,构造请求,header为null,需要加上feign远程调用的请求拦截器 p267【值得学习】
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
@Configuration
public class GuliFeignConfig {
@Bean
public RequestInterceptor requestInterceptor() {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
//1. 使用RequestContextHolder拿到老请求的请求数据
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
HttpServletRequest request = requestAttributes.getRequest();
if (request != null) {
//2. 将老请求得到cookie信息放到feign请求上
String cookie = request.getHeader("Cookie");
template.header("Cookie", cookie);
}
}
}
};
}
}
异步线程下feign请求无请求头,【值得学习】
CompletableFuture.supplyAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> checkedItems = cartFeignService.getCheckedItems();
return checkedItems;
}, executor)
接口幂等性
不能重复提交订单,多次点击按钮
页面回退提交
微服务请求失败,feign重试
Token机制:什么时候删除令牌,先删和后删?网络延迟,要原子性,lua脚本
【锁】悲观锁:
select * from xx where id = 1 for update
,id一定是索引,否则会锁表【锁】乐观锁,携带版本号:
update table set count = count -1, version = version +1 where good_id = 1 and version = 1
【锁】业务层分布式锁
【唯一约束】redis set防重
【唯一约束】mysql唯一约束
防重表,订单号orderNo唯一索引,订单解锁库存
全局唯一Id:nginx设置请求,proxy_set_header X-Request-Id $request_id,但是点击按钮没用,feign有用
提交订单
验证令牌、创建订单,验证价格(计算的价格和提交的价格对比是否有变)、锁库存(所有仓库的数量是否足够)
原子性令牌
String script= "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Long execute = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Arrays.asList( OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId() ),
submitVo.getOrderToken()
);
BigDecimal【值得学习】
进行BigDecimal进行计算时,要使用String构造器,否则可能发生损失精度问题
bigDecimalPrice.multiply(new BigDecimal(count.toString()));
// 精度
Math.abs( onePrice.substract( otherPrice ).doubleValue() ) < 0.01;
唯一订单号:
事务【值得学习】
// 默认运行时异常就回滚
@Transactional(rollbackFor = RuntimeException.class)
分布式下事务的问题,根据feign的状态码(扣库存),第二次抛出异常,但是主业务(订单服务中的扣积分)出网络等异常,已经执行的副业务(远程服务,扣库存feign)将不会回滚。
使用seata,使用rabbitmq延时队列
解决本地事务下远程调用无法恢复【值得学习】
使用分布式事务太慢了,创建订单(本地会回滚),锁定库存(本地错误,远程不会回滚),使用延时队列
下单成功,过期没支付、取消订单,都要解锁库存
下单成功,锁定成功,业务调用失败,订单回滚,锁定的库存也要回滚,使用延时队列
怎么知道要不要回滚?将订单信息、订单详细信息保存起来,通过延时队列
如果订单详细记录查不到,说明已经回滚,不用恢复
如果订单状态查不到,说明订单已经回滚,要恢复库存
如果查得到,说明没有回滚,但是可能是未支付的或着取消订单的(除了已经缴费的),要恢复库存
订单定时取消
【值得学习】
在提交订单时,添加到延时队列中,进行订单关闭,关闭时告诉mq进行库存回滚
收单
支付页不支付,订单过期了才成功支付,但是库存解锁了
- 自动收单,一段时间不支付,就不能支付(easySDK不能添加)
时延,订单解锁完成,解锁库存的时候,异步通知才到
- 订单解锁时,手动使用支付宝收单
秒杀
独立的模块,崩了也不影响其他模块
预热,quartz、xxl-job
corn表达式神器:https://www.pppet.net,定时任务分布式锁
商品数据缓存:seckill:skus 1:87
valueJson
{
"endTime": 1638547200000,
"id": 2,
"promotionSessionId": 1,
"randomCode": "33713d990fd2469caa18ef8d03473dd2",
"seckillCount": 11,
"seckillLimit": 1,
"seckillPrice": 10,
"seckillSort": 0,
"skuId": 87,
"startTime": 1638374400000
}
场次缓存:seckill:sessions:1638374400000_1638547200000 ,活动商品 [ 活动id1-87 ],获取所有redisTemplate.opsForList().range(key, 0, -1);
库存信号量:seckill:stock:#商品随机码
商品随机码:以免一直请求
异步
自带的cron使用线程池,同时只有一个线程,spring.task.scheduling.pool.size=5
不一定好用,TaskSchedulingAutoConfiguration
CompletableFuture.runAsync(()-> {
}, executor);
@EnableAsync
@EnableScheduing
// 方法,提交给线程池,TaskExecutionAutoConfiguration,默认无限
@Async
// spring.task.execution.pool.max-pool=50
提前上架
cron(分布式锁内)提前三天上架,幂等性处理,防止添加多条数据
日期处理LocalDate
//当前天数的 00:00:00
private String getStartTime() {
LocalDate now = LocalDate.now();
LocalDateTime time = now.atTime(LocalTime.MIN);
String format = time.format(
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
);
return format;
}
高并发
三宝:
缓存、异步、队排好
需要注意的 问题:
- 服务单一职责,独立部署,挂掉不影响别人
- 秒杀链接加密,防止恶意攻击
- 预热库存,快速扣减,直接存入redis,信号量控制请求
- 动静分离,CDN,保证动态才进入后端集群
- 恶意请求拦截,网关识别非法攻击并拦截
- 流量错峰(验证码)
- 限流 熔断 降级,前端限流(按钮灰色),后端限流(次数、总量),快速失败降级运行,熔断隔离防止雪崩
- 队列消峰,秒杀成功进入队列,慢慢扣库存
一、
二
随机订单号:IdWorker.getTimeId()
mybatis-plus
void deleteBatchRelation(@Param("attrAttrGroupRelationVos") List<AttrAttrGroupRelationVo> attrAttrGroupRelationVos);
<delete id="deleteBatchRelation" parameterType="list">
delete from pms_attr_attrgroup_relation
<where>
<foreach
collection="attrAttrGroupRelationVos"
item="attrAttrGroupRelationVo"
separator=" or ">
attr_id = #{attrAttrGroupRelationVo.attrId}
and
attr_group_id = #{attrAttrGroupRelationVo.attrGroupId}
</foreach>
</where>
</delete>
常用方法
BeanUtils.copyProperties(old, new);
CollectionUtils.isEmpty(list); // isNotBlack
String.join(",", arr);
前端
pubsub
如何使用?
1、npm install –save pubsub-js
2、在src下的main.js中引用:
① import PubSub from ‘pubsub-js’
② Vue.prototype.PubSub = PubSub
mysql
set session transaction isolation level read uncommitted
# 能够查看到到未提交的数据
java基础
R
远程调用,每次调用r.get("")
都要转化类型,可以使用T泛型
- R使用泛型,添加data
,注意在hashmap中无效 - 直接返回想要的类型,不返回R
// R中用fastjson进行逆转
public <T> T getData(TypeReference<T> typeReference) {
Object data = get("data");
String s = JSON.toJSONString(data);
T t = JSON.parseObject(s, typeReference);
return t;
}
调用
TypeReference<List<SkuHasStockVo>> typeReference = new TypeReference<List<SkuHasStockVo>>() {};
r.getData(typeReference);
R泛型,远程调用自动转化
问题,无法接受,有可能是HashMap的json,序列化方式是hashMap的
public class R<T> extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
@RequestMapping("/ware/waresku/getSkuHasStocks")
R<List<SkuHasStockVo>> getSkuHasStocks(@RequestBody List<Long> ids);
R<List<SkuHasStockVo>> skuHasStocksRes = wareFeignService.getSkuHasStocks(longList);
List<SkuHasStockVo> skuHasStocks = skuHasStocksRes.getData();
异步
继承Thread
实现Runnable接口
实现Callable + FutureTask(可以得到返回结果,可以处理异常)
带来的问题,资源消耗大,创造大量的线程,所以应该将多线程异步任务交给线程池执行
- 隆低资源的消耗
- 通过重复利用已经创建好的线程降低线程的创建和销毁带来的损耗
- 提高响应速度
- 因为线程池中的线程数没有超过线程池的最大上限时,有的线程处于等待分配任务的状态,当任务来时无需创建新的线程就能执行
- 提高线程的可管理性
- 线程池会根据当前系统特点对池内的线程进行优化处理,减少创建和销毁线程带来的系统开销。无限的创建和销毁线程不仅消耗系统资源,还降低系统的稳定性,使用线程池进行统一分配
// 1
main() {
Thread01 thread = new Thread01();
thread.start();
}
class Thread01 extends Thread {
@Override
public void run() {
}
}
// 2
new Thread( new Runable01() ).start;
class Runable01 implements Runable{
@Override
public void run() {
}
}
// 3
class Callable01 implements Callable<Integer>{
@Override
public Integer call throws Exception {
return i;
}
}
FutureTask<Integer> = new FutureTask<>( new Callable01() );
new Thread(futureTask).start();
Integer integer = futureTask.get();
线程池
public static ExecutorService service = Executors.newFixedThreadPool( 10 );
public void test() {
service.execute( new Runable01() );
/*
核心线程数: 一直存在
最大线程数量
存活时间: 线程空闲一段时间就释放,最少线程数为核心线程数
超时时间单位
阻塞队列: 如果任务很多,就会放入队列中,有线程空闲就取出
线程创建工厂:默认
拒绝策略: 队列满了的措施
*/
ThreadPoolExecutor executor = new ThreadPollExecutor(
5,
200,
10,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(100000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
}
运行流程:
1、线程池创建,准备好core数量的核心线程,准备接受任务
2、新的任务进来,用core准备好的空闲线程执行
core满了,就将再进来的任务放入阻塞队列中。空闲的core就会自己去阻塞队列获取任务执行
阻塞队列满了,就直接开新线程执行,最大只能开到max指定的数
max都执行好了。 Max-core数量空闲的线程会在 keepAlive Time指定的时间后自动销毁。最终保持到core大小
如果线程数开到了max的数量,队列也满了,还有新任务进来,就会使用 reject指定的拒绝策略进行处理
3、所有的线程创建都是由指定的 factory创建的。
Executors.newCachedThreadPool(); // core是,所有都可回收
Executors.newFixedthreadPool(); // 固定大小,core=max;都不可回收
Executors.newScheduledthreadpool(); // 定时任务的线程池
Executors.newSingleThreadExecutor(); // 单线程的线程池,后台从队列里面获取任务,挨个执行
CompletableFutrue
场景
public static ExecutorService executor = Executors.newFixedthreadPool();
public static void main(String[] args) {
// 1.runAsync,无结果
CompletableFuture<Void> futrue = CompletableFuture.runAsync(() -> {
}, executor);
// 2.supplyAsync,有结果
CompletableFuture<Integer> futrue = CompletableFuture.supplyAsync(() -> {
}, executor);
future.get(); // 获取结果
}
supplyAsync: 有结果
runAsync: 无结果
回调方法
// 方法完成后的感知
whenComplete(); // 急,执行完后当前任务的线程 继续 执行 whenComplete 的任务
whenCompleteAsync(); // 不急,把CompleteAsyn的任务继续提交给线程池
exceptionally(); // 直接拿到异常
// 方法执行后的处理
handle(); //
CompletableFuture<Integer> futrue = CompletableFuture.supplyAsync(() -> {
}, executor).whenComplete( (result, exception) -> { // 能得到异常信息,不能修改结果
}).exceptionally( throwable -> { // 能够感知异常,返回默认值
return 10;
});
future.get();
// 方法执行后的处理
CompletableFuture<Integer> futrue = CompletableFuture.supplyAsync(() -> {
}, executor).handle( (result, thr) -> {
if(result != null) {
return res * 2;
}
if(thr ! = null) {
return 0
}
return 0;
}
串行化
A -> B
thenApply: 能获取返回值且有返回值,一个线程依赖另一个线程,获取上一个任务返回的结果,并返回当前任务的返回值
thenAccept: 能获取返回值,消费处理结果,接受任务的处理结果,并处理消费,无返回结果
thenRun: 不能获取返回值,只要上面的任务执行完成,就执行
# 带Async则放入线程池中处理
两任务组合
# 两个都运行完
thenCombine: 获取结果,修改返回值
thenAcceptBoth: 获取结果
runAfterBoth: 运行完后,运行该任务
# 最先运行完
applyToEither: 获取结果,修改返回值
acceptEither: 只能获取结果
runAfterEither: 运行完后,运行该任务
# async ,加入线程池运行
CompletableFuture<Integer> futrue = CompletableFuture.supplyAsync(() -> {
}, executor);
// 两个都运行完
future01.thenCombineAsync(funture02, (f1, f2) -> { // .applyToEither(funture02, res -> {})
return f1 + f2;
}, executor);
多任务
allOf
anyOf
CompletableFutrue<void> allof = CompletableFuture.allOf(future01, future02, future03);
allOf.get(); // 等待所有运行完
// future01.get(); future02.get();
CompletableFutrue<Object> anyof = CompletableFuture.anyOf(future01, future02, future03);
anyof.get(); // 其中一个运行完
使用
Properties
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
@ConfigurationProperties(prefix = "ming.thread")
@Component // 自动注入
@Data
public class ThreadPoolConfigProperties {
private Integer coreSize;
private Integer maxSize;
private Integer keepAliveTime;
}
ming:
thread:
core-size: 20
max-size: 200
keep-alive-Time: 10
Bean
// @EnableConfigurationProperties( ThreadPoolConfigProperties.class ) // 如果上面没写@Component就需要写
@Configuration
public class MyThreadConfig {
@Bean
public ThreadPoolExecutor( ThreadPoolConfigProperties pool ) {
return new ThreadPollExecutor(
pool.getCoreSize(),
pool.getMaxSize(),
pool.getKeepAliveTime(),
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(100000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
}
}
@Autowired
ThreadPoolExecutor executor;
CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
// 1、sku基本信息的获取 pms_sku_info
SkuInfoEntity skuInfoEntity = this.getById(skuId);
skuItemVo.setInfo(skuInfoEntity);
return skuInfoEntity;
}, executor);
// 4、获取spu的介绍-> 依赖1 获取spuId
CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync((info) -> {
SpuInfoDescEntity byId = spuInfoDescService.getById(info.getSpuId());
skuItemVo.setDesc(byId);
}, executor);
// 等待所有任务执行完成
try {
CompletableFuture.allOf(imageFuture, saleFuture, descFuture, attrFuture).get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
常量
public class ProductConstant {
public enum AttrEnum{
ATTR_TYPE_BASE(1,"基本属性"),
ATTR_TYPE_SALE(0,"销售属性");
private int code;
private String msg;
AttrEnum(int code,String msg){
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
public enum ProductStatusEnum {
}
}
springMVC
空视图映射
@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {
/**
* 视图映射
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login.html").setViewName("login");
registry.addViewController("/reg.html").setViewName("reg");
}
}
转发、重定向
// 转发,携带数据
return "index"; // 拼接前后缀得到转发的页面
return "forward:/reg.html"; // 不拼接,直接得到完整地址,还会去找controller映射,多走一步
return "redirect:http://auth.mingyuefusu.cn/index.html"; // 重定向,不携带数据
// 重定向想携带数据,将其存储在session中
@PostMapping("/register")
public String register(RedirectAttributes attributes) {
Map<String, String> errors = new HashMap<>();
attributes.addFlashAttribute("errors", errors); // 放到session中,只能取一次
attributes.addAttribute("param", param); // ?param=param
return "redirect:http://auth.gulimall.com/reg.html";
}
// <div style="color: red" th:text="${errors != null ? (#maps.containsKey(errors, 'msg') ? errors.msg : '') : ''}"/>
接收日期
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") // 入参
@JSONField(format="yyyy-MM-dd HH:mm:ss", name="resType") // 入参
@JsonFormat(pattern="yyyy-MM-dd", timezone="GMT+8") // 出参
<el-form-item label="每日结束时间" prop="endTime">
<el-date-picker type="datetime" placeholder="每日结束时间" v-model="dataForm.endTime" value-format="yyyy-MM-dd HH:mm:ss"></el-date-picker>
</el-form-item>
Date.prototype.format = function(fmt) {
var o = {
"M+" : this.getMonth()+1, //月份
"d+" : this.getDate(), //日
"h+" : this.getHours(), //小时
"m+" : this.getMinutes(), //分
"s+" : this.getSeconds(), //秒
"q+" : Math.floor((this.getMonth()+3)/3), //季度
"S" : this.getMilliseconds() //毫秒
};
if(/(y+)/.test(fmt)) {
fmt=fmt.replace(RegExp.$1, (this.getFullYear()+"").substr(4 - RegExp.$1.length));
}
for(var k in o) {
if(new RegExp("("+ k +")").test(fmt)){
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length==1) ? (o[k]) : (("00"+ o[k]).substr((""+ o[k]).length)));
}
}
return fmt;
}
new Date().format("yyyy-MM-dd hh:mm:ss")
BUG
购物车一次全部选完了
五、工具
插件集
浏览器
GitHub加速
VScode
IntelliJ IDEA Keybindings:快捷键
json处理
idea插件
https://plugins.jetbrains.com/search
GenerateAllSetter
自动生成对象set alt + enter
Lombok
GsonFormat plus
alt + s
模块 | 设置 | 是否默认 | 说明 |
---|---|---|---|
Convert Method | object/arrayFromData | 否 | Gson自定义生成对象 |
Generate | virgo mode | 是 | virgo模式,生成代码之前可自定义调整字段 |
Generate | generate comments | 否 | 是否生成注释 |
Generate | split generate | 否 | 是否单独生成子类 |
Bean | reuse bean | 否 | TODO |
Field | name suffix | 是 | 生成类名后缀 |
Field | field(private/public) | 是 | 字段私有/公开 |
Field | name prefix | 是 | 生成字段名前缀 |
Field | use serialized name | 是 | 使用序列化名,类字段为驼峰与添加json注解声明 |
Field | use wrapper class | 是 | 使用包装类,int 转 Integer |
Field | use lombok | 是 | 使用Lombok替代Getter和Setter |
Field | use number key as map | 是 | 使用数字类型key替换为Map结构,待完善 |
Convert library | jackson/fastjson | 是 | jackson/fastjson等转换注解 |
RestfulTool
mybatisx / Free Mybatis plugin
Camel Case
大小写 alt + shift + u
插件install jar
setting 修改要变换的格式
Grep Console
控制台输出处理
Rainbow Brackets
彩虹括号
Alibaba Java Code Guidelines
代码检查
Codota
代码智能提示
idea其他
主题
https://www.jb51.net/article/133897.htm
模板
editor》live Template,记得选择生效范围
调整java内存
-Xmx100m
启动多服务
右键copy configuration
program arguments: –server.port=10000
六、记录
注意
连表查询注意判断空指针
第一次请求,数据库连接池等可能没创建,很容易失败
业务总结
树形结构(分类)
前端
使用tree、拖拽时需要判断层级关系
分类显示需要发返回分类id路径[1,2,34]
后端
递归获取树形结构,先获取父节点,递归方法中获取每一个节点的子节点,可以使用到stream、map、filter、collect(Collectors.toList())、sort(要加入的,原来的)
数据库逻辑分析
需要powerdesign分析
属性表修改
如果没有建立关联,只进行修改操作将失效,因为没有关联,需要先判断是否建立了关联
接收前端数据
记得@RequestBody、RequestParam、PathVariable
获取分组未关联的属性
- 获取分组的分类id
- 获取所有已经匹配的属性
- notin进行剔除以上属性
- 并筛选获取同一分类、基本属性
- 再拼接like
二月
3日
使用docker中的数据库连接一会后异常
无效
datasource:
username: root
password:
url: jdbc:mysql://106.75.103.68:3306/gulimall_pms?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
minimum-idle: 3 # 最小空闲连接数量
maximum-pool-size: 5 # 连接池最大连接数,默认是10
max-lifetime: 1800000 # 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟
connection-test-query: SELECT 1
4日
基础不牢,mybatis操作不会,springboot相关注解生疏
疑问
nacos配置失效
spring.application.name=gulimall-auth-server
spring.cloud.nacos.config.server-addr=101.227.11.219:8848
spring.cloud.nacos.discovery.server-addr=101.227.11.219:8848