# mall **Repository Path**: luckma/mall ## Basic Information - **Project Name**: mall - **Description**: springcloud分布式电商平台 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 3 - **Created**: 2020-08-13 - **Last Updated**: 2021-09-04 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # mall #### 介绍 自己测试分布式项目 #### 软件架构 软件架构说明 ##### Nginx 进行反向代理 ##### API 网关 spring-cloud-gateway ##### 注册中心 spring-cloud-alibaba-Nacos ##### 配置中心 spring-cloud-alibaba-Nacos ##### 分布式事务 spring-cloud-alibaba-Seata https://seata.io/zh-cn/docs/overview/what-is-seata.html ##### 远程服务调用 Spring-Cloud-Feign ##### 服务的熔断限流降级 Sentinel 哨兵 ##### 服务链路追踪 Sleuth+Zipkin ##### 文件服务器 阿里云 oss ##### 短信服务 阿里云短信API接口 ##### 缓存中间件 redis ##### 分布式锁 redisson ##### **全文检索 ElasticSearch** ##### 消息队列 RabbitMQ ###### mall-auth-server 登录注册服务 ###### **mall-common 通用模块** ###### **mall-coupon 优惠服务** ###### **mall-gateway api网关服务** ###### **mall-member 会员服务** ###### **mall-order 订单服务** ###### **mall-product 产品服务** ###### **mall-ware 仓储服务** ###### **mall-third-party 第三方服务 (阿里云oss)** ###### **renren-fast 前端服务** ###### **renren-generator 自动生成代码** ###### mall-test-sso-client 单点登录客户端1 ###### mall-test-sso-client2 单点登录客户端2 ###### mall-test-sso-server 单点登录服务器 ###### mall-cart 购物车模块 ###### mall-seckill 秒杀模块 ###### **数据库 MySQL8.0.12** ###### thymeleaf 模板引擎 ###### 测试工具 JMeter #### 安装教程 ##### 整合mybatis-plus 1.导入依赖 ```xml com.baomidou mybatis-plus-boot-starter 3.3.2 ``` 2. 配置 官方文档:https://baomidou.com/ 1) 配置数据源 1. 导入数据库驱动 2. 在application.yml配置数据源 2) 配置mybatis-plus 1. 使用@MapperScan("com.mall.product.dao") 2. 告诉mybatis-plus,sql映射文件位置 和配置主键自增 mybatis-plus: mapper-locations: classpath:/mapper/**/*.xml global-config: db-config: id-type: auto #### 使用说明 ##### 引入redis ```xml org.springframework.boot spring-boot-starter-data-redis ``` ```yaml spring: redis: host: 192.168.137.141 port: 6379 ``` ##### 引入redisson ```xml org.redisson redisson 3.13.3 ``` 配置redis ##### 整合spring cache简化缓存开发 1.引入依赖 ```xml org.springframework.boot spring-boot-starter-cache ``` 2.写配置 CacheAutoConfiguration中导入了RedisCacheConfiguration 自动配好了缓存管理器RedisCacheManager 配置redis作为缓存 spring.cache.type=redis 3.测试 @EnableCaching 开启缓存功能 4.默认行为 5.标签 @CacheEvict 级联删除所有的关联数据 6. spring cache的不足 ##### 引入thymeleaf 1.引入starter ```xml org.springframework.boot spring-boot-starter-thymeleaf ``` 2.关闭缓存 ```yaml spring: thymeleaf: cache: false ``` 静态资源放在static文件夹下 html页面放在templates中 3.实现不重启服务就能更新页面 关闭缓存并导入 ```xml org.springframework.boot spring-boot-devtools true ``` 按ctrl+f9 代码修改建议重启 ##### 想要访问远程服务 1. 引入open-feign 2. 编写一个接口,告诉springcloud这个接口需要调用远程服务 1)声明接口的每一个方法都是调用哪个远程服务的哪个请求 3. 开启远程服务调用功能 ##### 如何使用nacos当作配置中心 1.引入依赖 ```xml com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config ``` 2.创建一个bootstrap.properties ```properties spring.application.name=mall-coupon spring.cloud.nacos.config.server-addr=192.168.137.145:8848 ``` 3.给配置中心默认添加一个 数据集(Data Id) 默认规则, 应用名.propertiey 4.给应用名.properties添加任何配置 5.动态获取 @RefreshScope 动态获取并刷新配置 @Value("${配置项的名}") 获取某个配置的值 如果配置中心和当前应用的配置文件中都配置了相同的项,优先使用配置中心的 6.细节 1)命名空间 public(保留空间);默认新增的配置都在public空间 1.开发、测试、生产环境 利用命名空间来做环境隔离 ```properties spring.cloud.nacos.config.namespace=123f84f9-1a70-4123-8f19-d47c822f6735 ``` 2.基于每一个微服务之间互相隔离配置,每一个微服务都创建自己的命名空间,只加载自己命名空间下的所有配置 2)配置集 所有配置的集合 3)配置集id 类似于文件名 4)配置分组 5)同时加载多个配置集 只需要在bootstrap.properties中说明配置中心 #### 参与贡献 1. Fork 本仓库 2. 新建 Feat_xxx 分支 3. 提交代码 4. 新建 Pull Request #### 细节 ##### 逻辑删除 1.配置全局的逻辑删除规则 加上逻辑删除注解@TableLogin 跨域访问 ####后台校验 JSR303 !!注意 导入的包是 ```xml org.hibernate.validator hibernate-validator 7.0.0.Alpha2 ``` 给bean加注解 import javax.validation.*; 1. 给需要校验的属性添加@NotBlank(message = "品牌名必须提交") 2. @Valid开启校验 效果:校验错误会有默认的响应 3. 在提交的后边紧跟BindingResult result,可以获取到校验的结果 ```java @RequestMapping("/save") public R save(@Valid @RequestBody BrandEntity brand, BindingResult result){ brandService.save(brand); return R.ok(); } ``` 缺点:每个表单都需要校验,很繁琐 4.统一的异常处理 @ControllerAdvice 4. 分组校验 1)给校验注解上标注什么情况需要进行校验 ```java @Validated @NotBlank(message = "品牌名必须提交",groups = {AddGroup.class,UpdateGroup.class}) private String name; ``` 2) @RequestMapping("/save") public R save(@Validated(AddGroup.class) @RequestBody BrandEntity brand/*, BindingResult result*/){ 3) 没有指定分组的校验注解,在分组校验情况下不会被校验 5. 自定义校验 1)编写一个自定义的校验注解 ```java @Documented @Constraint(validatedBy = { ListValueConstraintValidator.class }) //可以指定多个不同的校验器,适配类型的校验 @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) public @interface ListValue { ``` 2)编写一个自定义的校验器 3)关联自定义的校验器和自定义的校验注解 6. 解决视图映射 当一个请求只跳转时可以配置 ```java @Configuration public class MallWebConfig implements WebMvcConfigurer { //视图映射 ,解决跳转问题 @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/login.html").setViewName("login"); registry.addViewController("/reg.html").setViewName("reg"); } } ``` ### 分布式session 同一个服务,复制多份在不同的服务器,session不同步 ####解决: 1.session复制 tomcat原生支持 缺点:数据量传输大 2.客户端存储 缺点:不安全 **3. hash一致性 √** **4. 统一存储√** 不同服务,不能跨域 解决: 子域session共享 #### 配置解决跨域和复制 1. 导入依赖 ```xml org.springframework.session spring-session-data-redis ``` 2. 配置使用session redis 将session数据保存到redis中 ```properties spring.session.store-type=redis ``` 3. 编写配置文件 配置序列化器和设置spring session完成跨域访问 ```java package com.mall.product.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.session.web.http.CookieSerializer; import org.springframework.session.web.http.DefaultCookieSerializer; /** * @Description: springSession配置类 * @Created: with IntelliJ IDEA. **/ @Configuration public class MallSessionConfig { @Bean public CookieSerializer cookieSerializer() { DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer(); //放大作用域 cookieSerializer.setDomainName("mall.com"); cookieSerializer.setCookieName("MALLSESSION"); return cookieSerializer; } @Bean public RedisSerializer springSessionDefaultRedisSerializer() { return new GenericJackson2JsonRedisSerializer(); } } ``` ### 单点登录 SSO (Single Sign On) 单点登录(SingleSignOn,SSO),就是通过用户的一次性鉴别登录。当用户在身份认证服务器上登录一次以后,即可获得访问单点登录系统中其他关联系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统。 单个用户登录成功之后 ssoserver发送一个cookie表示已经登录过,如果在登录别的系统时,再访问登录请求的时候会携带这个cookie,判断携带的cookie值来判断是否登录,如果登陆过,就直接返回 ### 第三方登录 #### 微博登录 ##### 1. 获取code码 ##### 2. 根据code码获取access_token ##### 3. 根据access_token获取用户信息 ### 引入RabbitMQ 1. 引入依赖 ```xml org.springframework.boot spring-boot-starter-amqp ``` 2. RabbitAutoConfiguration 给容器中配置了RabbitTemplate\amqpAdmin 等组件 3. @EnableRabbit 4. 给配置文件中配置信息 ```properties spring.rabbitmq.host=server.com spring.rabbitmq.port=5672 spring.rabbitmq.virtual-host=/ ``` 部分测试代码如下: ```java package com.mall.order; import com.mall.order.entity.OrderEntity; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.*; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class MallOrderApplicationTests { @Autowired AmqpAdmin amqpAdmin; //用于创建 Exchange,Queue,Binding @Autowired RabbitTemplate rabbitTemplate; //发送消息等操作 @Test void createExchange() { System.out.println(amqpAdmin); DirectExchange directExchange = new DirectExchange("hello-exchange"); amqpAdmin.declareExchange(directExchange); } @Test void createQueue() { Queue queue = new Queue("hello-queue"); amqpAdmin.declareQueue(queue); } @Test void createBinding() { Binding binding = new Binding("hello-queue", Binding.DestinationType.QUEUE,"hello-exchange","hello.#",null); amqpAdmin.declareBinding(binding); } @Test void sendMessage() { rabbitTemplate.convertAndSend("hello-exchange","hello.#",new OrderEntity()); rabbitTemplate.convertAndSend("hello-exchange","hello.#","hello"); } } ``` 5. 监听消息 @RabbitListener ```java /** * 需要开启@EnableRabbitMQ * * @RabbitListener(queues = {"hello-queue"}) * * queues:需要监听的队列 * 使用时可以RabbitListener加在类上,静定某个队列,在方法上使用@RabbitHandler,可以重载方法,监听到不同的类型的消息 * @RabbitListener 类+方法上 * @RabbitHandler 只能标注在方法上 * @param message 消息头 * @param orderEntity 发送时序列化的对象可以直接反序列化 * @param channel 传输数据的通道 */ @RabbitListener(queues = {"hello-queue"}) public void receiveMessage(Message message, OrderEntity orderEntity, Channel channel){ System.out.println("接收到消息内容" + message); System.out.println("aaaaaaaa" + orderEntity); } ``` #### rabbitMQ的确认机制 ##### 配置发送端确认 1. 开启发送端确认 ```properties #开启发送端确认 spring.rabbitmq.publisher-returns=true #被废弃了 #需要改成以下的 spring.rabbitmq.publisher-confirm-type=correlated ``` 2. 设置确认回调 ```java package com.mall.order.conf; import org.springframework.amqp.rabbit.connection.CorrelationData; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.annotation.PostConstruct; /** * @Description: * @Created: with IntelliJ IDEA. **/ @Configuration public class MyRabbitConfig { @Autowired private RabbitTemplate rabbitTemplate; /** 设置消息转换为json */ @Bean public MessageConverter messageConverter() { return new Jackson2JsonMessageConverter(); } @PostConstruct // 在构造方法执行之后执行 public void initRabbitTemplate(){ rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() { /** * * @param correlationData 当前消息的唯一关联时据 * @param ack 成功或者失败 * @param cause 失败原因 */ @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { System.out.println(correlationData.getId()); System.out.println(ack); System.out.println(cause); } }); } } ``` ### Feign远程过程调用丢失请求头问题 浏览器发送请求头自带cookie Feign远程过程调用创建一个新的请求头,此使丢失了请求头 没有请求头就会以为没有登录 加上feign远程调用用的请求拦截器 ```java package com.mall.order.conf; import feign.RequestInterceptor; import feign.RequestTemplate; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; /** * @Description: feign拦截器功能 * @Created: with IntelliJ IDEA. **/ @Configuration public class MallFeignConfig { @Bean("requestInterceptor") public RequestInterceptor requestInterceptor() { RequestInterceptor requestInterceptor = new RequestInterceptor() { @Override public void apply(RequestTemplate template) { //1、使用RequestContextHolder拿到刚进来的请求数据 ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (requestAttributes != null) { //老请求 HttpServletRequest request = requestAttributes.getRequest(); if (request != null) { //2、同步请求头的数据(主要是cookie) //把老请求的cookie值放到新请求上来,进行一个同步 String cookie = request.getHeader("Cookie"); template.header("Cookie", cookie); } } } }; return requestInterceptor; } } ``` 如果是异步调用feign的拦截器,因为RequestContextHolder.getRequestAttributes()内部是 ```java private static final ThreadLocal requestAttributesHolder = new NamedThreadLocal<>("Request attributes"); ``` 因此开启线程池会导致在新的线程中执行找不到getRequestAttributes,因此需要将数据同步到不同的线程 ```java //TODO :获取当前线程请求头信息(解决Feign异步调用丢失请求头问题) RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); //开启第一个异步任务 CompletableFuture addressFuture = CompletableFuture.runAsync(() -> { //每一个线程都来共享之前的请求数据 RequestContextHolder.setRequestAttributes(requestAttributes); //1、远程查询所有的收获地址列表 List address = memberFeignService.getAddress(memberResponseVo.getId()); confirmVo.setMemberAddressVos(address); }, threadPoolExecutor); ``` ### 幂等性 天然幂等操作,以SQL为例 查询:无论执行多少次都不会改变状态 更新:无论执行成功多少次状态都是一致的,a=1,成功多少次都是1;幂等操作。 删除:多次操作,结果一样。幂等 插入:如果有唯一主键,那样页只会插入一条数据 ### 分布式事务 #### 本地事务 数据库事务的四个特性 ACID 原子性 一致性 隔离性 持久性 一致性算法:RAFT、paxos BASE理论 事务的传播行为 spring 事务使用代理做的 ##### 本地事务失效问题 同一个对象内方法互调默认失效,原因:绕过了代理对象,事务是使用代理对象来控制的 解决:使用代理对象来调用事务方法 1) 引入 aop-starter aspectj 即使没有接口,也可以创建动态代理 ```xml org.springframework.boot spring-boot-starter-aop ``` 2)开启动态代理 对外暴漏代理对象 ```java @EnableAspectJAutoProxy(exposeProxy = true) ``` #### 分布式事务的几种方案 1)2pc模式 二阶段提交 2)柔性事务-TCC事务补偿型方案 3)柔性事务-最大努力通知型方案 3)柔性事务-可靠消息+最大努力通知型方案 ### 使用Seata控制分布式事务 1) 每一个微服务必须创建undo_logo表 ```sql -- 注意此处0.3.0+ 增加唯一索引 ux_undo_log 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; ``` 2) 安装事务协调器 Seata-server https://github.com/seata/seata/releases 3)导入依赖 ```xml com.alibaba.cloud spring-cloud-starter-alibaba-seata ``` 4) 启动seata服务器 ```shell script sh seata-server.sh -p 8091 -h 127.0.0.1 -m file ``` 5) 在需要开启事务的方法上标注 @GlobalTransactional 所有想要用到分布式事务的微服务都需要使用seata DataSourceProxy代理自己的数据源 ```java package com.mall.order.conf; import com.zaxxer.hikari.HikariDataSource; import io.seata.rm.datasource.DataSourceProxy; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.util.StringUtils; import javax.sql.DataSource; /** * @Description: * @Created: with IntelliJ IDEA. **/ @Configuration public class MySeataConfig { @Autowired DataSourceProperties dataSourceProperties; @Bean public DataSource dataSource(DataSourceProperties dataSourceProperties) { HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build(); if (StringUtils.hasText(dataSourceProperties.getName())) { dataSource.setPoolName(dataSourceProperties.getName()); } return new DataSourceProxy(dataSource); } } ``` 每个微服务都必须导入file.conf registry.conf ### 定时任务 ```java @Scheduled(cron = "*/5 * * ? * 3") public void hello() { log.info("hello..."); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } } ``` ### Sentinel 引入依赖 ```xml com.alibaba.cloud spring-cloud-starter-alibaba-sentinel ``` 启动sentinel控制台 ```shell script java -jar sentinel-dashboard-1.7.1.jar --server.port=8333 ``` 配置sentinel控制台地址信息 ```properties spring.cloud.sentinel.transport.dashboard=server.com:8333 spring.cloud.sentinel.transport.port=8719 ``` 导入actuator ```xml org.springframework.boot spring-boot-starter-actuator ``` 暴漏 ```properties management.endpoints.web.exposure.include=* ``` 自定义流控返回数据 ####开启熔断降级 #####调用方的熔断保护 ```properties feign.sentinel.enabled=true ``` 配置远程调用失败时回调哪个函数 ```java @FeignClient(value = "mall-seckill",fallback = SeckillFeignServiceFallBack.class) public interface SeckillFeignService { /** * 根据skuId查询商品是否参加秒杀活动 * @param skuId * @return */ @GetMapping(value = "/sku/seckill/{skuId}") R getSkuSeckilInfo(@PathVariable("skuId") Long skuId); } ``` 回调的函数 ```java import com.common.exception.BizCodeEnume; import com.common.utils.R; import com.mall.product.feign.SeckillFeignService; import org.springframework.stereotype.Component; /** * @Description: * @Created: with IntelliJ IDEA. **/ @Component public class SeckillFeignServiceFallBack implements SeckillFeignService { @Override public R getSkuSeckilInfo(Long skuId) { return R.error(BizCodeEnume.TO_MANY_REQUEST.getCode(),BizCodeEnume.TO_MANY_REQUEST.getMsg()); } } ``` #####调用方手动指定远程服务的降级策略,远程服务被降级处理。触发我们的熔断回调方法 ##### 超大流量的时候,必须牺牲一些远程服务,在远程服务指定降级策略 远程服务是在运行的,但是不运行自己的业务逻辑。返回的是默认的熔断数据(限流的数据) ####自定义受保护的资源 #####代码方式 ```java try (Entry entry = SphU.entry("seckillSkus")) { //业务代码 } catch (BlockException e) { log.error("资源被限流{}",e.getMessage()); } } ``` #####基于注解 ```java //指定哪个方法 @SentinelResource(value = "getCurrentSeckillSkusResource",blockHandler = "blockHandler") ``` ```java public List blockHandler(BlockException e) { log.error("getCurrentSeckillSkusResource被限流了,{}",e.getMessage()); return null; } ``` ####在网关层熔断 环境配置 ```xml com.alibaba.cloud spring-cloud-alibaba-sentinel-gateway ``` ###服务链路追踪 Sleuth+Zipkin Sleuth依赖 ```xml org.springframework.cloud spring-cloud-starter-sleuth ``` Zipkin 可视化界面 ```xml org.springframework.cloud spring-cloud-sleuth-zipkin ``` docker 安装 ```shell script docker run -d -p 9411:9411 openzipkin/zipkin ``` ```properties #服务追踪 spring.zipkin.base-url=http://server.com:9411/ #关闭服务发现 spring.zipkin.discovery-client-enabled=false spring.zipkin.sender.type=web #配置采样器 spring.sleuth.sampler.probability=1 ```