做项目不是为了敲代码,而是为了学知识,学原理,不深入去理解底层原理的话就是普通的 CRUD 工程师。 项目中涉及的比较重要的内容可以查看 Wiki 页面,或者 awesome-architect。
现在文档中的知识点还比较有限,内容还在持续完善中。以后将会逐渐深入源码,分析学习运行原理与机制。
微服务架构电商系统,主要分为三个阶段。
- 第一阶段:分布式基础篇-全栈开发
- 快速地开发一个前后端分离的电商系统
- Spring Boot + Spring Cloud + Vue + Docker + MyBatis Plus
- 第二阶段:分布式高级-微服务架构,打通分布式开发中的所有技术栈。
- ElasticSearch
- Redis 基本使用与 Lua 脚本
- Redisson 分布式锁
- 性能压测模拟
- Nginx 反向代理、动静分离、负载均衡
- 多线程与异步
- 单点登录与社交登录
- RabbitMQ 消息队列
- Nacos 服务注册、发现、配置中心
- 分布式事务与 Seata
- 秒杀系统设计
- 定时任务与分布式调度
- Sentinel 服务容错
- Sleuth & Zipkin 链路追踪
- 实现一整套的微服务整合,包括秒杀,结算,库存...
- 第三阶段:高可用集群-架构师提升
- 搭建 Kubernetes 集群,实现全流程 DevOps。
- 搭建 MySQL 集群,Redis 集群,RabbitMQ 集群,ElasticSearch 集群。
- 《分布式基础篇-全栈开发》
- 《分布式高级篇-微服务架构》
- 《高可用集群篇-架构师提升》
- 完善系统功能
- 完善用户评论、收藏、物流
- 系统自动生成了
Apache Shiro
权限控制 - 增加卖家角色及相关功能
- 增加推荐子系统
- 增加数据仓库与数据挖掘
因为准备秋招耽误了许久,现在重新开始,重新启动项目时遇到许多麻烦,下面记录一下大致步骤,避免以后忘记。
系统为 macOS BigSur
,包管理工具为 Homebrew
。
- 使用
MySQL
建立相应数据库,并运行相应sql
文件,数据库账号密码为root/root
。 - 安装
mall-commons
到本地Maven
仓库。
# 进入到 mall-commons 所在目录
$ mvn clean
$ mvn install
- 添加静态文件到
nginx
服务器中,并且启动nginx
服务器。在渲染商城首页三级菜单时,如果没有正确显示,需要注意catalogLoader.js
这个文件中的请求地址。- 为减少一些 GitHub 仓库大小,放到了百度云上。静态资源下载地址:链接,密码:
nca7
,下载后将其放到/usr/local/var/www
目录下,非Homebrew
安装的Nginx
需要注意一些路径。 - 现在包括
cart、index、item、login、member、order、register、search
几个静态资源目录。
- 为减少一些 GitHub 仓库大小,放到了百度云上。静态资源下载地址:链接,密码:
$ brew install nginx
$ brew info nginx
# 静态文件目录 /usr/local/var/www/static
- 下载并启动
Redis
服务器
$ brew install redis
$ redis-server /usr/local/etc/redis.conf
- nacos server 用于服务注册与发现,本项目版本
1.2.1
,或者下载 Release 版本。
# 启动命令 ( standalone 代表着单机模式运行,非集群模式)
$ sh startup.sh -m standalone
// localhost:8848/nacos
- 下载并启动 Zipkin,本项目版本
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
- 配置MySQL编码
[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
- 重启 MySQL
$ docker restart mysql
$ docker exec -it mysql /bin/bash
- 安装 Redis
$ 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
- 增加 Redis 持久化
$ vi /mydata/redis/conf/redis.conf
# appendonly yes
$ docker restart redis
- 免费的 mac/windows redis 客户端:AnotherRedisDesktopManager - GitHub
- 项目基础功能模块:
- 商品模块:
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
- 短信服务
- OSS 对象存储
- 配置文件:
config-file
- 利用 SQL 文件建库建表
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
模块 - 修改配置文件中数据库连接信息
- 运行项目,进入 Web 界面,生成基础增删改查及
Vue
模板文件
- Spring Cloud Alibaba - GitHub
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
- 命名空间、配置集、配置集ID、配置分组
<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
与阿里云发送的短信提醒(可能泄密)。 - Spring Cloud Alibaba OSS
- OSS 获取服务器签名
<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
(标准产品单位)- SPU 是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。
- 通俗点讲,属性值、特性相同的商品就可以称为一个 SPU。
- 例如:
iPhone 12
就是一个 SPU,与商家,与颜色、款式、套餐都无关。
-
SKU: Stock Keeping Unit
(库存量单位)- SKU 即库存进出计量的单位, 可以是以件、盒、托盘等为单位。
- SKU 是物理上不可分割的最小存货单元。在使用时要根据不同业态,不同管理模式来处理。
- 在服装、鞋类商品中使用最多最普遍。
- 例如:
iPhone 12
的颜色(深空灰等),存储容量(64GB 256GB)。
- 通过 Docker 安装
$ 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;
}
- HPS(Hits Per Second):每秒点击次数
- TPS(Transaction Per Second):每秒处理事务次数
- QPS(Query Per Scond):每秒查询次数
- 最大响应时间
- 最少响应时间
- 90% 响应时间
jconsole
jvisualvm
安装插件visualgc
- visualvm插件更新地址
# 执行测试计划
$ jmeter -n -t testplan/RedisLock.jmx -l testplan/result/result.txt -e -o testplan/webreport
- JVM
- 索引
- 逻辑优化
相关问题已整理至 Wiki 页面。
-
第一个使用场景:缓存商品首页三级菜单
-
遇到的问题:堆外内存(直接内存)溢出
OutOfDirectMemoryError
-
分析思路:
- SpringBoot2.0 之后默认使用 lettuce 作为操作 Redis 的客户端,lettuce 使用 Netty 进行网络通信。
- lettuce 的 bug 导致 Netty 堆外内存溢出,Netty 如果没有指定对外内存 默认使用 JVM 设置的参数,可以通过
-Dio.netty.maxDirectMemory
设置堆外内存。
-
解决方案:
- 不能仅仅使用
-Dio.netty.maxDirectMemory
去调大堆外内存,堆外内存也是有限的; - 可以选择升级 lettuce 客户端;
- 可以切换使用 jedis 作为客户端;
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
解决数据易购
- 使用
- 本系统的一致性解决方案
- 为所有缓存数据设置过期时间,数据过期下一次查询触发主动更新。
- 读写数据的时候,加上分布式的读写锁。(读多写少时几乎无影响)
- SpringBoot 所有的组件在容器中默认都是单例的,使用
synchronized (this)
可以实现加锁; - 得到锁之后,应该再去缓存中确定一次,如果没有的话才需要继续查询;
- 假如有 100W 个并发请求,首先得到锁的请求开始查询,此时其他的请求将会排队等待锁;
- 等到获得锁的时候再去执行查询,但是此时有可能前一个加锁的请求已经查询成功并且将结果添加到了缓存中。
在每一个微服务中的
synchronized(this)
加锁的对象只是当前实例,但是并未对其他微服务的实例产生影响,即使每个微服务加锁后只允许一个请求,假如有 8 个微服务,仍然会有 8 个线程存在。
- 确认缓存-查询数据库-结果放入缓存 这三个操作必须当做一个事务来执行,放在同一把锁里面完成。
RLock lock = redissonClient.getLock("my-lock");
lock.lock();
- 阻塞式等待,默认加的锁都是 30s 时间。
- 锁的自动续期,如果业务超长,如果业务运行时间较长,运行期间自动给锁续上新的 30s,不用担心业务时间过长(大于锁的过期时间)导致锁被删掉。
- 加锁的业务只要运行完成就不会给当前锁续期,即使不手动解锁,锁也会在 30s 后自动删除。
lock.lock(10, TimeUnit.SECONDS);
- 在锁时间到了以后,不会自动续期。
- 如果我们传递了锁的超时时间,就发送给 redis 执行脚本,进行占锁,默认超时就是我们指定的时间。
- 如果我们未指定锁的超时时间,就使用 30*1000[Lockwatchdog Timeout 看门狗的默认时间]
- 只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间,每隔10s自动续期成30s】,
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
- 如果命中缓存,方法不再被调用。
key
默认自动生成category::SimpleKey []
- 自定义接收SpEL:
@Cacheable(value = {"category"}, key= "'name'")
@Cacheable(value = {"category"}, key = "#root.method.name")
- 自定义接收SpEL:
- 缓存的
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;
}
}
- 读模式
- 缓存穿透:
cache-null-values=true
- 缓存击穿:
sync=true
- 缓存雪崩:
spring.cache.redis.time-to-live=时间
- 缓存穿透:
- 写模式
- 读写加锁
- 引入
Canal
, 感知到MySQL
的更新则去更新缓存 - 读多写多,直接去数据库查询
- 总结
- 常规数据(度多写少,即时性,一致性要求不高的数据),完全可以使用
Spring Cache
- 特殊数据,特殊设计。
- 常规数据(度多写少,即时性,一致性要求不高的数据),完全可以使用
坑:在从首页点击分类名跳转到搜索页时,跳转链接在
catalogLoader.js
中,原静态资源链接为http://search.gmall.com/
,需要改为自己在 HOST 文件中配置的域名。
// 不要直接删除重建 会丢失已上架的商品数据
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>
- 获取 SKU 基本信息
- 获取 SKU 图片信息
- 获取 SKU 促销信息
- 获取 SPU 所有销售属性
- 获取规格参数组及组下的规格参数
- SPU 详情
-
创建异步对象
-
计算完成时回调方法
-
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("/**");
}
}
本系统消息队列工作图
- 异步处理
- 应用解耦
- 流量控制(削峰、填谷)
- 队列(点对点)
- 订阅
- JMS
- AMQP
- Docker
- 4369,25672: Erlang 发现 & 集群端口
- 5672,5671: AMQP端口
- 15672: Web管理后台端口
- 1883,8883: MQTT协议端口
- 61613, 61614: STOMP协议端口
$ 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
- Homebrew 安装
$ 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
又重新发送- 消费者的业务消费接口应该设计成幂等性的,比如扣库存工作单的状态标志
- 使用 防重表(redis, mysql) ,发送消息每一个都有业务的唯一标识,处理过就不用再处理。
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
重试机制 - 其他业务情况
- Token机制
Redis Lua
脚本
- 各种锁机制
- 数据库悲观锁、乐观锁
- 业务层分布式锁
- 各种唯一性约束
- 数据库唯一性约束
redis set
防重- 防重表
- 全局请求唯一ID
- CAP 定理
- C: 一致性,在分布式系统中的所有数据备份,在同一时刻是否有同样的值。
- A: 可用性,再急群众一部分结点故障后,集群整体是否还能响应客户端的读写请求。
- P: 分区容错性,大多数分布式系统都分布在多个子网络,每个子网络就叫做一个区,分区容错的意思是,区间通信可能失败。
- CAP 定理指的是以上三点至多只能同时保证两点,不能三者兼顾,一般来说在分布式系统中 P 不可避免,所以一个系统至多只能满足 CP 或 AP。
- Raft定理动画
- BASE 定理
- 选择 AP,舍弃实现 C (强一致性),选择实现弱一致性,保证实现最终一致性。
- 基本可用
- 软状态
- 最终一致性
- 本地事务失效问题
- 同一个对象内事务互调默认失败,原因是绕过了代理对象,而事务是通过代理对象来控制的。
- 解决方法
- 使用代理对象来调用事务方法,引入
spring-boot-starter-aop
,aop
又引入了aspectj
@EnableAspectJAutoProxy(exposeProxy = true)
,开启aspectj
动态代理功能,如果不开启的话,默认使用的是JDKProxy
,开启后以后创建对象采用aspectj
动态代理(即使没有接口也可以创建代理对象, JDKProxy要求被代理的对象有接口定义)- 本类事务互相调用此时可以实现
AopContext.currentProxy
- 使用代理对象来调用事务方法,引入
- 2PC(2 phase commit, 二阶段提交)模式
- 柔性事务-TCC事务补偿性方案
- 刚性事务:遵循ACID
- 柔性事务:遵循BASE
- 柔性事务-最大努力通知型方案
- 柔性事务-可靠消息+最终一致性(异步确保型)
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
- 添加主机名与 IP 映射
$ 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
- 卸载系统之前到docker
$ sudo yum remove docker docker-client docker-client-latest docker-common docker-latest docker-latest-logrotate docker-logrotate docker-engine
- 安装 Docker-CE
$ 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
- docker 加速
$ 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 授权用户来同步数据
# 进入 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}'}
- 使用 Redis 建立集群
$ 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