电商项目总结
电商项目(谷粒商城)
一、项目目标:
谷粒商城是一个B2C的项目电商项目,是销售自营商品给客户。
二、项目架构
1、技术架构
谷粒商城采取了前后端分离开发 是一个微服务项目
前端用vue 开发
后端用springboot + springcloud + springalibaba 开发
对于业务模块我们用了 springboot 开发
-
由于各个模块之间存在 微服务之间的调用 所以我们就引入openFeign 来进行微服务之间的通信
-
要用feign 就需要服务注册与服务发现 所以我们用 nacos来做微服务的注册与发现
-
我们服务上线了需要改配置的时候 非常麻烦 所以我们可以用nacos 作为配置中心 来统一配置
-
我们使用getWay 网关来进行动态路由与反向代理 请求过滤
-
在服务之间调用超时 和服务不可用的时候,或者是很大的用户访问 防止服务雪崩 我们就用 sentinel 来做服务熔断 与服务降级
-
用sleuth 和 zipkin 来进行服务追踪 看那处服务出错 和缓慢 从而对她进行修改
-
用oauth2 和springsecurity 作为 认证与授权 服务
-
用mybatisPlus 来简化我们的开发
对于数据存储
-
使用的是mysql来进行数据的持久化 为了让服务相应跟快
-
我们用redis做缓存
-
因为要对整个上架的商品做全文检索 和 数据分析 我们就是用elasticsearch 做全文检索与数据分析
-
因为是微服务架构 所以要有分布式事务 我们采取的是 柔性事务 + 最大通知 + 最终一致性 来实现分布式事务
用了rabbitMQ 的延迟队列和死信队列 来进行分布式事务。
-
我们还引入了阿里云的oss作为 图片和对象的存储
2、服务架构
前端 是由后台管理页面和 用户页面两个 服务组成
后台我们把它 分成 后台管理 和 商品服务 库存服务 用户服务 订单服务 购物车服务 检索服务 支付服务 中央认证服务 秒杀服务
前端页面发送 http请求 先进过网关 然后由网关进行过滤 路由到指定服务
1.后端管理系统的开发:
1.我们需要 统一返回结果封装。分为成功与失败两种
2.我们还编写了统一 返回异常信息 分为状态码和异常原因 状态码为五位 前两位表示模块信息 后三位表示异常类型(通过枚举来实现)
/***
* 错误码和错误信息定义类
* 1. 错误码定义规则为5为数字
* 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常
* 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
* 错误码列表:
* 10: 通用
* 001:参数格式校验
* 11: 商品
* 12: 订单
* 13: 购物车
* 14: 物流
*/
public enum BizCodeEnume {
UNKNOW_EXCEPTION(10000,"系统未知异常"),
VAILD_EXCEPTION(10001,"参数格式校验失败"),
PRODUCT_EXCEPTION(11000,"商品上架异常");
private int code;
private String msg;
BizCodeEnume(int code,String msg){
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
-
全局异常统一处理
使用RestControllerAdvice 来对controller 进行统一操作
加上handlerException注解 进行统一异常处理
@Slf4j @RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller") public class GulimallExceptionControllerAdvice { @ExceptionHandler(value= MethodArgumentNotValidException.class) public R handleVaildException(MethodArgumentNotValidException e){ log.error("数据校验出现问题{},异常类型:{}",e.getMessage(),e.getClass()); BindingResult bindingResult = e.getBindingResult(); Map<String,String> errorMap = new HashMap<>(); bindingResult.getFieldErrors().forEach((fieldError)->{ errorMap.put(fieldError.getField(),fieldError.getDefaultMessage()); }); return R.error(BizCodeEnume.VAILD_EXCEPTION.getCode(),BizCodeEnume.VAILD_EXCEPTION.getMsg()).put("data",errorMap); } @ExceptionHandler(value = Throwable.class) public R handleException(Throwable throwable){ log.error("错误:",throwable); return R.error(BizCodeEnume.UNKNOW_EXCEPTION.getCode(),BizCodeEnume.UNKNOW_EXCEPTION.getMsg()); } }
数据库结构 商品库 库存库
库存表 需要显示商品有多少件
一件商品会有sku和spu两个属性,spu是描述一类商品,sku是描述一种商品。华为手机 就是spu 华为手机宝石蓝 8+64G时尚手机就是他的sku。
所以数据库就从spu和sku这两个方面去设计 就会涵盖商品的所有信息 spuInfo skuInfo spuAttr skuAttr images 商品的分类和品牌
先是编写了三级商品的分类 sku 和 spu 的属性 并把sku spu 和三级分类起来 商品的上架功能 会员的管理
商品的上架功能 分为 基本信息的填写 规格参数 销售属性(spu)的填写 与 sku的录入 最后保存完成 保存到商品库中和库存库中
用户模块 需要编写 会员等级 成长经验 是否免邮 是否禁用。
他们所有的请求都是vue发出要通过网关 到达 后台服务
2.后端页面
3.性能优化
商品的详情页
先展示三级分类 根据三级分类来查询商品的展示 这里需要销售属性之间的切换 根据spu的id 查询所有的sku 然后展示
那袋sku的id与spu 就可以拿到商品的详细信息。
商品详情页 是给用户一进去就展示的,所以会有访问量大,高并发的问题。所以我们要对系统进行优化,
- 优化 应该从sql 出发 分为两种
一个是优化sql , 尽可能的使用索引, 减少查询的行与列 减少大表的关联 分库分表 来减少单点的压力
二是减少查询数据库的次数,我们可以引入缓存,使用redis做缓存。展示数据的时候,我们先去缓存查询数据,若缓存中没有,我们在去数据库中查询数据,查询的数据加入缓存。若缓存中有数据,则直接返回。
4.全文检索
在商品的检索服务,要对 商品属性 价格 功能 屏幕尺寸 长度 cpu 内存 摄像头 等等好多属性进行检索查询,这个时候mysql已经不能适应这个功能了,我们就引入了ElasticSearch来进行全文检索与数据分析。
我们存入elasticSearch的时候存的是spu的信息,因为存sku的话就太大了,spu是一类商品的总和。
根据spuid查询spu商品的基本信息和产品规格 库存信息,把它们存入elasticSearch。
5.缓存
-
缓存应该存 及时性 数据一致性要求不大
-
读到写少的数据
data = cache.load(info); //从缓存中加载数据
if(data == null){
data = db.load(info); //从数据库中加载数据
cache.put(id,data,ttl); //放到缓存中,并设置过期时间
}
return data;
给缓存中的数据一定要设置过期时间,避免数据没有主动更新,也能触发自动更新 ,避免数据崩溃永远不一致。
在缓存中存储json数据(推荐),因为json跨平台 好阅读
-
缓存具有三个问题
-
缓存击穿
-
缓存雪崩
-
缓存穿透
-
缓存击穿是 大量请求查询一个数据(热点数据),若这个数据的key过期,则查询数据库
-
缓存雪崩是 一批数据正好在同一时间失效, 大量请求来查询这个数据,全部进入数据库 导致系统不可用
-
缓存穿透 大量请求查询一个并不存在的数据,需要一直去数据库中做io查询,增大系统的性能消耗。
缓存击穿 加分布式锁
缓存雪崩 随机设置过期时间
缓存穿透 空结果进行缓存,设置很短的过期时间
-
-
缓存不一致问题
- 双写模式 修改数据库的时候,修改缓存
- 失效模式 修改数据库的时候删除缓存
- 给缓存加上过期时间
- 加读写锁
6.分布式锁
使用redisson 来进行分布式锁。
在 很多请求都来查询 一个商品的时候,由于这时候缓存 中没有,所以都会到数据库中查询,这时候数据库压力就很大,消耗资源很多。
我们这时候可以给他加锁,所有人进来之后都获取这个锁,谁获取到了谁就去差数据库,没有获取的就继续等待。这样就解决了缓存穿透的问题。
- 有可重入锁(Reentrant Lock)
没有锁的时候,每个人都去抢占锁,谁抢到就是谁的
redisson内部实现了看门狗机制,若redisson实例没有关闭的时候,这时候就自动延长锁的时间30s
// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
...
} finally {
lock.unlock();
}
}
-
公平锁(fair lock)
当有多个线程来获取锁的时候,那么会优先给最先发出请求的线程。若线程宕机,redisson会等5s在试其他的线程。
先到先得
RLock fairLock = redisson.getFairLock("anyLock"); // 最常见的使用方法 fairLock.lock();
-
加超时时间的公平锁
// 10秒钟以后自动解锁 // 无需调用unlock方法手动解锁 fairLock.lock(10, TimeUnit.SECONDS); // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁 boolean res = fairLock.tryLock(100, 10, TimeUnit.SECONDS); ... fairLock.unlock();
-
异步的公平锁
RLock fairLock = redisson.getFairLock("anyLock"); fairLock.lockAsync(); fairLock.lockAsync(10, TimeUnit.SECONDS); Future<Boolean> res = fairLock.tryLockAsync(100, 10, TimeUnit.SECONDS);
-
联锁(MultiLock)
所有锁都上锁成功才算上锁成功
RLock lock1 = redissonInstance1.getLock("lock1"); RLock lock2 = redissonInstance2.getLock("lock2"); RLock lock3 = redissonInstance3.getLock("lock3"); RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3); // 同时加锁:lock1 lock2 lock3 // 所有的锁都上锁成功才算成功。 lock.lock(); ... lock.unlock();
超时时间
RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
// 给lock1,lock2,lock3加锁,如果没有手动解开的话,10秒钟后将会自动解开
lock.lock(10, TimeUnit.SECONDS);
// 为加锁等待100秒时间,并在加锁成功10秒钟后自动解开
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();
-
红锁(red lock)
多个服务在同一时刻 一个用户只能有一个请求
RLock lock1 = redissonInstance1.getLock("lock1"); RLock lock2 = redissonInstance2.getLock("lock2"); RLock lock3 = redissonInstance3.getLock("lock3"); RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3); // 同时加锁:lock1 lock2 lock3 // 红锁在大部分节点上加锁成功就算成功。 lock.lock(); ... lock.unlock();
-
读写锁
读写锁允许有多个读锁,一个写锁
读锁是共享锁,写锁是排它锁
RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock"); // 最常见的使用方法 rwlock.readLock().lock(); // 或 rwlock.writeLock().lock();
// 10秒钟以后自动解锁 // 无需调用unlock方法手动解锁 rwlock.readLock().lock(10, TimeUnit.SECONDS); // 或 rwlock.writeLock().lock(10, TimeUnit.SECONDS); // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁 boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS); // 或 boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS); ... lock.unlock();
-
信号量(Semaphore)
信号量可以设置一个初始值,所有线程都来获取信号量,一个线程消耗一个信号量,若信号量为负数,则无法获取信号量,线程执行完要释放信号量
RSemaphore semaphore = redisson.getSemaphore("semaphore"); semaphore.acquire(); //或 semaphore.acquireAsync(); semaphore.acquire(23); semaphore.tryAcquire(); //或 semaphore.tryAcquireAsync(); semaphore.tryAcquire(23, TimeUnit.SECONDS); //或 semaphore.tryAcquireAsync(23, TimeUnit.SECONDS); semaphore.release(10); semaphore.release(); //或 semaphore.releaseAsync();
7.异步
我们要获取 商品的详细信息 spu的介绍 spu的规格参数 图片 是否参加优惠 这些都查询出来封装一个对象然后返回
spu的介绍 spu的规格参数 是否参加优惠 是依赖于 商品的基本信息 来查询的,而图片是直接可以查询的 ,spu规格参数,是否参加优惠 spu介绍 又是相互不影响的所以,我们可以吧他们进行异步编排,使用户体验更好。
/**
* @Description: 线程池配置类
**/
@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()
);
}
}
@Override
public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {
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();
return skuItemVo;
}
thenApply 接收上一个线程的返回值 自己有返回值
thenAccept 接收上一个线程的返回值 自己没有返回值
ThenRun 不接受上一个返回值 直接运行自己
8.认证服务
我们做了统一认证登录功能。使用springsecurity来控制认证和登录。
9.购物车服务
我们把商品存到redis中,数据结构选用map<String,Map<String,String>>
每个用户都应该有自己独立的购物车,以用户id来做key商品信息作为value。每个用户对购物车中的内容可以crud,所以value也要是一个map,以商品id作为key,商品参数作为值
10.消息队列rabbitMQ
rabbitMQ消息的确认机制 有三种 confirmCallback, returnCallBack ,ack
confirmCallBack 是消息发送到达交换机 触发的回调
returnCallBack 是消息达到 队列触发的 回调
ack 是消息从队列到消费者触发的回调
ack ack() 确认消息 消息会被队列删除
nack() 消息处理错误 消息会被其他人处理 可以批量
reject() 消息处理错误 消息会被其他人处理
在顾客下订单后,订单状态有 未付款,已付款,已发货,已收货,已取消五中状态
若顾客在下单后3个小时还没有付款我们就人为它不会再下单,我们就应该取消订单并减库存,我们在下单的时候就可以发送一个消息进入延迟队列,若消息到期后还没有人处理,那就代表着顾客已经取消订单。这时我们就可以监听死信队列中的消息。若死信队列职工有消息我们就根据死信队列 中的消息解锁库存。
11.接口幂等性
接口执行一次和执行多次效果是一样的(比如支付的执行,我们不能不限制的扣款)
- token机制解决接口幂等性
我们在执行业务的时候,可以生成token,把token存在redis中,并放在请求头当中,在请求接口的时候,获取token并查询redis,若redis中有token,则是第一次请求,我们删除redis中的token并执行业务。若没有token则是多次请求接口,我们直接返回重复标记结client
token = server.createToken
redis.put(payId,token)
request.header.add(payId,token)
token = request.header.get(payId)
if(token.equals(redis.get(payId))){
try{
执行业务;
redis.del(payId); //token的获取比较删除都应该是原子性,可以用lua脚本实现
return data;
}
catch(error){
redis.put(payId,token); //先删除token,若业务执行失败,设置token在此请求
重新请求
}
}
return repeatMark
"if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" //token的获取比较删除都应该是原子性,可以用lua脚本实现
数据库的乐观锁
分布式锁
redis的set防重
MD5 = 根据数据生成MD5
redis.sadd(MD5)
MD5 = request.header.get(MD5)
if(redis.sadd(MD5)){
redis.srem(MD5)
第一次请求
执行业务
}
全局唯一请求ID 存到redis当中去(set)
12.事务
四大特性 (acid) 原子性 一致性 隔离性 持久性
四大隔离级别 读未提交 (脏读) 读已提交 (不可重复读) 可重复的(幻读) 序列化
事务的传播机制
propagation required 如果没有事务 就创建事务 若有事务就加入事务
propagation support 若有事务就加入事务 若没有事务就以非事务方式执行
propagation mandatory 若有事务则加入事务,没有事务就抛出异常
propagation requires News 无论如何都新建事务
propagation no support 不支持事务 存在事务就挂起事务
propagation never 以非事务的方式运行,
13.分布式事务
CAP原则 consistency 一致性 available 可用性 partition tolerance 分区容错性
CAP原则只能有支持两个 不能够三个兼顾。使用RAFT算法
Raft (thesecretlivesofdata.com)
基于base理论
我们分布式事务采取ap原则 做到弱一致性
基本可用 软状态 最终一致性
我们使用基于base理论的柔性事务 == 可靠消息 + 最大努力通知 + 最终一致性方案
14.秒杀服务
因为秒杀会客流量非常大的特点,我们应该提前上架商品到缓存中去
- 单一职责原则,独立部署 属于高并发的服务,所以我们应该遵循单一职责原则,独立部署
我们新开一个微服务专门处理秒杀业务
-
秒杀的链接应该加密
防止秒杀没有开始的时候就有人参与秒杀
-
库存预热,快速扣减
不应该在秒杀的时候在去扣减库存,我们应该设置一个信号量,它的值就是库存的量
信号量完了,秒杀结束
-
恶意请求拦截 (在网关) 一秒请求10次以上我们就判定为恶意请求
-
流量削峰 我们可以设置验证码机制,在秒杀的时候让用户出入验证码,把请求分散在各个时间结点
-
限流 熔断 降级 一秒进来100万请求,我们就让他等待撒花姑娘2s,在接收请求;在服务调用时间长的时候,我们让他快速失败,而不是在哪等待;流量是在太大,我们可以将一部分流量引导至一个降级页
-
队列削峰 在请求进来的时候我们让他抢信号量,抢到信号量,然后给队列发送消息让他执行秒杀业务
定时任务@schedule(秒 分 时 日 月 周)日和周随便一个写?就行
- 商品上架
我们可以用定时任务来上架商品
查询秒杀信息(活动id 活动开始时间,活动结束时间 List<商品> )
存在redis当中 存两个Map形式
一个map 存 key存储 开始时间 和 结束时间 value存成商品id
另一个Map存储key为商品 场次__id,value 存商品 的详细信息
我们在商品信息当中应该设置一个token,它可以防止一些人用 恶意脚本来攻击秒杀商品
他们来请求的时候必须带着这个token 来请求
我们应该给要秒杀的商品设置分布式信号量,信号量为商品Skell+stock+token 值为商品的库存。
商品上架我们要保证接口幂等性
我们可以在定时任务开启的时候设置分布式锁,获取到锁的服务上架商品
我们可以在给redis添加商品的时候判断key时候已经存在,若不存在我们就设置商品
- 查询活动时间的商品
给MQ发送秒杀单的信息(订单号,商品信息,会员信息),订单服务监听MQ的队列,拿到秒杀单的信息给用户创建订单。
创建订单的时候需要扣库存,但是用户也可以取消订单;也可能 库存扣减成功后,由于网络原因远程调用失败,订单回滚。
- 我们创建订单,订单30min没有被支付,我们就人为它已经被关闭了
我们锁定库存 40min后,若订单还没有被支付,我们就应该解锁库存
- 创建订单的时候 ,调用库存 然后发送消息给MQ(订单号);
关单服务监听MQ,获得订单号,查询订单状态,若订单状态是未付款,则关闭订单;然后发送消息给MQ 订单已关闭,
解锁库存服务监听这个队列,收到关单消息后就解锁库存
- 锁定库存是一个事务
库存锁定成功,给发消息给MQ (库存锁定详情单 id )
库存解锁服务 监听MQ 拿到库存详情id 查询库存id 根据库存id查询 库存详情单,若没有则说明库存没有锁定成功,则无需解锁;若有库存,则根据订单id,查询订单状态,若有订单并且订单状态是已取消,则解锁库存,若已支付,无需解锁(要处理好手动ack机制;保证消息可靠性传递)
15.httpclient工具类
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class HttpUtils {
/**
* get
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @return
* @throws Exception
*/
public static HttpResponse doGet(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpGet request = new HttpGet(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
return httpClient.execute(request);
}
/**
* post form
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param bodys
* @return
* @throws Exception
*/
public static HttpResponse doPost(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
Map<String, String> bodys)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPost request = new HttpPost(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (bodys != null) {
List<NameValuePair> nameValuePairList = new ArrayList<NameValuePair>();
for (String key : bodys.keySet()) {
nameValuePairList.add(new BasicNameValuePair(key, bodys.get(key)));
}
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(nameValuePairList, "utf-8");
formEntity.setContentType("application/x-www-form-urlencoded; charset=UTF-8");
request.setEntity(formEntity);
}
return httpClient.execute(request);
}
/**
* Post String
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPost(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
String body)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPost request = new HttpPost(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (StringUtils.isNotBlank(body)) {
request.setEntity(new StringEntity(body, "utf-8"));
}
return httpClient.execute(request);
}
/**
* Post stream
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPost(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
byte[] body)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPost request = new HttpPost(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (body != null) {
request.setEntity(new ByteArrayEntity(body));
}
return httpClient.execute(request);
}
/**
* Put String
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPut(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
String body)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPut request = new HttpPut(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (StringUtils.isNotBlank(body)) {
request.setEntity(new StringEntity(body, "utf-8"));
}
return httpClient.execute(request);
}
/**
* Put stream
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPut(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
byte[] body)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPut request = new HttpPut(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (body != null) {
request.setEntity(new ByteArrayEntity(body));
}
return httpClient.execute(request);
}
/**
* Delete
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @return
* @throws Exception
*/
public static HttpResponse doDelete(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpDelete request = new HttpDelete(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
return httpClient.execute(request);
}
private static String buildUrl(String host, String path, Map<String, String> querys) throws UnsupportedEncodingException {
StringBuilder sbUrl = new StringBuilder();
sbUrl.append(host);
if (!StringUtils.isBlank(path)) {
sbUrl.append(path);
}
if (null != querys) {
StringBuilder sbQuery = new StringBuilder();
for (Map.Entry<String, String> query : querys.entrySet()) {
if (0 < sbQuery.length()) {
sbQuery.append("&");
}
if (StringUtils.isBlank(query.getKey()) && !StringUtils.isBlank(query.getValue())) {
sbQuery.append(query.getValue());
}
if (!StringUtils.isBlank(query.getKey())) {
sbQuery.append(query.getKey());
if (!StringUtils.isBlank(query.getValue())) {
sbQuery.append("=");
sbQuery.append(URLEncoder.encode(query.getValue(), "utf-8"));
}
}
}
if (0 < sbQuery.length()) {
sbUrl.append("?").append(sbQuery);
}
}
return sbUrl.toString();
}
private static HttpClient wrapClient(String host) {
HttpClient httpClient = new DefaultHttpClient();
if (host.startsWith("https://")) {
sslClient(httpClient);
}
return httpClient;
}
private static void sslClient(HttpClient httpClient) {
try {
SSLContext ctx = SSLContext.getInstance("TLS");
X509TrustManager tm = new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
@Override
public void checkClientTrusted(X509Certificate[] xcs, String str) {
}
@Override
public void checkServerTrusted(X509Certificate[] xcs, String str) {
}
};
ctx.init(null, new TrustManager[] { tm }, null);
SSLSocketFactory ssf = new SSLSocketFactory(ctx);
ssf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
ClientConnectionManager ccm = httpClient.getConnectionManager();
SchemeRegistry registry = ccm.getSchemeRegistry();
registry.register(new Scheme("https", 443, ssf));
} catch (KeyManagementException ex) {
throw new RuntimeException(ex);
} catch (NoSuchAlgorithmException ex) {
throw new RuntimeException(ex);
}
}
}
16.跨域问题
package com.xunqi.gulimall.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
@Configuration
public class GulimallCorsConfiguration {
@Bean
public CorsWebFilter corsWebFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
//1、配置跨域
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.setAllowCredentials(true);
source.registerCorsConfiguration("/**",corsConfiguration);
return new CorsWebFilter(source);
}
}
17.SentinelGatewayConfig
package com.xunqi.gulimall.gateway.config;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.fastjson.JSON;
import com.xunqi.common.exception.BizCodeEnum;
import com.xunqi.common.utils.R;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Configuration
public class SentinelGatewayConfig {
public SentinelGatewayConfig() {
GatewayCallbackManager.setBlockHandler(new BlockRequestHandler() {
//网关限流了请求,就会调用此回调
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable t) {
R error = R.error(BizCodeEnum.TO_MANY_REQUEST.getCode(), BizCodeEnum.TO_MANY_REQUEST.getMessage());
String errorJson = JSON.toJSONString(error);
Mono<ServerResponse> body = ServerResponse.ok().body(Mono.just(errorJson), String.class);
return body;
}
});
}
}
17.网关配置
spring:
cloud:
sentinel:
transport:
#配置sentinel dashboard地址
dashboard: localhost:8080
# http请求 配置
gateway:
routes:
# - id: test_route
# uri: https://www.baidu.com
# predicates:
# - Query=uri,baidu
#
# - id: qq_route
# uri: https://www.qq.com
# predicates:
# - Query=uri,qq
- id: product_route
uri: lb://gulimall-product
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/(?<segment>/?.*),/$\{segment}
- id: coupon_route
uri: lb://gulimall-coupon
predicates:
- Path=/api/coupon/**
filters:
- RewritePath=/api/(?<segment>/?.*),/$\{segment}
# 域名配置
- id: gulimall_host_route
uri: lb://gulimall-product
predicates:
- Host=gulimall.com,item.gulimall.com
- id: gulimall_search_route
uri: lb://gulimall-search
predicates:
- Host=search.gulimall.com
18.解决openfeign 请求头丢失问题
package com.xunqi.gulimall.member.config;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
@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;
}
}
19.rabbitMQ配置
package com.xunqi.gulimall.order.config;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@Configuration
public class MyRabbitConfig {
private RabbitTemplate rabbitTemplate;
@Primary
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
this.rabbitTemplate = rabbitTemplate;
rabbitTemplate.setMessageConverter(messageConverter());
initRabbitTemplate();
return rabbitTemplate;
}
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
/**
* 定制RabbitTemplate
* 1、服务收到消息就会回调
* 1、spring.rabbitmq.publisher-confirms: true
* 2、设置确认回调
* 2、消息正确抵达队列就会进行回调
* 1、spring.rabbitmq.publisher-returns: true
* spring.rabbitmq.template.mandatory: true
* 2、设置确认回调ReturnCallback
*
* 3、消费端确认(保证每个消息都被正确消费,此时才可以broker删除这个消息)
*
*/
// @PostConstruct //MyRabbitConfig对象创建完成以后,执行这个方法
public void initRabbitTemplate() {
/**
* 1、只要消息抵达Broker就ack=true
* correlationData:当前消息的唯一关联数据(这个是消息的唯一id)
* ack:消息是否成功收到
* cause:失败的原因
*/
//设置确认回调
rabbitTemplate.setConfirmCallback((correlationData,ack,cause) -> {
System.out.println("confirm...correlationData["+correlationData+"]==>ack:["+ack+"]==>cause:["+cause+"]");
});
/**
* 只要消息没有投递给指定的队列,就触发这个失败回调
* message:投递失败的消息详细信息
* replyCode:回复的状态码
* replyText:回复的文本内容
* exchange:当时这个消息发给哪个交换机
* routingKey:当时这个消息用哪个路邮键
*/
rabbitTemplate.setReturnCallback((message,replyCode,replyText,exchange,routingKey) -> {
System.out.println("Fail Message["+message+"]==>replyCode["+replyCode+"]" +
"==>replyText["+replyText+"]==>exchange["+exchange+"]==>routingKey["+routingKey+"]");
});
}
}
20 rabbitMQ队列创建与绑定
package com.xunqi.gulimall.order.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.Exchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
@Configuration
public class MyRabbitMQConfig {
/* 容器中的Queue、Exchange、Binding 会自动创建(在RabbitMQ)不存在的情况下 */
/**
* 死信队列
*
* @return
*/@Bean
public Queue orderDelayQueue() {
/*
Queue(String name, 队列名字
boolean durable, 是否持久化
boolean exclusive, 是否排他
boolean autoDelete, 是否自动删除
Map<String, Object> arguments) 属性
*/
HashMap<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); // 消息过期时间 1分钟
Queue queue = new Queue("order.delay.queue", true, false, false, arguments);
return queue;
}
/**
* 普通队列
*
* @return
*/
@Bean
public Queue orderReleaseQueue() {
Queue queue = new Queue("order.release.order.queue", true, false, false);
return queue;
}
/**
* TopicExchange
*
* @return
*/
@Bean
public Exchange orderEventExchange() {
/*
* String name,
* boolean durable,
* boolean autoDelete,
* Map<String, Object> arguments
* */
return new TopicExchange("order-event-exchange", true, false);
}
@Bean
public Binding orderCreateBinding() {
/*
* String destination, 目的地(队列名或者交换机名字)
* DestinationType destinationType, 目的地类型(Queue、Exhcange)
* String exchange,
* String routingKey,
* Map<String, Object> arguments
* */
return new Binding("order.delay.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.create.order",
null);
}
@Bean
public Binding orderReleaseBinding() {
return new Binding("order.release.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.order",
null);
}
/**
* 订单释放直接和库存释放进行绑定
* @return
*/
@Bean
public Binding orderReleaseOtherBinding() {
return new Binding("stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.other.#",
null);
}
/**
* 商品秒杀队列
* @return
*/
@Bean
public Queue orderSecKillOrrderQueue() {
Queue queue = new Queue("order.seckill.order.queue", true, false, false);
return queue;
}
@Bean
public Binding orderSecKillOrrderQueueBinding() {
//String destination, DestinationType destinationType, String exchange, String routingKey,
// Map<String, Object> arguments
Binding binding = new Binding(
"order.seckill.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.seckill.order",
null);
return binding;
}
}
21RabbitMQ监听
package com.xunqi.gulimall.order.listener;
import com.rabbitmq.client.Channel;
import com.xunqi.common.to.mq.SeckillOrderTo;
import com.xunqi.gulimall.order.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Slf4j
@Component
@RabbitListener(queues = "order.seckill.order.queue")
public class OrderSeckillListener {
@Autowired
private OrderService orderService;
@RabbitHandler
public void listener(SeckillOrderTo orderTo, Channel channel, Message message) throws IOException {
log.info("准备创建秒杀单的详细信息...");
try {
orderService.createSeckillOrder(orderTo);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
22.分布式锁redssion的配置
package com.xunqi.gulimall.product.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
@Configuration
public class MyRedissonConfig {
/**
* 所有对Redisson的使用都是通过RedissonClient
* @return
* @throws IOException
*/
@Bean(destroyMethod="shutdown")
public RedissonClient redisson() throws IOException {
//1、创建配置
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.77.130:6379");
//2、根据Config创建出RedissonClient实例
//Redis url should start with redis:// or rediss://
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}