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;
}
}
1、application.yml:
thyemleaf-starter:关闭缓存
2、静态资源都放在static文件夹下就可以按照路径直接访问
3、页面放在templates下,直接访问
SpringBoot,访问项目的时候,默认会找index
4、页面修改不重启服务器实时更新
1)、引入dev-tools
2)、修改完页面 ctrl shift f9重新自动编译当前页面
ctrl f9编译当前服务
1)、访问gulimall.com,nginx监听后访问http://gulimall
2)、http块中配置upstream
3)、跳转到http://192.168.56.1:88网关
内存泄漏、并发与同步
JMeter压测,通过报告查看性能情况,Jvisualvm可以查看虚拟机空间占用
关日志
日志打印设为error级别
开缓存
优化数据库
数据库增加索引
thymeleaf关闭缓存
当访问到gulimall.com/static目录,自动请求nginx/static静态文件
jvm空间太小,频繁gc
调高最大堆内存(堆内存太小)Eden区内存不够,进行MinorGC,内存还不够,分配到老年代,老年代对象不够分配,则进行一次full gc,非常耗费资源。
堆内存设置为1024m,新生代堆内存设置为512m
1、引入data-redis-starter
2、简单配置redis的host等信息
3、使用SpringBoot自动配置好的StringRedisTemplate来操作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;
}
<!--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";
}
保证一定能读到最新数据,修改期间,写锁是一个排他锁(互斥锁、独享锁)。读锁是一个共享锁
写锁没释放,读就必须等待
应用:分布式限流操作
tryAcquire()判断是否能获取成功,进而执行操作
SpringCache中采用@CacheEvict注解(失效模式)| @CachePut注解(双写模式)
1)、引入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-cacheartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
<exclusions>
<exclusion>
<groupId>io.lettucegroupId>
<artifactId>lettuce-coreartifactId>
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负责缓存的读写
自定义配置类,将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;
}
}
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)、写模式:(缓存与数据库一致)
总结:
常规数据(读多写少,即时性,一致性要求不高的数据);完全可以使用Spring-Cache;写模式(只要缓存的数据有过期时间就足够了)
特殊数据:特殊设计
原理:
CacheManager(RedisCacheManager)->Cache(RedisCache)->Cache负责缓存的读写
//当前系统中池只有一两个,每个异步任务,提交给线程池让他自己去执行就行
/**
* 七大参数
* corePoolSize:[5] 核心线程数[一直存在除非(allowCoreThreadTimeOut)]; 线程池,创建好以后就准备就绪的线程数量,就等待来接受异步任务去执行。
* 5个 Thread thread = new Thread(); thread.start();
* maximumPoolSize:[200] 最大线程数量; 控制资源
* keepAliveTime:存活时间。如果当前的线程数量大于core数量。
* 释放空闲的线程(maximumPoolSize-corePoolSize)。只要线程空闲大于指定的keepAliveTime;
* unit:时间单位
* BlockingQueue 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提供了四个静态方法来创建一个异步操作
1、runXxxx都是没有返回结果的,supplyXxxx都是可以获取返回结果的
2、可以传入自定义的线程池,否则就用默认的线程池
exceptionally
exceptionally比whenComplete多了一个return返回值
whenComplete 和 whenCompleteAsync 的区别:
4、线程串行化方法
thenApply方法:当一个线程依赖另一个线程时,获取上一个任务返回的结果,并返回当前任务的返回值。
thenAccept方法:消费处理结果。接收任务的处理结果,并消费处理,无返回结果。
thenRun方法:只要上面的任务执行完成,就开始执行thenRun,只是处理完任务后,执行thenRun的后续操作。
Function super T,? extends U>
1、写出要注入的参数属性,在类上加上@ConfigurationProperties(prefix="")注解,@Component加入容器
2、poperties中配置值
加上properties提示依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
任务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;
}
业务逻辑:用户登录成功,获取到用户信息,携带用户信息跳转到gulimall.com。
问题:1、不同域名session不共享
文档:https://docs.spring.io/spring-session/reference/2.6.1/guides/boot-redis.html
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-data-redisartifactId>
dependency>
配置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)导包
(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();
}
}
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,存在则不需要继续登录
代码:
@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";
}
}
}
@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";
}
}
<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>
D:\Desktop\面试\RabbitMQ\rabbitmq\笔记
使用步骤
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
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 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();
}
}
RabbitConnectionFactoryBean
RabbitMessagingTemplate
//提供了自动配置的所有属性
@ConfigurationProperties(prefix = "spring.rabbitmq")
public class RabbitProperties
给配置文件(application.properties)中配置spring.rabbitmq信息
@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);
}
#开启发送端确认
spring.rabbitmq.publisher-confirms=true
Java中该注解的说明:@PostConstruct该注解被用来修饰一个非静态的void()方法。被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。PostConstruct在构造函数之后执行,init()方法之前执行。
通常我们会是在Spring框架中使用到@PostConstruct注解 该注解的方法在整个Bean初始化中的执行顺序:
Constructor(构造方法) -> @Autowired(依赖注入) -> @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+"]");
}
});
}
}
#开启发送端消息抵达队列的确认
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+"]");
}
});
}
消费端接收到队列的消息,默认接受模式下,只要接收到队列中的一个消息,不管有没有全部接收,都会清空队列中的消息。
手动ack消息配置
#手动ack消息
spring.rabbitmq.listener.simple.acknowledge-mode=manual
消费端确认(保证每个消息被正确消费,此时才可以broker删除这个消息)。
1、默认是自动确认的,只要消息接收到,客户端会自动确认,服务端就会移除这个消息
问题:
我们收到很多消息,自动回复给服务器ack,只有一个消息处理成功,宕机了。就会发生消息丢失;
消费者手动确认模式。只要我们没有明确告诉MQ,货物被签收。没有Ack,消息就一直是unacked状态。即使Consumer宕机。消息不会丢失,会重新变为Ready,下一次有新的Consumer连接进来就发给他
2、如何签收:
channel.basicAck(deliveryTag,false);签收;业务成功完成就应该签收
/**
* 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){
//网络中断
}
}
订单模块使用
创建交换机、创建队列、创建绑定:AmqpAdmin
发送消息:RabbitTemplate
接收消息:@RabbitListener、@RabbitHandler
可靠抵达:ConfirmCallback、ReturnCallback、ACK消费确认
浏览器带了请求头和参数过来,调用远程服务,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);
}
};
}
}
异步编排进行远程调用,在每一个新线程都来设置浏览器线程发送过来的请求数据。
@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
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
2)、@EnableAspectJAutoProxy(exposeProxy = true)
;开启 aspectj 动态代理功能。以后所有的动态代理都是aspectj创建的(即使没有接口也可以创建动态代理)。
对外暴露代理对象
OrderServiceImpl orderService = (OrderServiceImpl) AopContext.currentProxy();
orderService.b();
orderService.c();
raft中,一个节点可以有三种状态:Follower,Candidate,Leader。两大核心:领导选举,日志复制
http://thesecretlivesofdata.com/raft/
适用场景:适合并发量少的简单场景
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依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
dependency>
找到对应的依赖版本为
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的消息TTL和死信Exchange结合
1、设置消息过期时间实现延时队列(不推荐)
2、设置队列过期时间实现延时队列(推荐)
延时队列设计
@Configuration
public class MyMQConfig {
//Queue Exchange Binding
/*
* String name, boolean durable, boolean exclusive, boolean autoDelete, Map 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 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);
}
}
监听器
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 = 返回给前端的
@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);
}
采用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 [email protected]
* @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("用户名已存在");
}
}
Spring自带的盐值加密
访问登录页,判断是否登录
@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 [email protected]
* @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;
}
@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {
/*
* 视图映射
* */
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login.html").setViewName("login");
registry.addViewController("/reg.html").setViewName("reg");
}
}
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";
}
@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 [email protected]
* @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 [email protected]
* @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;
}
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;
}
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;
}
}
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
application.properties
spring.redis.host=192.168.56.10
购物车分为游客模式和登录模式,需要先判断用户是否登录。
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-data-redisartifactId>
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();
}
}
@Data
public class UserInfoTo {
private Long userId;
private String userKey;
private boolean tempUser = false;
}
/*
* 在执行目标方法之前,判断用户的登录状态。并封装传递给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);
}
}
GulimallWebConfig.java
@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new CartInterceptor()).addPathPatterns("/**");
}
}
@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;
}
/*
* 添加商品到购物车
* @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;
}
@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);
}
@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>>
保证用户访问cart.gulimall.com/cart.html能获取到添加的购物车信息
浏览器有一个cookie;user-key;标识用户身份,一个月后过期;
如果第一次使用jd的购物车功能,都会给一个临时的用户身份,不管有没有登录;
浏览器以后保存,每次访问都会带上这个cookie;
登录:session有
没登录:按照cookie里面带来user-key来做。
第一次:如果没有临时用户,帮忙创建一个临时用户。
@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='秒杀活动场次'
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表达式
给方法执行加定时任务,若方法延时执行,则定时任务需要在当前方法执行完成后才能开启。异步任务确保每一个方法执行都开启一个新的线程执行,互不影响。
定时任务
@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服务获取秒杀场次及对应的商品
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 [email protected]
* @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;
}
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;
}
秒杀服务可能部署在多台机器上,不同机器同时启动了定时任务,可能会导致并发上架商品。
可用分布式锁解决多台机器启动定时任务导致商品重复上架的问题。
/*
* 返回当前时间可以参与的秒杀商品信息
* */
@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 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消息未处理,订单服务崩溃。需要处理该逻辑。
//上面两个方法为阻塞方法,只有在其他线程释放了,当前线程才能获取
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)