文章目录
十五、商品秒杀
1.后台管理系统增加秒杀
- id: gulimall-coupon
uri: lb://gulimall-coupon
predicates:
- Path=/api/coupon/**
filters:
- RewritePath=/api/(?<segment>/?.*),/$\{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);
}
}
2.定时任务上架商品秒杀
例如:在每天的晚上11点,查询明天所有的秒杀商品
3.cron表达式
- 七位表达式:秒、分、时、日、月、周、年
- 周(日——六)对应数字 1——7
- 但SpringBoot 整合的cron 并不包含年,同时周(一——日)对应数字 1——7。
在线生成器:cron.qqe2.com
4.SpringBoot 整合 cron
SpringBoot 整合的 cron 只有六位字符。同时周(一——日)对应数字 1——7。
使用:
- 两个注解
任务的阻塞性:如果每秒执行一次任务,当当前任务阻塞时,后续的任务会在当前任务完成阻塞之后的一秒后开始执行。
解决方法:
-
可以让业务以异步的方式运行,自己提交到线程池
CompletableFuture.runAsync(() -> { xxxxService.xxx(); },executor);
-
默认只有一个线程池:
在有的spring版本中不生效
修改配置文件:
spring.task.scheduling.pool.size=5
-
@EnableAsync
、@Async
5.秒杀商品的查询
查询近三天的秒杀商品:
@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;
}
6.保存秒杀商品到缓存
@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);
});
});
}
}
7.分布式服务和缓存的幂等性
执行上面的代码发现,当定时任务被触发 redis 中回不断地缓存相同的数据,违背了缓存的幂等性
同时,如果不同的服务在相同时间定时任务被触发,也会向redis 中缓存相同的数据,所有需要引入分布式锁。
7.1 服务的幂等性:加分布式锁
@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();
}
}
}
7.2 活动信息、库存的幂等性
结果:
8.在首页展示秒杀商品
在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) {
$("<li οnclick='toDetail(" + item.skuId + ")'></li>").append($("<img style='width: 130px; height: 130px' src='" + item.skuInfo.skuDefaultImg + "' />"))
.append($("<p>"+item.skuInfo.skuTitle+"</p>"))
.append($("<span>" + item.seckillPrice + "</span>"))
.append($("<s>" + item.skuInfo.price + "</s>"))
.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;
}
9.商品详情页渲染
/**
* 返回商品详情页的秒杀信息
* @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;
}
如果当前商品处于秒杀中,显示秒杀价格,如果在以后的场次中,显示开始秒杀的时间。
10.商品秒杀流程
10.1 发送消息
商品模块获取到秒杀的各种信息:
前端绑定随机码等数据,发送给秒杀模块
秒杀模块秒杀商品:
@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;
}
}
10.2 消息队列设置
秒杀模块发送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);
}
}
测试:
十六、Sentinel 高并发
1.SpringCloud Aliababa Sentinel
1.1 熔断降级限流
-
什么是熔断
A 服务调用 B 服务的某个功能,由于网络不稳定问题,或者 B 服务卡机,导致功能时间超长。如果这样子的次数太多。我们就可以直接将 B 断路了(A 不再请求 B 接口),凡是调用 B 的直接返回降级数据,不必等待 B 的超长执行。 这样 B 的故障问题,就不会级联影响到 A。
-
什么是降级
整个网站处于流量高峰期,服务器压力剧增,根据当前业务情况及流量,对一些服务和页面进行有策略的降级[停止服务,所有的调用直接返回降级数据]。以此缓解服务器资源的的压力,以保证核心业务的正常运行,同时也保持了客户和大部分客户的得到正确的相应。
异同:
-
相同点:
- 为了保证集群大部分服务的可用性和可靠性,防止崩溃,牺牲小我
- 用户最终都是体验到某个功能不可用
-
不同点:
- 熔断是被调用方故障,触发的系统主动规则
- 降级是基于全局考虑,停止一些正常服务,释放资源
-
什么是限流
对打入服务的请求流量进行控制,使服务能够承担不超过自己能力的流量压力
1.2 Sentinel 简介
-
官方文档:https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D
-
项目地址:https://github.com/alibaba/Sentinel
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
-
Sentinel 具有以下特征:
- 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
- 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
- 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
- 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
-
Sentinel 分为两个部分:
- 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时 对 Dubbo / Spring Cloud 等框架也有较好的支持。
- 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。
-
Sentinel 基本概念
-
资源
资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提 供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。在接下来的文 档中,我们都会用资源来描述代码块。
-
-
只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下, 可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。
-
规则
围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规 则。所有规则可以动态实时调整。
Hystrix 与 Sentinel 比较
-
Hystric隔离是线程池隔离,对于某个请求如只允许50个线程并发访问,多的并发会被拒绝,多个请求对应多个线程池,这样会浪费线程池资源,增加服务器压力,而Sential使用的是类似redis的信号量
-
Sentinel 和 Hystrix 的原则是一致的: 当检测到调用链路中某个资源出现不稳定的表现,例
如请求响应时间长或异常比例升高的时候,则对这个资源的调用进行限制,让请求快速失败,
避免影响到其它的资源而导致级联故障。
2.官方文档 quick-start
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的使用主要包括这三步:
- 定义资源
- 定义规则
- 检验规则是否生效
3.SpringBoot 整合 Sentinel
官方文档:Sentinel · alibaba/spring-cloud-alibaba Wiki (github.com)
3.1 导入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
3.2 启动Sentinel 的控制台
在官网下载对应版本的 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,即每秒只能通过一个请求:
3.3 信息审计功能引入
前面的测试存在问题
- 在控制台调整限流的参数,都保存在内存中,重启失效
- 为了保证能够持久的保存限流规则,需要导入信息审计模块
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
新版已经不需要暴露端口了
实时监控:
3.4 自定义Sentinel 限流返回信息
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));
}
}
4.Sentinel 整合 feign
上面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());
}
}
其中遇到了一些版本问题的报错,一些参考解决方法:
5.自定义受保护的资源
1.try catch方法
try(Entry entry = SphU.entry("seckillSkus"))
设置了资源名为 seckillSku
,可以在控制台中熔断降级
2.注解@SentinelReource
设置资源名为getCurrentSeckillSkusResource
更多定义资源的方法可以参考官网:basic-api-resource-rule (sentinelguard.io)
6.网关流控
api-gateway-flow-control (sentinelguard.io)
导入依赖
<!-- Sentinel网关限流-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
<version>2.2.6.RELEASE</version>
</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;
/**
* <p>Title: SentinelGateWayConfig</p>
* 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;
});
}
}
7.SpringCloud Sleuth + Zipkin服务链路追踪
7.1 为什么用
- 定位问题微服务,更好的熔断降级
- 微服务架构是一个分布式架构,它按业务划分服务单元,一个分布式系统往往有很多个服务
单元。由于服务单元数量众多,业务的复杂性,如果出现了错误和异常,很难去定位。主要
体现在,一个请求可能需要调用很多个服务,而内部服务的调用复杂性,决定了问题难以
定位。所以微服务架构中,必须实现分布式链路追踪,去跟进一个请求到底有哪些服务参与,
参与的顺序又是怎样的,从而达到每个请求的步骤清晰可见,出了问题,很快定位。 - 链路追踪组件有 Google 的 Dapper,Twitter 的 Zipkin,以及阿里的 Eagleeye (鹰眼)等,它
们都是非常优秀的链路追踪开源组件。
7.2 基本术语
-
每经过调用一个微服务,更新span,记录cs、sr的时间戳
-
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 时间戳便可以得到整个请求所消耗的时间。
-
官方文档:https://cloud.spring.io/spring-cloud-static/spring-cloud-sleuth/2.1.3.RELEASE/single/spring-cloud-sleuth.html
如果服务调用顺序如下:
那么用以上概念完整的表示出来如下:
Span 之间的父子关系如下:
7.3 整合 Sleuth
1、服务提供者与消费者导入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
2、打开 debug 日志
logging:
level:
org.springframework.cloud.openfeign: debug
org.springframework.cloud.sleuth: debug
3、发起一次远程调用,观察控制台
DEBUG [user-service,541450f08573fff5,541450f08573fff5,false]
user-service:服务名
- 541450f08573fff5:是 TranceId,一条链路中,只有一个 TranceId
- 541450f08573fff5:是 spanId,链路中的基本工作单元 id
- false:表示是否将数据输出到其他服务,true 则会把信息输出到其他可视化的服务上观察
7.4 整合 zipkin 可视化观察
- 通过 Sleuth 产生的调用链监控信息,可以得知微服务之间的调用链路,但监控信息只输出 到控制台不方便查看。我们需要一个图形化的工具-zipkin。
- Zipkin 是 Twitter 开源的分布式跟 踪系统,主要用来收集系统的时序数据,从而追踪系统的调用问题。
- zipkin 官网地址: https://zipkin.io
1、docker 安装 zipkin 服务器
docker run -d -p 9411:9411 openzipkin/zipkin
2、导入
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</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。
7.5 Zipkin数据持久化
Zipkin 默认是将监控数据存储在内存的,如果 Zipkin 挂掉或重启的话,那么监控数据就会丢 失。所以如果想要搭建生产可用的 Zipkin,就需要实现监控数据的持久化。而想要实现数据 持久化,自然就是得将数据存储至数据库。好在 Zipkin
- 内存(默认)
- MySQL
- Elasticsearch
- Cassandra
Zipkin 数据持久化相关的官方文档地址如下: https://github.com/openzipkin/zipkin#storage-component
- Zipkin 支持的这几种存储方式中,内存显然是不适用于生产的,这一点开始也说了。
- 而使用 MySQL 的话,当数据量大时,查询较为缓慢,也不建议使用。
- Twitter 官方使用的是 Cassandra 作为 Zipkin 的存储数据库,
- 但国内大规模用 Cassandra 的公司较少,而且 Cassandra 相关文 档也不多。
综上,故采用 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