1、秒杀业务

秒杀具有瞬间高并发的特点,针对这一特点,必须要做限流 + 异步 + 缓存(页面静态化)+ 独立部署。

秒杀系统设计

image-20220304221850788

image-20220304221910750

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
+ 秒杀(高并发)系统关注的问题
+ 独立部署
+ 防止影响其他服务
+ 链接加密
+ 防止请求暴露,恶意攻击
+ 库存预热
+ redis信号量
+ redis高可用
+ 动静分离
+ nginx
+ cdn
+ 恶意请求拦截
+ 网关层
+ 流量错峰
+ 小米:验证码
+ 验证正常请求
+ 流量错峰
+ 加入购物车
+ 结算时间不同,流量错峰
+ 限流&熔断&降级
+ 限流
+ 前端限流
+ 点击次数、间隔限制
+ 后端限流
+ 筛选非用户行为
+ 限制请求次数、总量
+ nginx限流
+ 直接负载部分请求到错误的静态页面
+ 令牌算法 漏斗算法
+ 网关限流
+ 限流的过滤器
+ 熔断、降级
+ 调用链路异常,快速失败熔断
+ 流量太大,引导部分请求降级
+ 队列削峰(杀手锏)
+ 抢到信号量的放行后台
+ 后台发送消息到队列,返回秒杀成功回调
+ 订单服务等监听队列,慢慢创建订单处理成功秒杀的消息

秒杀商品上架(定时、异步、幂等性)、展示、渲染

image-20220304222418396

定时任务&异步任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
+ 开启定时任务@EnableScheduling
+ 定时方法注解@Scheduled
+ spring中区别
+ cron语法
+ 默认阻塞
+ 解决阻塞
+ 异步编排运行,自己提交到线程池
+ 支持定时任务线程池(线程池默认大小为一)
+ 配置文件设置线程池大小
+ 不太好使
+ 让定时任务异步执行(异步任务)

+ 异步任务
+ 开启异步任务@EnableAsync
+ 异步方法注解@Async
+ 默认使用线程池,大小为8
+ 可配置线程池属性

+ 在Spring中表达式是6位组成,不允许第七位的年份
+ 在周几的的位置,1-7代表周一到周日
+ 定时任务不该阻塞。默认是阻塞的
+ 可以让业务以异步的方式,自己提交到线程池
+ CompletableFuture.runAsync(() -> {},execute);
+ 支持定时任务线程池;设置 TaskSchedulingProperties
+ spring.task.scheduling.pool.size: 5
+ 让定时任务异步执行
+ 异步任务
+ 解决:使用异步任务 + 定时任务来完成定时任务不阻塞的功能

上架商品

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
+ 上架service
+ 远程调用优惠系统,扫描最近三天需要秒杀的商品
+ 查询最近三天秒杀活动
+ 得到最近三天格式化日期
+ 查询起始时间between起始和结束之间的活动
+ 遍历每一个活动,查询关联商品list
+ 根据活动id查询关联表
+ 封装返回数据Vo
+ 上架商品
+ 缓存到reids
+ 缓存活动信息
+ 遍历活动,获取开始结束时间作为key
+ 收集活动所有商品id list作为value
+ 缓存活动的关联商品信息
+ 准备hash操作
+ 遍历每个商品,商品skuid作为key
+ 商品To(sku秒杀信息、详细信息、随机码、开始结束时间)作为value(转为json)
+ 远程调用商品服务(查询详细信息)
+ 根据sku id查询商品详细信息
+ 随机码(UUID)
+ 防止提前脚本发请求秒杀
+ 秒杀开始才暴露
+ 秒杀商品设置分布式信号量(限流),作为库存扣减信息
+ 引入redisson
+ 配置redisson客户端
+ key:商品随机码
+ value:商品秒杀数量

image-20220304223213629

幂等性保证

image-20220304223954144

1
2
3
4
5
6
7
8
+ 加入分布式锁
+ 防止多台机器同一时间上架
+ 同时判断key不存在,导致同时追加,所以必须有锁
+ 幂等性保证
+ 缓存活动信息,判断key sessionid是否存在
+ 缓存sku信息,key:场次 + 商品id,判断key sessionid_skuid是否存在
+ 解决多场次同一商品问题
+ 缓存信号量,跟商品是否上架一起执行

查询秒杀商品

1
2
3
4
5
6
7
8
+ 展示首页秒杀商品
+ controller:判断符合当前时间秒杀场次的商品(获取当前时间可以参与的秒杀商品信息)
+ 确定当前时间属于哪个秒杀场次
+ 遍历所有keys,截取字符串,判断时间
+ 获取秒杀场次的所有商品信息
+ 获取场次所有商品id
+ 根据id获取hash中的商品详情
+ 封装Vo

秒杀页面渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
+ 秒杀预告
+ 商品页面,查询当前sku是否参与秒杀优惠
+ 远程调用秒杀服务,根据skuid 查询redis商品详情hash
+ 找到所有秒杀商品的key
+ 遍历key正则匹配,找到商品
+ 判断秒杀是否开始,未开始则不暴露随机码
+ feign调用、封装
+ 异步编排
+ 前端取值,格式化日期,三个时间区间判断
+ 未到
+ 正在秒杀
+ 已过,不显示
+ 首页跳转

秒杀业务(登录检查、)

登录检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
+ 处于秒杀阶段商品,加入购物车改为立即抢购
+ 秒杀前端跳转请求
+ 登录判断
+ 参数
+ 场次_商品id
+ 随机码(令牌)
+ 件数
+ 处理秒杀请求
+ controller
+ 登录拦截
+ 引入spring-session
+ 序列化器等配置
+ 存储类型为redis
+ 引入登录拦截器
+ 注册拦截器

秒杀流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
第一套流程:
+ 购物车——订单确认页——提交订单结算
+ 多个服务都收到高并发流量
+ 同时也错峰了一部分流量
+ 融合和兼容秒杀购买与正常购买流程

第二套流程:
+ 点击秒杀,合法校验通过直接获取信号量
+ 发送消息给MQ,直接返回秒杀成功
+ 订单等其他服务慢慢处理MQ请求
+ 订单创建完成,用户确认结算即可
+ 优点
+ 流程快速,除了创建订单无需操作数据库、远程调用等
+ 订单服务需要高可用
+ 整个业务独立

image-20220304224859675

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
@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:"; //+商品随机码

@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;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
+ Service kill
+ 登录判断 √
+ 合法性校验
+ 获取当前秒杀商品的详细信息
+ redis根据killid 获取sku信息
+ json转为To数据
+ 判断当前时间是否为秒杀时间
+ 比较开始结束时间
+ 判断随机码是否正确
+ 判断购物数量是否合适
+ 判断是否重复秒杀(幂等性)
+ 到redis站位,setnx
+ 指定key:userid_sessionid_skuid
+ 拦截器获取user信息
+ 设置自动过期时间(当前场次结束时间)
+ 结束时间减去当前时间作为ttl
+ 站位成功则说明未秒杀
+ 获取分布式信号量
+ 根据购买件数扣减信号量
+ 调用tryAcquire(尝试性获取,非阻塞)
//+ 100毫秒尝试时间
+ 无需等待
+ 拿到信号量则成功秒杀
+ 快速下单
+ 创建订单号
+ 发送消息给mq
+ 引入rabbitmq
+ 序列化、可靠消息等配置
+ 秒杀订单To
+ 发送消息
+ 直接返回订单号
+ 订单服务
+ 队列、binding
+ 监听秒杀队列
+ 创建秒杀单,保存订单信息
+ 保存订单项信息(订单关联sku表)
+ TODO 其他

2、Sentinel、Sleuth + Zipkin

Sentinel:流控、熔断降级、系统负载保护

image-20220304230038114image-20220304230504257

熔断:

A 服务调用 B 服务的某个功能,由于网络不稳定问题,或者 B 服务卡机,导致功能时 间超长。如果这样子的次数太多。我们就可以直接将 B 断路了(A 不再请求 B 接口),凡是 调用 B 的直接返回降级数据,不必等待 B 的超长执行。 这样 B 的故障问题,就不会级联影响到 A。 熔断是被调方多次故障,触发系统的主动保护规则。

降级:

整个网站处于流量高峰期,服务器压力剧增,根据当前业务情况及流量,对一些服务和 页面进行有策略的降级[停止服务,所有的调用直接返回降级数据]。以此缓解服务器资源的 的压力,以保证核心业务的正常运行,同时也保持了客户和大部分客户的得到正确的相应。

流量控制几个角度:

资源的调用关系,例如资源的调用链路,资源和资源之间的关系;

运行指标,例如 QPS、线程池、系统负载等;

控制的效果,例如直接限流、冷启动、排队等。

Sentinel相比Hystrix:

1
2
3
4
5
6
7
8
+ Sentinel相比Hystrix:
+ 隔离策略
+ 线程池隔离
+ 为请求分配自带线程池
+ 优点
+ 线程池之间隔离,池子炸了与其他请求无关
+ 信号量隔离
+ 为请求分配信号量

sentinel-定义资源

1
2
3
4
5
6
7
8
9
10
11
12
+ 定义resource
+ 主流框架默认适配
+ 抛出异常方式定义资源
+ 返回布尔值方式定义资源
+ 注解方式定义资源(可配置回调)
+ 异步调用支持
+ 自定义定义资源
+ try-catch
+ 基于注解
+ 调用链路
+ 自定义blockhandler:限流、降级、系统保护时调用
+ fallback:处理异常

sentinel-限流模式

1
2
3
4
5
6
7
8
9
+ 限流模式
+ 链路限流模式
+ 只统计某个入口进入的流量
+ 关联模式
+ 其他人流量大限制自己
+ 直接拒绝
+ warm up
+ 预热:指定时间内将流量慢慢增加到限流阈值
+ 排队等待

Sentinel-熔断降级

1
2
3
4
5
6
7
8
9
10
11
+ 熔断降级
+ 限流是对请求进行流控
+ 远程调用需要用熔断降级进行保护
+ 调用方熔断保护
+ 开启feign-sentinel
+ 对调用方接口加入fallback
+ 实现feign接口
+ 实现fallback方法
+ 指定降级策略
+ 远程服务(服务提供方)降级保护
+ 流量过大时全局考虑,将某些服务提供方降级处理

Sentinel-网关流控

image-20220304231223789

1
2
3
4
5
6
7
8
9
+ 网关限流功能
+ 引入sentinel与gateway适配整合
+ 网关页面
+ 流控
+ api分组(对整个组进行流控设置)
+ 定制网关流控返回
+ Mono Webflux
+ 响应式编程
+ 天然支持高并发系统

Sleuth + Zipkin:链路追踪

为什么用

微服务架构是一个分布式架构,它按业务划分服务单元,一个分布式系统往往有很多个服务 单元。由于服务单元数量众多,业务的复杂性,如果出现了错误和异常,很难去定位。主要 体现在,一个请求可能需要调用很多个服务,而内部服务的调用复杂性,决定了问题难以 定位。所以微服务架构中,必须实现分布式链路追踪,去跟进一个请求到底有哪些服务参与, 参与的顺序又是怎样的,从而达到每个请求的步骤清晰可见,出了问题,很快定位

链路追踪组件有 Google 的 Dapper,Twitter 的 Zipkin,以及阿里的 Eagleeye (鹰眼)等,它们都是非常优秀的链路追踪开源组件。

基本术语

Span(跨度):基本工作单元,发送一个远程调度任务 就会产生一个 Span,Span 是一 个 64 位 ID 唯一标识的,Trace 是用另一个 64 位 ID 唯一标识的,Span 还有其他数据信息,比如摘要、时间戳事件、Span 的 ID、以及进度 ID。

Trace(跟踪):一系列 Span 组成的一个树状结构。请求一个微服务系统的 API 接口, 这个 API 接口,需要调用多个微服务,调用每个微服务都会产生一个新的 Span,所有 由这个请求产生的 Span 组成了这个 Trace。

Annotation(标注):用来及时记录一个事件的,一些核心注解用来定义一个请求的开 始和结束 。这些注解包括以下:

  • cs - Client Sent -客户端发送一个请求,这个注解描述了这个 Span 的开始

  • sr - Server Received -服务端获得请求并准备开始处理它,如果将其 sr 减去 cs 时间戳 便可得到网络传输的时间。

  • ss - Server Sent (服务端发送响应)–该注解表明请求处理的完成(当请求返回客户 端),如果 ss 的时间戳减去 sr 时间戳,就可以得到服务器请求的时间。

  • cr - Client Received (客户端接收响应)-此时 Span 的结束,如果 cr 的时间戳减去 cs 时间戳便可以得到整个请求所消耗的时间。

Sleuth原理

image-20220304231626939

1
2
3
4
5
+ 每一个服务一个span
+ trace id
+ span id
+ annotation标注
+ 用来计算时间

Sleuth + Zipkin

image-20220304231702938

链路追踪

image-20220304231828856