谷粒商城分布式高级篇学习笔记

ElasticSearch

在这里插入图片描述

Feign调用流程

SynchronousMethodHandler.java的invoke()方法

1、构造请求数据,将对象转为json
SynchronousMethodHandler.java的invoke()方法

1、构造请求数据,将对象转为json

RequestTemplate template = buildTemplateFromArgs.create(argv);

2、发送请求进行执行(执行成功会解码响应数据)

	executeAndDecode(template);

3、执行请求会有重试机制

	while(true) {
		try{
            executeAndDelete(template);
        }catch() {
            try{
                retryer.continueOrPropagate(e);
            }catch() {
                throw ex;
            }
            continue;
        }}

R

在这里插入图片描述
在这里插入图片描述

模板引擎(Thymeleaf)

1、application.yml:
在这里插入图片描述
​ thyemleaf-starter:关闭缓存

2、静态资源都放在static文件夹下就可以按照路径直接访问

3、页面放在templates下,直接访问

​ SpringBoot,访问项目的时候,默认会找index
在这里插入图片描述
4、页面修改不重启服务器实时更新

​ 1)、引入dev-tools
在这里插入图片描述
​ 2)、修改完页面 ctrl shift f9重新自动编译当前页面

​ ctrl f9编译当前服务

Nginx

在这里插入图片描述
在这里插入图片描述
1)、访问gulimall.com,nginx监听后访问http://gulimall
在这里插入图片描述
2)、http块中配置upstream
在这里插入图片描述
3)、跳转到http://192.168.56.1:88网关
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

JMeter性能压测

内存泄漏、并发与同步
在这里插入图片描述
JMeter压测,通过报告查看性能情况,Jvisualvm可以查看虚拟机空间占用

  • 关日志

    日志打印设为error级别

  • 开缓存

  • 优化数据库

    数据库增加索引

thymeleaf关闭缓存

  • thymeleaf占用Tomcat资源(Nginx动静分离)
    在这里插入图片描述
    在nginx目录下,将后端项目static目录下的index静态文件剪切到nginx的html/static挂载目录下,设置nginx路由规则

当访问到gulimall.com/static目录,自动请求nginx/static静态文件

在这里插入图片描述
重启nginx
在这里插入图片描述

  • jvm空间太小,频繁gc

    调高最大堆内存(堆内存太小)Eden区内存不够,进行MinorGC,内存还不够,分配到老年代,老年代对象不够分配,则进行一次full gc,非常耗费资源。
    在这里插入图片描述
    在这里插入图片描述
    堆内存设置为1024m,新生代堆内存设置为512m

Redis

在这里插入图片描述

整合redis

1、引入data-redis-starter

2、简单配置redis的host等信息

3、使用SpringBoot自动配置好的StringRedisTemplate来操作redis

并发访问redis,可能产生堆外溢出异常

在这里插入图片描述
解决方案一:排除lettuce,引入jedis
在这里插入图片描述
解决方案二:升级lettuce客户端

基本使用

 @Autowired
    StringRedisTemplate stringRedisTemplate; 
@Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {
        String catalogJSONString = stringRedisTemplate.opsForValue().get("catalogJSON");
        if (StringUtils.isEmpty(catalogJSONString)) { //缓存中没有
            Map<String, List<Catelog2Vo>> catalogJson = getCatalogJsonFromDb();
            //对象要转换为String保存在缓存
            catalogJSONString = JSON.toJSONString(catalogJson);
            stringRedisTemplate.opsForValue().set("catalogJSON",catalogJSONString);
            return catalogJson;
        }
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSONString,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
        return result;
    }

加锁解决缓存击穿

在这里插入图片描述
本地部署多台相同的服务:

右键copy properties,设置服务名端口号
在这里插入图片描述
在这里插入图片描述

Redis分布式锁

在这里插入图片描述
在这里插入图片描述
整合redisson作为分布式锁等功能框架
在这里插入图片描述

  • 引入redisson依赖
 <!--redis分布	式锁框架-->
 <dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
   	<version>3.12.0</version>
 </dependency>
  • 生成RedissonClient

    最佳实战:指定时间lock.lock(xx,TimeUnit.SECONDS) 省掉了整个续期操作。手动解锁

   @Autowired
    RedissonClient redisson;   
@GetMapping("/hello")
    public String hello() {
        //1、获取一把锁,只要锁的名字一样,就是同一把锁
        RLock lock = redisson.getLock("my-lock");

        //2、加锁
        lock.lock();//阻塞式等待。默认加的锁都是30s时间
        //1)、锁的自动续期。如果业务超长,运行期间自动给锁续上新的30s。不用担心业务时间长,锁自动过期被删掉
        //2)、加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s以后自动删除。

//        lock.lock(10, TimeUnit.SECONDS); //10秒自动解锁,自动解锁时间一定要大于业务的指定时间。
        //lock.lock(10, TimeUnit.SECONDS); 在锁时间到了以后,不会自动续期
        //1、如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是我们指定的时间
        //2、如果我们未指定锁的超时时间,就使用30*1000【LockWatchdogTimeout看门狗的默认时间】
        //只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10s都会自动续期,续成30s
        //internalLockLeaseTime【看门狗时间】/3
        try {
            System.out.println("加锁成功,执行业务..."+Thread.currentThread().getId());
            Thread.sleep(30000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            //3、解锁  假设解锁代码没有运行,redisson会不会出现死锁
            System.out.println("释放锁..."+Thread.currentThread().getId());
            lock.unlock();
        }
        return "abc";
    }

Redisson读写锁

保证一定能读到最新数据,修改期间,写锁是一个排他锁(互斥锁、独享锁)。读锁是一个共享锁

写锁没释放,读就必须等待

  • 读+读:相当于无锁,并发读,只会在redis中记录好,所有当前的读锁。他们都会同时加锁成功
  • 写+读:等待写锁释放
  • 写+写:阻塞方式
  • 读+写:有读锁,写也需要等待
  • 只要有写的存在,都必须等待
    在这里插入图片描述
    在这里插入图片描述
    Redisson CountDownLatch闭锁
    在这里插入图片描述
    在这里插入图片描述

Redisson Semaphore信号量

应用:分布式限流操作
在这里插入图片描述
tryAcquire()判断是否能获取成功,进而执行操作
在这里插入图片描述

缓存数据一致性问题

SpringCache中采用@CacheEvict注解(失效模式)| @CachePut注解(双写模式)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

整合SpringCache简化缓存开发

1)、引入依赖

<!--SpringCache-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>

2)、写配置

​ (1)、自动配置了哪些

​ CacheAutoConfiguration会导入RedisCacheConfiguration

​ 自动配好了缓存管理器RedisCacheManager

​ (2)、配置使用redis作为缓存

​ spring.cache.type=redis

3)、测试使用缓存

在这里插入图片描述
1)、启动类开启缓存功能 @EnableCaching
在这里插入图片描述
​ 2)只需要使用注解就能完成缓存操作

1、每一个需要缓存的数据我们都来指定要放到哪个名字的缓存。【缓存的分区(按照业务类型分)】

2、@Cacheable({“category”})

​ 代表当前方法的结果需要缓存,如果缓存中有,方法不用调用。

​ 如果缓存中没有,会调用方法,最后将方法的结果放入缓存

3、默认行为

​ 1)、如果缓存中有,方法不用调用。

​ 2)、key默认自动生成;缓存的名字::SimpleKey

​ 3)、缓存的value的值。默认使用jdk序列化机制,将序列化后的数据存到redis

​ 4)、默认ttl时间 -1;

自定义:
1)、指定生成的缓存使用的key: key属性指定,接受一个SpEL
在这里插入图片描述
SpEL的详细https://docs.spring.io/spring/docs/5.1.12.RELEASE/spring-framework-reference/integration.html#cache-spel-context
2)、指定缓存的数据的存活时间: 配置文件中修改ttl
3)、将数据保存为json格式:
自定义RedisCacheConfiguration即可

4、Spring-Cache的不足;
1)、读模式:
缓存穿透:查询一个null数据。解决:缓存空数据;ache-null-values=true
缓存击穿:大量并发进来同时查询一个正好过期的数据。解决:加锁;?默认是无加锁的;sync = true(加锁,解决击穿)
缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间。:spring.cache.redis.time-to-live=3600000
2)、写模式:(缓存与数据库一致)
1)、读写加锁。

​ 2)、引入Canal,感知到MySQL的更新去更新数据库
​ 3)、读多写多,直接去数据库查询就行
​ 总结:
​ 常规数据(读多写少,即时性,一致性要求不高的数据);完全可以使用Spring-Cache;写模式(只要缓存的数据有过期时间就足够了)
​ 特殊数据:特殊设计

原理:

​ CacheManager(RedisCacheManager)->Cache(RedisCache)->Cache负责缓存的读写

在这里插入图片描述
value指定某一片缓存
在这里插入图片描述

SpringCache自定义配置

自定义配置类,将redis缓存key以字符串形式存储,value以Json格式存储。

注解

@EnableCaching //启用缓存

@EnableConfigurationProperties(CacheProperties.class) //配置完自定义缓存,properties的自定义配置失效,需将properties引入进来

@Configuration
@EnableCaching
@EnableConfigurationProperties(CacheProperties.class)
public class MyCacheConfig {
    @Bean
    RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
        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的不足:

1)、读模式

  • 缓存穿透:查询一个null数据。解决:缓存空数据;cache-null-values=true

  • 缓存击穿:大量并发进来同时查询一个正好过期的数据。解决:加锁;默认没有加锁,配置sync=true(加锁,解决击穿 本地锁

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k6HclnCn-1646212567127)(C:\Users\Gyf\AppData\Roaming\Typora\typora-user-images\image-20211217111801946.png)]

  • 缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间。:spring.cache.redis.time-to-live=3600000

2)、写模式:(缓存与数据库一致)

  • 读写加锁
  • 引入Canal,感知MySQL的更新去更新数据库
  • 读多写多,直接去数据库查询

总结:

  • 常规数据(读多写少,即时性,一致性要求不高的数据);完全可以使用Spring-Cache;写模式(只要缓存的数据有过期时间就足够了)

  • 特殊数据:特殊设计

原理

CacheManager(RedisCacheManager)->Cache(RedisCache)->Cache负责缓存的读写

线程与线程池

ThreadPoolExecutor

        //当前系统中池只有一两个,每个异步任务,提交给线程池让他自己去执行就行
        /**
         * 七大参数
         * corePoolSize:[5] 核心线程数[一直存在除非(allowCoreThreadTimeOut)]; 线程池,创建好以后就准备就绪的线程数量,就等待来接受异步任务去执行。
         *        5个  Thread thread = new Thread();  thread.start();
         * maximumPoolSize:[200] 最大线程数量;  控制资源
         * keepAliveTime:存活时间。如果当前的线程数量大于core数量。
         *      释放空闲的线程(maximumPoolSize-corePoolSize)。只要线程空闲大于指定的keepAliveTime;
         * unit:时间单位
         * BlockingQueue<Runnable> workQueue:阻塞队列。如果任务有很多,就会将目前多的任务放在队列里面。
         *              只要有线程空闲,就会去队列里面取出新的任务继续执行。
         * threadFactory:线程的创建工厂。
         * RejectedExecutionHandler handler:如果队列满了,按照我们指定的拒绝策略拒绝执行任务
         *
         *
         *
         * 工作顺序:
         * 1)、线程池创建,准备好core数量的核心线程,准备接受任务
         * 1.1、core满了,就将再进来的任务放入阻塞队列中。空闲的core就会自己去阻塞队列获取任务执行
         * 1.2、阻塞队列满了,就直接开新线程执行,最大只能开到max指定的数量
         * 1.3、max满了就用RejectedExecutionHandler拒绝任务
         * 1.4、max都执行完成,有很多空闲.在指定的时间keepAliveTime以后,释放max-core这些线程
         *
         *      new LinkedBlockingDeque<>():默认是Integer的最大值。内存不够
         *
         * 一个线程池 core 7; max 20 ,queue:50,100并发进来怎么分配的;
         * 7个会立即得到执行,50个会进入队列,再开13个进行执行。剩下的30个就使用拒绝策略。
         * 如果不想抛弃还要执行。CallerRunsPolicy;
         *
         */
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5,
                200,
                10,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(100000),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
//        Executors.newCachedThreadPool() core是0,所有都可回收
//        Executors.newFixedThreadPool() 固定大小,core=max;都不可回收
//        Executors.newScheduledThreadPool() 定时任务的线程池
//        Executors.newSingleThreadExecutor() 单线程的线程池,后台从队列里面获取任务,挨个执行
        //
        System.out.println("main....end....");
    }

CompletableFuture异步编排

在这里插入图片描述

  • JDK1.8以后新增的功能

  • 实现了Future接口,可以获取异步执行的结果
    在这里插入图片描述
    提供了函数式接口(@FunctionalInterface),可以使用lambda表达式,简化开发细节
    在这里插入图片描述
    在这里插入图片描述

1、创建异步对象

​ CompletableFuture提供了四个静态方法来创建一个异步操作
在这里插入图片描述
1、runXxxx都是没有返回结果的,supplyXxxx都是可以获取返回结果的

2、可以传入自定义的线程池,否则就用默认的线程池

  1. runAsync(Runnable runnable)
    在这里插入图片描述
    2、supplyAsync(Supplier supplier)
    在这里插入图片描述
    2、计算完成时回调方法
    在这里插入图片描述
    whenComplete可以处理正常和异常的计算结果,exceptionally处理异常情况。

exceptionally

exceptionally比whenComplete多了一个return返回值
在这里插入图片描述
whenComplete 和 whenCompleteAsync 的区别

  • whenComplete:是执行当前任务的线程执行继续执行 whenComplete 的任务。
    在这里插入图片描述
    在这里插入图片描述
  • whenCompleteAsync:是执行把 whenCompleteAsync 这个任务继续提交给线程池 来进行执行。 方法不以 Async 结尾,意味着 Action 使用相同的线程执行,而 Async 可能会使用其他线程 执行(如果是使用相同的线程池,也可能会被同一个线程选中执行

3、handle方法

在这里插入图片描述
在这里插入图片描述
4、线程串行化方法
在这里插入图片描述
thenApply方法:当一个线程依赖另一个线程时,获取上一个任务返回的结果,并返回当前任务的返回值。

thenAccept方法:消费处理结果。接收任务的处理结果,并消费处理,无返回结果。

thenRun方法:只要上面的任务执行完成,就开始执行thenRun,只是处理完任务后,执行thenRun的后续操作。

Function<? super T,? extends U>

  • T:上一个任务返回结果的类型
  • U:当前任务的返回值类型

properties配置注入

1、写出要注入的参数属性,在类上加上@ConfigurationProperties(prefix="")注解,@Component加入容器
在这里插入图片描述
2、poperties中配置值

加上properties提示依赖

  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-configuration-processor</artifactId>
      <optional>true</optional>
   </dependency>

在这里插入图片描述
3、引用即可
在这里插入图片描述

CompletableFuture异步编排项目中的使用(210、商品详情)

任务3、4、5依赖于任务1的结果,任务2与任务1可独立运行

在这里插入图片描述

@Override
    public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {
        SkuItemVo skuItemVo = new SkuItemVo();

        //1、sku基本信息获取 pms_sku_info
        CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
            SkuInfoEntity info = getById(skuId);

            skuItemVo.setInfo(info);
            return info;
        }, executor);

        //3、获取spu的销售属性组合
        CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> {
            List<SkuItemVo.SkuItemSaleAttrVo> saleAttr = skuSaleAttrValueService.getSaleAttrsBySpuId(res.getSpuId());
            skuItemVo.setSaleAttr(saleAttr);
        }, executor);

        //4、获取spu的介绍
        CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync((res) -> {
            SpuInfoDescEntity infoDescEntity = spuInfoDescService.getById(res.getSpuId());
            skuItemVo.setDesp(infoDescEntity);
        }, executor);

        //5、获取spu的规格参数信息
        CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync((res)->{
            List<SkuItemVo.SpuItemAttrGroupVo> spuItemAttrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(),res.getCatalogId());
            skuItemVo.setGroupAttrs(spuItemAttrGroupVos);
        },executor);

      
        //2、sku的图片信息 pms_sku_images
        CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {
            List<SkuImagesEntity> images = skuImagesService.getImagesBySkuId(skuId);
            skuItemVo.setImages(images);
        }, executor);


        CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,imageFuture).get();

        return skuItemVo;
    }

分布式session

在这里插入图片描述
不同域名,session默认不共享
在这里插入图片描述

方法一、session复制(不采用)

在这里插入图片描述

方法二、session客户端存储(不采用)

在这里插入图片描述

方法三、hash一致性(使用较广)

让某些用户固定的访问某一服务器
在这里插入图片描述

方法四、SpringSession统一存储(使用较广)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
业务逻辑:用户登录成功,获取到用户信息,携带用户信息跳转到gulimall.com。

​ 问题:1、不同域名session不共享

整合SpringSession(解决session共享问题)

文档:https://docs.spring.io/spring-session/reference/2.6.1/guides/boot-redis.html

一、更新依赖
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
二、application.properties

配置session类型

spring.session.store-type=redis # Session store type.
#session超时时间
server.servlet.session.timeout= # Session timeout. If a duration suffix is not specified, seconds is used.
#redis刷新策略
spring.session.redis.flush-mode=on_save # Sessions flush mode.
spring.session.redis.namespace=spring:session # Namespace for keys used to store sessions.

配置redis连接

spring.redis.host=localhost # Redis server host.
spring.redis.password= # Login password of the redis server.
spring.redis.port=6379 # Redis server port.
三、启动类上加上@EnableRedisHttpSession注解

在这里插入图片描述

四、实操

1、配置完毕后,SpringSession会将session设置的数据保存到redis,MemberRespVo对象需要实现序列化才能保存

在这里插入图片描述
2、redis中保存了session
在这里插入图片描述
3、product服务要获取auth服务保存的springsession数据

(1)导包

(2)在这里插入图片描述

(3)启动类加上@EnableRedisHttpSession注解

需要解决的问题

1、解决子域session共享问题: 默认发的令牌。session=dasaczcas。默认作用域:当前域

文档:https://docs.spring.io/spring-session/reference/api.html#api-cookieserializer

2、使用JSON的序列化方式来序列化对象数据到redis中:之前的实现方式:对象实现serializable接口

(各个使用分布式session的微服务序列化器必须保证全局统一)

@Configuration
public class GulimallSessionConfig {
    //对cookie进行配置 
    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        cookieSerializer.setDomainName(".gulimall.com");//设置作用域
        cookieSerializer.setCookieName("GULISESSION");//设置session name
        return cookieSerializer;
    }
    //实现redis序列化器 (不用将类实现serializable)
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }

}

在这里插入图片描述

五、SpringSession核心原理(装饰者模式)

在这里插入图片描述
1)、@EnableRedisHttpSession导入了RedisHttpSessionConfiguration配置

​ 1、给容器中添加了一个组件

​ SessionRepository=》》【RedisOperationsSessionRepository】:redis操作session。session的增删改查封装类

​ 2、SessionRepositoryFilter==》Filter:session存储过滤器,每个请求过来都必须经过filter

​ 1、创建的时候,就自动从容器中获取到了SessionRepository;

​ 2、原始的request,response都被包装 SessionRepositoryRequestWrapper SessionRepositoryResponseWrapper

​ 3、以后获取session。request.getSession();

​ 4、就变成了wrappedRequest.getSession();====>SessionRepository中获取到的
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

单点登录

在这里插入图片描述
在这里插入图片描述
基本流程

两个关键点:

ssoserver服务中

1、数据保存在redis,key作为token

2、ssoserver.com域名下保存cookie值为token的信息在浏览器。下次其他服务访问,ssoserver会获取cookie,判断是否存在该token,存在则不需要继续登录
在这里插入图片描述
代码:
在这里插入图片描述

client1、client2服务
@Controller
public class HelloController {


    @Value("${sso.server.url}")
    String ssoServerUrl;


    /**
     * 无需登录就可访问
     * @return
     */
    @ResponseBody
    @GetMapping("/hello")
    public String hello(){
        return "hello";
    }


    /**
     * 感知这次是在 ssoserver 登录成功跳回来的。
     * @param model
     * @param session
     * @param token 只要去ssoserver登录成功跳回来就会带上
     * @return
     */
    @GetMapping("/employees")
    public String employees(Model model, HttpSession session,
                            @RequestParam(value = "token",required = false) String token){
        //
        if(!StringUtils.isEmpty(token)){
            //去ssoserver登录成功跳回来就会带上
            //TODO 1、去ssoserver获取当前token真正对应的用户信息
            RestTemplate restTemplate = new RestTemplate();
            ResponseEntity<String> forEntity = restTemplate.getForEntity("http://ssoserver.com:8080/userInfo?token=" + token, String.class);
            String body = forEntity.getBody();
            session.setAttribute("loginUser",body);
        }

        Object loginUser = session.getAttribute("loginUser");
        if(loginUser==null){
            //没登录,跳转到登录服务器进行登录


            //跳转过去以后,使用url上的查询参数标识我们自己是哪个页面
            //redirect_url=http://client1.com:8080/employees
            return "redirect:"+ssoServerUrl+"?redirect_url=http://client1.com:8081/employees";
        }else{
            List<String> emps = new ArrayList<>();
            emps.add("张三");
            emps.add("李四");

            model.addAttribute("emps",emps);
            return "list";
        }


    }



}
ssoserver服务
@Controller
public class LoginController {


    @Autowired
    StringRedisTemplate redisTemplate;

    @ResponseBody
    @GetMapping("/userInfo")
    public String userInfo(@RequestParam("token") String token){
        String s = redisTemplate.opsForValue().get(token);
        return s;
    }

    @GetMapping("/login.html")
    public String loginPage(@RequestParam("redirect_url") String url, Model model,
                            @CookieValue(value = "sso_token",required = false) String sso_token){
        if(!StringUtils.isEmpty(sso_token)){
            //说明之前有人登录过,浏览器留下了痕迹
            return "redirect:"+url+"?token="+sso_token;
        }

        model.addAttribute("url",url);
        return "login";
    }

    @PostMapping("/doLogin")
    public String doLogin(@RequestParam("username") String username,
                          @RequestParam("password")String password,
                          @RequestParam("url")String url,
                          HttpServletResponse response){

        if(!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)){
            //登录成功,跳回之前页面

            //把登录成功的用户存起来。
            String uuid = UUID.randomUUID().toString().replace("-","");
            redisTemplate.opsForValue().set(uuid,username);
            Cookie sso_token = new Cookie("sso_token",uuid);
            response.addCookie(sso_token);
            return "redirect:"+url+"?token="+uuid;
        }
        //登录失败,展示登录页
        return "login";
    }
}

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录页</title>
</head>
<body>

<form action="/doLogin" method="post">
    用户名:<input name="username" /><br/>
    密码:<input name="password" type="password"/><br/>
    <input type="hidden" name="url" th:value="${url}"/>
    <input type="submit" value="登录"/>
</form>

</body>
</html>

RabbitMQ

D:\Desktop\面试\RabbitMQ\rabbitmq\笔记

功能:

一、异步处理

在这里插入图片描述

二、应用解耦、流量控制

在这里插入图片描述

基本概念

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

SpringBoot整合RabbitMQ

使用步骤

1、引入amqp场景;RabbitAutoConfiguration就会自动生效
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2、RabbitAutoConfiguration给容器中自动配置了:

AmqpAdmin

创建交换机、创建队列、创建绑定(一般通过容器的方式创建

  @Autowired
    AmqpAdmin amqpAdmin;  
	@Test
    public void createExchange() {
        DirectExchange exchange = new DirectExchange("hello-java-exchange",false,false,null);
        amqpAdmin.declareExchange(exchange);//声明一个交换机
        log.info("Exchange[{}]创建成功","hello-java-exchange");
    }
    @Test
    public void createQueue() {
        Queue queue = new Queue("hello-java-queue",true,false,false);
        amqpAdmin.declareQueue(queue);
        log.info("Queue[{}]创建成功","hello-java-queue");
    }
    @Test
    public void bindingQueue() {
        /*
        * String destination【目的地】,
        * DestinationType destinationType,
        *  String exchange,
        * String routingKey,
			Map<String, Object> arguments
        * */
        Binding binding = new Binding("hello-java-queue", Binding.DestinationType.QUEUE,"hello-java-exchange","hello.java",null);
        amqpAdmin.declareBinding(binding);
    }

RabbitTemplate

1、发送消息

发送对象的实现方式:

(1)、对象实现序列化接口
在这里插入图片描述
在这里插入图片描述
(2)、将对象转换为Json发送
在这里插入图片描述

//消息转换器
@Configuration
public class MyRabbitConfig {
    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }
}

在这里插入图片描述
CachingConnectionFactory

RabbitConnectionFactoryBean

RabbitMessagingTemplate

3、配置属性
//提供了自动配置的所有属性
@ConfigurationProperties(prefix = "spring.rabbitmq")
public class RabbitProperties 

给配置文件(application.properties)中配置spring.rabbitmq信息
在这里插入图片描述

4、主启动类@EnableRabbit开启功能

在这里插入图片描述

5、监听消息:使用@RabbitListener;必须有@EnableRabbit
  • @RabbitListener:类+方法上(监听哪些队列即可)

    ​ json发送到队列的某一对象,监听方法的参数上声明该对象即可

  • @RabbitHandler:标在方法上(重载区分不同的消息)

    ​ 需要@RabbitListener标注在类上,不同的方法上标注@RabbitHandler,并根据接收对象信息类型的不同在方法参数上声明不同对象类型即可。

    发送消息

    @RestController
    public class RabbitController {
    
        @Autowired
        RabbitTemplate rabbitTemplate;
    
    
        @GetMapping("/sendMq")
        public String sendMq(@RequestParam(value = "num",defaultValue = "10") Integer num){
            for (int i=0;i<num;i++){
                if(i%2 == 0){
                    OrderReturnReasonEntity reasonEntity = new OrderReturnReasonEntity();
                    reasonEntity.setId(1L);
                    reasonEntity.setCreateTime(new Date());
                    reasonEntity.setName("哈哈-"+i);
                    rabbitTemplate.convertAndSend("hello-java-exchange", "hello.java", reasonEntity,new CorrelationData(UUID.randomUUID().toString()));
                }else {
                    OrderEntity entity = new OrderEntity();
                    entity.setOrderSn(UUID.randomUUID().toString());
                    rabbitTemplate.convertAndSend("hello-java-exchange", "hello22.java", entity,new CorrelationData(UUID.randomUUID().toString()));
                }
            }
    
            return "ok";
        }
    }
    
    

    监听消息

    @RabbitListener(queues = {"hello-java-queue"})
    @Service("orderItemService")
    public class OrderItemServiceImpl extends ServiceImpl<OrderItemDao, OrderItemEntity> implements OrderItemService {
    
        @Override
        public PageUtils queryPage(Map<String, Object> params) {
            IPage<OrderItemEntity> page = this.page(
                    new Query<OrderItemEntity>().getPage(params),
                    new QueryWrapper<OrderItemEntity>()
            );
    
            return new PageUtils(page);
        }
    
        /**
         * queues:声明需要监听的所有队列
         *
         * org.springframework.amqp.core.Message
         *
         * 参数可以写一下类型
         * 1、Message message:原生消息详细信息。头+体
         * 2、T<发送的消息的类型> OrderReturnReasonEntity content;
         * 3、Channel channel:当前传输数据的通道
         *
         * Queue:可以很多人都来监听。只要收到消息,队列删除消息,而且只能有一个收到此消息
         * 场景:
         *    1)、订单服务启动多个;同一个消息,只能有一个客户端收到
         *    2)、 只有一个消息完全处理完,方法运行结束,我们就可以接收到下一个消息
         */
    //    @RabbitListener(queues = {"hello-java-queue"})
        @RabbitHandler
        public void receiveMessage(Message message,
                                   OrderReturnReasonEntity content,
                                   Channel channel) throws InterruptedException {
            //{"id":1,"name":"哈哈","sort":null,"status":null,"createTime":1581144531744}
            System.out.println("接收到消息..."+content);
            byte[] body = message.getBody();
            //消息头属性信息
            MessageProperties properties = message.getMessageProperties();
    //        Thread.sleep(3000);
            System.out.println("消息处理完成=>"+content.getName());
            //channel内按顺序自增的。
            long deliveryTag = message.getMessageProperties().getDeliveryTag();
            System.out.println("deliveryTag==>"+deliveryTag);
    
            //签收货物,非批量模式
            try {
                if(deliveryTag%2 == 0){
                    //收货
                    channel.basicAck(deliveryTag,false);
                    System.out.println("签收了货物..."+deliveryTag);
                }else {
                    //退货 requeue=false 丢弃  requeue=true 发回服务器,服务器重新入队。
                    //long deliveryTag, boolean multiple, boolean requeue
                    //签收了货物...6
                    channel.basicNack(deliveryTag,false,true);
                    //long deliveryTag, boolean requeue
    //                channel.basicReject();
                    System.out.println("没有签收了货物..."+deliveryTag);
                }
    
            }catch (Exception e){
                //网络中断
            }
    
        }
    
        @RabbitHandler
        public void recieveMessage2(OrderEntity content) throws InterruptedException {
            //{"id":1,"name":"哈哈","sort":null,"status":null,"createTime":1581144531744}
            System.out.println("接收到消息..."+content);
        }
    
6、RabbitMQ消息确认机制-可靠抵达

在这里插入图片描述

6.1 ConfirmCallback:服务端收到消息就回调

在这里插入图片描述

#开启发送端确认
spring.rabbitmq.publisher-confirms=true

Java中该注解的说明:@PostConstruct该注解被用来修饰一个非静态的void()方法。被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。PostConstruct在构造函数之后执行,init()方法之前执行。

通常我们会是在Spring框架中使用到@PostConstruct注解 该注解的方法在整个Bean初始化中的执行顺序:

Constructor(构造方法) -> @Autowired(依赖注入) -> @PostConstruct(注释的方法)

引申:@PostConstruct
@Configuration
public class MyRabbitConfig {
    @Autowired
    RabbitTemplate rabbitTemplate;
    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }
    @PostConstruct  //MyRabbitConfig对象创建完成以后,执行这个方法
    public void initRabbitTemplate() {
        //设置确认回调
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            /*
             * correlationData  当前消息的唯一关联数据(这个是消息的唯一id)
             * ack 消息是否成功收到
             * cause 失败的原因
             * */
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                //服务器收到了;
                //修改消息的状态
                System.out.println("confirm...correlationData["+correlationData+"]==>ack["+ack+"]==>cause["+cause+"]");
            }
        });
    }

}
6.2ReturnCallback:消息正确抵达队列进行回调

在这里插入图片描述

#开启发送端消息抵达队列的确认
spring.rabbitmq.publisher-returns=true
#只要抵达队列,以异步发送优先回调我们这个return confirm
spring.rabbitmq.template.mandatory=true
    @PostConstruct //MyRabbitConfig对象创建完成以后,执行这个方法
    public void initRabbitTemplate(){
        //设置确认回调
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {

            /**
             *
             * 1、只要消息抵达Broker就ack=true
             * @param correlationData 当前消息的唯一关联数据(这个是消息的唯一id)
             * @param ack  消息是否成功收到
             * @param cause 失败的原因
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {

                /**
                 * 1、做好消息确认机制(pulisher,consumer【手动ack】)
                 * 2、每一个发送的消息都在数据库做好记录。定期将失败的消息再次发送一遍
                 */
                //服务器收到了;
                //修改消息的状态
                System.out.println("confirm...correlationData["+correlationData+"]==>ack["+ack+"]==>cause["+cause+"]");
            }
        });

        //设置消息抵达队列的确认回调
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            /**
             * 只要消息没有投递给指定的队列,就触发这个失败回调
             * @param message   投递失败的消息详细信息
             * @param replyCode 回复的状态码
             * @param replyText 回复的文本内容
             * @param exchange  当时这个消息发给哪个交换机
             * @param routingKey 当时这个消息用哪个路由键
             */
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                //报错误了。修改数据库当前消息的状态->错误。
                System.out.println("Fail Message["+message+"]==>replyCode["+replyCode+"]==>replyText["+replyText+"]===>exchange["+exchange+"]===>routingKey["+routingKey+"]");
            }
        });
    }
6.3ACK消费端确认

在这里插入图片描述
消费端接收到队列的消息,默认接受模式下,只要接收到队列中的一个消息,不管有没有全部接收,都会清空队列中的消息。

手动ack消息配置

#手动ack消息
spring.rabbitmq.listener.simple.acknowledge-mode=manual

消费端确认(保证每个消息被正确消费,此时才可以broker删除这个消息)。

1、默认是自动确认的,只要消息接收到,客户端会自动确认,服务端就会移除这个消息

问题:

  • 我们收到很多消息,自动回复给服务器ack,只有一个消息处理成功,宕机了。就会发生消息丢失;

  • 消费者手动确认模式。只要我们没有明确告诉MQ,货物被签收。没有Ack,消息就一直是unacked状态。即使Consumer宕机。消息不会丢失,会重新变为Ready,下一次有新的Consumer连接进来就发给他

2、如何签收:

  • channel.basicAck(deliveryTag,false);签收;业务成功完成就应该签收

  • channel.basicNack(deliveryTag,false,true);拒签;业务失败,拒签
    在这里插入图片描述

  /**
     * queues:声明需要监听的所有队列
     *
     * org.springframework.amqp.core.Message
     *
     * 参数可以写一下类型
     * 1、Message message:原生消息详细信息。头+体
     * 2、T<发送的消息的类型> OrderReturnReasonEntity content;
     * 3、Channel channel:当前传输数据的通道
     *
     * Queue:可以很多人都来监听。只要收到消息,队列删除消息,而且只能有一个收到此消息
     * 场景:
     *    1)、订单服务启动多个;同一个消息,只能有一个客户端收到
     *    2)、 只有一个消息完全处理完,方法运行结束,我们就可以接收到下一个消息
     */
//    @RabbitListener(queues = {"hello-java-queue"})
    @RabbitHandler
    public void recieveMessage(Message message,
                               OrderReturnReasonEntity content,
                               Channel channel) throws InterruptedException {
        //{"id":1,"name":"哈哈","sort":null,"status":null,"createTime":1581144531744}
        System.out.println("接收到消息..."+content);
        byte[] body = message.getBody();
        //消息头属性信息
        MessageProperties properties = message.getMessageProperties();
//        Thread.sleep(3000);
        System.out.println("消息处理完成=>"+content.getName());
        //channel内按顺序自增的。
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        System.out.println("deliveryTag==>"+deliveryTag);

        //签收货物,非批量模式
        try {
            if(deliveryTag%2 == 0){
                //收货
                channel.basicAck(deliveryTag,false);
                System.out.println("签收了货物..."+deliveryTag);
            }else {
                //退货 requeue=false 丢弃  requeue=true 发回服务器,服务器重新入队。
                //long deliveryTag, boolean multiple, boolean requeue
                //签收了货物...6
                channel.basicNack(deliveryTag,false,true);
                //long deliveryTag, boolean requeue
//                channel.basicReject();
                System.out.println("没有签收了货物..."+deliveryTag);
            }

        }catch (Exception e){
            //网络中断
        }

    }
7、延时队列

订单模块使用

8、使用小结

创建交换机、创建队列、创建绑定:AmqpAdmin

发送消息:RabbitTemplate

接收消息:@RabbitListener、@RabbitHandler
在这里插入图片描述
可靠抵达:ConfirmCallback、ReturnCallback、ACK消费确认

订单服务

一、confirm.html页面功能

显示收货地址,商品信息,库存,价格
在这里插入图片描述

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

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
浏览器带了请求头和参数过来,调用远程服务,Feign自己创建了新的请求,导致请求丢失

//Feign远程调用会先遍历请求拦截器,因此创建一个请求拦截器添加的容器中
//需要保证浏览器发送的请求与远程调用请求在同一个线程,否则需要添加浏览器请求到新的Feign线程请求
@Configuration
public class GulimallFeignConfig {
    @Bean("requestInterceptor")
    public RequestInterceptor requestInterceptor() {
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate requestTemplate) {
                //1、RequestContextHolder拿到刚进来的当前线程的请求
                ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();//老请求
                HttpServletRequest request = attributes.getRequest();//老请求
                //同步请求头数据 Cookie
                String cookie = request.getHeader("Cookie");//老请求
                //给新请求同步了老请求的cookie
                requestTemplate.header("Cookie",cookie);
            }
        };
    }
}

在这里插入图片描述

Feign异步情况丢失上下文问题

异步编排进行远程调用,在每一个新线程都来设置浏览器线程发送过来的请求数据。

  @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        //获取浏览器发送过来的请求
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
            //每一个线程都来共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);
            //1、远程查询所有的收货地址列表
            List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
            confirmVo.setAddress(address);
        }, executor);

        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
            //每一个线程都来共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);
            //2、远程查询购物车所有选中的购物项
            List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
            confirmVo.setItems(currentUserCartItems);
        }, executor);

        //3、查询用户积分
        Integer integration = memberRespVo.getIntegration();
        confirmVo.setIntegration(integration);
        //4、其他数据自动计算

        //TODO:防重令牌

        CompletableFuture.allOf(getAddressFuture,cartFuture).get();

        return null;
    }
提交订单 接口幂等性

显示订单页,生成token唯一令牌,并保存到redis中;提交订单时,携带token与redis的token比较,相同则删除,比较并删除token需保证原子操作

用户购物车去结算,生成token,跳转到订单页
在这里插入图片描述
提交订单,校验token
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

分布式事务

在这里插入图片描述
本地事务在分布式下的问题

订单服务,下单方法中,设置本地事务@Transactional,方法中调用了库存服务远程锁库存,接下来调用其他服务远程扣减积分。存在以下问题

1、库存服务已成功锁库存(扣库存),但由于网络问题,没有返回给订单服务,导致调用超时,因此抛出异常,事务回滚,但无法回滚其他事务的数据;

2、库存服务锁库存成功,但接下来的远程扣减积分出了异常,远程锁库存也不能回滚。

本地服务(@Transacional)远程调用不同服务,出了异常无法回滚其他服务

本地事务配置传播行为失效问题

同一个对象内事务方法互调默认失效,原因是默认采用jdk动态代理(代理类需要实现接口),绕过了代理对象,事务是使用代理对象来控制的。

问题:同一个类中,一个方法(加了@Transactional)调用另一个方法(设置了相同的传播行为),默认使用同一个 事务。

解决使用代理对象来调用事务方法

1)、引入aop-starter;spring-boot-starter-aop;引入了aspectj

 <!--引入aspect-aop-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

在这里插入图片描述
2)、@EnableAspectJAutoProxy(exposeProxy = true);开启 aspectj 动态代理功能。以后所有的动态代理都是aspectj创建的(即使没有接口也可以创建动态代理)。

对外暴露代理对象

在这里插入图片描述
3)、本类互调用调用对象

  •  OrderServiceImpl orderService = (OrderServiceImpl) AopContext.currentProxy();
    
  •  orderService.b();
    
  •  orderService.c();
    

在这里插入图片描述

分布式事务CAP定理与BASE理论

在这里插入图片描述
在这里插入图片描述

如何保证CP:raft算法

raft中,一个节点可以有三种状态:Follower,Candidate,Leader。两大核心:领导选举,日志复制

http://thesecretlivesofdata.com/raft/

如何保证AP:BASE理论

在这里插入图片描述
在这里插入图片描述

SEATA分布式解决方案

在这里插入图片描述
在这里插入图片描述

一、SEATA AT 分布式事务(不适合高并发场景)

适用场景:适合并发量少的简单场景

1、在需要用到分布式事务的数据库上创建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依赖

 <!-- 配置seata分布式事务 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

找到对应的依赖版本为

在这里插入图片描述
3、下载对应版本的服务器软件包(事务协调器TC)

https://github.com/seata/seata/releases

4、在conf/registry.conf文件配置注册中心为nacos、启动seata
在这里插入图片描述
nacos发现该服务

在这里插入图片描述
5、想要用到分布式事务的微服务使用seata DataSourceProxy代理自己的数据源

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

6、每个微服务都必须导入(3-4)中配置的file.conf、registry.conf配置文件

在这里插入图片描述
7、每个微服务的file.conf文件修改名称为微服务名
在这里插入图片描述
8、给分布式大事务的入口标注@GlobalTransactional

​ 每一个远程的小事务用@Transactional

二、RabbitMQ延时队列(保证事务最终一致性)

在这里插入图片描述
在这里插入图片描述
定时任务存在时效性问题,因此采用延时队列

在这里插入图片描述

延时队列的组成

rabbitmq的消息TTL和死信Exchange结合

在这里插入图片描述
在这里插入图片描述

延时队列的实现

1、设置消息过期时间实现延时队列(不推荐
在这里插入图片描述
2、设置队列过期时间实现延时队列(推荐
在这里插入图片描述
延时队列设计
在这里插入图片描述

@Configuration
public class MyMQConfig {
    //Queue Exchange Binding
    /*
    * String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
    * */
    @Bean
    public Queue orderDelayQueue() {
        Map<String,Object> arguments = new HashMap<>();
        arguments.put("x-dead-letter-exchange","order-event-exchange");
        arguments.put("x-dead-letter-routing-key","order.release.order");
        arguments.put("x-message-ttl","60000");
        Queue queue = new Queue("order.delay.queue", true, false, false, arguments);
        return queue;
    }
    @Bean
    public Queue orderReleaseOrderQueue() {
        Queue queue = new Queue("order.release.order.queue", true, false, false);
        return queue;
    }
    @Bean
    public Exchange orderEventExchange() {
        //String name, boolean durable, boolean autoDelete, Map<String, Object> arguments
        return new TopicExchange("order-event-exchange",true,false);
    }
    @Bean
    public Binding orderCreateOrderBinding() {
        return new Binding("order.delay.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.create.order",
                null);
    }
    @Bean
    public Binding orderReleaseOrderBinding() {
        return new Binding("order.release.order.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.release.order",
                null);
    }
}

监听器

在这里插入图片描述
生产者
在这里插入图片描述

订单系统

在这里插入图片描述
在这里插入图片描述

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

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

支付宝支付业务

在这里插入图片描述
公钥加密,私钥解密;私钥签名,公钥验签

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

内网穿透

在这里插入图片描述
在这里插入图片描述
运行natstat.exe

natapp -authtoken=0969a0244e9b02e6
http://qyeurv.natappfree.cc  order.gulimall.com:80

在这里插入图片描述
内网穿透软件映射了order.gulimall.com,外网发送请求过来,与浏览器不同,没有携带请求头Host,即nginx收到后(满足server_name),但$host没有携带域名order.gulimall.com,因此访问网关失败。因此通过配置指定路径,自己添加上host即可

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
订单及支付模块由于前面静态页面复制的代码,包含了秒杀功能,导致报错;没有运行项目,可能会有各种问题。功能流程不是很清晰。

DO = 数据库实体

DTO = 数据传输实体接口与接口之间用

VO = 返回给前端的

简历项目

一、认证服务

在这里插入图片描述

一、帐号密码注册登录

在这里插入图片描述
LoginController.java

@Controller
public class LoginController {
    @Autowired
    ThirdPartFeignService  thirdPartFeignService;
    @Autowired
    StringRedisTemplate redisTemplate;
    @Autowired
    MemberFeignService memberFeignService;
一、验证码发送

redis结构:key: sms:code:13715909625 value: 验证码_时间

验证码发送,保存至redis,通过判断redis中该用户是否发送验证码,及发送验证码时间是否超过60秒,来决定是否给用户发送验证码,防止同一用户多次发送。

Controller

    @ResponseBody
    @GetMapping ("/sms/sendcode")
    public R sendCode(@RequestParam("phone") String phone) {
        //TODO 1、接口防刷

        //2、验证码的再次校验  redis
        String redisCode = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);
        if (!StringUtils.isEmpty(redisCode)) {
            Long l = Long.parseLong(redisCode.split("_")[1]);
            if (System.currentTimeMillis()-l < 60000) {
                //60秒内不能再发
                return R.error(BizCodeEnume.SMS_CODE_EXCEPTION.getCode(), BizCodeEnume.SMS_CODE_EXCEPTION.getMsg());
            }
            //大于60秒重发
        }
        //2、验证码的再次校验。redis。存key-phone,value-code   sms:code:17512080612 -> 45678
        String code = UUID.randomUUID().toString().substring(0, 5);
        String substring = code+"_"+System.currentTimeMillis();
        //redis缓存验证码,防止同一个phone在60秒内再次发送验证码

        redisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX+phone,substring,10, TimeUnit.MINUTES);
        thirdPartFeignService.sendCode(phone,code);
        return R.ok();
    }

ThirdPartyFeignService.java

@FeignClient("gulimall-third-party")
public interface ThirdPartFeignService {
    @GetMapping("/sms/sendcode")
    public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code);
}
二、用户注册
实体类UserRegistVo

​ 采用JSR303校验用户字段是否规范

@Data
public class UserRegistVo {
    @NotEmpty(message = "用户名必须提交")
    @Length(min = 6,max = 18,message = "用户名必须是6-18位字符")
    private String userName;

    @NotEmpty(message="密码必须填写")
    @Length(min = 6,max = 18,message = "密码必须是6-18位字符")
    private String password;

    @NotEmpty(message = "手机号必须填写")
    @Pattern(regexp = "^[1]([3-9])[0-9]{9}",message = "手机号格式不正确")
    private String phone;

    @NotEmpty(message = "验证码必须填写")
    private String code;
}

LoginController.java

    /*
    * TODO 重定向s携带数据,利用session原理。将数据放在session中。
    *  只要跳到下一个页面取出这个数据以后,session里面的数据就会删掉
    * 分布式下的session问题
    * RedirectAttributes redirectAttributes:模拟重定向携带数据
    * */
    @PostMapping("/regist")
    public String regist(@Valid UserRegistVo vo, BindingResult result, RedirectAttributes redirectAttributes) {
        if (result.hasErrors()) {
            Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
//            model.addAttribute("errors",errors);
            redirectAttributes.addFlashAttribute("errors",errors);
            //校验出错,转达到注册页
            return "redirect:http://auth.gulimall.com/reg.html";
        }
        //注册成功回到首页,回到登录页
        //1、校验验证码
        String code = vo.getCode();
        String s = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
        if (!StringUtils.isEmpty(s) && code.equals(s.split("_")[0])) {
            //删除验证码;令牌机制
            redisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX+vo.getPhone());
            //验证码通过。//真正注册。调用远程服务进行注册
            R r = memberFeignService.regist(vo);
            if (r.getCode() == 0) {
                //成功
                return "redirect:http://auth.gulimall.com/login.html";
            }else {
                Map<String,String> errors = new HashMap<>();
                errors.put("msg",r.getData("msg",new TypeReference<String>(){}));
                redirectAttributes.addFlashAttribute("errors",errors);
                return "redirect:http://auth.gulimall.com/reg.html";
            }
        }else {
            Map<String,String> errors = new HashMap<>();
            errors.put("code","验证码错误");
            redirectAttributes.addFlashAttribute("errors",errors);
            //校验注册,转发到注册页
            return "redirect:http://auth.gulimall.com/reg.html";
        }
    }

MemberFeignService.java

@FeignClient("gulimall-member")
public interface MemberFeignService {
    @PostMapping("/member/member/regist")
    public R regist(@RequestBody UserRegistVo vo);
}    
会员服务

MemberRegistVo.java

@Data
public class MemberRegistVo {
    private String userName;
    private String password;
    private String phone;
}

MemberController.java

/**
 * 会员
 *
 * @author Guoyifan
 * @email 1074840013@qq.com
 * @date 2021-11-26 17:02:46
 */
@RestController
@RequestMapping("member/member")
public class MemberController {
    @Autowired
    private MemberService memberService;

    @Autowired
    CouponFeignService couponFeignService;
  @PostMapping("/regist")
    public R regist(@RequestBody MemberRegistVo vo) {
        try{
            memberService.regist(vo);
        }catch (UsernameExistException e) {
            return R.error(BizCodeEnume.USER_EXIST_EXCEPTION.getCode(),BizCodeEnume.USER_EXIST_EXCEPTION.getMsg());
        }catch (PhoneExistException e) {
            return R.error(BizCodeEnume.PHONE_EXIST_EXCEPTION.getCode(),BizCodeEnume.PHONE_EXIST_EXCEPTION.getMsg());
        }
        return R.ok();
    }

MemberServiceImpl.java

盐值加密

 @Override
    public void regist(MemberRegistVo vo) {
        checkPhoneUnique(vo.getPhone());
        checkUsernameUnique(vo.getUserName());
        MemberDao memberDao = this.baseMapper;
        MemberEntity memberEntity = new MemberEntity();
        //设置默认等级
        MemberLevelEntity levelEntity = memberLevelService.getDefaultLevel();
        //密码加密存储
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String encode = passwordEncoder.encode(vo.getPassword());//盐值加密
        memberEntity.setPassword(encode);
        memberEntity.setLevelId(levelEntity.getId());
        memberEntity.setMobile(vo.getPhone());
        memberEntity.setUsername(vo.getUserName());
        //保存
        memberDao.insert(memberEntity);
    }


    @Override
    public void checkPhoneUnique(String phone) throws PhoneExistException {
        MemberDao memberDao = this.baseMapper;
        Integer nums = memberDao.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));
        if (nums > 0) {
            throw new PhoneExistException();
        }
    }

    @Override
    public void checkUsernameUnique(String username) throws UsernameExistException {
        MemberDao memberDao = this.baseMapper;
        Integer nums = memberDao.selectCount(new QueryWrapper<MemberEntity>().eq("username", username));
        if (nums > 0) {
            throw new UsernameExistException();
        }
    }

PhoneExistException.java

public class PhoneExistException extends RuntimeException{
    public PhoneExistException() {
        super("手机号已存在");
    }
}

UsernameExistException.java

public class UsernameExistException extends RuntimeException{
    public UsernameExistException() {
        super("用户名已存在");
    }
}
盐值加密

在这里插入图片描述

密码md5盐值加密

Spring自带的盐值加密

在这里插入图片描述

三、用户登录

在这里插入图片描述
LoginController.java

访问登录页,判断是否登录

    @Autowired
    ThirdPartFeignService  thirdPartFeignService;
    @Autowired
    StringRedisTemplate redisTemplate;
    @Autowired
    MemberFeignService memberFeignService;   
   /*
    * 已经登录的用户,要跳转回gulimall.com
    * */
    @GetMapping("/login.html")
    public String loginPage(HttpSession session) {
        Object attribute = session.getAttribute(AuthServerConstant.LOGIN_USER);
        if (attribute == null) {
            //没登陆
            return "login";
        }else {
            return "redirect:http://gulimall.com";
        }
    }
  @PostMapping("/login")
    public String login(UserLoginVo vo, RedirectAttributes redirectAttributes, HttpSession session) {
        //远程登录
        R login = memberFeignService.login(vo);
        if (login.getCode() == 0) { //登录成功,获取用户信息返回
            MemberRespVo data = login.getData("data", new TypeReference<MemberRespVo>() {
            });
            session.setAttribute(AuthServerConstant.LOGIN_USER,data);
            return "redirect:http://gulimall.com";
        }else {
            Map<String,String> errors = new HashMap<>();
            errors.put("msg",login.getData("msg",new TypeReference<String>(){}));
            redirectAttributes.addFlashAttribute("errors",errors);
            return "redirect:http://auth.gulimall.com/login.html";
        }
    }

MemberFeignService.java

@FeignClient("gulimall-member")
public interface MemberFeignService {
    @PostMapping("/member/member/login")
    public R login(@RequestBody UserLoginVo vo);
}

gulimall-member

MemberController.java

/**
 * 会员
 *
 * @author Guoyifan
 * @email 1074840013@qq.com
 * @date 2021-11-26 17:02:46
 */
@RestController
@RequestMapping("member/member")
public class MemberController {
    @Autowired
    private MemberService memberService;
    @PostMapping("/login")
    public R login(@RequestBody MemberLoginVo vo) {
        MemberEntity entity = memberService.login(vo);
        if (entity == null) {
            return R.error(BizCodeEnume.LOGINACCT_PASSWORD_INVALID_EXCEPTION.getCode(), BizCodeEnume.LOGINACCT_PASSWORD_INVALID_EXCEPTION.getMsg());
        }else {
            return R.ok().setData(entity);
        }

    }

MemberLoginVo.java

@Data
public class MemberLoginVo {
    private String loginacct;
    private String password;
}

MemberServiceImpl.java

   @Override
    public MemberEntity login(MemberLoginVo vo) {
        MemberDao memberDao = this.baseMapper;
        String username = vo.getLoginacct();
        String password = vo.getPassword();
        MemberEntity memberEntity = memberDao.selectOne(new QueryWrapper<MemberEntity>().eq("username", username).or().eq("mobile", username));
        if (memberEntity != null) {
            //盐值加密与提交密码匹配
            BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
            boolean matches = encoder.matches(password, memberEntity.getPassword());
            if (matches) { //匹配
                return memberEntity;
            }
        }
        //不匹配或找不到用户
        return null;
    }

SpringMVC viewcontroller处理页面映射请求

在这里插入图片描述
此方式一个映射就写一个方法,不推荐
在这里插入图片描述
推荐方式

@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {
    /*
    * 视图映射
    * */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login.html").setViewName("login");
        registry.addViewController("/reg.html").setViewName("reg");
    }
}

RedirectAttributes

TODO 重定向携带数据,利用session原理。将数据放在session中。
	 只要跳到下一个页面取出这个数据以后,session里面的数据就会删掉
     会存在分布式情况下session问题
 @PostMapping("/regist")
    public String regist(@Valid UserRegistVo vo, BindingResult result, RedirectAttributes redirectAttributes) {
        if (result.hasErrors()) {
            Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
//            model.addAttribute("errors",errors);
            redirectAttributes.addFlashAttribute("errors",errors);
            //校验出错,转达到注册页
            return "redirect:http://auth.gulimall.com/reg.html";
        }
        //注册成功回到首页,回到登录页
        return "redirect:/login.html";
    }

二、OAuth2.0第三方社交登录

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
Controller

@Slf4j
@Controller
public class OAuth2Controller {
    @Autowired
    MemberFeignService memberFeignService;
    @GetMapping("/oauth2.0/weibo/success")
    public String weibo(@RequestParam("code") String code, HttpSession session) throws Exception {
        Map<String,String> header = new HashMap<>();
        Map<String,String> query = new HashMap<>();
        Map<String,String> map = new HashMap<>();
        map.put("client_id","2874630085");
        map.put("client_secret","7180a6aeec100d0296acfe6fa52051d4");
        map.put("grant_type","authorization_code");
        map.put("redirect_uri","http://auth.gulimall.com/oauth2.0/weibo/success");
        map.put("code",code);
        //1、根据code换取accessToken
        HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", header, query, map);
        //处理
        if (response.getStatusLine().getStatusCode() == 200) {
            String jsonEntity = EntityUtils.toString(response.getEntity());
            SocialUser socialUser = JSON.parseObject(jsonEntity,SocialUser.class);
            R oauthLogin = memberFeignService.oauthLogin(socialUser);
            if (oauthLogin.getCode() == 0) {
                MemberRespVo data = oauthLogin.getData("data",new TypeReference<MemberRespVo>(){});
                log.info("登录成功:用户:{}",data.toString());

                session.setAttribute(AuthServerConstant.LOGIN_USER,data);
                //2、登录成功就跳回首页
                return "redirect:http://gulimall.com";
            }else {
                //登录失败
                return "redirect:http://auth.gulimall.com/login.html";

            }
        }else {
            return "redirect:http://auth.gulimall.com/login.html";
        }


    }
}

SocialUser.java

@Data
public class SocialUser {

    private String access_token;
    private String remind_in;
    private long expires_in;
    private String uid;
    private String isRealName;
}

MemberFeignService.java

@FeignClient("gulimall-member")
public interface MemberFeignService {
    @PostMapping("/member/member/oauth2/login")
    public R oauthLogin(@RequestBody SocialUser socialUser);   
}

MemberController.java

/**
 * 会员
 *
 * @author Guoyifan
 * @email 1074840013@qq.com
 * @date 2021-11-26 17:02:46
 */
@RestController
@RequestMapping("member/member")
public class MemberController {
	@Autowired
    private MemberService memberService;
    @PostMapping("/oauth2/login")
    public R oauthLogin(@RequestBody SocialUser socialUser) throws Exception {
        MemberEntity entity = memberService.login(socialUser);
        if (entity == null) {
            return R.error(BizCodeEnume.LOGINACCT_PASSWORD_INVALID_EXCEPTION.getCode(), 		                                        BizCodeEnume.LOGINACCT_PASSWORD_INVALID_EXCEPTION.getMsg());
        }else {
            return R.ok().setData(entity);
        }
    }
}

MemberServiceImpl.java

@Service("memberService")
public class MemberServiceImpl extends ServiceImpl<MemberDao, MemberEntity> implements MemberService {
    @Autowired
    MemberLevelService memberLevelService; 
	@Override
    public MemberEntity login(SocialUser socialUser) throws Exception {
        String uid = socialUser.getUid();
        //查看用户是否注册过
        MemberDao memberDao = baseMapper;
        MemberEntity update = memberDao.selectOne(new QueryWrapper<MemberEntity>().eq("social_uid", uid));
        if (update != null) { //用户注册过
            update.setAccessToken(socialUser.getAccess_token());
            update.setExpiresIn(socialUser.getExpires_in());
            memberDao.updateById(update);
            return update;
        }else {
            //注册一个用户
            MemberEntity regist = new MemberEntity();
            regist.setAccessToken(socialUser.getAccess_token());
            regist.setExpiresIn(socialUser.getExpires_in());
            regist.setSocialUid(socialUser.getUid());
            try{
                //查询当前社交用户的社交帐号信息(昵称、性别等)
                Map<String,String> headers = new HashMap<>();
                Map<String,String> querys = new HashMap<>();
                querys.put("access_token",socialUser.getAccess_token());
                querys.put("uid",socialUser.getUid());
                HttpResponse response = HttpUtils.doGet("https://api.weibo.com", "/2/users/show.json", "GET", headers, querys);
                if (response.getStatusLine().getStatusCode() == 200) {
                    String json = EntityUtils.toString(response.getEntity());
                    JSONObject jsonObject = JSON.parseObject(json);
                    String name = jsonObject.getString("name");
                    String gender = jsonObject.getString("gender");
//                String location = jsonObject.getString("location");
//                regist.setCity(location);
                    regist.setUsername(name);
                    regist.setGender("m".equals(gender) ? 1 : 0);


                }
            }catch (Exception e) {}
            //将redist用户存放进ums_member
            memberDao.insert(regist);
            return regist;
        }
    }
}   

MemberEntity.java

/**
 * 会员
 * 
 * @author Guoyifan
 * @email 1074840013@qq.com
 * @date 2021-11-26 17:02:46
 */
@Data
@TableName("ums_member")
public class MemberEntity implements Serializable {
	private static final long serialVersionUID = 1L;

	/**
	 * id
	 */
	@TableId
	private Long id;
	/**
	 * 会员等级id
	 */
	private Long levelId;
	/**
	 * 用户名
	 */
	private String username;
	/**
	 * 密码
	 */
	private String password;
	/**
	 * 昵称
	 */
	private String nickname;
	/**
	 * 手机号码
	 */
	private String mobile;
	/**
	 * 邮箱
	 */
	private String email;
	/**
	 * 头像
	 */
	private String header;
	/**
	 * 性别
	 */
	private Integer gender;
	/**
	 * 生日
	 */
	private Date birth;
	/**
	 * 所在城市
	 */
	private String city;
	/**
	 * 职业
	 */
	private String job;
	/**
	 * 个性签名
	 */
	private String sign;
	/**
	 * 用户来源
	 */
	private Integer sourceType;
	/**
	 * 积分
	 */
	private Integer integration;
	/**
	 * 成长值
	 */
	private Integer growth;
	/**
	 * 启用状态
	 */
	private Integer status;
	/**
	 * 注册时间
	 */
	private Date createTime;

	private String socialUid;
	private String accessToken;
	private Long expiresIn;
}

SpringSession

GulimallSessionConfig.java

@Configuration
public class GulimallSessionConfig {
    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        cookieSerializer.setDomainName("gulimall.com");
        cookieSerializer.setCookieName("GULISESSION");
        return cookieSerializer;
    }
    //实现redis序列化器 (不用将类实现serializable)
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }

}

访问login.html,若已登录,跳转到gulimall.com

  /*
    * 已经登录的用户,要跳转回gulimall.com
    * */
    @GetMapping("/login.html")
    public String loginPage(HttpSession session) {
        Object attribute = session.getAttribute(AuthServerConstant.LOGIN_USER);
        if (attribute == null) {
            //没登陆
            return "login";
        }else {
            return "redirect:http://gulimall.com";
        }
    }

二、购物车

静态资源在nginx中存放目录:
在这里插入图片描述
nginx conf配置
在这里插入图片描述
gulimall.conf

访问gulimall.com 或 *.gulimall.com代理到gulimall,upstream匹配跳转到网关

在这里插入图片描述

server {
    listen       80;
    server_name  gulimall.com *.gulimall.com qyeurv.natappfree.cc;

    #charset koi8-r;
    #access_log  /var/log/nginx/log/host.access.log  main;
    location /static/ {
    	root /usr/share/nginx/html;
    }
    location /payed/ {
 	proxy_set_header Host order.gulimall.com;
        proxy_pass http://gulimall;

    }
    location / {
	proxy_set_header Host $host;
    	proxy_pass http://gulimall;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    #location ~ \.php$ {
    #    root           html;
    #    fastcgi_pass   127.0.0.1:9000;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;
    #}

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    #    deny  all;
    #}
}


nginx.conf

​ 跳转到网关

在这里插入图片描述

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;
    upstream gulimall {
    	server 192.168.56.1:88;
    }
    include /etc/nginx/conf.d/*.conf;
}

gateway服务
在这里插入图片描述

实体类设计

Cart.java

/*
* 整个购物车
* 需要计算的属性,必须重写他的get方法,保证每次获取属性都会进行计算
* */
@Data
public class Cart {
    List<CartItem> items;

    private Integer countNum; //商品数量

    private Integer countType;//商品类型数量

    private BigDecimal totalAmount; //商品总价

    private BigDecimal reduce = new BigDecimal("0.00"); //减免价格

    public Integer getCountNum() {
        int count = 0;
        if (items != null && items.size() != 0) {
            for (CartItem item : items) {
                count += item.getCount();
            }
        }
        return count;
    }

    public Integer getCountType() {
        return (items == null || items.size() == 0) ? 0 : items.size();
    }

    public void setCountType(Integer countType) {
        this.countType = countType;
    }

    public BigDecimal getTotalAmount() {
        BigDecimal amount = new BigDecimal("0");
        //1、计算购物项总价
        if (items != null && items.size() > 0) {
            for (CartItem item : items) {
                if (item.getCheck()) {
                    amount = amount.add(item.getTotalPrice());
                }
            }
        }
        //2、减去优惠总价
        amount = amount.subtract(getReduce()).compareTo(new BigDecimal("0")) == -1 ? amount : amount.subtract(getReduce());
        return amount;
    }

    public void setTotalAmount(BigDecimal totalAmount) {
        this.totalAmount = totalAmount;
    }

    public BigDecimal getReduce() {
        return reduce;
    }

    public void setReduce(BigDecimal reduce) {
        this.reduce = reduce;
    }
}

CartItem.java

/*
* 购物项
* */
@Data
public class CartItem {
    private Long skuId;
    private Boolean check = true;
    private String image;
    private List<String> skuAttr;
    private BigDecimal price;
    private Integer count;
    private BigDecimal totalPrice;
    private String title;
    public BigDecimal getTotalPrice() {
        return this.price.multiply(new BigDecimal(""+this.count));
    }
    public void setTotalPrice(BigDecimal totalPrice) {
        this.totalPrice = totalPrice;
    }
}

配置redis

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

application.properties

spring.redis.host=192.168.56.10
购物车redis存储结构设计

在这里插入图片描述

SpringSession

购物车分为游客模式和登录模式,需要先判断用户是否登录。

   <!--整合springsession-->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
spring.session.store-type=redis
配置
@EnableRedisHttpSession
@Configuration
public class GulimallSessionConfig {
    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        cookieSerializer.setDomainName("gulimall.com");
        cookieSerializer.setCookieName("GULISESSION");
        return cookieSerializer;
    }
    //实现redis序列化器 (不用将类实现serializable)
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }

}

Interceptor登录拦截

UserInfoTo
@Data
public class UserInfoTo {
    private Long userId;
    private String userKey;
    private boolean tempUser = false;
}
CartInterceptor

在这里插入图片描述

/*
* 在执行目标方法之前,判断用户的登录状态。并封装传递给controller目标请求
* */
public class CartInterceptor implements HandlerInterceptor {

    public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        UserInfoTo userInfoTo = new UserInfoTo();
        MemberRespVo memberRespVo = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
        if (memberRespVo != null) {
            //用户登录过
           userInfoTo.setUserId(memberRespVo.getId());
        }
        Cookie[] cookies = request.getCookies();
        if (cookies != null && cookies.length != 0) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals(CartConstant.TEMP_USER_COOKIE_NAME)) {//"user-key"
                    userInfoTo.setUserKey(cookie.getValue());
                    userInfoTo.setTempUser(true);
                }
            }
        }
        if (StringUtils.isEmpty(userInfoTo.getUserKey())) {
            String uuid = UUID.randomUUID().toString();
            userInfoTo.setUserKey(uuid);
        }
        //目标方法执行之前
        threadLocal.set(userInfoTo);
        return true;
    }
	/*
		业务执行之后,分配临时用户,让浏览器保存
	*/
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        UserInfoTo userInfoTo = threadLocal.get();
        //如果没有临时用户,一定保存临时用户
        if (!userInfoTo.isTempUser()) {
            Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME,userInfoTo.getUserKey());
            cookie.setDomain("gulimall.com");//设置作用域
            cookie.setMaxAge(CartConstant.TEMP_USER_COOKIE_TIMEOUT);//设置cookie过期时间
            response.addCookie(cookie);
        }
    }
添加Interceptor拦截器到配置

GulimallWebConfig.java

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

Controller

@Controller
public class CartController {
    @Autowired
    CartService cartService;
一、访问购物车页面
/*
	浏览器有一个cookie:user-key;标识用户身份,一个月后过期;
	如果第一次使用jd的购物车功能,都会给一个临时的用户身份;
	浏览器保存以后,每次访问都会带上这个cookie;
	
	登录:session有
	没登录:按照cookie里面带来user-key来做
	第一次,如果没有临时用户,帮忙创建一个临时用户
*/

@GetMapping("/cart.html")
public String cartListPage(Model model) throws ExecutionException, InterruptedException {
    //        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
    //        System.out.println(userInfoTo.toString());
    Cart cart = cartService.getCart();
    model.addAttribute("cart",cart);
    return "cartList";
}

获取购物车

​ 用户已登录,则需要将临时购物车项合并到用户购物车

    @Override
    public Cart getCart() throws ExecutionException, InterruptedException {
        Cart cart = new Cart();
        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        if (userInfoTo.getUserId() != null) { //用户已登录
            String tempCartKey = CART_PREFIX + userInfoTo.getUserKey();
            //先判断临时用户的购物车中是否有商品
            List<CartItem> tempItems = getCartItems(tempCartKey);
            if (tempItems != null) {
                for (CartItem tempItem : tempItems) {
                    addToCart(tempItem.getSkuId(), tempItem.getCount());
                }
            }
            clearCart(tempCartKey);
            //合并临时用户购物车后,将登录用户的购物车数据返回
            cart.setItems(getCartItems(CART_PREFIX+userInfoTo.getUserId()));
        }else {
            String tempCartKey = CART_PREFIX + userInfoTo.getUserKey();
            //用户未登录
            cart.setItems(getCartItems(tempCartKey));
        }
        return cart;
    }

获取购物项

    private List<CartItem> getCartItems(String cartKey) {
        BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(CART_PREFIX + cartKey);
        List<Object> values = operations.values();
        if (values != null && values.size() > 0) {
            List<CartItem> collect = values.stream().map(opt -> {
                String s = (String) opt;
                CartItem cartItem = JSON.parseObject(s, CartItem.class);
                return cartItem;
            }).collect(Collectors.toList());
            return collect;
        }
        return null;
    }
二、添加商品到购物车

在这里插入图片描述
CartController.java

    /*
    * 添加商品到购物车
    * @param skuId 商品id
    * @param num 商品数量
    * */
    @GetMapping("/addToCart")
    public String addToCart(@RequestParam("skuId") Long skuId,
                            @RequestParam("num") Integer num,
                            RedirectAttributes ra) throws ExecutionException, InterruptedException {
        CartItem cartItem = cartService.addToCart(skuId,num);
        ra.addAttribute("skuId",skuId);

        return "redirect:http://cart.gulimall.com/addToCartSuccess.html";
    }

跳转到成功页

 /*
    * 跳转到成功页
    * */
    @GetMapping("/addToCartSuccess.html")
    public String addToCartSuccessPage(@RequestParam("skuId") Long skuId,Model model) {
       //重定向到成功页面。再次查询购物车数据即可
        CartItem item = cartService.getCartItem(skuId);
        model.addAttribute("item",item);
        return "success";
    }
异步编排

1、创建线程池

MyThreadConfig.java

//@EnableConfigurationProperties(ThreadPoolConfigProperties.class)
@Configuration
public class MyThreadConfig {
    @Bean
    public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool) {
        return new ThreadPoolExecutor(pool.getCoreSize(), pool.getMaxSize(),
                pool.getKeepAliveTime(), TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(10000),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
    }
}

ThreadPoolConfigProperties.java

@ConfigurationProperties(prefix = "gulimall.thread")
@Component
@Data
public class ThreadPoolConfigProperties {
    private Integer coreSize;
    private Integer maxSize;
    private Integer keepAliveTime;
}

application.properties

gulimall.thread.core-size=20
gulimall.thread.max-size=200
gulimall.thread.keep-alive-time=10

CartServiceImpl.java

@Slf4j
@Service
public class CartServiceImpl implements CartService {
    @Autowired
    StringRedisTemplate redisTemplate;
    @Autowired
    ProductFeignService productFeignService;
    @Autowired
    ThreadPoolExecutor executor;

    private final String CART_PREFIX = "gulimall:cart:";
    /*
    * 添加商品到购物车
    */
     @Override
    public CartItem addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
        String res = (String) cartOps.get(skuId.toString());

        if (StringUtils.isEmpty(res)) {
            CartItem cartItem = new CartItem();

            //1、远程查询当前要添加的商品信息
            CompletableFuture<Void> getSkuInfoTask = CompletableFuture.runAsync(() -> {

                R skuInfo = productFeignService.getSkuInfo(skuId);
                SkuInfoVo data = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                });
                //2、商品添加到购物车
                cartItem.setCheck(true);
                cartItem.setCount(num);
                cartItem.setImage(data.getSkuDefaultImg());
                cartItem.setSkuId(skuId);
                cartItem.setPrice(data.getPrice());
                cartItem.setTitle(data.getSkuTitle());
            }, executor);
            //2、远程查询sku的组合信息
            CompletableFuture<Void> getSkuSaleAttrValues = CompletableFuture.runAsync(()->{
                List<String> skuSaleAttrValues = productFeignService.getSkuSaleAttrValues(skuId);
                cartItem.setSkuAttr(skuSaleAttrValues);
            },executor);

            CompletableFuture.allOf(getSkuInfoTask,getSkuSaleAttrValues).get();
            String s = JSON.toJSONString(cartItem);
            cartOps.put(skuId.toString(),s);
            return cartItem;

        }else {
            CartItem cartItem = JSON.parseObject(res, CartItem.class);
            cartItem.setCount(cartItem.getCount()+num);
            cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));


            return cartItem;
        }
    }
}  

获取到要操作的购物车

    /*
    * 获取到要操作的购物车
    * */
    private BoundHashOperations<String, Object, Object> getCartOps() {
        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        String cartKey = "";
        if (userInfoTo.getUserId() != null) {
            cartKey = CART_PREFIX+userInfoTo.getUserId();
        }else {
            cartKey = CART_PREFIX+userInfoTo.getUserKey();
        }
        BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
        return operations;
    }
三、选中购物项

在这里插入图片描述
Controller

  @GetMapping("/checkItem")
    public String checkItem(@RequestParam("skuId") Long skuId,
                            @RequestParam("check") Integer check) {
        cartService.checkItem(skuId,check);
        return "redirect:http://cart.gulimall.com/cart.html";
    }

Service

    @Override
    public void checkItem(Long skuId, Integer check) {
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
        CartItem cartItem = getCartItem(skuId);
        cartItem.setCheck(check == 1?true:false);
        String s = JSON.toJSONString(cartItem);
        cartOps.put(skuId.toString(),s);
    }
四、改变购物项数量

在这里插入图片描述
Controller

@GetMapping("/countItem")
    public String countItem(@RequestParam("skuId") Long skuId,
                            @RequestParam("num") Integer num) {
        cartService.changeItemCount(skuId,num);
        return "redirect:http://cart.gulimall.com/cart.html";
    }

Service

  @Override
    public void changeItemCount(Long skuId, Integer num) {
        CartItem cartItem = getCartItem(skuId);
        cartItem.setCount(num);
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
        cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));
    }
五、删除购物项

Controller

@GetMapping("/deleteItem")
    public String deleteItem(@RequestParam("skuId") Long skuId) {
        cartService.deleteItem(skuId);
        return "redirect:http://cart.gulimall.com/cart.html";
    }

Service

  @Override
    public void deleteItem(Long skuId) {
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
        cartOps.delete(skuId.toString());
    }

在这里插入图片描述
RedirectAttribute ra的ra.addAttribute(“skuId”,skuId);方法相当于在重定向路径追加?skuId=xxx

涉及知识点:线程池、异步编排;session,cookie相关知识点;SpringSession一级域名下浏览器保存用户登录sessionId;拦截器:无论是否登录,客户端cookie都保存user_key;Redis:购物车用到的数据结构:

<UserId,<SkuId,CartItem>>

在这里插入图片描述

一、获取user-key临时用户以及GULISESSION登录用户cookie

保证用户访问cart.gulimall.com/cart.html能获取到添加的购物车信息

浏览器有一个cookie;user-key;标识用户身份,一个月后过期;

如果第一次使用jd的购物车功能,都会给一个临时的用户身份,不管有没有登录;

浏览器以后保存,每次访问都会带上这个cookie;

登录:session有

没登录:按照cookie里面带来user-key来做。

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

在这里插入图片描述
访问cart.gulimall.com

@Controller
public class CartController {
    /*
    * 浏览器有一个cookie;user-key;标识用户身份,一个月后过期
    * */
    @GetMapping("/cart.html")
    public String cartListPage() {
        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        System.out.println(userInfoTo.toString());
        return "cartList";
    }
}

配置拦截器

/*
* 在执行目标方法之前,判断用户的登录状态。并封装传递给controller目标请求
* */
public class CartInterceptor implements HandlerInterceptor {

    public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        UserInfoTo userInfoTo = new UserInfoTo();
        MemberRespVo memberRespVo = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
        if (memberRespVo != null) {
            //用户登录过
           userInfoTo.setUserId(memberRespVo.getId());
        }
        Cookie[] cookies = request.getCookies();
        if (cookies != null && cookies.length != 0) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals(CartConstant.TEMP_USER_COOKIE_NAME)) {
                    userInfoTo.setUserKey(cookie.getValue());
                    userInfoTo.setTempUser(true);
                }
            }
        }
        if (StringUtils.isEmpty(userInfoTo.getUserKey())) {
            String uuid = UUID.randomUUID().toString();
            userInfoTo.setUserKey(uuid);
        }
        //目标方法执行之前
        threadLocal.set(userInfoTo);
        return true;
    }
/**
     * 业务执行之后;分配临时用户,让浏览器保存
     * @param request
     * @param response
     * @param handler
     * @param modelAndView
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        UserInfoTo userInfoTo = threadLocal.get();
        if (!userInfoTo.isTempUser()) {
            Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME,userInfoTo.getUserKey());
            cookie.setDomain("gulimall.com");
            cookie.setMaxAge(CartConstant.TEMP_USER_COOKIE_TIMEOUT);
            response.addCookie(cookie);
        }
    }
}

配置拦截路径

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

二、加入购物车实现

点击加入购物车

在这里插入图片描述
成功后跳转
在这里插入图片描述
查询属性List<String>
在这里插入图片描述
RedirectAttribute ra

ra.addFlashAttribute(); 将数据放在session里面可以在页面取出,但是只能取一次

ra.addAttribute(“skuId”,skuId) 在url路径携带?skuId=xxx

用户点击加入购物车=》判断用户是否登录=》

​ 1、已登录=》将未登录状态的购物车项加入已登录帐号的购物车中,清除临时购物车

​ 2、未登录=》在临时购物车上新增购物项

=》新增商品skuId是否存在

​ 1、已存在=》获取当前商品在购物车中的数量,新增(修改数量)

​ 2、不存在=》直接新增

=》删除购物车

三、秒杀

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

限流方式:

1、前端限流,一些高并发的网站直接在前端页面开始限流,例如:小米的验证码设计。

2、nginx限流,直接负载部分请求到错误的静态页面:令牌算法 漏斗算法

3、网关限流,限流的过滤器

4、代码中使用分布式信号量

5、rabbitmq限流(能者多劳:channel.basicQos(1)),保证发挥所有服务器的性能。

具体业务

在这里插入图片描述

一、设置秒杀场次及秒杀商品信息

在这里插入图片描述

数据库设计

sms_seckill_session

CREATE TABLE `sms_seckill_session` (
	`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
    `name` varhcar(200) DEFAULT NULL COMMENT '场次名称',
    `start_time` datetime DEFAULT NULL COMMENT '每日开始时间',
    `end_time` datetime DEFAULT NULL COMMENT '每日结束时间',
    `status` tinyint(1) DEFAULT NULL COMMENT '启用状态',
    `create_time` datetime DEFAULT NULL COMMENT '创建时间',
    PRIMARY_KEY(`id`)
)ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 COMMENT='秒杀活动场次'

在这里插入图片描述
sms_seckill_sku_relation

CREATE TABLE `sms_seckill_sku_relation` (
	`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
    `promotion_id` bigint(20) DEFAULT NULL COMMENT '活动id',
    `promotion_session_id` bigint(20) DEFAULT NULL COMMENT '活动场次id',
    `sku_id` bigint(20) DEFAULT NULL COMMENT '商品id',
    `seckill_price` decimal(10,0) DEFAULT NULL COMMENT '秒杀价格',
    `seckill_count` decimal(10,0) DEFAULT NULL COMMENT '秒杀总量',
    `seckill_limit` decimal(10,0) DEFAULT NULL COMMENT '每人限购数量',
    `seckill_sort`  int(11) DEFAULT NULL COMMENT '排序',
    PRIMARY_KEY(`id`)
)ENGINE=InnoDB COMMENT='秒杀活动商品关联'

在这里插入图片描述

二、秒杀商品的定时上架
定时任务

应用场景:对账单、财务汇总、统计信息数据等

cron表达式

SpringBoot整合定时任务与异步任务

给方法执行加定时任务,若方法延时执行,则定时任务需要在当前方法执行完成后才能开启。异步任务确保每一个方法执行都开启一个新的线程执行,互不影响。

定时任务

  •  @EnableScheduling 开启定时任务
    
  •  @Scheduled  开启一个定时任务
    
  •  自动配置类 TaskSchedulingAutoConfiguration
    

异步任务

  • @EnableAsync 开启异步任务功能

  • @Async 给希望异步执行的方法上标注

  • 自动配置类 TaskExecutionAutoConfiguration 属性绑定在TaskExecutionProperties
    在这里插入图片描述

  • 配置线程池

application.properties

spring.task.scheduling.pool.size=5
spring.task.execution.pool.max-size=50
/**

 * 1、Spring中6位组成,不允许第7位的年
 * 2、在周几的位置,1-7代表周一到周日; MON-SUN
 * 3、定时任务不应该阻塞。默认是阻塞的
 * 1)、可以让业务运行以异步的方式,自己提交到线程池
 * CompletableFuture.runAsync(()->{
 * 		xxxxService.hello();
 * },executor);
 * 2)、支持定时任务线程池;设置 TaskSchedulingProperties;
 * spring.task.scheduling.pool.size=5
    *
 * 3)、让定时任务异步执行
 * 异步任务;
    *
 * 解决:使用异步+定时任务来完成定时任务不阻塞的功能;
    *
    *
    */
/*
* 秒杀商品的定时上架:
*   每天晚上3点,上架最近三天需要秒杀的商品
*   当天00:00:00 - 23:59:59
*   当天00:00:00 - 23:59:59
*   当天00:00:00 - 23:59:59
* */
@Slf4j
@Service
@EnableAsync
@EnableScheduling
public class SeckillSkuScheduled {
    @Autowired
    SeckillService seckillService;
    @Autowired
    RedissonClient redissonClient;
    public final String UPLOAD_LOCK = "seckill:upload:lock";
    //TODO 幂等性处理
    @Async
    @Scheduled(cron = "* * * * * ?")
    public void uploadSeckillSkuLatest3Days() {
        //1、重复上架无需处理
        log.info("上架秒杀的商品信息");
        //分布式锁
        RLock lock = redissonClient.getLock(UPLOAD_LOCK);
        lock.lock(10, TimeUnit.SECONDS);
        try{
            seckillService.uploadSeckillSkuLatest3Days();
        }finally {
            lock.unlock();
        }

    }
}

配置RedissionClient

@Configuration
public class MyRedissonConfig {
    /*
    * 所有对Redisson的使用都是通过RedissonClient对象
    * */
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redisson() throws IOException {
        //1、创建配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.56.10:6379");
        //2、根据Config创建出RedissonClient示例
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }
}

ps:为什么用分布式锁

将最近三天的商品上架

1、秒杀服务设置3天的定时任务任务

2、调用coupon服务获取秒杀场次及对应的商品

gulimall-coupon

SeckillSessionController.java

@Autowired
private SeckillSessionService seckillSessionService;
@GetMapping("/latest3DaySession")
public R getLatest3DaySession() {
    List<SeckillSessionEntity> sessions = seckillSessionService.getLatest3DaySession();
    return R.ok().setData(sessions);
}

SeckillSessionServiceImpl.java

 @Override
public List<SeckillSessionEntity> getLatest3DaySession() {
    List<SeckillSessionEntity> list = this.list(new QueryWrapper<SeckillSessionEntity>().between("start_time", startTime(), endTime()));
    if (list != null && list.size() != 0) {
        list = list.stream().map(session -> {
            //找出当前任务相关场次
            List<SeckillSkuRelationEntity> relationEntities = relationService.list(new QueryWrapper<SeckillSkuRelationEntity>().eq("promotion_session_id", session.getId()));
            session.setRelationSkus(relationEntities);
            return session;
        }).collect(Collectors.toList());
    }
    return list;
}
//开始时间 2022-02-11 00:00:00
private String startTime() {
    LocalDate now = LocalDate.now();
    LocalTime min = LocalTime.MIN;
    LocalDateTime start = LocalDateTime.of(now, min);
    String format = start.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    return format;
}
//结束时间 2022-02-14 23:59:59
private String endTime() {
    LocalDate now = LocalDate.now();
    LocalDate localDate = now.plusDays(2);
    LocalTime max = LocalTime.MAX;
    LocalDateTime end = LocalDateTime.of(localDate, max);
    String format = end.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    return format;
}

SeckillSessionEntity.java

/**
 * 秒杀活动场次
 * 
 * @author Guoyifan
 * @email 1074840013@qq.com
 * @date 2021-11-26 16:22:06
 */
@Data
@TableName("sms_seckill_session")
public class SeckillSessionEntity implements Serializable {
	private static final long serialVersionUID = 1L;

	/**
	 * id
	 */
	@TableId
	private Long id;
	/**
	 * 场次名称
	 */
	private String name;
	/**
	 * 每日开始时间
	 */
	private Date startTime;
	/**
	 * 每日结束时间
	 */
	private Date endTime;
	/**
	 * 启用状态
	 */
	private Integer status;
	/**
	 * 创建时间
	 */
	private Date createTime;
    
    //活动关联的所有商品
	@TableField(exist = false)
	private List<SeckillSkuRelationEntity> relationSkus;

}

gulimall-seckill

SeckillServiceImpl.java

 	@Autowired
    CouponFeignService couponFeignService;
    @Autowired
    StringRedisTemplate redisTemplate;
    @Autowired
    ProductFeignService productFeignService;
    @Autowired
    RedissonClient redissonClient;
    @Autowired
    RabbitTemplate rabbitTemplate;

    public static final String SESSIONS_CACHE_PREFIX = "seckill:sessions:";

    private final String SKUKILL_CACHE_PREFIX = "seckill:skus";

    private final String SKU_STOCK_SEMAPHORE = "seckill:stock:";//+商品随机码
 @Override
    public void uploadSeckillSkuLatest3Days() {
        /*
        * coupon服务获取最近三天所有场次
        * */
        R session = couponFeignService.getLatest3DaySession();
        if (session.getCode() == 0) {
            //上架商品
            List<SeckillSessionWithSkus> sessionData = session.getData(new TypeReference<List<SeckillSessionWithSkus>>() {
            });
            //缓存到redis
            //1、缓存活动信息
            saveSessionInfos(sessionData);
            //2、缓存活动的关联商品信息
            saveSessionSkuInfos(sessionData);
        }
    }

ps:缓存活动信息和关联商品信息的redis结构?

ps:对stream流的了解

//1、缓存活动信息
private void saveSessionInfos(List<SeckillSessionWithSkus> sessions) {
        if (sessions != null)
            sessions.stream().forEach(session -> {

                Long startTime = session.getStartTime().getTime();
                Long endTime = session.getEndTime().getTime();
                String key = SESSIONS_CACHE_PREFIX + startTime + "_" + endTime;
                Boolean hasKey = redisTemplate.hasKey(key);
                if (!hasKey) { //缓存中不含有该场活动
                    //seckill:sessions:2022.1.18 00:00:00_2022.1.21 23:59:59      1_9
                    //
                    List<String> collect = session.getRelationSkus().stream().map(item -> item.getPromotionSessionId() + "_" + item.getSkuId().toString()).collect(Collectors.toList());
                    //缓存活动信息
                    if(collect != null) {
                        redisTemplate.opsForList().leftPushAll(key, collect);
                        //TODO 设置过期时间[已完成]
                       // redisTemplate.expireAt(key, new Date(endTime));
                    }

                }
            });
    }

ps:redis的list添加

//2、缓存活动的关联商品信息
private void saveSessionSkuInfos(List<SeckillSessionWithSkus> sessions) {
        if(sessions != null && sessions.size() != 0) {
            sessions.stream().forEach(session->{
                //准备hash操作
                BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
                session.getRelationSkus().stream().forEach(seckillSkuVo -> {
                    //4、随机码 seckill?skuId=1&key=dadlajldj;
                    String token = UUID.randomUUID().toString().replace("-","");
                    if (!ops.hasKey(seckillSkuVo.getPromotionSessionId().toString()+"_"+seckillSkuVo.getSkuId().toString())) {
                        //缓存商品
                        SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
                        //1、sku的基本数据
                        R skuInfo = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());
                        if (skuInfo.getCode() == 0) {
                            SkuInfoVo info = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                            });
                            redisTo.setSkuInfoVo(info);
                        }
                        //2、sku的秒杀信息
                        BeanUtils.copyProperties(seckillSkuVo,redisTo);
                        //3、设置上当前商品的秒杀时间信息
                        redisTo.setStartTime(session.getStartTime().getTime());
                        redisTo.setEndTime(session.getEndTime().getTime());
                        redisTo.setRandomCode(token);
                        String jsonString = JSON.toJSONString(redisTo);
                        //TODO 每个商品的过期时间不一样。所以,我们在获取当前商品秒杀信息的时候,做主动删除,代码在 getSkuSeckillInfo 方法里面
                        ops.put(seckillSkuVo.getPromotionSessionId().toString()+"_"+seckillSkuVo.getSkuId().toString(),jsonString);
                        //如果当前这个场次的商品的库存信息已经上架就不需要上架
                        //5、使用库存作为分布式的信号量  限流;
                        RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                        //商品可以秒杀的数量作为信号量
                        semaphore.trySetPermits(seckillSkuVo.getSeckillCount().intValue());
                        //TODO 设置过期时间。
                       // semaphore.expireAt(session.getEndTime());
                    }
                });
            });
        }
    }

SeckillSessionWithSkus.java

@Data
public class SeckillSessionWithSkus {
    private Long id;
    /**
     * 场次名称
     */
    private String name;
    /**
     * 每日开始时间
     */
    private Date startTime;
    /**
     * 每日结束时间
     */
    private Date endTime;
    /**
     * 启用状态
     */
    private Integer status;
    /**
     * 创建时间
     */
    private Date createTime;

    private List<SeckillSkuVo> relationSkus;
}

SeckillSkuVo.java

@Data
public class SeckillSkuVo {
    private Long id;
    /**
     * 活动id
     */
    private Long promotionId;
    /**
     * 活动场次id
     */
    private Long promotionSessionId;
    /**
     * 商品id
     */
    private Long skuId;
    /**
     * 秒杀价格
     */
    private BigDecimal seckillPrice;
    /**
     * 秒杀总量
     */
    private BigDecimal seckillCount;
    /**
     * 每人限购数量
     */
    private BigDecimal seckillLimit;
    /**
     * 排序
     */
    private Integer seckillSort;

}

SeckillSkuRedisTo.java

@Data
public class SeckillSkuRedisTo {
    private Long id;
    /**
     * 活动id
     */
    private Long promotionId;
    /**
     * 活动场次id
     */
    private Long promotionSessionId;
    /**
     * 商品id
     */
    private Long skuId;
    /*
    * 商品秒杀随机码
    * */
    private String randomCode;
    /**
     * 秒杀价格
     */
    private BigDecimal seckillPrice;
    /**
     * 秒杀总量
     */
    private BigDecimal seckillCount;
    /**
     * 每人限购数量
     */
    private Integer seckillLimit;
    /**
     * 排序
     */
    private Integer seckillSort;

    //当前商品秒杀的开始时间
    private Long startTime;
    //当前商品秒杀到的结束时间
    private Long endTime;

    //商品详细信息
    private SkuInfoVo skuInfoVo;

}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Nc0GFvfU-1646276796239)(C:\Users\Gyf\AppData\Roaming\Typora\typora-user-images\image-20220213162258846.png)]
在这里插入图片描述
在这里插入图片描述
秒杀服务可能部署在多台机器上,不同机器同时启动了定时任务,可能会导致并发上架商品。

可用分布式锁解决多台机器启动定时任务导致商品重复上架的问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b8jOcBPT-1646276842527)(C:\Users\Gyf\AppData\Roaming\Typora\typora-user-images\image-20220116113154470.png)]
在这里插入图片描述

秒杀需要注意的问题
  1. 服务单一职责+独立部署
  2. 秒杀链接加密
  3. 库存预热+快速扣减
  4. 动静分离
  5. 恶意请求拦截
  6. 流量错峰
  7. 限流&熔断&降级
  8. 队列削峰

在这里插入图片描述
在这里插入图片描述

三、展示当前时间可以秒杀的商品
/*
    * 返回当前时间可以参与的秒杀商品信息
    * */
@ResponseBody
@GetMapping("/currentSeckillSkus")
public R getCurrentSeckillSkus() {
    List<SeckillSkuRedisTo> vos = seckillService.getCurrentSeckillSkus();
    return R.ok().setData(vos);
}

ps: 为什么用scan,不用key

    /*
     * 返回当前时间可以参与的秒杀商品信息
     * */
    @Override
    public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
        //1、确定当前时间属于哪个秒杀场次
        long time = new Date().getTime();
        //Set<String> keys = redisTemplate.keys(SESSIONS_CACHE_PREFIX + "*");
        Set<String> keys = redisTemplate.execute((RedisCallback<Set<String>>) connection->{
           Set<String> keysTmp = new HashSet<>();
           Cursor<byte[]> cursor = connection.scan(new ScanOptions.ScanOptionsBuilder().match(SESSIONS_CACHE_PREFIX+"*").count(1000).build());
           while (cursor.hasNext()) {
               keysTmp.add(new String(cursor.next()));
           }
           return keysTmp;
        });
        for (String key : keys) {
            String replace = key.replace(SESSIONS_CACHE_PREFIX, "");
            String[] s = replace.split("_");
            long start = Long.parseLong(s[0]);
            long end = Long.parseLong(s[1]);
            if (time >= start && time <= end) {
                //2、获取这个秒杀场次需要的所有商品信息
                List<String> range = redisTemplate.opsForList().range(key, 0L, -1L);
                BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
                List<String> list = hashOps.multiGet(range);
               if (list != null) {
                   List<SeckillSkuRedisTo> collect = list.stream().map(item -> {
                       SeckillSkuRedisTo redisTo = JSON.parseObject(item, SeckillSkuRedisTo.class);
                       return redisTo;
                   }).collect(Collectors.toList());
                   return collect;
               }

            }
        }
        return null;
    }
四、获取秒杀商品的详细信息
    @ResponseBody
    @GetMapping("/sku/seckill/{skuId}")
    public R getSkuSeckillInfo(@PathVariable("skuId") Long skuId) {
        SeckillSkuRedisTo to = seckillService.getSkuSeckillInfo(skuId);
        return R.ok().setData(to);
    }
    @Override
    public SeckillSkuRedisTo getSkuSeckillInfo(Long skuId) {
        //1、找到需要参与秒杀的商品的key
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
        Set<String> keys = hashOps.keys();
        if (keys != null && keys.size() != 0) {
            String regx = "\\d_"+skuId;
            for (String key : keys) {
                if (Pattern.matches(regx,key)) {
                    String json = hashOps.get(key);
                    SeckillSkuRedisTo skuRedisTo = JSON.parseObject(json, SeckillSkuRedisTo.class);
                    long current = new Date().getTime();
                    if (current >= skuRedisTo.getStartTime() && current <= skuRedisTo.getEndTime()) {
                    }else {
                        skuRedisTo.setRandomCode(null);
                    }
                    return skuRedisTo;
                }
            }
        }
        return null;
    }
五、点击商品抢购按钮完成秒杀

用户点击商品秒杀,后台需要校验用户是否已登录

在这里插入图片描述

@Component
public class LoginUserInterceptor implements HandlerInterceptor {
    public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String uri = request.getRequestURI();
        AntPathMatcher antPathMatcher = new AntPathMatcher();
        boolean match = antPathMatcher.match("/kill", uri);
        if (match) {
            MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
            if (attribute != null) {
                loginUser.set(attribute);
                return true;
            }else {
                //用户未登录,跳转到登录页面
                request.getSession().setAttribute("msg","请先进行登录");
                response.sendRedirect("http://auth.gulimall.com/login.html");
                return false;
            }
        }
        return true;

    }
}

ps: ThreadLocal线程本地化用途?

/*
	killId  商品id(skuId)
	key     商品秒杀随机码
	num     商品的秒杀数量
*/
@GetMapping("/kill")
    public String secKill(@RequestParam("killId") String killId,
                          @RequestParam("key") String key,
                          @RequestParam("num")Integer num,
                          Model model) {
        String orderSn = seckillService.kill(killId,key,num);
        //1、判断是否登录
        model.addAttribute("orderSn",orderSn);
        return "success";
    }
   //TODO 上架秒杀商品的时候,每一个数据都有过期时间
    //TODO 秒杀后续的流程,简化了收货地址等信息
    @Override
    public String kill(String killId, String key, Integer num) {
        MemberRespVo respVo = LoginUserInterceptor.loginUser.get();
        //1、获取当前秒杀商品的详细信息
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
        String json = hashOps.get(killId);
        if (StringUtils.isEmpty(json)) {
            return null;
        }
        SeckillSkuRedisTo redis = JSON.parseObject(json, SeckillSkuRedisTo.class);
        //校验合法性
        Long startTime = redis.getStartTime();
        Long endTime = redis.getEndTime();
        long time = new Date().getTime();
        long ttl = endTime - time;
        //1、校验时间合法性
        if (time>=startTime && time<=endTime) {
            //2、校验随机码
            String randomCode = redis.getRandomCode();
            if (randomCode.equals(key)) {
                //3、验证购物数量是否合理
                if(num <= 0 || num > redis.getSeckillLimit()) return null;
                //4、验证这个人是否购买过。幂等性;如果只要秒杀成功,就去占位  userId_sessionId_skuId
                //SETNX
                String redisKey = respVo.getId()+"_"+redis.getPromotionSessionId()+"_"+redis.getSkuId();
                Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, String.valueOf(num),ttl,TimeUnit.MILLISECONDS);
                if (aBoolean) {
                    //占位成功说明从来没买过
                    RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
                    boolean b = semaphore.tryAcquire(num);
                    //秒杀成功
                    //快速下单,发送mq消息
                    String timeId = IdWorker.getTimeId();
                    SeckillOrderTo orderTo = new SeckillOrderTo();
                    orderTo.setOrderSn(timeId);
                    orderTo.setMemberId(respVo.getId());
                    orderTo.setNum(num);
                    orderTo.setPromotionSessionId(redis.getPromotionSessionId());
                    orderTo.setSkuId(redis.getSkuId());
                    orderTo.setSeckillPrice(redis.getSeckillPrice());
                    rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",orderTo);
                    return timeId;

                }
            }
        }
        return null;
    }     

ps:RabbitMq相关知识?

引申:用户登录怎么实现

秒杀流程

一、

缺点:流量会级联地映射到其他服务(购物车、订单)

优点:与普通商品加入购物车业务相似

在这里插入图片描述
在这里插入图片描述

二、

优点:点击抢购到创建订单,只用到了秒杀服务。流程很快(数据存放在队列)

缺点:创建完订单提前发给用户,告知用户秒杀成功;如果此时,MQ消息未处理,订单服务崩溃。需要处理该逻辑。

在这里插入图片描述

Semaphore信号量

//上面两个方法为阻塞方法,只有在其他线程释放了,当前线程才能获取
acquire(); 
acquire(int i);

//异步获取
tryAcquire();
tryAcquire(int i);

https://juejin.cn/post/6844903537508368398
public class ToiletRace{
    private static final int THREAD_COUNT = 30;

    private static ExecutorService threadPool = Executors
            .newFixedThreadPool(THREAD_COUNT);

    private static Semaphore s = new Semaphore(10);
    public static void main(String[] args) {
        for (int i = 0; i < THREAD_COUNT; i++) {
            threadPool.execute(new Employee(String.valueOf(i), s));
        }

        threadPool.shutdown();
    }
}

class Employee implements Runnable{
    private String id;
    private Semaphore semaphore;
    private static Random rand = new Random(47);
    public Employee(String id, Semaphore semaphore) {
        this.id = id;
        this.semaphore = semaphore;
    }
    @Override
    public void run() {
        try {
            semaphore.acquire();
            System.out.println(this.id+"is using the toilet");
            TimeUnit.MILLISECONDS.sleep(rand.nextInt(2000));
            semaphore.release();
            System.out.println(this.id+"is leaving");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
扩展

Java并发工具类(闭锁CountDownLatch)

Java并发工具类(栅栏CyclicBarrier)

在这里插入图片描述

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值