- id: gulimall-coupon
uri: lb://gulimall-coupon
predicates:
- Path=/api/coupon/**
filters:
- RewritePath=/api/(?>/?.*),/$\{segment}
查询秒杀场次关联的秒杀商品
@Service("seckillSkuRelationService")
public class SeckillSkuRelationServiceImpl extends ServiceImpl<SeckillSkuRelationDao, SeckillSkuRelationEntity> implements SeckillSkuRelationService {
@Override
public PageUtils queryPage(Map<String, Object> params) {
QueryWrapper<SeckillSkuRelationEntity> queryWrapper = new QueryWrapper<SeckillSkuRelationEntity>();
//场次id
String promotionSessionId = (String) params.get("promotionSessionId");
if (!StringUtils.isEmpty(promotionSessionId)){
queryWrapper.eq("promotion_session_id",promotionSessionId);
}
IPage<SeckillSkuRelationEntity> page = this.page(
new Query<SeckillSkuRelationEntity>().getPage(params),
queryWrapper
);
return new PageUtils(page);
}
}
例如:在每天的晚上11点,查询明天所有的秒杀商品

在线生成器:cron.qqe2.com


SpringBoot 整合的 cron 只有六位字符。同时周(一——日)对应数字 1——7。
使用:

任务的阻塞性:如果每秒执行一次任务,当当前任务阻塞时,后续的任务会在当前任务完成阻塞之后的一秒后开始执行。
解决方法:
可以让业务以异步的方式运行,自己提交到线程池
CompletableFuture.runAsync(() -> {
xxxxService.xxx();
},executor);
默认只有一个线程池:
在有的spring版本中不生效


修改配置文件:
spring.task.scheduling.pool.size=5
@EnableAsync、@Async

查询近三天的秒杀商品:
@GetMapping("/latest3DaySession")
public R getLatest3DaySession(){
List<SeckillSessionEntity> sessions = seckillSessionService.getLatest3DaySession();
return R.ok().setData(sessions);
}
@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<SeckillSessionEntity> collect = list.stream().map(session -> {
Long id = session.getId();
List<SeckillSkuRelationEntity> relationEntities = seckillSkuRelationService.list(new QueryWrapper<SeckillSkuRelationEntity>().eq("promotion_session_id", id));
session.setRelationSkus(relationEntities);
return session;
}).collect(Collectors.toList());
return collect;
}
return null;
}
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;
}
private String endTime(){
LocalDate now = LocalDate.now().plusDays(2);
LocalTime max = LocalTime.MAX;
LocalDateTime end = LocalDateTime.of(now,max);
String format = end.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH-mm-ss"));
return format;
}
@Slf4j
@Component
@EnableScheduling
@EnableAsync
public class SeckillSkuScheduled {
@Autowired
SeckillService seckillService;
@Autowired
RedissonClient redissonClient;
private final String upload_lock = "seckill:upload:lock";
@Async
@Scheduled(cron = "0 * * * * ?")
public void hello() throws InterruptedException {
log.info("上架秒杀的商品信息...");
//分布式锁
RLock lock = redissonClient.getLock(upload_lock);
lock.lock(10, TimeUnit.SECONDS);
try {
seckillService.uploadSeckillSkuLatest3Days();
}finally {
lock.unlock();
}
}
}
@Service
public class SeckillServiceImpl implements SeckillService {
@Autowired
CouponFeignService couponFeignService;
@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
RedissonClient redissonClient;
@Autowired
ProductFeignService productFeignService;
private 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() {
//1.扫描要参加秒杀的活动
R r = couponFeignService.getLatest3DaySession();
if (r.getCode() == 0){
List<SeckillSessionsWithSkus> data = r.getData(new TypeReference<List<SeckillSessionsWithSkus>>() {
});
//上架商品
//缓存到Redis
//1.缓存活动信息
saveSessionInfos(data);
//2.缓存活动的关联商品信息
saveSessionSkuInfos(data);
}
}
/**
* 缓存活动信息
* @param sessions
*/
private void saveSessionInfos(List<SeckillSessionsWithSkus> sessions){
sessions.stream().forEach(session -> {
Long startTime = session.getStartTime().getTime();
Long endTime = session.getEndTime().getTime();
String key = SESSIONS_CACHE_PREFIX + startTime + "_" + endTime;
List<String> collect = session.getRelationSkus().stream().map(item -> item.getSkuId().toString()).collect(Collectors.toList());
//缓存活动信息
stringRedisTemplate.opsForList().leftPushAll(key,collect);
});
}
/**
* 缓存活动相关联的商品信息
* @param sessions
*/
private void saveSessionSkuInfos(List<SeckillSessionsWithSkus> sessions){
sessions.stream().forEach(session -> {
//准备hash操作
BoundHashOperations<String, Object, Object> ops = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
session.getRelationSkus().stream().forEach(seckillSkuVo -> {
//缓存商品
SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
//1.sku的基本信息
R info = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());
if (info.getCode() == 0){
SkuInfoTo skuInfo = info.getData("skuInfo", new TypeReference<SkuInfoTo>() {
});
redisTo.setSkuInfo(skuInfo);
}
//2.sku的秒杀信息
BeanUtils.copyProperties(seckillSkuVo,redisTo);
//3.设置上架商品的秒杀时间信息
redisTo.setStartTime(session.getStartTime().getTime());
redisTo.setEndTime(session.getEndTime().getTime());
//4.随机码
String token = UUID.randomUUID().toString().replace("-", "");
redisTo.setRandomCode(token);
//5.使用库存作为分布式的信号量
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
//商品可以秒杀的数量作为信号量
semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
String s = JSON.toJSONString(redisTo);
ops.put(seckillSkuVo.getSkuId().toString(),s);
});
});
}
}
执行上面的代码发现,当定时任务被触发 redis 中回不断地缓存相同的数据,违背了缓存的幂等性
同时,如果不同的服务在相同时间定时任务被触发,也会向redis 中缓存相同的数据,所有需要引入分布式锁。
@EnableAsync
public class SeckillSkuScheduled {
@Autowired
SeckillService seckillService;
@Autowired
RedissonClient redissonClient;
private final String upload_lock = "seckill:upload:lock";
@Async
@Scheduled(cron = "0 * * * * ?")
public void hello() throws InterruptedException {
log.info("上架秒杀的商品信息...");
//分布式锁
RLock lock = redissonClient.getLock(upload_lock);
lock.lock(10, TimeUnit.SECONDS);
try {
seckillService.uploadSeckillSkuLatest3Days();
}finally {
lock.unlock();
}
}
}


结果:

在product服务中
/**
* 返回当前时间可以参与的秒杀商品信息
* @return
*/
@ResponseBody
@GetMapping("currentSeckillSkus")
public R getCurrentSeckillSkus(){
List<SeckillSkuRedisTo> vos = seckillService.getCurrentSeckillSkus();
return R.ok().setData(vos);
}
==================================================================================
$.get("http://seckill.gulimall.com/currentSeckillSkus", function (res) {
if (res.data.length > 0) {
res.data.forEach(function (item) {
$("- + item.skuId + ")'>
").append($("
"))
.append($(""
+item.skuInfo.skuTitle+""))
.append($("" + item.seckillPrice + ""))
.append($("" + item.skuInfo.price + ""))
.appendTo("#seckillSkuContent");
})
}
})
/**
* 返回当前时间可以参与的商品秒杀的信息
* @return
*/
@Override
public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
//1.确定当前时间属于哪个秒杀场次
long cuTime = new Date().getTime();
Set<String> keys = stringRedisTemplate.keys(SESSIONS_CACHE_PREFIX + "*");
for (String key : keys) {
String time = key.replace(SESSIONS_CACHE_PREFIX, "");
String[] s = time.split("_");
Long startT = Long.parseLong(s[0]);
Long endT = Long.parseLong(s[1]);
if (cuTime >= startT && cuTime <= endT){
//2.获取这个秒杀场次需要的所有商品信息
List<String> range = stringRedisTemplate.opsForList().range(key,0, -1);
BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
List<String> list = hashOps.multiGet(range);
if (list != null){
List<SeckillSkuRedisTo> collect = list.stream().map(item -> {
SeckillSkuRedisTo redisTo = JSON.parseObject((String) item, SeckillSkuRedisTo.class);
return redisTo;
}).collect(Collectors.toList());
return collect;
}
break;
}
}
return null;
}

/**
* 返回商品详情页的秒杀信息
* @param skuId
* @return
*/
@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) {
BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.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 currentTime = new Date().getTime();
Long startTime = skuRedisTo.getStartTime();
Long endTime = skuRedisTo.getEndTime();
if (!(currentTime >= startTime && currentTime <= endTime)){
skuRedisTo.setRandomCode(null);
}
return skuRedisTo;
}
}
}
return null;
}
如果当前商品处于秒杀中,显示秒杀价格,如果在以后的场次中,显示开始秒杀的时间。





商品模块获取到秒杀的各种信息:

前端绑定随机码等数据,发送给秒杀模块


秒杀模块秒杀商品:
@ResponseBody
@GetMapping("/kill")
public R secKill(@RequestParam("killId") String killId,@RequestParam("key") String key,@RequestParam("num") Integer num){
String orderSn = seckillService.kill(killId,key,num);
return R.ok().setData(orderSn);
}
@Override
public String kill(String killId, String key, Integer num) {
MemberResponseTo memberResponseTo = LoginUserInterceptor.loginUser.get();
//1.获取当前秒杀商品的详细信息
BoundHashOperations<String, String, String> ops = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
String s = ops.get(killId);
if (StringUtils.isEmpty(s)){
return null;
}else {
SeckillSkuRedisTo redis = JSON.parseObject(s,SeckillSkuRedisTo.class);
//校验 合法性
//1.校验时间
Long startTime = redis.getStartTime();
Long endTime = redis.getEndTime();
long time = new Date().getTime();
long ttl = endTime - startTime;
if (time >= startTime && time <= endTime){
//2.校验随机码和商品id
String randomCode = redis.getRandomCode();
String skuId = redis.getPromotionSessionId() + "-" + redis.getSkuId();
if (randomCode.equals(key) && killId.equals(skuId)){
//3.判断购物数量是否合理(每个人购买的秒杀商品有一个限制)
if (num <= redis.getSeckillLimit()){
//4.验证这个人是否已经买过。幂等性;只要秒杀成功,就去占位。 userId-SessionId-skuId
//SETNX
String redisKey = memberResponseTo.getId() + "-" +skuId;
//自动过期
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(redisKey,num.toString(),ttl, TimeUnit.MILLISECONDS);
if (aBoolean){
//从未买过,占位
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
try {
boolean b = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
if (b){
//生成订单号
String timeId = IdWorker.getTimeId();
SeckillOrderTo orderTo = new SeckillOrderTo();
orderTo.setOrderSn(timeId);
orderTo.setMemberId(memberResponseTo.getId());
orderTo.setNum(num);
orderTo.setPromotionSessionId(redis.getPromotionSessionId());
orderTo.setSkuId(redis.getSkuId());
orderTo.setSeckillPrice(redis.getSeckillPrice());
//发送MQ消息
rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",orderTo);
return timeId;
}
return null;
} catch (InterruptedException e) {
return null;
}
}
}
}
}
return null;
}
}
秒杀模块发送MQ消息给订单模块监听的队列,由订单模块监听并创建秒杀订单。
消息发送流程:

在订单模块接收秒杀模块发送的消息,并处理
配置rabbitMQ相关消息:
spring:
rabbitmq:
host: 192.168.137.128
port: 5672
virtual-host: /
# publisher-confirms: true
publisher-returns: true
创建消息队列:

监听消息队列:
@Slf4j
@RabbitListener(queues = "order.seckill.order.queue")
@Component
public class OrderSeckillListener {
@Autowired
OrderService orderService;
@RabbitHandler
public void listener(SeckillOrderTo seckillOrder, Channel channel, Message message) throws IOException {
try {
log.info("准备创建秒杀单的详细信息。。。");
orderService.createSeckillOrder(seckillOrder);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}catch (Exception e){
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
监听到秒杀订单消息->创建订单:
@Override
public void createSeckillOrder(SeckillOrderTo seckillOrder) {
MemberResponseTo memberResponseVo = LoginUserInterceptor.loginUser.get();
//1. 创建订单
OrderEntity orderEntity = new OrderEntity();
orderEntity.setOrderSn(seckillOrder.getOrderSn());
orderEntity.setMemberId(seckillOrder.getMemberId());
if (memberResponseVo!=null){
orderEntity.setMemberUsername(memberResponseVo.getUsername());
}
orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
orderEntity.setCreateTime(new Date());
orderEntity.setPayAmount(seckillOrder.getSeckillPrice().multiply(new BigDecimal(seckillOrder.getNum())));
this.save(orderEntity);
//2. 创建订单项
R r = productFeignService.getSpuInfoBySkuId(seckillOrder.getSkuId());
if (r.getCode() == 0) {
SeckillSkuInfoVo skuInfo = r.getData("skuInfo", new TypeReference<SeckillSkuInfoVo>() {
});
OrderItemEntity orderItemEntity = new OrderItemEntity();
orderItemEntity.setOrderSn(seckillOrder.getOrderSn());
orderItemEntity.setSpuId(skuInfo.getSpuId());
orderItemEntity.setCategoryId(skuInfo.getCatalogId());
orderItemEntity.setSkuId(skuInfo.getSkuId());
orderItemEntity.setSkuName(skuInfo.getSkuName());
orderItemEntity.setSkuPic(skuInfo.getSkuDefaultImg());
orderItemEntity.setSkuPrice(skuInfo.getPrice());
orderItemEntity.setSkuQuantity(seckillOrder.getNum());
orderItemService.save(orderItemEntity);
}
}
测试:



什么是熔断
A 服务调用 B 服务的某个功能,由于网络不稳定问题,或者 B 服务卡机,导致功能时间超长。如果这样子的次数太多。我们就可以直接将 B 断路了(A 不再请求 B 接口),凡是调用 B 的直接返回降级数据,不必等待 B 的超长执行。 这样 B 的故障问题,就不会级联影响到 A。
什么是降级
整个网站处于流量高峰期,服务器压力剧增,根据当前业务情况及流量,对一些服务和页面进行有策略的降级[停止服务,所有的调用直接返回降级数据]。以此缓解服务器资源的的压力,以保证核心业务的正常运行,同时也保持了客户和大部分客户的得到正确的相应。
异同:
相同点:
不同点:
什么是限流
对打入服务的请求流量进行控制,使服务能够承担不超过自己能力的流量压力
官方文档:https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D
项目地址:https://github.com/alibaba/Sentinel
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
Sentinel 具有以下特征:

Sentinel 分为两个部分:
Sentinel 基本概念
资源
资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提 供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。在接下来的文 档中,我们都会用资源来描述代码块。
只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下, 可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。
规则
围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规 则。所有规则可以动态实时调整。
Hystrix 与 Sentinel 比较

Hystric隔离是线程池隔离,对于某个请求如只允许50个线程并发访问,多的并发会被拒绝,多个请求对应多个线程池,这样会浪费线程池资源,增加服务器压力,而Sential使用的是类似redis的信号量
Sentinel 和 Hystrix 的原则是一致的: 当检测到调用链路中某个资源出现不稳定的表现,例
如请求响应时间长或异常比例升高的时候,则对这个资源的调用进行限制,让请求快速失败,
避免影响到其它的资源而导致级联故障。
https://github.com/alibaba/Sentinel/wiki/%E4%B8%BB%E9%A1%B5
什么是熔断降级
除了流量控制以外,降低调用链路中的不稳定资源也是 Sentinel 的使命之一。由于调用关 系的复杂性,如果调用链路中的某个资源出现了不稳定,最终会导致请求发生堆积。

熔断降级设计理念
在限制的手段上,Sentinel 和 Hystrix 采取了完全不一样的方法:
Hystrix 通过 线程池隔离 的方式,来对依赖(在 Sentinel 的概念中对应 资源)进行了隔 离。这样做的好处是资源和资源之间做到了最彻底的隔离。缺点是除了增加了线程切换的成 本(过多的线程池导致线程数目过多),还需要预先给各个资源做线程池大小的分配。
Sentinel 对这个问题采取了两种手段:
通过并发线程数进行限制
和资源池隔离的方法不同,Sentinel 通过限制资源并发线程的数量,来减少不稳定资源对其 它资源的影响。这样不但没有线程切换的损耗,也不需要您预先分配线程池的大小。当某个 资源出现不稳定的情况下,例如响应时间变长,对资源的直接影响就是会造成线程数的逐步 堆积。当线程数在特定资源上堆积到一定的数量之后,对该资源的新请求就会被拒绝。堆积 的线程完成任务后才开始继续接收请求。
通过响应时间对资源进行降级
除了对并发线程数进行控制以外,Sentinel 还可以通过响应时间来快速降级不稳定的资源。 当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的 时间窗口之后才重新恢复
整合限流测试
官方文档:quick-start (sentinelguard.io)
sentinel的使用主要包括这三步:

官方文档:Sentinel · alibaba/spring-cloud-alibaba Wiki (github.com)
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
在官网下载对应版本的 sentinel jar包 。Releases · alibaba/Sentinel (github.com)

直接启动默认使用的是80端口,可能会被占用
使用命令:java -jar sentinel-dashboard-1.8.1.jar --server.port=8033
启动成功:

访问8033端口,账号密码默认都是 sentinel。
sentinel 的控制台是懒加载机制,只有当请求进来的时候,才会有各种操作选项。

配置控制台地址:
spring.cloud.sentinel.transport.dashboard=localhost:8033
spring.cloud.sentinel.transport.port=8719 //控制台与后端微服务之间传输数据的端口
重启服务:
出现报错:The Bean Validation API is on the classpath but no implementation could be found
Add an implementation, such as Hibernate Validator, to the classpath 以及依赖循环
发起请求之后:

设置每秒QPS 为1,即每秒只能通过一个请求:


前面的测试存在问题
导入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
新版已经不需要暴露端口了
实时监控:

WebCallbackManager已经不能使用了
package com.henu.soft.merist.seckill.config;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.fastjson.JSON;
import com.henu.soft.merist.common.exception.BizCodeEnume;
import com.henu.soft.merist.common.utils.R;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class SecKillSentinelConfig implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
R error = R.error(BizCodeEnume.TOO_MANY_REQUEST.getCode(), BizCodeEnume.TOO_MANY_REQUEST.getMsg());
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json");
httpServletResponse.getWriter().write(JSON.toJSONString(error));
}
}

上面Sentinel 没有识别到 feign 远程调用的链路,接下来整合feign

配置feign开启熔断降级:
feign.sentinel.enabled=true
指定远程调用失败返回的配置类

package com.henu.soft.merist.gulimall.product.feign.fallback;
import com.henu.soft.merist.common.exception.BizCodeEnume;
import com.henu.soft.merist.common.utils.R;
import com.henu.soft.merist.gulimall.product.feign.SeckillFeignService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class SeckillFeignServiceFallback implements SeckillFeignService {
@Override
public R getSkuSeckillInfo(Long skuId) {
log.info("熔断触发");
return R.error(BizCodeEnume.TOO_MANY_REQUEST.getCode(), BizCodeEnume.TOO_MANY_REQUEST.getMsg());
}
}
其中遇到了一些版本问题的报错,一些参考解决方法:

1.try catch方法
try(Entry entry = SphU.entry("seckillSkus")) 设置了资源名为 seckillSku,可以在控制台中熔断降级

2.注解@SentinelReource
设置资源名为getCurrentSeckillSkusResource


更多定义资源的方法可以参考官网:basic-api-resource-rule (sentinelguard.io)

api-gateway-flow-control (sentinelguard.io)
导入依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-sentinel-gatewayartifactId>
<version>2.2.6.RELEASEversion>
dependency>
重启服务,重启控制台的jar包
gateway模块有api管理服务

请求链路也能获取到其他服务的请求

可以直接在流控规则中配置各个微服务的流控
API 名称就是网关配置的id

网关流控的各种熟悉在官网都有:

定制网关流控返回
package com.henu.soft.merist.gulimall.gateway.config;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.fastjson.JSON;
import com.henu.soft.merist.common.exception.BizCodeEnume;
import com.henu.soft.merist.common.utils.R;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
/**
* Title: SentinelGateWayConfig
* Description:
* date:2020/7/10 17:57
*/
@Configuration
public class SentinelGateWayConfig {
public SentinelGateWayConfig(){
GatewayCallbackManager.setBlockHandler((exchange, t) ->{
// 网关限流了请求 就会回调这个方法
R error = R.error(BizCodeEnume.TOO_MANY_REQUEST.getCode(), BizCodeEnume.TOO_MANY_REQUEST.getMsg());
String errJson = JSON.toJSONString(error);
Mono<ServerResponse> body = ServerResponse.ok().body(Mono.just(errJson), String.class);
return body;
});
}
}
每经过调用一个微服务,更新span,记录cs、sr的时间戳
Span(跨度):
基本工作单元,发送一个远程调度任务 就会产生一个 Span,Span 是一个 64 位 ID 唯一标识的,Trace 是用另一个 64 位 ID 唯一标识的,Span 还有其他数据信息,比如摘要、时间戳事件、Span 的 ID、以及进度 ID。
Trace(跟踪):
一系列 Span 组成的一个树状结构。请求一个微服务系统的 API 接口,这个 API 接口,需要调用多个微服务,调用每个微服务都会产生一个新的 Span,所有由这个请求产生的 Span 组成了这个 Trace。
Annotation(标注):
用来及时记录一个事件的,一些核心注解用来定义一个请求的开始和结束 。这些注解包括以下:
官方文档:https://cloud.spring.io/spring-cloud-static/spring-cloud-sleuth/2.1.3.RELEASE/single/spring-cloud-sleuth.html
如果服务调用顺序如下:

那么用以上概念完整的表示出来如下:

Span 之间的父子关系如下:

1、服务提供者与消费者导入依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-sleuthartifactId>
dependency>
2、打开 debug 日志
logging:
level:
org.springframework.cloud.openfeign: debug
org.springframework.cloud.sleuth: debug
3、发起一次远程调用,观察控制台
DEBUG [user-service,541450f08573fff5,541450f08573fff5,false]
user-service:服务名

1、docker 安装 zipkin 服务器
docker run -d -p 9411:9411 openzipkin/zipkin
2、导入
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-zipkinartifactId>
dependency>
zipkin 依赖也同时包含了 sleuth,可以省略 sleuth 的引用
3、添加 zipkin 相关配置
spring:
application:
name: user-service
zipkin:
base-url: http://192.168.56.10:9411/ # zipkin 服务器的地址
# 关闭服务发现,否则 Spring Cloud 会把 zipkin 的 url 当做服务名称
discoveryClientEnabled: false
sender:
type: web # 设置使用 http 的方式传输数据
sleuth:
sampler:
probability: 1 # 设置抽样采集率为 100%,默认为 0.1,即 10%
发送远程请求,测试 zipkin。

Zipkin 默认是将监控数据存储在内存的,如果 Zipkin 挂掉或重启的话,那么监控数据就会丢 失。所以如果想要搭建生产可用的 Zipkin,就需要实现监控数据的持久化。而想要实现数据 持久化,自然就是得将数据存储至数据库。好在 Zipkin
Zipkin 数据持久化相关的官方文档地址如下: https://github.com/openzipkin/zipkin#storage-component
综上,故采用 Elasticsearch 是个比较好的选择,关于使用 Elasticsearch 作为 Zipkin 的存储数 据库的官方文档如下:
elasticsearch-storage: https://github.com/openzipkin/zipkin/tree/master/zipkin-server#elasticsearch-storage
zipkin-storage/elasticsearch https://github.com/openzipkin/zipkin/tree/master/zipkin-storage/elasticsearch
通过 docker 的方式
docker run --env STORAGE_TYPE=elasticsearch --env ES_HOSTS=192.168.56.10:9200 openzipkin/zipkin-dependencie
