做项目不是为了敲代码,而是为了学知识,学原理,不深入去理解底层原理的话就是普通的 CRUD 工程师。 项目中涉及的比较重要的内容可以查看 Wiki 页面,或者 awesome-architect。
现在文档中的知识点还比较有限,内容还在持续完善中。以后将会逐渐深入源码,分析学习运行原理与机制。
微服务架构电商系统,主要分为三个阶段。
Apache Shiro
权限控制因为准备秋招耽误了许久,现在重新开始,重新启动项目时遇到许多麻烦,下面记录一下大致步骤,避免以后忘记。
系统为 macOS BigSur
,包管理工具为 Homebrew
。
MySQL
建立相应数据库,并运行相应 sql
文件,数据库账号密码为 root/root
。mall-commons
到本地 Maven
仓库。# 进入到 mall-commons 所在目录
$ mvn clean
$ mvn install
nginx
服务器中,并且启动 nginx
服务器。在渲染商城首页三级菜单时,如果没有正确显示,需要注意 catalogLoader.js
这个文件中的请求地址。
nca7
,下载后将其放到 /usr/local/var/www
目录下,非 Homebrew
安装的 Nginx
需要注意一些路径。cart、index、item、login、member、order、register、search
几个静态资源目录。$ brew install nginx
$ brew info nginx
# 静态文件目录 /usr/local/var/www/static
Redis
服务器$ brew install redis
$ redis-server /usr/local/etc/redis.conf
1.2.1
,或者下载 Release 版本。# 启动命令 ( standalone 代表着单机模式运行,非集群模式)
$ sh startup.sh -m standalone
// localhost:8848/nacos
2.23.0
。$ curl -sSL https://zipkin.io/quickstart.sh | bash -s
$ java -jar zipkin.jar
Elasticsearch
,建立相应的索引 product
(下文有)。$ brew install elasticsearch
# 如果内存不是特别大的话,最好设置一下 ES 启动时的虚拟机参数,在 mac 中 Brew 安装目录是
# /usr/local/Cellar/elasticsearch/7.10.0/libexec/config/jvm.options
ElasticSearch
版本对应的 ik
分词器,并将其放入 ES
目录下的 plugins
目录下。下载链接 elasticsearch-analysis-ik。# mac 终端打开
$ open /usr/local/Cellar/elasticsearch/7.10.0/libexec/plugins
RabbitMQ
,在此选择客户端。$ brew cask install rabbitmq
OSS
对象存储配置信息(用于图片存储)、短信配置,在启动 mall-thirdparty
模块时需要配置 endpoint
。完成上面几个步骤之后,项目正常启动。
CentOS 虚拟机
阿里云服务器
本地虚拟机
安装 MySQL
$ docker pull mysql:5.7
$ docker run -p 3306:3306 --name mysql \
-v /mydata/mysql/log:/var/log/mysql \
-v /mydata/mysql/data:/var/lib/mysql \
-v /mydata/mysql/conf:/etc/mysql \
-e MYSQL_ROOT_PASSWORD=root \
-d mysql:5.7
$ docker ps
[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
$ docker restart mysql
$ docker exec -it mysql /bin/bash
$ docker pull redis
$ mkdir -p /mydata/redis/conf
$ touch /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 ps
$ docker run --restart=always # 随机自启
$ docker update --restart=always <CONTAINER ID> # 随机自启
$ docker exec -it redis redis-cli
$ vi /mydata/redis/conf/redis.conf
# appendonly yes
$ docker restart redis
mall-product
mall-ware
mall-member
mall-coupon
mall-order
mall-seckill
xxl-sso
mall-commons
renren-fast
mall-gateway
mall-auth-server
mall-third-party
config-file
settings.xml
<!-- 阿里云镜像 -->
<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
<mirror>
<id>nexus-aliyun</id>
<mirrorOf>central</mirrorOf>
<name>Nexus Aliyun</name>
<url>https://maven.aliyun.com/nexus/content/groups/public</url>
</mirror>
<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<name>阿里云Google仓库</name>
<url>https://maven.aliyun.com/repository/google</url>
</mirror>
<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<name>阿里云Apache仓库</name>
<url>https://maven.aliyun.com/repository/apache-snapshots</url>
</mirror>
<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<name>阿里云Spring仓库</name>
<url>https://maven.aliyun.com/repository/spring</url>
</mirror>
<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<name>阿里云Spring插件仓库</name>
<url>https://maven.aliyun.com/repository/spring-plugin</url>
</mirror>
<!-- 编译环境 -->
<profile>
<id>jdk-1.8</id>
<activation>
<activeByDefault>true</activeByDefault>
<jdk>1.8</jdk>
</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>
$ npm config set registry http://registry.npm.taobao.org/
$ npm config get registry
$ npm config set registry https://registry.npmjs.org/
node-sass
Module build failed: Error: Node Sass does not yet support your current environment: OS X 64-bit with Unsupported runtime (72)
解决办法:
$ npm uninstall node-sass
$ npm i node-sass --sass_binary_site=https://npm.taobao.org/mirrors/node-sass/
$ npm run dev # 此时可成功
renren-generator
模块Vue
模板文件Spring Cloud Alibaba Nacos
: 注册中心(服务发现/注册),配置中心(动态配置管理)Spring Cloud Alibaba Sentinel
: 服务容错(限流、降级、熔断)Spring Cloud Alibaba Seata
: 分布式事务解决方案Spring Cloud OpenFeign
: 声明式 HTTP
客户端,远程服务调用。Spring Cloud Ribbon
: 负载均衡Spring Cloud Sleuth
: 调用链路监控追踪<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.1.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
spring.application.name: mall-coupon # 微服务名
spring.cloud.nacos.discovery.server-addr: localhost:8848 # 注册地址
// 主启动类
@EnableDiscoveryClient
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
// 编写接口
@FeignClient("mall-coupon") // 微服务名
public interface CouponFeign {
@GetMapping("/coupon/coupon/member/list") // 全限定路径
R memberList();
}
// 主启动类 basePackages 可加可不加
@EnableFeignClients(basePackages = "edu.dlut.catmall.member.feign")
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
# bootstrap.properties 启动优先级高于
spring.application.name=mall-coupon
spring.cloud.nacos.config.server-addr=localhost:8848
// Controller 动态刷新
@RefreshScope
在nacos
配置中心添加配置文件 servicename.properties
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
CREATE TABLE `pms_category` (
`cat_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '分类id',
`name` char(50) DEFAULT NULL COMMENT '分类名称',
`parent_cid` bigint(20) DEFAULT NULL COMMENT '父分类id',
`cat_level` int(11) DEFAULT NULL COMMENT '层级',
`show_status` tinyint(4) DEFAULT NULL COMMENT '是否显示[0-不显示,1显示]',
`sort` int(11) DEFAULT NULL COMMENT '排序',
`icon` char(255) DEFAULT NULL COMMENT '图标地址',
`product_unit` char(50) DEFAULT NULL COMMENT '计量单位',
`product_count` int(11) DEFAULT NULL COMMENT '商品数量',
PRIMARY KEY (`cat_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1433 DEFAULT CHARSET=utf8mb4 COMMENT='商品三级分类';
@Data
@TableName("pms_category")
public class CategoryEntity implements Serializable {
private static final long serialVersionUID = 1L;
@TableId
private Long catId;
private String name;
private Long parentCid;
private Integer catLevel;
private Integer showStatus;
private Integer sort;
private String icon;
private String productUnit;
private Integer productCount;
@TableField(exist = false)
private List<CategoryEntity> children;
}
public List<CategoryEntity> listWithTree() {
// 这个类继承了 ServiceImpl
// 1. 查出所有分类列表
List<CategoryEntity> entities = baseMapper.selectList(null); // 传入 null 代表查询所有
// 2. 组装成树形结构
List<CategoryEntity> levelMenu = entities.stream()
.filter(categoryEntity -> categoryEntity.getParentCid() == 0)
.map(menu -> {
menu.setChildren(getChildren(menu, entities));
return menu;
}).sorted((m1, m2) -> m1.getSort() == null ? 0 : m1.getSort() - (m2.getSort() == null ? 0 : m2.getSort())).collect(Collectors.toList());
return levelMenu;
}
/**
* 递归查找所有菜单的子菜单
*
* @param root
* @param all
* @return
*/
private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> all) {
List<CategoryEntity> children = all.stream()
.filter(categoryEntity -> categoryEntity.getParentCid() == root.getCatId())
.map(categoryEntity -> {
categoryEntity.setChildren(getChildren(categoryEntity, all));
return categoryEntity;
})
.sorted((m1, m2) -> m1.getSort() == null ? 0 : m1.getSort() - (m2.getSort() == null ? 0 : m2.getSort()))
.collect(Collectors.toList());
return children;
}
renren-fast
自带的跨域配置给关闭掉。@Configuration
public class CORSConfig {
@Bean
public CorsWebFilter corsWebFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.setAllowCredentials(true);
source.registerCorsConfiguration("/**", corsConfiguration);
return new CorsWebFilter(source);
}
}
show_status
字段,逻辑上的删除是指在前端页面进行隐藏,而并不真正删除数据库这条数据。src/util/index.js
export function isAuth (key) {
// return JSON.parse(sessionStorage.getItem('permissions') || '[]').indexOf(key) !== -1 || false
return true
}
eslint
,路径 build/webpack.base.conf.js
。const createLintingRule = () => ({
test: /\.(js|vue)$/,
loader: 'eslint-loader',
enforce: 'pre',
include: [resolve('src'), resolve('test')],
options: {
formatter: require('eslint-friendly-formatter'),
emitWarning: !config.dev.showEslintErrorsInOverlay
}
})
nacos-server
上,不要直接写在项目配置文件中,不然总会收到 GitHub
与阿里云发送的短信提醒(可能泄密)。<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alicloud-oss</artifactId>
</dependency>
JSR303
校验
javax.validation.constraints
,定义自己的校验规则。Controller @Valid
,校验的Bean
之后添加BindingResult
可以获得校验结果。@RestControllerAdvice
,使用ExceptionHandler
标注方法可以处理的异常。JSR
分组校验
public interface UpdateGroup{},public interface AddGroup{}
@NotBlank(message = "品牌名不能为空", groups = {AddGroup.class, UpdateGroup.class})
Controller
添加@Validated({UpdateGroup.class})
ConstraintValidator
自定义注解:@ListValue,用作值域,用于验证某个字段取值是否在此值域内。
@Documented
@Constraint(validatedBy = { ListValueConstraintValidator.class})
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
String message() default "{edu.dlut.common.valid.ListValue.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
int[] value() default { };
}
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
private Set<Integer> set = new HashSet<>();
//初始化方法
@Override
public void initialize(ListValue constraintAnnotation) {
int[] value = constraintAnnotation.value();
for (int val : value)
set.add(val);
}
/**
*
* @param value 需要校验的值
* @param context
* @return
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return set.contains(value);
}
}
# ValidationMessages.properties
edu.dlut.common.valid.ListValue.message=必须提交指定的值
@ListValue(value = {0, 1}, groups = {AddGroup.class, UpdateStatusGroup.class})
这两个名词将会贯穿从此开始到高级篇结束的所有内容。
SPU: Standard Product Unit
(标准产品单位)
iPhone 12
就是一个 SPU,与商家,与颜色、款式、套餐都无关。SKU: Stock Keeping Unit
(库存量单位)
iPhone 12
的颜色(深空灰等),存储容量(64GB 256GB)。$ docker pull elasticsearch:7.4.2 # 存储和检索数据
$ dock pull kibana:7.4.2 # 可视化检索数据
Homebrew
安装$ brew tap elastic/tap
$ brew install elastic/tap/elasticsearch-full
$ elasticsearch
$ brew services start elastic/tap/elasticsearch-full # 开机自启 可选
$ brew install kibana/tap/kibana-full
$ kibana
$ brew services start elastic/tap/kibana-full # 开机自启 可选
# ik 分词
$ /usr/local/var/elasticsearch/plugins/ik/config
ElasticSearch
配置与运行参数$ mkdir -p /mydata/elasticsearch/config
$ mkdir -p /mydata/elasticsearch/data
$ echo "http.host:0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml
$ docker run --name elasticsearch -p 9200:9200 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms128m -Xmx128m" \
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
-v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.4.2
$ chmod -R 777 /mydata/elasticsearch
Kibana
# 其中IP地址一定要改为自己机器或服务器的IP
$ docker run --name kibana -e ELASTICSEARCH_HOSTS=http://xxx.xx.xx.xxx:9200 -p 5601:5601 -d kibana:7.4.2
GET/_cat/nodes
GET/_cat/health
GET/_cat/master
GET/_cat/indices // 查看所有索引
// 保存一条数据 保存在哪个索引的哪个类型下 指定用哪一个标识
PUT customer/external/1 // PUT 和 POST 均可 PUT必须带ID,POST可带可不带
{
"name": "John Snow"
}
此索引后来会出现问题,下面有修改。
PUT product
{
"mappings": {
"properties": {
"skuId": {
"type": "long"
},
"spuId": {
"type": "keyword"
},
"skuTitle": {
"type": "text",
"analyzer": "ik_smart"
},
"skuPrice": {
"type": "keyword"
},
"skuImg": {
"type": "keyword",
"index": false,
"doc_values": false
},
"saleCount": {
"type": "long"
},
"hasStock": {
"type": "boolean"
},
"hotScore": {
"type": "long"
},
"brandId": {
"type": "long"
},
"catalogId": {
"type": "long"
},
"catalogName": {
"type": "keyword",
"index": false,
"doc_values": false
},
"brandName": {
"type": "keyword",
"index": false,
"doc_values": false
},
"brandImg": {
"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"
}
}
}
}
}
}
// 执行结果
{
"acknowledged" : true,
"shards_acknowledged" : true,
"index" : "product"
}
docker container cp nginx:/etc/nginx .
docker run -p 80:80 -name nginx \
-v /mydata/nginx/html:/usr/share/nginx/html \
-v /mydata/nginx/logs:/var/log/nginx \
-v /mydata/nginx/conf:/ect/nginx \
-d nginx:1.10
推荐阅读:
总结版
Feign
在远程调用之前要构造请求,此时会丢失请求头headers
,request
中包含许多拦截器。
在构建新请求的时候需要吧“老请求”中的数据获取并保存传递到新请求中。
@Configuration
public class MallFeignConfig {
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor() {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String cookie = requestAttributes.getRequest().getHeader("Cookie");
template.header("Cookie", cookie);
}
};
}
}
RequestContextHolder
中的ThreadLocal
只在当前线程可用,线程间独立,而在异步编排时会创建不同的线程执行任务,ThreadLocal
中的数据将会丢失。RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
,然后在异步任务开始前重新设置进去,RequestContextHolder.setRequestAttributes(requestAttributes);
spring.thymeleaf.cache=false
static
文件夹下就可以按照路径直接访问templates
下直接访问SpringBoot
访问项目时会默认寻找index.html
Nginx
代理给网关的时候,会丢失请求的host
信息,手动设置 proxy_set_header Host $host
。#user nobody;
worker_processes 1;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
upstream catmall{
server 127.0.0.1:8888;
}
server {
listen 80;
server_name catmall.com;
location / {
proxy_set_header HOST $host;
proxy_pass http://catmall;
#root html;
#index index.html index.htm;
}
}
include servers/*;
}
将项目中 static/
下的静态资源移动到 nginx
服务器中,mac
为 /usr/local/var/www
;
替换 index.html
中的文件路径;
配置 nginx
;
重载配置 nginx -s reload
。
// 在 server 块中添加
location /static/ {
root /usr/local/var/www;
}
jconsole
jvisualvm
安装插件 visualgc
# 执行测试计划
$ jmeter -n -t testplan/RedisLock.jmx -l testplan/result/result.txt -e -o testplan/webreport
相关问题已整理至 Wiki 页面。
第一个使用场景:缓存商品首页三级菜单
遇到的问题:堆外内存(直接内存)溢出 OutOfDirectMemoryError
分析思路:
-Dio.netty.maxDirectMemory
设置堆外内存。解决方案:
-Dio.netty.maxDirectMemory
去调大堆外内存,堆外内存也是有限的;RedisTemplate
对 lettuce
与 jedis
均进行了封装,可以直接使用。@Override
public Map<String, List<Catelog2VO>> getCatalogJson() {
// 给缓存中放入JSON字符串,取出JSON字符串还需要逆转为能用的对象类型
// 1. 加入缓存逻辑, 缓存中存的数据是 JSON 字符串
String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
if (StringUtils.isEmpty(catalogJSON)) {
// 2 如果缓存未命中 则查询数据库
Map<String, List<Catelog2VO>> catalogJsonFromDB = getCatalogJsonFromDB();
// 3 查到的数据再放入缓存 将对象转为JSON放入缓存
String cache = JSON.toJSONString(catalogJsonFromDB);
stringRedisTemplate.opsForValue().set("catalogJSON", cache);
// 4 返回从数据库中查询的数据
return catalogJsonFromDB;
}
Map<String, List<Catelog2VO>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2VO>>>() {});
return result;
}
关于上面提到的 lettuce Bug,Lettuce 源码。
private static void incrementMemoryCounter(int capacity) {
if (DIRECT_MEMORY_COUNTER != null) {
long newUserMemory = DIRECT_MEMORY_COUNTER.addAndGet((long) capacity);
if (newUsedMemory > DIRECT_MEMORY_LIMIT) {
DIRECT_MEMORY_COUNTER.addAndGet((long) (-capacity));
throw new OutOfDirectMemoryError("failed to allocate " + capacity + " byte(s) of direct memory.")
}
}
}
类型 | 描述 | 解决 |
---|---|---|
缓存击穿 | 对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。如果这个 key 在大量请求同时进来前正好失效,那么所有对这个 key 的数据査询都落到db. | 加锁。大量并发只让一个去查,其他人等待,査到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去 db |
缓存穿透 | 指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去査询,失去了缓存的意义。利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃 | nul 结果缓存,并加入短暂过期时间 |
缓存雪崩 | 缓存雪崩是指在我们设置缓存时 key 采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到 DB, DB 瞬时压力过重雪崩。 | 原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。 |
canal
canal
更新缓存canal
解决数据易购synchronized (this)
可以实现加锁;在每一个微服务中的
synchronized(this)
加锁的对象只是当前实例,但是并未对其他微服务的实例产生影响,即使每个微服务加锁后只允许一个请求,假如有 8 个微服务,仍然会有 8 个线程存在。
RLock lock = redissonClient.getLock("my-lock");
lock.lock();
lock.lock(10, TimeUnit.SECONDS);
internalLockLeaseTime
[看门狗时间/3 = 10s]@Cacheable
: Triggers cache population.@CacheEvict
: Triggers cache eviction.@CachePut
: Updates the cache without interfering with the method execution.@Caching
: Regroups multiple cache operations to be applied on a method.@CacheConfig
: Shares some common cache-related settings at class-level.CacheManager
Cache
@Cacheable({"category"})
)key
默认自动生成category::SimpleKey []
@Cacheable(value = {"category"}, key= "'name'")
@Cacheable(value = {"category"}, key = "#root.method.name")
value
的值,默认使用JDK
序列化机制,将序列化后的数据存到Redis
JSON
格式原理CacheAutoConfiguration
-> RedisCacheConfiguration
-> 自动配置了RedisCacheManager
-> 初始化所有的缓存 -> 每个缓存决定用什么配置 -> 如果redisCacheConfiguration
有就用已有的,没有就用默认配置 -> 想改缓存配置,只需要给容器中存放一个RedisCacheConfiguration
即可 -> 就会应用到当前RedisCacheManager
管理的所有缓存分区中。TTL=-1
spring.cache.redis.time-to-live=3600000
@Configuration
@EnableCaching
@EnableConfigurationProperties(CacheProperties.class)
public class MyCacheConfig {
@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
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;
}
}
Spring Cache
总结cache-null-values=true
sync=true
spring.cache.redis.time-to-live=时间
Canal
, 感知到MySQL
的更新则去更新缓存Spring Cache
坑:在从首页点击分类名跳转到搜索页时,跳转链接在
catalogLoader.js
中,原静态资源链接为http://search.gmall.com/
,需要改为自己在 HOST 文件中配置的域名。
mapping
// 不要直接删除重建 会丢失已上架的商品数据
PUT product
{
"mappings": {
"properties": {
"skuId": {
"type": "long"
},
"spuId": {
"type": "keyword"
},
"skuTitle": {
"type": "text",
"analyzer": "ik_smart"
},
"skuPrice": {
"type": "keyword"
},
"skuImg": {
"type": "keyword"
},
"saleCount": {
"type": "long"
},
"hasStock": {
"type": "boolean"
},
"hotScore": {
"type": "long"
},
"brandId": {
"type": "long"
},
"catalogId": {
"type": "long"
},
"catalogName": {
"type": "keyword"
},
"brandName": {
"type": "keyword"
},
"brandImg": {
"type": "keyword"
},
"attrs": {
"type": "nested",
"properties": {
"attrId": {
"type": "long"
},
"attrName": {
"type": "keyword"
},
"attrValue": {
"type": "keyword"
}
}
}
}
}
}
PUT mall_product
POST _reindex
{
"source": {
"index": "product"
},
"dest": {
"index": "mall_product"
}
}
mall-search/src/main/java/包名/service/impl/MallSearchServiceImpl
CompletableFuture<T>
创建异步对象
计算完成时回调方法
handle
方法
线程串行化
两任务组合
多任务组合
微博登录出现的问题:
auth.catmall.com
,而不是catmall.com
;OAuthController
:
doPost
方法后三个参数数据类型是 Map
,均不能传入null
,而是传入空的 Map
;Map
的顺序有变化,在构建请求条件时应该将map
传入查询参数querys
中,而不是请求体bodys
。HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", new HashMap<>(), map, new HashMap<>());
@EnableRedisHttpSession
注解中导入了RedisHttpSessionConfiguration
类
SessionRepository
-> RedisOperationsSessionRepository
,利用Redis
来进行Session
的增删改查等各种操作。SessionRepositoryFilter
: Session
存储过滤器,每个请求都必须经过filter
SessionRepository
request、response
都被包装成SessionRepositoryRequestWrapper.SessionRepositoryResponseWrapper
用户可以在登录状态下将商品加入[在线购物车/用户购物车]
MongoDB
;MySQL
;Redis
(采用),登录以后,会将临时购物车中的数据合并过来。用户可以在未登录状态下将商品加入[离线购物车/游客购物车]
localStorage
;Cookie
;WebSQL
;Redis
(采用),即使浏览器关闭,临时购物车数据都在。用户可以使用购物车一起结算下单
用户可以添加商品到购物车
用户可以查询自己购物车
用户可以选中购物车中商品
用户可以在购物车中修改购买的商品数量
用户可以在购物车中删除商品
在购物车中展示优惠信息
提示购物车商品价格变化
京东给每个用户生成一个值类似于 UUID 的
user-key
,有效期一个月,存储在Cookie
,浏览器保存以后,每次访问都会带上这个cookie
。登录后:
session
有用户信息未登录:
cookie
中的user-key
第一次使用时,如果没有临时用户,帮忙创建一个临时用户。
public class CartInterceptor implements HandlerInterceptor{}
重写preHandle, postHandle
,不用加@Component
MallWebConfig
@Configuration
public class MallWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new CartInterceptor()).addPathPatterns("/**");
}
}
本系统消息队列工作图
$ docker pull rabbitmq
$ docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 25672:25672 -p 15671:15671 -p 15672:15672 rabbitmq:management
$ cd 安装目录
$ # 启用 rabbitmq management插件
$ sudo sbin/rabbitmq-plugins enable rabbitmq_management
$ # 配置环境变量(可选)
$ rabbitmq-server -detached # 后台启动
# 查看状态 浏览器内输入 http://localhost:15672, 默认的用户名密码都是 guest。
$ rabbitmqctl status
$ rabbitmqctl stop # 关闭
# Setting for RabbitMQ
export RABBIT_HOME=/usr/local/Cellar/rabbitmq/3.8.9_1
export PATH=$PATH:$RABBIT_HOME/sbin
$ brew install rabbitmq
# OR
$ brew cask install rabbitmq
@RabbitListener
@RabbitHandler
如果是传输对象的话,传输的对象必须实现序列化接口,默认的序列化方式是 JDK 序列化,但是也可以手动指定序列化的方式。
@Configuration
public class MyRabbitConfig {
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
}
try-catch
),发送消息可能会网络失败,失败后要有重试机制,可记录到系统数据库,采用定期扫描重发的方式。Broker
,Broker
要将消息写入磁盘才算成功,此时Broker
尚未持久化完成,宕机。publisher
必须加入确认回调机制,确认成功的消息,修改数据库消息状态ACK
状态下,消费者收到消息,但没来得及消费便宕机ACK
,消息成功才移除,失败或者没来得及处理就noACK
并重新入队。ack
时,机器宕机,导致没有ack
成功,Broker
的消息重新由unack-> ready
,并发送给其他消费者。ack
时宕机,消息由unack
变为ready
,Broker
又重新发送RabbitMQ
的每一个消息都有redelivered
字段,可以获取消息是否是被重新投递的。create table `mq_message` (
`message_id` char(32) not null,
`content` text,
`to_exchange` varchar(255) default null,
`routing_key` varchar(255) default null,
`class_type` varchar(255) default null,
`message_status` int(1) default '0' comment '0-新建 1-已发送 2-错误抵达 3-已抵达',
`create_time` datetime default null,
`update_time` datetime default null,
primary key (`message_id`)
) engine InnoDB default charset=utf8mb4
Nginx
静态资源,网关等。下单->创建订单->验证令牌->核算价格->锁定库存
在确认页点击 提交订单 时,用户可能不小心点击多次,所以即使用户点击次数大于1次,也应该保证只提交一次。
feign
重试机制Redis Lua
脚本redis set
防重spring-boot-starter-aop
,aop
又引入了aspectj
@EnableAspectJAutoProxy(exposeProxy = true)
,开启aspectj
动态代理功能,如果不开启的话,默认使用的是JDKProxy
,开启后以后创建对象采用aspectj
动态代理(即使没有接口也可以创建代理对象, JDKProxy要求被代理的对象有接口定义)AopContext.currentProxy
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
秒杀业务具有瞬间高并发的特点,必须要做限流+异步+缓存(页面静态化)+独立部署
限流方式:
nginx
限流: 直接负载部分请求到错误的静态页面,令牌算法,漏斗算法。RabbitMQ
限流,保证发挥所有服务器的性能。@EnableScheduling
开启定时任务@Scheduled
开启一个定时任务@EnableAsync
开启异步任务@Async
标注在需要异步执行的方法上查看总的代码行,包括添加了多少行,删除了多少行,现在总共多少行。
$ git log --pretty=tformat: --numstat | awk '{ add += $1; subs += $2; loc += $1 - $2 } END { printf "added lines: %s, removed lines: %s, total lines: %s\n", add, subs, loc }' -
Sentinel | Hystrix | |
---|---|---|
隔离策略 | 信号量隔离 | 线程池隔离/信号量隔离 |
熔断降级策略 | 基于响应时间或失败比率 | 基于失败比率 |
实时指标实现 | 滑动窗口 | 滑动窗口(基于 RxJava) |
规则配置 | 支持多种数据源 | 支持多种数据源 |
扩展性 | 多个扩展点 | 插件的形式 |
基于注解的支持 | 支持 | 支持 |
限流 | 基于 QPS,支持基于调用关系的限流 | 有限的支持 |
流量整形 | 支持慢启动、匀速器模式 | 不支持 |
系统负载保护 | 支持 | 不支持 |
控制台 | 开箱即用,可配置规则、查看秒级监控、机器发现等 | 不完善 |
常见框架的适配 | Servlet、Spring Cloud、Dubbo、gRPC 等 | Servlet、Spring Cloud Netflix |
# docker
$ docker run -d -p 9411:9411 openzipkin/zipkin
# java
$ curl -sSL https://zipkin.io/quickstart.sh | bash -s
$ java -jar zipkin.jar
root
的密码访问权限。$ vargrant ssh xxxxx
$ su root # password vargrant
$ vi /etc/ssh/sshd_config
# 修改 PasswordAuthentication yes
$ service sshd restart
# 所有虚拟机设置为 4 core 4G
linux
环境(三个结点都要执行)# 关闭防火墙
$ systemctl stop firewalld
$ systemctl disable firewalld
# 关闭 selinux
$ sed -i 's/enforcing/disabled/' /etc/selinux/config
# 关闭内存交换
$ swapoff -a # 临时
$ sed -ri 's/.*swap.*/#&/' /etc/fstab # 永久
$ free -g # 验证 swap 必须为 0
$ vi /etc/hosts
# 前边为网卡地址 后边为集群结点名
xxxxxxx k8s-node1
xxxxxxx k8s-node2
xxxxxxx k8s-node3
IPv4
流量传递到 iptables
链cat > /etc/sysctl.d/k8s.conf << EOF
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
EOF
sysctl --system
$ sudo yum remove docker docker-client docker-client-latest docker-common docker-latest docker-latest-logrotate docker-logrotate docker-engine
$ sudo yum install -y yum-utils device-mapper-persistent-data lvm2
# 设置 docker repo 到 yum 位置
$ sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
# 安装 docker docker-cli
$ sudo yum install -y docker-ce docker-ce-cli containerd.io
$ sudo mkdir -p /etc/docker
$ sudo tee /etc/docker/daemon.json << -'EOF'
{
"registry-mirrors": [阿里云是个不错的选择]
}
EOF
$ sudo systemctl daemon-reload
$ sudo systemctl restart docker
$ sudo systemctl enable docker # 开机自启
感觉哪里少了东西
$ docker run -p 3306:3306 --name mysql-master \
-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
$ docker run -p 3306:3306 --name mysql-slave-1 \
-v /mydata/mysql/slave/log:/var/log/mysql \
-v /mydata/mysql/slave/data:/var/lib/mysql \
-v /mydata/mysql/slave/conf:/etc/mysql \
-e MYSQL_ROOT_PASSWORD=root \
-d mysql:5.7
$ docker run -p 3306:3306 --name mysql-slave-2 \
-v /mydata/mysql/slave/log:/var/log/mysql \
-v /mydata/mysql/slave/data:/var/lib/mysql \
-v /mydata/mysql/slave/conf:/etc/mysql \
-e MYSQL_ROOT_PASSWORD=root \
-d mysql:5.7
$ vi /mydata/mysql/master/conf/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
# 主从的这个配置仅在于 id 的不同
server_id=1
log-bin=mysql-bin
read-only=0
binlog-do-db=catmall_ums
binlog-do-db=catmall_pms
binlog-do-db=catmall_oms
binlog-do-db=catmall_sms
binlog-do-db=catmall_wms
binlog-do-db=catmall_admin
replicate-ignore-db=mysql
replicate-ignore-db=sys
replicate-ignore-db=information_schema
replicate-ignore-db=performance_schema
# 进入 master 容器
$ docker exec -it mysql /bin/bash
$ mysql -uroot -p
mysql> grant all priviledges on *.* to 'root'@'%' identified by 'root' with grant option;
mysql> flush priviledges;
mysql> GRANT REPLICATION SLAVE ON *.* to 'backup'@'%' identified by '123456';
show master status
change master to master_host='xxxxxxx', matser_user='backup', master_password='123456', master_log_file='mysql-bin.000001', master_log_pos=0, master_port=3307;
# 启动主从同步
start slave
# 查看从库状态
show slave status
for port in $(seq 7001 7006) \
do \
mkdir -p /mydata/redis/node-${port}/conf
touch /mydata/redis/node-${port}/conf/redis.conf
cat << EOF >/mydata/redis/node-${port}/conf/redis.conf
port ${port}
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-announce-ip xxxxxx
cluster-announce-port ${port}
cluster-announce-bus-port ${port}
appendonly yes
EOF
docker run -p ${port}:${port} -p 1${port}:1${port} --name redis-${port} \
-v /mydata/redis/node-${port}/data:data \
-v /mydata/redis/node-${port}/conf/redis.conf:/etc/redis/redis.conf \
-d redis:5.0.7 redis-server /etc/redis/redis.conf
done
$ docker stop ${docker ps -a | grep redis-700 | awk '{print $1}'}
$ docker rm ${docker ps -a | grep redis-700 | awk '{print $1}'}
$ docker exec -it redis-7001 bash
$ redis-cli --cluster create 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 127.0.0.1:7006 --cluster-replicas 1