raymond-zhao / cat-mall

尚硅谷《谷粒商城》-开发笔记,另有学习文档笔记,期待大家的加入,
https://raymond-zhao.top/campus-interview/#/Home
Apache License 2.0
254 stars 104 forks source link

前言

做项目不是为了敲代码,而是为了学知识,学原理,不深入去理解底层原理的话就是普通的 CRUD 工程师。 项目中涉及的比较重要的内容可以查看 Wiki 页面,或者 awesome-architect

现在文档中的知识点还比较有限,内容还在持续完善中。以后将会逐渐深入源码,分析学习运行原理与机制。

微服务架构电商系统,主要分为三个阶段。

谷粒商城-微服务架构图

项目重启日志

因为准备秋招耽误了许久,现在重新开始,重新启动项目时遇到许多麻烦,下面记录一下大致步骤,避免以后忘记。

系统为 macOS BigSur,包管理工具为 Homebrew

# 进入到 mall-commons 所在目录
$ mvn clean
$ mvn install
$ brew install nginx
$ brew info nginx
# 静态文件目录 /usr/local/var/www/static
$ brew install redis
$ redis-server /usr/local/etc/redis.conf
# 启动命令 ( standalone 代表着单机模式运行,非集群模式)
$ sh startup.sh -m standalone
// localhost:8848/nacos
$ curl -sSL https://zipkin.io/quickstart.sh | bash -s
$ java -jar zipkin.jar
$ brew install elasticsearch
# 如果内存不是特别大的话,最好设置一下 ES 启动时的虚拟机参数,在 mac 中 Brew 安装目录是
# /usr/local/Cellar/elasticsearch/7.10.0/libexec/config/jvm.options
# mac 终端打开
$ open /usr/local/Cellar/elasticsearch/7.10.0/libexec/plugins
$ brew cask install rabbitmq

完成上面几个步骤之后,项目正常启动。

分布式基础篇-全栈开发

基础环境

不少于一台 PC

Docker 环境

$ 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

开发环境

微服务模块

初始化数据库

逆向工程

Maven

<!-- 阿里云镜像 -->
<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>

Node.js

$ 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 # 此时可成功

生成基本CRUD代码

Spring Cloud Alibaba

<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>

Nacos 作为服务注册与发现中心

<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

OpenFeign 作为服务通信组件

<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")

Nacos 作为配置中心

<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

Spring Cloud Gateway 作为网关

<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;
}

跨域问题

@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);
    }
}

MyBatis Plus 逻辑删除

前端代码调整

关闭前端权限认证

export function isAuth (key) {
  // return JSON.parse(sessionStorage.getItem('permissions') || '[]').indexOf(key) !== -1 || false
  return true
}

关闭 ESLint

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
  }
})

阿里云 OSS

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alicloud-oss</artifactId>
</dependency>

数据验证

自定义注解:@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})

SKU 与 SPU

这两个名词将会贯穿从此开始到高级篇结束的所有内容。

分布式高级篇-微服务架构

ElasticSearch

前置工作

$ docker pull elasticsearch:7.4.2 # 存储和检索数据
$ dock pull kibana:7.4.2 # 可视化检索数据
$ 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
$ 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
# 其中IP地址一定要改为自己机器或服务器的IP
$ docker run --name kibana -e ELASTICSEARCH_HOSTS=http://xxx.xx.xx.xxx:9200 -p 5601:5601 -d kibana:7.4.2

倒排索引

官方开发手册

_cat

GET/_cat/nodes
GET/_cat/health
GET/_cat/master
GET/_cat/indices // 查看所有索引

索引文档

// 保存一条数据 保存在哪个索引的哪个类型下 指定用哪一个标识
PUT customer/external/1 // PUT 和 POST 均可 PUT必须带ID,POST可带可不带
{
  "name": "John Snow"
}

整合 Spring Boot

Kibana 创建 sku 索引

此索引后来会出现问题,下面有修改。

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"
}

安装 nginx 将词库放入nginx,ik分词器向nginx发送请求。

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调用流程

推荐阅读:

Feign 远程调用丢失请求头问题

Feign在远程调用之前要构造请求,此时会丢失请求头headersrequest中包含许多拦截器。

在构建新请求的时候需要吧“老请求”中的数据获取并保存传递到新请求中。

@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);
            }
        };
    }
}

Feign异步编排丢失请求头问题

页面渲染

整合 Thymeleaf

Nginx

域名配置

#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/*;
}

动静分离

本项目可能问到的 Nginx 面试题

// 在 server 块中添加
location /static/ {
    root /usr/local/var/www;
}

性能压测

基本概念

JVM

Apache JMeter

# 执行测试计划
$ jmeter -n -t testplan/RedisLock.jmx -l testplan/result/result.txt -e -o testplan/webreport

压测优化

Redis

相关问题已整理至 Wiki 页面

基本使用

@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 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

缓存数据一致性

分布式锁

在每一个微服务中的synchronized(this)加锁的对象只是当前实例,但是并未对其他微服务的实例产生影响,即使每个微服务加锁后只允许一个请求,假如有 8 个微服务,仍然会有 8 个线程存在。

锁-时序性问题

Redis 实现分布式锁🔐

Redisson

Redisson-GitHub Wiki

看门狗

RLock lock = redissonClient.getLock("my-lock");
lock.lock();
lock.lock(10, TimeUnit.SECONDS);

Spring Cache

Spring Cache Documentation

常用注解

重点类

默认行为(@Cacheable({"category"}))

@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总结

检索服务

坑:在从首页点击分类名跳转到搜索页时,跳转链接在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"
    }
}

构建检索请求与封装检索结果

多线程与异步

业务场景 - 商品详情页

CompletableFuture 主要功能

认证服务

短信验证

OAuth2.0 之微博登录

微博登录出现的问题:

HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", new HashMap<>(), map, new HashMap<>());

Spring Session

对象 JDK 序列化

Spring Session核心原理

购物车

京东给每个用户生成一个值类似于 UUID 的user-key,有效期一个月,存储在Cookie,浏览器保存以后,每次访问都会带上这个cookie

登录后:session有用户信息

未登录:cookie中的user-key

第一次使用时,如果没有临时用户,帮忙创建一个临时用户。

ThreadLocal 用户身份鉴别

@Configuration
public class MallWebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new CartInterceptor()).addPathPatterns("/**");
    }
}

消息队列 - RabbitMQ (面试高频)

本系统消息队列工作图

本系统消息队列工作图

应用场景

类型

执行标准

安装

$ 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

发送消息

如果是传输对象的话,传输的对象必须实现序列化接口,默认的序列化方式是 JDK 序列化,但是也可以手动指定序列化的方式。

@Configuration
public class MyRabbitConfig {
    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }
}

可靠投递

延时队列

Dead Letter Exchanges(DLX) - 死信路由

消息积压、重复、丢失等问题解决方案

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

订单服务

基本环境搭建

订单流程

下单->创建订单->验证令牌->核算价格->锁定库存

接口幂等性

在确认页点击 提交订单 时,用户可能不小心点击多次,所以即使用户点击次数大于1次,也应该保证只提交一次。

应用情况

幂等性解决方案

分布式事务

image-20200525153506445

事务传播

解决方案

Seata

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;

支付宝支付

加密与解密

沙箱环境

文档-沙箱环境

RSA生成器

内网穿透

支付宝异步通知

秒杀业务

秒杀(高并发)系统关注的问题

秒杀业务具有瞬间高并发的特点,必须要做限流+异步+缓存(页面静态化)+独立部署

限流方式:

CRON表达式

定时与异步

幂等性保证

查看总的代码行,包括添加了多少行,删除了多少行,现在总共多少行。

$ 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流控熔断降级

Sentinel Wiki - 中文

Sentinel Hystrix
隔离策略 信号量隔离 线程池隔离/信号量隔离
熔断降级策略 基于响应时间或失败比率 基于失败比率
实时指标实现 滑动窗口 滑动窗口(基于 RxJava)
规则配置 支持多种数据源 支持多种数据源
扩展性 多个扩展点 插件的形式
基于注解的支持 支持 支持
限流 基于 QPS,支持基于调用关系的限流 有限的支持
流量整形 支持慢启动、匀速器模式 不支持
系统负载保护 支持 不支持
控制台 开箱即用,可配置规则、查看秒级监控、机器发现等 不完善
常见框架的适配 Servlet、Spring Cloud、Dubbo、gRPC 等 Servlet、Spring Cloud Netflix

Sleuth + Zipkin 链路追踪

Zipkin

# docker
$ docker run -d -p 9411:9411 openzipkin/zipkin

# java
$ curl -sSL https://zipkin.io/quickstart.sh | bash -s
$ java -jar zipkin.jar

高可用集群篇-架构师提升

Kubernetes

Kubernetes-中文文档

环境准备

$ vargrant ssh xxxxx
$ su root # password vargrant
$ vi /etc/ssh/sshd_config
# 修改 PasswordAuthentication yes
$ service sshd restart
# 所有虚拟机设置为 4 core 4G
# 关闭防火墙
$ 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
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
$ 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 # 开机自启

MySQL 集群

集群类型

Docker 安装 MySQL - 一主两从

感觉哪里少了东西

$ 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

Sharding-Sphere

Redis集群 - 三主三从

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

ElasticSearch集群

RabbitMQ集群

Kubernetes部署

流水线