尚硅谷《谷粒商城》-开发笔记,另有学习文档笔记,期待大家的加入,
No Data
做项目不是为了敲代码,而是为了学知识,学原理,不深入去理解底层原理的话就是普通的 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这个文件中的请求地址。
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
# 启动命令 ( standalone 代表着单机模式运行,非集群模式) $ sh startup.sh -m standalone
$ curl -sSL https://zipkin.io/quickstart.sh | bash -s $ java -jar zipkin.jar
Elasticsearch,建立相应的索引
product。
# 如果内存不是特别大的话,最好设置一下 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。
完成上面几个步骤之后,项目启动正常。
安装 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 # 随机自启 $ 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
sql
settings.xml
aliyunmaven * 阿里云公共仓库 https://maven.aliyun.com/repository/public nexus-aliyun central Nexus Aliyun https://maven.aliyun.com/nexus/content/groups/public aliyunmaven * 阿里云Google仓库 https://maven.aliyun.com/repository/google aliyunmaven * 阿里云Apache仓库 https://maven.aliyun.com/repository/apache-snapshots aliyunmaven * 阿里云Spring仓库 https://maven.aliyun.com/repository/spring aliyunmaven * 阿里云Spring插件仓库 https://maven.aliyun.com/repository/spring-plugin
jdk-1.8 true 1.8 1.8 1.8 1.8
$ npm config set registry http://registry.npm.taobao.org/ $ npm config get registry $ npm config set registry https://registry.npmjs.org/
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: 调用链路监控追踪
com.alibaba.cloud spring-cloud-alibaba-dependencies 2.2.1.RELEASE pom import
com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery
spring.application.name: mall-coupon # 微服务名 spring.cloud.nacos.discovery.server-addr: localhost:8848 # 注册地址
// 主启动类 @EnableDiscoveryClient
org.springframework.cloud spring-cloud-starter-openfeign
// 编写接口 @FeignClient("mall-coupon") // 微服务名 public interface CouponFeign { @GetMapping("/coupon/coupon/member/list") // 全限定路径 R memberList(); }// 主启动类 basePackages 可加可不加 @EnableFeignClients(basePackages = "edu.dlut.catmall.member.feign")
com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config
# bootstrap.properties 启动优先级高于 spring.application.name=mall-coupon spring.cloud.nacos.config.server-addr=localhost:8848
// Controller 动态刷新 @RefreshScope
在
nacos配置中心添加配置文件
servicename.properties
org.springframework.cloud spring-cloud-starter-gateway
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 listWithTree() { // 这个类继承了 ServiceImpl // 1. 查出所有分类列表 List 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;
}
/**
.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与阿里云发送的短信提醒(可能泄密)。
com.alibaba.cloud spring-cloud-starter-alicloud-oss
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 {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
================================================================================ Don't use GUI mode for load testing !, only for Test creation and Test debugging. For load testing, use CLI Mode (was NON GUI): jmeter -n -t [jmx file] -l [results file] -e -o [Path to web report folder] & increase Java Heap to meet your test requirements: Modify current env variable HEAP="-Xms1g -Xmx1g -XX:MaxMetaspaceSize=256m" in the jmeter batch file Check : https://jmeter.apache.org/usermanual/best-practices.html ================================================================================
# 执行测试计划 $ jmeter -n -t testplan/RedisLock.jmx -l testplan/result/result.txt -e -o testplan/webreport
-n This specifies JMeter is to run in cli mode -t [name of JMX file that contains the Test Plan]. -l [name of JTL file to log sample results to]. -j [name of JMeter run log file]. -r Run the test in the servers specified by the JMeter property "remote_hosts" -R [list of remote servers] Run the test in the specified remote servers -g [path to CSV file] generate report dashboard only -e generate report dashboard after load test -o output folder where to generate the report dashboard after load test. Folder must not exist or be empty The script also lets you specify the optional firewall/proxy server information: -H [proxy server hostname or ip address] -P [proxy server port]
相关问题已整理至 Wiki 页面。
第一个使用场景:缓存商品首页三级菜单
遇到的问题:堆外内存(直接内存)溢出
OutOfDirectMemoryError
分析思路:
-Dio.netty.maxDirectMemory设置堆外内存。
解决方案:
-Dio.netty.maxDirectMemory去调大堆外内存,堆外内存也是有限的;
RedisTemplate对
lettuce与
jedis均进行了封装,所以直接使用。
@Override public Map> getCatalogJson() { // 给缓存中放入JSON字符串,取出JSON字符串还需要逆转为能用的对象类型 // 1. 加入缓存逻辑, 缓存中存的数据是 JSON 字符串 String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON"); if (StringUtils.isEmpty(catalogJSON)) { // 2 如果缓存未命中 则查询数据库 Map> catalogJsonFromDB = getCatalogJsonFromDB(); // 3 查到的数据再放入缓存 将对象转为JSON放入缓存 String cache = JSON.toJSONString(catalogJsonFromDB); stringRedisTemplate.opsForValue().set("catalogJSON", cache); // 4 返回从数据库中查询的数据 return catalogJsonFromDB; }Map<string list>> result = JSON.parseObject(catalogJSON, new TypeReference<map list>>>() {}); 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
计算完成时回调方法
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/zipkinjava
$ 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