1、秒杀业务 秒杀具有瞬间高并发的特点,针对这一特点,必须要做限流 + 异步 + 缓存(页面静态化)+ 独立部署。
秒杀系统设计
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限流 + 直接负载部分请求到错误的静态页面 + 令牌算法 漏斗算法 + 网关限流 + 限流的过滤器 + 熔断、降级 + 调用链路异常,快速失败熔断 + 流量太大,引导部分请求降级 + 队列削峰(杀手锏) + 抢到信号量的放行后台 + 后台发送消息到队列,返回秒杀成功回调 + 订单服务等监听队列,慢慢创建订单处理成功秒杀的消息
秒杀商品上架(定时、异步、幂等性)、展示、渲染
定时任务&异步任务
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:商品秒杀数量
幂等性保证
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请求 + 订单创建完成,用户确认结算即可 + 优点 + 流程快速,除了创建订单无需操作数据库、远程调用等 + 订单服务需要高可用 + 整个业务独立
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 () { R lates3DaySession = couponFeignService.getLates3DaySession(); if (lates3DaySession.getCode() == 0 ) { List<SeckillSessionWithSkusVo> sessionData = lates3DaySession.getData("data" , new TypeReference<List<SeckillSessionWithSkusVo>>() { }); saveSessionInfos(sessionData); saveSessionSkuInfo(sessionData); } } private void saveSessionInfos (List<SeckillSessionWithSkusVo> sessions) { sessions.stream().forEach(session -> { long startTime = session.getStartTime().getTime(); long endTime = session.getEndTime().getTime(); String key = SESSION__CACHE_PREFIX + startTime + "_" + endTime; Boolean hasKey = redisTemplate.hasKey(key); if (!hasKey) { List<String> skuIds = session.getRelationSkus().stream() .map(item -> item.getPromotionSessionId() + "-" + item.getSkuId().toString()).collect(Collectors.toList()); redisTemplate.opsForList().leftPushAll(key,skuIds); } }); } private void saveSessionSkuInfo (List<SeckillSessionWithSkusVo> sessions) { sessions.stream().forEach(session -> { 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(); R info = productFeignService.getSkuInfo(skuId); if (info.getCode() == 0 ) { SkuInfoVo skuInfo = info.getData("skuInfo" ,new TypeReference<SkuInfoVo>(){}); redisTo.setSkuInfo(skuInfo); } BeanUtils.copyProperties(seckillSkuVo,redisTo); redisTo.setStartTime(session.getStartTime().getTime()); redisTo.setEndTime(session.getEndTime().getTime()); redisTo.setRandomCode(token); String seckillValue = JSON.toJSONString(redisTo); operations.put(seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString(),seckillValue); RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token); semaphore.trySetPermits(seckillSkuVo.getSeckillCount()); } }); }); } @SentinelResource(value = "getCurrentSeckillSkusResource",blockHandler = "blockHandler") @Override public List<SeckillSkuRedisTo> getCurrentSeckillSkus () { try (Entry entry = SphU.entry("seckillSkus" )) { long currentTime = System.currentTimeMillis(); Set<String> keys = redisTemplate.keys(SESSION__CACHE_PREFIX + "*" ); for (String key : keys) { String replace = key.replace(SESSION__CACHE_PREFIX, "" ); String[] s = replace.split("_" ); long startTime = Long.parseLong(s[0 ]); long endTime = Long.parseLong(s[1 ]); if (currentTime >= startTime && currentTime <= endTime) { 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); 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 ; } @Override public SeckillSkuRedisTo getSkuSeckilInfo (Long skuId) { BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX); Set<String> keys = hashOps.keys(); if (keys != null && keys.size() > 0 ) { String reg = "\\d-" + skuId; for (String key : keys) { if (Pattern.matches(reg,key)) { 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 ; } @Override public String kill (String killId, String key, Integer num) throws InterruptedException { long s1 = System.currentTimeMillis(); MemberResponseVo user = LoginUserInterceptor.loginUser.get(); 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) { String randomCode = redisTo.getRandomCode(); String skuId = redisTo.getPromotionSessionId() + "-" +redisTo.getSkuId(); if (randomCode.equals(key) && killId.equals(skuId)) { Integer seckillLimit = redisTo.getSeckillLimit(); String seckillCount = redisTemplate.opsForValue().get(SKU_STOCK_SEMAPHORE + randomCode); Integer count = Integer.valueOf(seckillCount); if (count > 0 && num <= seckillLimit && count > num ) { String redisKey = user.getId() + "-" + skuId; Long ttl = endTime - currentTime; Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS); if (aBoolean) { RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode); boolean semaphoreCount = semaphore.tryAcquire(num, 100 , TimeUnit.MILLISECONDS); if (semaphoreCount) { 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:流控、熔断降级、系统负载保护
熔断:
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-网关流控
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原理
1 2 3 4 5 + 每一个服务一个span + trace id + span id + annotation标注 + 用来计算时间
Sleuth + Zipkin
链路追踪