29、商品上架和ES的存储模型选择
上架概念:我们把商品存入es的过程叫上架,只有上架的商品才能被前台检索
es的数据保存位置:内存
对es的使用我们不能把所有的数据都放在进来,因为内存时很贵的,我们需要有用的信息放进来,
商品es的存储模型方案:
模型一:占用空间多
{
skuId:1
spuId:11
skuName:华为xxx
attr:[
{
尺寸:5
颜色:红色
。。。
。。。
}
]
}
模型二:查询时间长
sku索引{}
spu索引{}
30、nested的是使用
es在存储数组时必须要用 nested 来指定数据类型,负责查询查询出不想查的值
PUT my-index-000002
{
"mappings": {
"properties": {
"user": {
"type": "nested"
}
}
}
}
PUT my-index-000002/_doc/1
{
"group" : "fans",
"user" : [
{
"first" : "John",
"last" : "Smith"
},
{
"first" : "Alice",
"last" : "White"
}
]
}
GET my-index-000002/_search
{
"query": {
"bool": {
"must": [
{ "match": { "user.first": "Alice" }},
{ "match": { "user.last": "Smith" }}
]
}
}
}
31、Feigh的原理和流程
// Feign 的调用流程 重复调用?接口幂等性:重试机制
// 1. 构造请求,经对象转为json
// 2. 发送请求进行执行(执行成功会解码响应)
// 3. 执行请求会有重试机制,默认充实器处于关闭状态的
测试:略
32、动静分离
动:接口,链接数据库的请求
静:静态资源,图片,html,css,等
动态的东西应该放在微服务中,静态的东西放在nginx中,这样可以减少每个微服务tomcat的并发压力
前端的模板引擎用的时thymleaf,它的缺点是可能性能不如其他的模板引擎,但是我们可以通过缓存技术来优化它。
33、thymleaf的配置
spring:
#关闭thymeleaf缓存
thymeleaf:
cache: false
# 默认前缀
prefix: classpath:/templates/
# 默认后缀
suffix: .html
34、自动更新静态文件
第一步:
<!-- 自动给更新静态文件 true 这个才是关键-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
第二步:
ctrl+f9 重新编译项目
ctrl+shift+f9 重新编译当前页面
35、nginx反向代理
略
36、springCache 缓存的使用
略
37、Elasticsearch实现搜索功能
流程:
全文检索:
skuTitle-》keyword
排序:saleCount(销量)、hotScore(热度分)、skuPrice(价格)
过滤:hasStock、skuPrice区间、brandId、catalog3Id、attrs
聚合:attrs
一下式搜索条件
前台页面URL参数
keyword=小米&
sort=saleCount_desc/asc&
hasStock=0/1&
skuPrice=400_1900&
brandId=1&
catalog3Id=1&
attrs=1_3G:4G:5G&
attrs=2_骁龙845&
attrs=4_高清屏
搜索条件封装
@Data
public class SearchParam {
/**
* 页面传递过来的全文匹配关键字 skutitile
*/
private String keyword;
/**
* 品牌id,可以多选
*/
private List<Long> brandId;
/**
* 三级分类id
*/
private Long catalog3Id;
/**
* 排序条件:sort=price/salecount/hotscore_desc/asc
*/
private String sort;
/**
* 是否显示有货
*/
private Integer hasStock;
/**
* 价格区间查询
*/
private String skuPrice;
/**
* 按照属性进行筛选
*/
private List<String> attrs;
/**
* 页码
*/
private Integer pageNum = 1;
/**
* 原生的所有查询条件
*/
private String _queryString;
}
搜索结果封装
@Data
public class SearchResult {
/**
* 查询到的所有商品信息
*/
private List<SkuEsModel> product;
/**
* 当前页码
*/
private Integer pageNum;
/**
* 总记录数
*/
private Long total;
/**
* 总页码
*/
private Integer totalPages;
private List<Integer> pageNavs;
/**
* 当前查询到的结果,所有涉及到的品牌
*/
private List<BrandVo> brands;
/**
* 当前查询到的结果,所有涉及到的所有属性
*/
private List<AttrVo> attrs;
/**
* 当前查询到的结果,所有涉及到的所有分类
*/
private List<CatalogVo> catalogs;
//===========================以上是返回给页面的所有信息============================//
/* 面包屑导航数据 */
private List<NavVo> navs;
@Data
public static class NavVo {
private String navName;
private String navValue;
private String link;
}
@Data
public static class BrandVo {
private Long brandId;
private String brandName;
private String brandImg;
}
@Data
public static class AttrVo {
private Long attrId;
private String attrName;
private List<String> attrValue;
}
@Data
public static class CatalogVo {
private Long catalogId;
private String catalogName;
}
}
es索引
PUT gulimall_product
{
"mappings": {
"properties": {
"skuId": {
"type": "long"
},
"spuId": {
"type": "long"
},
"skuTitle": {
"type": "text",
"analyzer": "ik_smart"
},
"skuPrice": {
"type": "keyword"
},
"skuImg": {
"type": "keyword"
},
"saleCount": {
"type": "long"
},
"hosStock": {
"type": "boolean"
},
"hotScore": {
"type": "long"
},
"brandId": {
"type": "long"
},
"catelogId": {
"type": "long"
},
"brandName": {
"type": "keyword"
},
"brandImg": {
"type": "keyword"
},
"catalogName": {
"type": "keyword"
},
"attrs": {
"type": "nested",
"properties": {
"attrId": {
"type": "long"
},
"attrName": {
"type": "keyword"
},
"attrValue": {
"type": "keyword"
}
}
}
}
}
}
38、使用CompletableFuture 配合线程池 来进行商品详情的异步查询
JDK1.8以后才可以使用CompletableFuture
场景:当我们请求商品详情的时候,需要请求很多信息,如
- sku的信息
- 促销信息
- 图片信息
- 属性信息
假设每个请求都需要需要1s那么加起来也需要5s 显然是无法接收的
这个时候我们就可以用异步请求,利用多个线程来同时请求这些数据,可能只需要1.5s就能完成。
我们以后的业务代码中,对于比较耗时的操作,可以定义一两个线程池,每个异步任务提交到线程池里面,让它自己执行就行。
1、runXxxx 都是没有返回结果的,supplyXxx 都是可以获取返回结果的
2、可以传入自定义的线程池,否则就用默认的线程池;
3、方法中如果带有ASync 就表示是重新启动一个线程去执行,没有Async就表示还在原来的线程中执行。
38.1 感知异常的三种方式
whenComplete 虽然能得到异常信息,但是没法修改返回数据
exceptionally 可以感知异常,可以修改返回结果,但是拿不到
handle ★可感知异常,也可修改返回结果
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(()->{
System.out.println("maruis------>" + "异步请求完成。。。");
// int i = 10/0;
int i = 10/4;
return i;
},executor).whenComplete((res,e)->{
//虽然能得到异常信息,但是没法修改返回数据
System.out.println("maruis-----whenComplete->" + res);
System.out.println("maruis------>" + (e!=null?e.getMessage():"没问题"));
}).exceptionally((throwable)->{
//可以感知异常,同时返回默认值
System.out.println("maruis----exceptionally-->" + throwable.getMessage());
return 404;
}).handle((res,throwable)->{
// 可感知异常,也可修改返回结果
if(res!=null){
return res*2;
}else{
return 0;
}
});
Integer integer = future.get();
System.out.println("maruis----result-->" + integer);
38.2 线程串行化
thenApply 方法:当一个线程依赖另一个线程时,获取上一个任务返回的结果,并返回当前
任务的返回值。
thenAccept 方法:消费处理结果。接收任务的处理结果,并消费处理,无返回结果。
thenRun 方法:只要上面的任务执行完成,就开始执行 thenRun,只是处理完任务后,执行
thenRun 的后续操作
带有 Async 默认是异步执行的。同之前。
以上都要前置任务成功完成。
Function<? super T,? extends U>
T:上一个任务返回结果的类型
示例
/**
* 串行化
*/
private static void thread_then() throws ExecutionException, InterruptedException {
CompletableFuture<String> future = CompletableFuture.supplyAsync(()->{
System.out.println("maruis------>" + "执行了第一个任务");
// int i = 10/0;
int i = 10/4;
return i;
},executor).whenComplete((res,e)->{
//虽然能得到异常信息,但是没法修改返回数据
System.out.println("maruis-----whenComplete->" + res);
System.out.println("maruis------>" + (e!=null?e.getMessage():"没问题"));
}).exceptionally((throwable)->{
//可以感知异常,同时返回默认值
System.out.println("maruis----exceptionally-->" + throwable.getMessage());
return 404;
}).handle((res,throwable)->{
// 可感知异常,也可修改异常
if(res!=null){
return res*2;
}else{
return 0;
}
}).thenApplyAsync((res)->{
System.out.println("maruis------>" + "执行了第二个任务");
System.out.println("maruis------>" + "上次任务的返回结果:"+res);
return "hello:"+res;
},executor);
System.out.println("maruis----result-->" + future.get());
}
38.3 两任务组合 - 都要完成
两个任务必须都完成,触发该任务。
thenCombine:组合两个 future,获取两个 future 的返回结果,并返回当前任务的返回值
thenAcceptBoth:组合两个 future,获取两个 future 任务的返回结果,然后处理任务,没有
返回值。
runAfterBoth:组合两个 future,不需要获取 future 的结果,只需两个 future 处理完任务后,
处理该任务。
38.4 两任务组合 - 一个完成
当两个任务中,任意一个 future 任务完成的时候,执行任务。
applyToEither:两个任务有一个执行完成,获取它的返回值,处理任务并有新的返回值。
acceptEither:两个任务有一个执行完成,获取它的返回值,处理任务,没有新的返回值。
runAfterEither:两个任务有一个执行完成,不需要获取 future 的结果,处理任务,也没有返
回值。
38.5、多任务组合
allOf:等待所有任务完成
anyOf:只要有一个任务完成
示例
/**
* 多任务组合
*/
private static void thread_duorenwu() throws ExecutionException, InterruptedException {
CompletableFuture<String> futureImg = CompletableFuture.supplyAsync(()->{
System.out.println("maruis------>" + "查询商品图片信息");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "xxx.jpg";
},executor);
CompletableFuture<String> futureAttr = CompletableFuture.supplyAsync(()->{
System.out.println("maruis------>" + "查询商品属性");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "黑色+256G";
},executor);
CompletableFuture<String> futureDesc = CompletableFuture.supplyAsync(()->{
System.out.println("maruis------>" + "查询商品介绍");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "华为";
},executor);
// 所有任务都执行才返回
// CompletableFuture.allOf(futureImg, futureDesc, futureAttr).get();
// System.out.println("maruis----result-->" + futureImg.get()+futureAttr.get()+futureDesc.get());
// 只要有一个执行完就会返回
Object o = CompletableFuture.anyOf(futureImg, futureDesc, futureAttr).get();
System.out.println("maruis----result-->" + o.toString());
}
38.6 最佳实战-商品详情的异步编排
第一步:设置线程池
1.1 pom文件中配置文件的代码提示
<!-- 配置文件的代码提示-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
1.2 写一个线程池bean,用于公共调用
@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<>(100000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
}
}
1.3 添加配置类
@ConfigurationProperties(prefix = “gulimall.thread”)
// @Component
@Data
public class ThreadPoolConfigProperties {
private Integer coreSize;
private Integer maxSize;
private Integer keepAliveTime;
};
1.4 application.properties 中配置线程参数
#配置线程池
gulimall.thread.coreSize=20
gulimall.thread.maxSize=200
gulimall.thread.keepAliveTime=10
第二步:注入线程池,编写商品详情逻辑
/**
* 多线程,异步请求
* @param skuId
* @return
* @throws InterruptedException
* @throws ExecutionException
*/
@Resource
private ThreadPoolExecutor executor ;
private SkuItemVo getSkuItemVoThread(Long skuId) throws InterruptedException, ExecutionException {
long start = System.currentTimeMillis();
SkuItemVo skuItemVo = new SkuItemVo();
CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
//1、sku基本信息的获取 pms_sku_info
SkuInfoEntity info = this.getById(skuId);
skuItemVo.setInfo(info);
return info;
}, executor);
CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> {
//3、获取spu的销售属性组合
List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrBySpuId(res.getSpuId());
skuItemVo.setSaleAttr(saleAttrVos);
}, executor);
CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync((res) -> {
//4、获取spu的介绍 pms_spu_info_desc
SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId());
skuItemVo.setDesc(spuInfoDescEntity);
}, executor);
CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync((res) -> {
//5、获取spu的规格参数信息
List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
skuItemVo.setGroupAttrs(attrGroupVos);
}, executor);
// Long spuId = info.getSpuId();
// Long catalogId = info.getCatalogId();
//2、sku的图片信息 pms_sku_images
CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {
List<SkuImagesEntity> imagesEntities = skuImagesService.getImagesBySkuId(skuId);
skuItemVo.setImages(imagesEntities);
}, executor);
// CompletableFuture<Void> seckillFuture = CompletableFuture.runAsync(() -> {
// //3、远程调用查询当前sku是否参与秒杀优惠活动
// R skuSeckilInfo = seckillFeignService.getSkuSeckilInfo(skuId);
// if (skuSeckilInfo.getCode() == 0) {
// //查询成功
// SeckillSkuVo seckilInfoData = skuSeckilInfo.getData("data", new TypeReference<SeckillSkuVo>() {
// });
// skuItemVo.setSeckillSkuVo(seckilInfoData);
//
// if (seckilInfoData != null) {
// long currentTime = System.currentTimeMillis();
// if (currentTime > seckilInfoData.getEndTime()) {
// skuItemVo.setSeckillSkuVo(null);
// }
// }
// }
// }, executor);
//等到所有任务都完成
// CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,imageFuture,seckillFuture).get();
CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,imageFuture).get();
System.out.println("maruis----商品详情用时:-->" + (System.currentTimeMillis()-start)+"毫秒");
return skuItemVo;
}
39、认证中心(社交登录,OAuth2.0,单点登录)
39.1 验证码
使用的是阿里云的 短信服务
由于阿里的短信服务的签名和模板需要审核,不支持个人用户申请未上线业务,若产品未上线建议先升级企业账号,
所以我们暂不实现此功能。
验证码的总体思路,为了防止有人获取到appcode后恶意消耗我们的验证码,所以验证码应该要发送验证码应该发送给我们自己的服务
我们自己的服务中去给第三发发送验证码和进行验证。
1.接口防刷
2.验证码的再次校验
@ResponseBody
@GetMapping(value = "/sms/sendCode")
public R sendCode(@RequestParam("phone") String phone) {
//1、接口防刷
String redisCode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);
if (!StringUtils.isEmpty(redisCode)) {
//活动存入redis的时间,用当前时间减去存入redis的时间,判断用户手机号是否在60s内发送验证码
long currentTime = Long.parseLong(redisCode.split("_")[1]);
if (System.currentTimeMillis() - currentTime < 60000) {
//60s内不能再发
return R.error(BizCodeEnum.SMS_CODE_EXCEPTION.getCode(),BizCodeEnum.SMS_CODE_EXCEPTION.getMessage());
}
}
//2、验证码的再次效验 redis.存key-phone,value-code
int code = (int) ((Math.random() * 9 + 1) * 100000);
String codeNum = String.valueOf(code);
String redisStorage = codeNum + "_" + System.currentTimeMillis();
//存入redis,防止同一个手机号在60秒内再次发送验证码
stringRedisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX+phone,
redisStorage,10, TimeUnit.MINUTES);
thirdPartFeignService.sendCode(phone, codeNum);
return R.ok();
}
注册功能,注册时要验证验证码
/**
*
* TODO: 重定向携带数据:利用session原理,将数据放在session中。
* TODO:只要跳转到下一个页面取出这个数据以后,session里面的数据就会删掉
* TODO:分布下session问题
* RedirectAttributes:重定向也可以保留数据,不会丢失
* 用户注册
* @return
*/
@PostMapping(value = "/register")
// public String register(@Valid UserRegisterVo vos, BindingResult result,
// RedirectAttributes attributes) {
public String register(UserRegisterVo vos, BindingResult result,
RedirectAttributes attributes) {
//如果有错误回到注册页面
if (result.hasErrors()) {
Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
attributes.addFlashAttribute("errors",errors);
//效验出错回到注册页面
return "redirect:http://auth.gulimall.com/reg.html";
}
//1、效验验证码
String code = vos.getCode();
R register = memberFeignService.register(vos);
//成功
return "redirect:http://auth.gulimall.com/login.html";
// //获取存入Redis里的验证码
// String redisCode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vos.getPhone());
//
// if (!StringUtils.isEmpty(redisCode)) {
// //截取字符串
// if (code.equals(redisCode.split("_")[0])) {
// //删除验证码;令牌机制
// stringRedisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX+vos.getPhone());
// //验证码通过,真正注册,调用远程服务进行注册
// R register = memberFeignService.register(vos);
// if (register.getCode() == 0) {
// //成功
// return "redirect:http://auth.gulimall.com/login.html";
// } else {
// //失败
// Map<String, String> errors = new HashMap<>();
// errors.put("msg", register.getData("msg",new TypeReference<String>(){}));
// attributes.addFlashAttribute("errors",errors);
// return "redirect:http://auth.gulimall.com/reg.html";
// }
//
//
// } else {
// //效验出错回到注册页面
// Map<String, String> errors = new HashMap<>();
// errors.put("code","验证码错误");
// attributes.addFlashAttribute("errors",errors);
// return "redirect:http://auth.gulimall.com/reg.html";
// }
// } else {
// //效验出错回到注册页面
// Map<String, String> errors = new HashMap<>();
// errors.put("code","验证码错误");
// attributes.addFlashAttribute("errors",errors);
// return "redirect:http://auth.gulimall.com/reg.html";
// }
}
注册原理
第一步:发送验证码给服务器,服务器生成验证码,保存在redis中有过期时间为10分钟并发送给阿里云的短信服务
第二步:用户通过手机拿到验证码并填写
第三步:提交注册,服务器验证验证码的正确性,把用户出入系统
39.2 md5加密和盐值加密
md5加密时不可逆的,但是md5加密后会存在别人可以利用彩虹表进行暴力破解,比如把123,456等常用的md5密码列在一个表中进行暴力破解,为了防止这种现象,我们需要给md5加密后的密码再加上盐值。
测试:
@Test
public void md5yanzhi(){
String pass = "123456";
// md5 加密只要原文一样,密文一定时一样的。
// 利用这个特性,我们可以试下文件秒存,文件上前前我们可以获取这个文件的md5加密码,然后从数据库中找这个文件,如果找到了就不用上传了,变相实现了文件秒传
String s = DigestUtils.md5Hex(pass);
System.out.println("maruis------>" + s);
// 盐值一样得到的结果就一样,想要更保险,可以把盐值设置成一个随机值,并保存在数据库中
String s1 = Md5Crypt.md5Crypt(pass.getBytes(),"$1$qqqqqqqq");
System.out.println("maruis------>" + s1);
// BCryptPasswordEncoder 自动为我们实现了盐值加密,我们每次原文加密后的密文是不同的
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String p1 = bCryptPasswordEncoder.encode(pass);
String p2 = bCryptPasswordEncoder.encode(pass);
System.out.println("maruis----密码1-->" + p1);
System.out.println("maruis----密码2-->" + p2);
System.out.println("maruis----验证-->" + bCryptPasswordEncoder.matches(pass,p1));
System.out.println("maruis----验证-->" + bCryptPasswordEncoder.matches(pass,p2));
}
实例:
@Override
public void register(MemberUserRegisterVo vo) {
MemberEntity memberEntity = new MemberEntity();
//设置默认等级
MemberLevelEntity levelEntity = memberLevelDao.getDefaultLevel();
memberEntity.setLevelId(levelEntity.getId());
//设置其它的默认信息
//检查用户名和手机号是否唯一。感知异常,异常机制
checkPhoneUnique(vo.getPhone());
checkUserNameUnique(vo.getUserName());
memberEntity.setNickname(vo.getUserName());
memberEntity.setUsername(vo.getUserName());
//密码进行MD5加密
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String encode = bCryptPasswordEncoder.encode(vo.getPassword());
memberEntity.setPassword(encode);
memberEntity.setMobile(vo.getPhone());
memberEntity.setGender(0);
memberEntity.setCreateTime(new Date());
//保存数据
this.baseMapper.insert(memberEntity);
}
40、OAuth2.0 (社交登录,单点登录)
40.1 社交登录原理
40.2 session 原理
不同的域名,cookie是隔离的。
40.3 session 共享在分布式下存在的问题。
**在分布式下,session存在两个问题
- 同一个服务,复制多份,sessio不同步问题
- 不同服务,子域名session不能共享的问题。**
解决方案一:seesion复制(不退加)
方案二:客户端存储(不推荐)
方案三:hash一致性(可以)
★方案四:redis存储(推荐:解决1,2 两个问题)
官方文档:https://spring.io/projects/spring-session
第一步:添加依赖
<!-- 整合springsession -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
第二步:配置
2.1 application.properties 文件中配置
## 配置redis的连接信息
spring.redis.host=192.168.56.10
spring.redis.port=6379
## 配置springSession信息
spring.session.store-type=redis
server.servlet.session.timeout=30m
2.2 在Application 上添加
@EnableRedisHttpSession 注解
2.3 在config文件夹下添加文件配置
@Configuration
public class GulimallSessionConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
//放大作用域
cookieSerializer.setDomainName("gulimall.com");
// 设置cookie的名称
cookieSerializer.setCookieName("GULISESSION");
return cookieSerializer;
}
/**
* 用于json序列化
* @return
*/
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
第三步:实例
1.后端保存session
2.前端使用
<a th:if="${session.loginUser != null}">欢迎, [[${session.loginUser.nickname}]]</a>
第四步:在其他微服务上想要拿到session,也必须实现第一步,和第二步 两个步骤。
40.4 spring-session 原理
41、点单登录(一处登录,处处可用)
在多个系统中,实现一处登录,处处登录
41.1 开源的单点登录
官网地址:https://gitee.com/xuxueli0323/xxl-sso?_from=gitee_search
打开服务器的配置
演示:
第一步:修改host
127.0.0.1 xxlssoserver.com
127.0.0.1 client1.com
127.0.0.1 client2.com
第二步:配置xxl-sso-server 认证中心的配置
1)改xxl-sso-server 项目中的application.properties 文件
xxl.sso.redis.address=redis://92.168.56.10:6379
2)修改xxl-sso-web-sample-springboot下的配置文件
xxl.sso.redis.address=redis://92.168.56.10:6379
xxl.sso.server=http://xxlssoserver.com:8000/xxl-sso-server
第三步:利用maven命令打包整个项目
mvn clean package -Dmanven.skip.test=true
D:\workerspace_idea_2019\xxl-sso\xxl-sso-server\target>java -jar xxl-sso-server-1.1.1-SNAPSHOT.jar --server.port=8000
D:\workerspace_idea_2019\xxl-sso\xxl-sso-samples\xxl-sso-web-sample-springboot\target>java -jar --server.port=8001
第四步:启动这个三项项目测试单点登录
D:\workerspace_idea_2019\xxl-sso\xxl-sso-samples\xxl-sso-web-sample-springboot\target>java -jar xxl-sso-web-sample-springboot-1.1.1-SNAPSHOT.jar --server.port
D:\workerspace_idea_2019\xxl-sso\xxl-sso-samples\xxl-sso-web-sample-springboot\target>java -jar xxl-sso-web-sample-springboot-1.1.1-SNAPSHOT.jar --server.port=8001
D:\workerspace_idea_2019\xxl-sso\xxl-sso-samples\xxl-sso-web-sample-springboot\target>java -jar xxl-sso-web-sample-springboot-1.1.1-SNAPSHOT.jar --server.port=8002
第五步:测试
xxlssoserver.com:8000/xxl-sso-server
client1.com:8001/xxl-sso-web-sample-springboot
client2.com:8002/xxl-sso-web-sample-springboot
41.2 、单点登录的核心原理
核心:三个系统及时域名不一样,想办法给三个系统同步同一个用户的票据
1)中央认证中心服务器:xxlssoserver.com
2)其他系统想要登录,去xxlssoserver.com登录,登录成功,跳转回来
3)只要有一个登录成功,其他就不用登录
4)全系统统一一个sso.sessionid,所有系统可能域名都不相同
42、购物车
42.1 购物车功能
- 用户可以在登录状态下将商品添加到购物车
- 用户可以在未登录状态下将商品添加到购物车
- 用户可以使用购物车一起结算下单
- 用户可以查询自己的购物车
- 用户可以在购物车中修改购买商品的数量。
- 用户可以在购物车中删除商品。
- 在购物车中展示商品优惠信息
- 提示购物车商品价格变化
42.2 存储方式:redis
临时数据的存储方式有很多,可以前端存储也可以后端存储,对于不重要的数据可以前端存储,对于重要的数据要后端存储。
前端存储可以放在这些里面:
由于购物车中的数据的频繁的写入和频繁的读取的,所以购物在存储的时候应该选用存取性能比较高的,所以我们要是redis的持久对购物车数据进行存储
42.3 购物车逻辑
ThreadLocal toThreadLocal = new ThreadLocal<>(); 同一个线程共享数据
拦截器的用法:https://blog.csdn.net/fen_dou_shao_nian/article/details/118641407
注意:如果浏览器中有cookei那么它的每次请求,浏览器自动会为我们带上这个cookie的。
加入购物车的逻辑,如果购物车中没有相同的商品,就添加,如果有就需要修改数量。
@Override
public CartItemVo addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
//拿到要操作的购物车信息
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
//判断Redis是否有该商品的信息
String productRedisValue = (String) cartOps.get(skuId.toString());
//如果没有就添加数据
if (StringUtils.isEmpty(productRedisValue)) {
//2、添加新的商品到购物车(redis)
CartItemVo cartItemVo = new CartItemVo();
//开启第一个异步任务
CompletableFuture<Void> getSkuInfoFuture = CompletableFuture.runAsync(() -> {
//1、远程查询当前要添加商品的信息
R productSkuInfo = productFeignService.getInfo(skuId);
SkuInfoVo skuInfo = productSkuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {});
//数据赋值操作
cartItemVo.setSkuId(skuInfo.getSkuId());
cartItemVo.setTitle(skuInfo.getSkuTitle());
cartItemVo.setImage(skuInfo.getSkuDefaultImg());
cartItemVo.setPrice(skuInfo.getPrice());
cartItemVo.setCount(num);
}, executor);
//开启第二个异步任务
CompletableFuture<Void> getSkuAttrValuesFuture = CompletableFuture.runAsync(() -> {
//2、远程查询skuAttrValues组合信息
List<String> skuSaleAttrValues = productFeignService.getSkuSaleAttrValues(skuId);
cartItemVo.setSkuAttrValues(skuSaleAttrValues);
}, executor);
//等待所有的异步任务全部完成
CompletableFuture.allOf(getSkuInfoFuture, getSkuAttrValuesFuture).get();
String cartItemJson = JSON.toJSONString(cartItemVo);
cartOps.put(skuId.toString(), cartItemJson);
return cartItemVo;
} else {
//购物车有此商品,修改数量即可
CartItemVo cartItemVo = JSON.parseObject(productRedisValue, CartItemVo.class);
cartItemVo.setCount(cartItemVo.getCount() + num);
//修改redis的数据
String cartItemJson = JSON.toJSONString(cartItemVo);
cartOps.put(skuId.toString(),cartItemJson);
return cartItemVo;
}
}
批量操作redis的方法
/**
* 获取到我们要操作的购物车
* @return
*/
private BoundHashOperations<String, Object, Object> getCartOps() {
//先得到当前用户信息
UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get();
String cartKey = "";
if (userInfoTo.getUserId() != null) {
//gulimall:cart:1
cartKey = CART_PREFIX + userInfoTo.getUserId();
} else {
cartKey = CART_PREFIX + userInfoTo.getUserKey();
}
//绑定指定的key操作Redis
BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
return operations;
}
订单流程图:
43 、订单服务
1)、需要登录 spring-session,redis
2)、线程池的配置
3)、锁库存,解锁库存
4)、幂等性
5)、解决feign调用时丢失请求头的问题。
6)、为了防止重复提交,添加token令牌,令牌获取,判断,删除时保证原子性。
7)、延时队列,保证最终一致性。
订单流程图
订单确认流程图
消息队列流程图
43.1 订单确认页逻辑
要点:
1)异步编排
2)Feign请求头丢失丢失,导致远程调用时处于未登录状态
3)方式订单重复提交的验证。
第一步:获取订单信息
/**
* 订单确认页返回需要用的数据
* @return
*/
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
//构建OrderConfirmVo
OrderConfirmVo confirmVo = new OrderConfirmVo();
//获取当前用户登录的信息
MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
//TODO :获取当前线程请求头信息(解决Feign异步调用丢失请求头问题)
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//开启第一个异步任务
CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> {
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
//1、远程查询所有的收获地址列表
List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVo.getId());
confirmVo.setMemberAddressVos(address);
}, threadPoolExecutor);
//开启第二个异步任务
CompletableFuture<Void> cartInfoFuture = CompletableFuture.runAsync(() -> {
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
//2、远程查询购物车所有选中的购物项
List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();
confirmVo.setItems(currentCartItems);
//feign在远程调用之前要构造请求,调用很多的拦截器
}, threadPoolExecutor).thenRunAsync(() -> {
List<OrderItemVo> items = confirmVo.getItems();
//获取全部商品的id
List<Long> skuIds = items.stream()
.map((itemVo -> itemVo.getSkuId()))
.collect(Collectors.toList());
//远程查询商品库存信息
R skuHasStock = wmsFeignService.getSkuHasStock(skuIds);
List<SkuStockVo> skuStockVos = skuHasStock.getData("data", new TypeReference<List<SkuStockVo>>() {});
if (skuStockVos != null && skuStockVos.size() > 0) {
//将skuStockVos集合转换为map
Map<Long, Boolean> skuHasStockMap = skuStockVos.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
confirmVo.setStocks(skuHasStockMap);
}
},threadPoolExecutor);
//3、查询用户积分
Integer integration = memberResponseVo.getIntegration();
confirmVo.setIntegration(integration);
//4、价格数据自动计算
//TODO 5、防重令牌(防止表单重复提交)
//为用户设置一个token,三十分钟过期时间(存在redis)
String token = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set(USER_ORDER_TOKEN_PREFIX+memberResponseVo.getId(),token,30, TimeUnit.MINUTES);
confirmVo.setOrderToken(token);
CompletableFuture.allOf(addressFuture,cartInfoFuture).get();
return confirmVo;
}
第二步:解决Feign调用丢失请求头的原因和解决办法
解决办法:在当前微服务添加一个拦截器,当调用feign的时候就会先经过这个拦截器,在拦截器里面同步请求头
注意:如果调用feign的方法实在异步请求中,还要特别注意一下的写法才正确。
@Configuration
public class GuliFeignConfig {
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor() {
RequestInterceptor requestInterceptor = new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
//1、使用RequestContextHolder拿到刚进来的请求数据
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
//老请求
HttpServletRequest request = requestAttributes.getRequest();
if (request != null) {
//2、同步请求头的数据(主要是cookie)
//把老请求的cookie值放到新请求上来,进行一个同步
String cookie = request.getHeader("Cookie");
template.header("Cookie", cookie);
}
}
}
};
return requestInterceptor;
}
}
第三步:订单提交时解决重复提交问题,也叫做订单的幂等性
幂等性的相关概念和使用场景:https://blog.csdn.net/fen_dou_shao_nian/article/details/118750556
订单确认流程图:
防重令牌(防止表单重复提交)
43.2 下单流程
提交订单时为了防止重复提交,要保证令牌获取,判断和删除的原子性
//1、验证令牌是否合法【令牌的对比和删除必须保证原子性】
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
String orderToken = vo.getOrderToken();
//通过lure脚本原子验证令牌和删除令牌
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
Arrays.asList(USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()),
orderToken);
第一步:构建订单项
//1、商品的spu信息
//2、商品的sku信息
//3、商品的优惠信息
//4、商品的积分信息
//5、订单项的价格信息
//当前订单项的实际金额.总额 - 各种优惠价格
//原来的价格
//原价减去优惠价得到最终的价格
锁库存流程
43.3 支付功能
我们用支付宝来做演示,使用支付宝的沙箱功能进行演示
通过雷王穿透工具:续断:www.zhexi.tech
调用支付功能时一定要保证我们的项目是utf-8 的编码,否则支持可能不成功
内网穿透的几个常用软件
1、natapp:https://natapp.cn/ 优惠码:022B93FD(9 折)[仅限第一次使用]
2、续断:www.zhexi.tech 优惠码:SBQMEA(95 折)[仅限第一次使用]
3、花生壳:https://www.oray.com/
续断
nginx 配置
listen 80;
server_name gulimall.com *.gulimall.com 497n86m7k7.52http.net;
#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;
}
异步回调:
异步回调的方法
@PostMapping(value = "/payed/notify")
public String handleAlipayed(PayAsyncVo asyncVo, HttpServletRequest request) throws AlipayApiException, UnsupportedEncodingException {
// 只要收到支付宝的异步通知,返回 success 支付宝便不再通知
// 获取支付宝POST过来反馈信息
//TODO 需要验签
Map<String, String> params = new HashMap<>();
Map<String, String[]> requestParams = request.getParameterMap();
for (String name : requestParams.keySet()) {
String[] values = requestParams.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i]
: valueStr + values[i] + ",";
}
//乱码解决,这段代码在出现乱码时使用
// valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
params.put(name, valueStr);
}
boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayTemplate.getAlipay_public_key(),
alipayTemplate.getCharset(), alipayTemplate.getSign_type()); //调用SDK验证签名
if (signVerified) {
System.out.println("签名验证成功...");
//去修改订单状态
String result = orderService.handlePayResult(asyncVo);
return result;
} else {
System.out.println("签名验证失败...");
return "error";
}
}
43.4 收单
1、订单在支付页,不支付,一直刷新,订单过期了才支付,订单状态改为已支付了,但是库 存解锁了。
使用支付宝自动收单功能解决。只要一段时间不支付,就不能支付了。
2、由于时延等问题。订单解锁完成,正在解锁库存的时候,异步通知才到
订单解锁,手动调用收单
3、网络阻塞问题,订单支付成功的异步通知一直不到达
查询订单列表时,ajax获取当前未支付的订单状态,查询订单状态时,再获取一下支付宝 此订单的状态
4、其他各种问题
每天晚上闲时下载支付宝对账单,一一进行对账
44 秒杀
44.1 在高并发系统里,要注意的问题。
1)单一职责,
秒杀服务即使自己扛不住压力, 挂掉。 不要影响别人
2)秒杀连接加密
防止恶意攻击, 模拟秒杀请求, 1000 次/ s攻击。
防止链接暴露, 自己工作人员, 提前秒杀商品
3)库存预热,快速扣减
秒杀读多写少。无需每次实时校验库存。我 们库存预热, 放到redis(或集群中)中。信号量控制进 来秒杀的请求
4)动静分离
nginx做好动静分离。保证秒杀和商品详情 页的动态请求才打到后端的服务集群。
使用CDN网络, 分担本集群压力(例如使用阿里云存储静态资源)
5)恶意请求的拦截
识别非法攻击请求并进行拦截, 网关层去拦截
6)流量错峰
使用各种手段, 将流量分担到更大宽度的时 间点。比如验证码, 加入购物车,由于有了这些操作,每个人的操作时间就会不一样,这样就可以实现流量的错峰。
7)限流&熔断&降级
前端限流(比如:只允许1s点击一次按钮)+ 后端限流(区分那些是用户的正常行为,哪些是恶意操作进行过滤)
限制次数, 限制总量, 快速失败降级运行, 熔断隔离防止雪崩
8)队列削峰-杀手锏
1 万个商品, 每个1000 件秒杀。双11
所有秒杀成功的请求, 进入队列, 慢慢创建 订单, 扣减库存即可。
44.2 上架流程
@Slf4j
@Service
public class SeckillServiceImpl implements SeckillService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private CouponFeignService couponFeignService;
@Autowired
private ProductFeignService productFeignService;
@Autowired
private RedissonClient redissonClient;
@Autowired
private RabbitTemplate rabbitTemplate;
private final String SESSION__CACHE_PREFIX = "seckill:sessions:";
private final String SECKILL_CHARE_PREFIX = "seckill:skus";
private final String SKU_STOCK_SEMAPHORE = "seckill:stock:"; //+商品随机码
/**
* 上架
* 上架是扫描出最近3天需要上架的商品
* 把场次信息 和 商品信息分别放在redis的两个key中
* 把库存放在redis的信号量中,这个信号量类似与之前juc错线程中的人走了关门那个线程工具类
*/
@Override
public void uploadSeckillSkuLatest3Days() {
//1、扫描最近三天的商品需要参加秒杀的活动
R lates3DaySession = couponFeignService.getLates3DaySession();
if (lates3DaySession.getCode() == 0) {
//上架商品
List<SeckillSessionWithSkusVo> sessionData = lates3DaySession.getData("data", new TypeReference<List<SeckillSessionWithSkusVo>>() {
});
//缓存到Redis
//1、缓存活动信息
saveSessionInfos(sessionData);
//2、缓存活动的关联商品信息
saveSessionSkuInfo(sessionData);
}
}
/**
* 缓存秒杀活动信息
* @param sessions
*/
private void saveSessionInfos(List<SeckillSessionWithSkusVo> sessions) {
sessions.stream().forEach(session -> {
//获取当前活动的开始和结束时间的时间戳
long startTime = session.getStartTime().getTime();
long endTime = session.getEndTime().getTime();
//存入到Redis中的key
String key = SESSION__CACHE_PREFIX + startTime + "_" + endTime;
//判断Redis中是否有该信息,如果没有才进行添加
Boolean hasKey = redisTemplate.hasKey(key);
//缓存活动信息
if (!hasKey) {
//获取到活动中所有商品的skuId
List<String> skuIds = session.getRelationSkus().stream()
.map(item -> item.getPromotionSessionId() + "-" + item.getSkuId().toString()).collect(Collectors.toList());
redisTemplate.opsForList().leftPushAll(key,skuIds);
}
});
}
/**
* 缓存秒杀活动所关联的商品信息
* @param sessions
*/
private void saveSessionSkuInfo(List<SeckillSessionWithSkusVo> sessions) {
sessions.stream().forEach(session -> {
//准备hash操作,绑定hash
BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
session.getRelationSkus().stream().forEach(seckillSkuVo -> {
//生成随机码
String token = UUID.randomUUID().toString().replace("-", "");
String redisKey = seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString();
if (!operations.hasKey(redisKey)) {
//缓存我们商品信息
SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
Long skuId = seckillSkuVo.getSkuId();
//1、先查询sku的基本信息,调用远程服务
R info = productFeignService.getSkuInfo(skuId);
if (info.getCode() == 0) {
SkuInfoVo skuInfo = info.getData("skuInfo",new TypeReference<SkuInfoVo>(){});
redisTo.setSkuInfo(skuInfo);
}
//2、sku的秒杀信息
BeanUtils.copyProperties(seckillSkuVo,redisTo);
//3、设置当前商品的秒杀时间信息
redisTo.setStartTime(session.getStartTime().getTime());
redisTo.setEndTime(session.getEndTime().getTime());
//4、设置商品的随机码(防止恶意攻击)
redisTo.setRandomCode(token);
//序列化json格式存入Redis中
String seckillValue = JSON.toJSONString(redisTo);
operations.put(seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString(),seckillValue);
//如果当前这个场次的商品库存信息已经上架就不需要上架
//5、使用库存作为分布式Redisson信号量(限流)
// 使用库存作为分布式信号量
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
// 商品可以秒杀的数量作为信号量
semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
}
});
});
}
/**
* 获取到当前可以参加秒杀商品的信息
* @return
*/
@SentinelResource(value = "getCurrentSeckillSkusResource",blockHandler = "blockHandler")
@Override
public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
try (Entry entry = SphU.entry("seckillSkus")) {
//1、确定当前属于哪个秒杀场次
long currentTime = System.currentTimeMillis();
//从Redis中查询到所有key以seckill:sessions开头的所有数据
Set<String> keys = redisTemplate.keys(SESSION__CACHE_PREFIX + "*");
for (String key : keys) {
//seckill:sessions:1594396764000_1594453242000
String replace = key.replace(SESSION__CACHE_PREFIX, "");
String[] s = replace.split("_");
//获取存入Redis商品的开始时间
long startTime = Long.parseLong(s[0]);
//获取存入Redis商品的结束时间
long endTime = Long.parseLong(s[1]);
//判断是否是当前秒杀场次
if (currentTime >= startTime && currentTime <= endTime) {
//2、获取这个秒杀场次需要的所有商品信息
List<String> range = redisTemplate.opsForList().range(key, -100, 100);
BoundHashOperations<String, String, String> hasOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
assert range != null;
List<String> listValue = hasOps.multiGet(range);
if (listValue != null && listValue.size() >= 0) {
List<SeckillSkuRedisTo> collect = listValue.stream().map(item -> {
String items = (String) item;
SeckillSkuRedisTo redisTo = JSON.parseObject(items, SeckillSkuRedisTo.class);
// redisTo.setRandomCode(null);当前秒杀开始需要随机码
return redisTo;
}).collect(Collectors.toList());
return collect;
}
break;
}
}
} catch (BlockException e) {
log.error("资源被限流{}",e.getMessage());
}
return null;
}
public List<SeckillSkuRedisTo> blockHandler(BlockException e) {
log.error("getCurrentSeckillSkusResource被限流了,{}",e.getMessage());
return null;
}
/**
* 根据skuId查询商品是否参加秒杀活动
* @param skuId
* @return
*/
@Override
public SeckillSkuRedisTo getSkuSeckilInfo(Long skuId) {
//1、找到所有需要秒杀的商品的key信息---seckill:skus
BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
//拿到所有的key
Set<String> keys = hashOps.keys();
if (keys != null && keys.size() > 0) {
//4-45 正则表达式进行匹配
String reg = "\\d-" + skuId;
for (String key : keys) {
//如果匹配上了
if (Pattern.matches(reg,key)) {
//从Redis中取出数据来
String redisValue = hashOps.get(key);
//进行序列化
SeckillSkuRedisTo redisTo = JSON.parseObject(redisValue, SeckillSkuRedisTo.class);
//随机码
Long currentTime = System.currentTimeMillis();
Long startTime = redisTo.getStartTime();
Long endTime = redisTo.getEndTime();
//如果当前时间大于等于秒杀活动开始时间并且要小于活动结束时间
if (currentTime >= startTime && currentTime <= endTime) {
return redisTo;
}
redisTo.setRandomCode(null);
return redisTo;
}
}
}
return null;
}
/**
* 当前商品进行秒杀(秒杀开始)
* @param killId
* @param key
* @param num
* @return
*/
@Override
public String kill(String killId, String key, Integer num) throws InterruptedException {
long s1 = System.currentTimeMillis();
//获取当前用户的信息
MemberResponseVo user = LoginUserInterceptor.loginUser.get();
//1、获取当前秒杀商品的详细信息从Redis中获取
BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
String skuInfoValue = hashOps.get(killId);
if (StringUtils.isEmpty(skuInfoValue)) {
return null;
}
//(合法性效验)
SeckillSkuRedisTo redisTo = JSON.parseObject(skuInfoValue, SeckillSkuRedisTo.class);
Long startTime = redisTo.getStartTime();
Long endTime = redisTo.getEndTime();
long currentTime = System.currentTimeMillis();
//判断当前这个秒杀请求是否在活动时间区间内(效验时间的合法性)
if (currentTime >= startTime && currentTime <= endTime) {
//2、效验随机码和商品id
String randomCode = redisTo.getRandomCode();
String skuId = redisTo.getPromotionSessionId() + "-" +redisTo.getSkuId();
if (randomCode.equals(key) && killId.equals(skuId)) {
//3、验证购物数量是否合理和库存量是否充足
Integer seckillLimit = redisTo.getSeckillLimit();
//获取信号量
String seckillCount = redisTemplate.opsForValue().get(SKU_STOCK_SEMAPHORE + randomCode);
Integer count = Integer.valueOf(seckillCount);
//判断信号量是否大于0,并且买的数量不能超过库存
if (count > 0 && num <= seckillLimit && count > num ) {
//4、验证这个人是否已经买过了(幂等性处理),如果秒杀成功,就去占位。userId-sessionId-skuId
//SETNX 原子性处理
String redisKey = user.getId() + "-" + skuId;
//设置自动过期(活动结束时间-当前时间)
Long ttl = endTime - currentTime;
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
if (aBoolean) {
//占位成功说明从来没有买过,分布式锁(获取信号量-1)
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
//TODO 秒杀成功,快速下单
boolean semaphoreCount = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
//保证Redis中还有商品库存
if (semaphoreCount) {
//创建订单号和订单信息发送给MQ
// 秒杀成功 快速下单 发送消息到 MQ 整个操作时间在 10ms 左右
String timeId = IdWorker.getTimeId();
SeckillOrderTo orderTo = new SeckillOrderTo();
orderTo.setOrderSn(timeId);
orderTo.setMemberId(user.getId());
orderTo.setNum(num);
orderTo.setPromotionSessionId(redisTo.getPromotionSessionId());
orderTo.setSkuId(redisTo.getSkuId());
orderTo.setSeckillPrice(redisTo.getSeckillPrice());
rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",orderTo);
long s2 = System.currentTimeMillis();
log.info("耗时..." + (s2 - s1));
return timeId;
}
}
}
}
}
long s3 = System.currentTimeMillis();
log.info("耗时..." + (s3 - s1));
return null;
}
}
44.3 秒杀流程
方式一:
优点:逻辑与之前的业务统一
缺点:调用了其他微服务,对其他微服务造成压力。
方式二:我们使用方式二
优点:速度快,隔离性好,高并发
缺点:做一套独立的业务。
随机码:用于防止恶意攻击,只有到了秒杀时间才会给客户返回这个随机码
幂等性:通过redis的占坑去判断这个人是否已经秒杀过了。
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
if (aBoolean) {
秒杀代码:
/**
* 当前商品进行秒杀(秒杀开始)
* @param killId
* @param key
* @param num
* @return
*/
@Override
public String kill(String killId, String key, Integer num) throws InterruptedException {
long s1 = System.currentTimeMillis();
//获取当前用户的信息
MemberResponseVo user = LoginUserInterceptor.loginUser.get();
//1、获取当前秒杀商品的详细信息从Redis中获取
BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
String skuInfoValue = hashOps.get(killId);
if (StringUtils.isEmpty(skuInfoValue)) {
return null;
}
//(合法性效验)
SeckillSkuRedisTo redisTo = JSON.parseObject(skuInfoValue, SeckillSkuRedisTo.class);
Long startTime = redisTo.getStartTime();
Long endTime = redisTo.getEndTime();
long currentTime = System.currentTimeMillis();
//判断当前这个秒杀请求是否在活动时间区间内(效验时间的合法性)
if (currentTime >= startTime && currentTime <= endTime) {
//2、效验随机码和商品id
String randomCode = redisTo.getRandomCode();
String skuId = redisTo.getPromotionSessionId() + "-" +redisTo.getSkuId();
if (randomCode.equals(key) && killId.equals(skuId)) {
//3、验证购物数量是否合理和库存量是否充足
Integer seckillLimit = redisTo.getSeckillLimit();
//获取信号量
String seckillCount = redisTemplate.opsForValue().get(SKU_STOCK_SEMAPHORE + randomCode);
Integer count = Integer.valueOf(seckillCount);
//判断信号量是否大于0,并且买的数量不能超过库存
if (count > 0 && num <= seckillLimit && count > num ) {
//4、验证这个人是否已经买过了(幂等性处理),如果秒杀成功,就去占位。userId-sessionId-skuId
//SETNX 原子性处理
String redisKey = user.getId() + "-" + skuId;
//设置自动过期(活动结束时间-当前时间)
Long ttl = endTime - currentTime;
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
if (aBoolean) {
//占位成功说明从来没有买过,分布式锁(获取信号量-1)
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
//TODO 秒杀成功,快速下单
boolean semaphoreCount = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
//保证Redis中还有商品库存
if (semaphoreCount) {
//创建订单号和订单信息发送给MQ
// 秒杀成功 快速下单 发送消息到 MQ 整个操作时间在 10ms 左右
String timeId = IdWorker.getTimeId();
SeckillOrderTo orderTo = new SeckillOrderTo();
orderTo.setOrderSn(timeId);
orderTo.setMemberId(user.getId());
orderTo.setNum(num);
orderTo.setPromotionSessionId(redisTo.getPromotionSessionId());
orderTo.setSkuId(redisTo.getSkuId());
orderTo.setSeckillPrice(redisTo.getSeckillPrice());
rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",orderTo);
long s2 = System.currentTimeMillis();
log.info("耗时..." + (s2 - s1));
return timeId;
}
}
}
}
}
long s3 = System.currentTimeMillis();
log.info("耗时..." + (s3 - s1));
return null;
}