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客户端

基本使用

 @AutowiredStringRedisTemplate stringRedisTemplate;
@Overridepublic 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) 省掉了整个续期操作。手动解锁

   @AutowiredRedissonClient 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【看门狗时间】/3try {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 {@BeanRedisCacheConfiguration 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可独立运行

@Overridepublic SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {SkuItemVo skuItemVo = new SkuItemVo();//1、sku基本信息获取 pms_sku_infoCompletableFuture<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_imagesCompletableFuture<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进行配置 @Beanpublic CookieSerializer cookieSerializer() {DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();cookieSerializer.setDomainName(".gulimall.com");//设置作用域cookieSerializer.setCookieName("GULISESSION");//设置session namereturn cookieSerializer;}//实现redis序列化器 (不用将类实现serializable)@Beanpublic 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/employeesreturn "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 {@AutowiredStringRedisTemplate 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

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

  @AutowiredAmqpAdmin amqpAdmin;  @Testpublic void createExchange() {DirectExchange exchange = new DirectExchange("hello-java-exchange",false,false,null);amqpAdmin.declareExchange(exchange);//声明一个交换机log.info("Exchange[{}]创建成功","hello-java-exchange");}@Testpublic void createQueue() {Queue queue = new Queue("hello-java-queue",true,false,false);amqpAdmin.declareQueue(queue);log.info("Queue[{}]创建成功","hello-java-queue");}@Testpublic 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 {@Beanpublic 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 {@AutowiredRabbitTemplate 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 {@Overridepublic 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"})@RabbitHandlerpublic 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//签收了货物...6channel.basicNack(deliveryTag,false,true);//long deliveryTag, boolean requeue
    //                channel.basicReject();System.out.println("没有签收了货物..."+deliveryTag);}}catch (Exception e){//网络中断}}@RabbitHandlerpublic 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 {@AutowiredRabbitTemplate rabbitTemplate;@Beanpublic MessageConverter messageConverter() {return new Jackson2JsonMessageConverter();}@PostConstruct  //MyRabbitConfig对象创建完成以后,执行这个方法public void initRabbitTemplate() {//设置确认回调rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {/** correlationData  当前消息的唯一关联数据(这个是消息的唯一id)* ack 消息是否成功收到* cause 失败的原因* */@Overridepublic 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 失败的原因*/@Overridepublic 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 当时这个消息用哪个路由键*/@Overridepublic 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"})@RabbitHandlerpublic 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//签收了货物...6channel.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() {@Overridepublic void apply(RequestTemplate requestTemplate) {//1、RequestContextHolder拿到刚进来的当前线程的请求ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();//老请求HttpServletRequest request = attributes.getRequest();//老请求//同步请求头数据 CookieString cookie = request.getHeader("Cookie");//老请求//给新请求同步了老请求的cookierequestTemplate.header("Cookie",cookie);}};}
}

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

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

  @Overridepublic 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 {@AutowiredDataSourceProperties dataSourceProperties;@Beanpublic 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* */@Beanpublic 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;}@Beanpublic Queue orderReleaseOrderQueue() {Queue queue = new Queue("order.release.order.queue", true, false, false);return queue;}@Beanpublic Exchange orderEventExchange() {//String name, boolean durable, boolean autoDelete, Map<String, Object> argumentsreturn new TopicExchange("order-event-exchange",true,false);}@Beanpublic Binding orderCreateOrderBinding() {return new Binding("order.delay.queue",Binding.DestinationType.QUEUE,"order-event-exchange","order.create.order",null);}@Beanpublic 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 {@AutowiredThirdPartFeignService  thirdPartFeignService;@AutowiredStringRedisTemplate redisTemplate;@AutowiredMemberFeignService 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、验证码的再次校验  redisString 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 -> 45678String 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 {@Autowiredprivate MemberService memberService;@AutowiredCouponFeignService 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

盐值加密

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

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

    @AutowiredThirdPartFeignService  thirdPartFeignService;@AutowiredStringRedisTemplate redisTemplate;@AutowiredMemberFeignService 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 {@Autowiredprivate 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

   @Overridepublic 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 {/** 视图映射* */@Overridepublic 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 {@AutowiredMemberFeignService 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换取accessTokenHttpResponse 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 {@Autowiredprivate 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 {@AutowiredMemberLevelService memberLevelService; @Overridepublic 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_membermemberDao.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*/@TableIdprivate 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 {@Beanpublic CookieSerializer cookieSerializer() {DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();cookieSerializer.setDomainName("gulimall.com");cookieSerializer.setCookieName("GULISESSION");return cookieSerializer;}//实现redis序列化器 (不用将类实现serializable)@Beanpublic 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 {@Beanpublic CookieSerializer cookieSerializer() {DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();cookieSerializer.setDomainName("gulimall.com");cookieSerializer.setCookieName("GULISESSION");return cookieSerializer;}//实现redis序列化器 (不用将类实现serializable)@Beanpublic 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<>();@Overridepublic 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;}/*业务执行之后,分配临时用户,让浏览器保存*/@Overridepublic 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 {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new CartInterceptor()).addPathPatterns("/**");}
}

Controller

@Controller
public class CartController {@AutowiredCartService 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";
}

获取购物车

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

    @Overridepublic 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 {@Beanpublic 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 {@AutowiredStringRedisTemplate redisTemplate;@AutowiredProductFeignService productFeignService;@AutowiredThreadPoolExecutor executor;private final String CART_PREFIX = "gulimall:cart:";/** 添加商品到购物车*/@Overridepublic 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

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

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

  @Overridepublic 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<>();@Overridepublic 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*/@Overridepublic 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 {@Overridepublic 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 {@AutowiredSeckillService seckillService;@AutowiredRedissonClient 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*/@TableIdprivate 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

     @AutowiredCouponFeignService couponFeignService;@AutowiredStringRedisTemplate redisTemplate;@AutowiredProductFeignService productFeignService;@AutowiredRedissonClient redissonClient;@AutowiredRabbitTemplate 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:";//+商品随机码
 @Overridepublic 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;}




秒杀服务可能部署在多台机器上,不同机器同时启动了定时任务,可能会导致并发上架商品。

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


秒杀需要注意的问题

  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

    /** 返回当前时间可以参与的秒杀商品信息* */@Overridepublic 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);}
    @Overridepublic SeckillSkuRedisTo getSkuSeckillInfo(Long skuId) {//1、找到需要参与秒杀的商品的keyBoundHashOperations<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<>();@Overridepublic 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 秒杀后续的流程,简化了收货地址等信息@Overridepublic 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//SETNXString 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;}@Overridepublic 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)

谷粒商城分布式高级篇学习笔记相关推荐

  1. 谷粒商城-分布式高级篇【业务编写】

    谷粒商城-分布式基础篇[环境准备] 谷粒商城-分布式基础[业务编写] 谷粒商城-分布式高级篇[业务编写]持续更新 谷粒商城-分布式高级篇-ElasticSearch 谷粒商城-分布式高级篇-分布式锁与 ...

  2. 谷粒商城-分布式高级篇[商城业务-检索服务]

    谷粒商城-分布式基础篇[环境准备] 谷粒商城-分布式基础[业务编写] 谷粒商城-分布式高级篇[业务编写]持续更新 谷粒商城-分布式高级篇-ElasticSearch 谷粒商城-分布式高级篇-分布式锁与 ...

  3. 谷粒商城分布式高级篇(中)

    谷粒商城分布式基础篇 谷粒商城分布式高级篇(上) 谷粒商城分布式高级篇(中) 谷粒商城分布式高级篇(下) 文章目录 商城业务 异步 异步复习 线程池详解 CompletableFuture Compl ...

  4. 谷粒商城-分布式高级篇[商城业务-秒杀服务]

    谷粒商城-分布式基础篇[环境准备] 谷粒商城-分布式基础[业务编写] 谷粒商城-分布式高级篇[业务编写]持续更新 谷粒商城-分布式高级篇-ElasticSearch 谷粒商城-分布式高级篇-分布式锁与 ...

  5. 谷粒商城分布式高级篇

    ElasticSearch 商品发布代码 es索引的设计 (1)方便检索{ skuId:1 spuId:1 skuTitle:华为xx price:9988 saleCount:99 attrs:[ ...

  6. 谷粒商城分布式高级篇总结文档

    目录 Elasticsearch Docker安装Elaticsearch(简称:ES) Docker安装Kibana (ES可视化界面) 启动Kibana遇到的坑 安装htop 如何删除卸载dock ...

  7. 谷粒商城--认证中心--高级篇笔记八

    谷粒商城–认证中心–高级篇笔记八 1. 环境搭建 1.1 新建模块gulimall-auth-server 1.2 pom文件 上面没选好直接复制下面的pom文件,记得排除gulimall-commo ...

  8. 谷粒商城-分布式基础篇-环境搭建

    1.写在前面 既个人博客系统和Java虚拟机学习后,深感技术点过于零散,于是照着尚硅谷教程写了谷粒商城这个项目.谷粒商城是一个完整的大型分布式架构电商平台,这个项目将我目前学到的知识点,以及还未学到的 ...

  9. 谷粒商城三阶段课件_谷粒商城分布式基础篇一

    微服务架构图 微服务划分图 搭建虚拟开发环境 1.下载安装VirtualBox 下载安装Vagrant 2.安装好后,创建一个存放vagrant box的目录,方便日后统一管理,比如叫做../cent ...

最新文章

  1. Windows Server AppFabric Caching
  2. 解决-Dmaven.multiModuleProjectDirectory system property is not set. Check $M2_HOME environment variabl
  3. http://www.cnblogs.com/Javame/p/3632473.html
  4. 距离剩者为王,服饰企业还要跨过很多道坎
  5. html dom获得父div,获取元素的父div
  6. 江西事业单位计算机管理岗怎么样,江西省直事业单位招聘出现不少冷门岗
  7. python中表示空类型的是_python中怎么表示空值
  8. Spring Boot基础学习笔记25:RabbitMQ - 发布/订阅工作模式
  9. java stream Collectors
  10. math.floor实现四舍五入
  11. HighChart 体验之旅 (后台传递JSON参数和数据的方法)
  12. ML/DL-复习笔记【三】- 算法的评价指标
  13. windows-API劫持(API-HOOK)
  14. 机器学习基石 作业一
  15. CS5532 HAL库版本工程代码
  16. FlashFXP中文破解解压版
  17. 最小二乘法概念和代码示例
  18. Java多线程篇--线程的等待通知
  19. 【题解】CSP-J2021第二轮题解
  20. 聚观早报 | 苹果2024年放弃高通;腾讯回应进军类 ChatGPT

热门文章

  1. 三菱FX3U与2台台达温控器modbus通讯案例 功能:三菱FX3U与2台台达温控器进行modbus通讯。
  2. mysql房源数据库_[宜配屋]听图阁
  3. 用Python实现电子邮件接收程序(POP3)
  4. WebRTC编译篇之Ninja 编译系统 二
  5. 三菱工控板底层源码_STM32 PLC底层源码/FX2N源码/断电保持
  6. 持安零信任入选数说安全《零信任安全产品研究报告》
  7. 读取csv和tsv文件以及两者的相互转换
  8. 2020年“华为杯”第十七届中国研究生数学建模竞赛B题心得(满纸荒唐言,一把辛酸泪)
  9. 大家在谈租房时都在谈些什么?--豆瓣租房小组词云图
  10. 公司组织结构图用PPT制作?