文章目录
前言
分析设计当前各平台的秒杀活动。
实现商品秒杀功能(缓存预热库存\随机码\布隆过滤器,检查重复购买和防止超卖,生成订单,消息队列,流控)
一、秒杀业务分析
(一) 秒杀模块
-
背景:用少量低价商品进行引流(低价引流)。
-
核心问题:
- 业务问题:抢购流程
- 进入商品列表
- 点击抢购按钮
- 进入商品详情
- 选择商品规格
- 添加购物车或者直接购买
- 生成订单
- 跳转支付页面
- 发送消息至配送系统
- 业务问题:抢购流程
-
技术问题:
- 架构设计
- 技术栈设计
(二) 业务需求分析
1.需求分析
角色及职责
(1)用户端:使用系统的用户
(2)产品端:设计产品的产品经理、产品专员
(3)开发端:基于需求进行产品落地的软件开发人员
(4)测试端:基于需求进行功能测试
(5)运维端:将开发好的项目部署到服务器上或云端
2.功能性需求
(1) 前端(用户端)
- 浏览商品
- 活动场次:场次时间、场次状态
- 商品列表: 商品名称、商品图片、商品价格、商品库存(原价、活动价)和秒杀按钮
- 购买商品
- 用户认证
- 会员注册
- 会员登录
(2)后端(管理端)
-
主题管理:id、标题、开始日期、结束日期、状态、备注、创建时间、创建用户。
-
场次管理:id、场次名称、所属主题、开始时间、结束时间、状态、创建时间、创建用户。
-
商品管理:id、商品id、主题id、场次id、标题、图片、原价、秒杀价、商家id、状态、库存数量、备注、创建时间、创建用户。
-
订单管理:id、商品id、订单金额、会员id、支付时间、状态、收货地址、联系电话、收货人、交易流水id、创建时间。
-
用户管理:id、用户名、密码、昵称、头像、等级id、手机号、邮箱、出生日期、城市、职业、签名、用户来源(手机端或电脑端)、积分(登陆积分、购买积分、分享积分)、创建时间、修改时间、登录次数、最后登录ip、最后登录时间。
3.非功能需求
非功能需求
-
高可用:全年365天每天24小时都可以访问,不能因为个别服务器的异常,导致整个项目的瘫痪
- 服务冗余
- 服务拆分
- 限流降级
- 超时重试
- 压力测试
- 应急预案
-
高性能:当用户访问服务器时,响应速度要尽量的快,即使并发高,也要有正常的响应速度
-
高扩展:能否在流量高峰内,短时间内完成扩容,从而更平稳地承接峰值流量
- 业务扩展
- 架构伸缩
-
系统安全
- 网络安全:防火墙、防DDOS、数据链路安全(SSL、VPN)等
- 应用安全:防CSRF、防XSS、防SQL注入
CSRF(Cross-site request forgery):跨站请求伪造,攻击者通过伪造用户的浏览器的请求,向访问一个用户自己曾经认证访问过的网站发送出去,使目标网站接收并误以为是用户的真实操作而去执行命令。常用于盗取账号、转账、发送虚假消息等。
XSS攻击:通过利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。- 终端安全:数据传输加密、APP加固
- 服务器安全:机房环境安全、物理安全、系统安全
- 数据库安全 :保证机密性、完整性、可用性,数据保护做到敏感数据“看不见”、核心数据“拿不走”、运维操作“能审计”
-
数据一致
- 对于超卖现象采用分布式锁来应对
- 对于重复下单采用幂等性设计来应对
(三) 领域建模
1.战略和战术建模
- 战略建模
- 领域模型
- 用户界面层:用户界面、管理员界面
- 应用层接口:用户接口、商品接口、订单接口
- 领域模型层:用户中心、商品中心、订单中心
- 基础设施层:MySQL、Redis、MQ
- 核心域和非核心域
- 核心域:主题管理、场次管理
- 非核心域:商品管理、库存管理、用户管理
- 领域业务边界和上下文
- 核心域上下文:主题、场次
- 支撑子域
- 商品上下文:基本属性、销售属性
- 库存上下文:实际库存、销售库存
- 通用域上下文:用户账号信息
- 领域模型
2.战术建模
分析活动领域中各个对象的类型
战术建模:从具体细节上构建领域模型,对战略建模中限界上下文的具体实现。
(四) 架构设计
使用架构五视图法
- 逻辑视图:着重考虑功能需求,系统应该向用户提供怎样的服务
- 开发视图:开发质量、组织方式
- 运行视图:高可用、高性能、高扩展、高安全、协议、线程并发及通讯
- 数据视图:数据需求、数据关系(例如E-R图)
- 物理视图:软件的安装和部署
(五) 软件介绍
Redis
Redis 是开源的高性能key-value非关系缓存数据库,使用 C 语言写成的,支持存储string(字符串)、list(链表)、set(集合)、zset(sorted set 有序集合)和 hash(哈希类型)作为value 的类型,Redis的操作是原子性的。
Redis的数据是基于缓存的,可以实现数据写入磁盘中,保证了数据的安全不丢失。
Sentinel
背景:
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。为了保证服务器运行的稳定性,在请求数到达设计最高值时,将过剩的请求限流,保证在设计的请求数内的请求能够稳定完成处理。
简介:
Sentinel是Spring Cloud Alibaba的组件,以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
作用:
- 完备的实时状态监控。
支持显示当前项目各个服务的运行和压力状态,并分析出每台服务器处理的秒级别的数据 - 广泛的开源生态。
很多技术可以和Sentinel进行整合,SpringCloud,Dubbo,而且依赖少配置简单 - 完善的SPI扩展。
Sentinel支持程序设置各种自定义的规则
用途: 双11,秒杀,12306抢火车票
二、秒杀业务准备
利用任务调度工具Quartz在指定的时间进行缓存预热准备工作。
第一方面:
在秒杀开始前的指定的时间,Redis进行缓存预热,将每个sku参与秒杀的库存数保存在Redis中,而且为了避免黄牛通过技术手段频繁访问,可以生成一个随机码,也保存在Redis中,用于验证是否为正常链接购买秒杀商品。
第二方面:
在每个批次秒杀开始前,将本批次所有秒杀商品的spuId保存在布隆过滤器中,减少缓存穿透的情况。
(一) 查询秒杀商品列表
代码部分:
持久层
- 创建包mapper,再创建SeckillSpuMapper
@Reposity
public interface SeckillSpuMapper{
//查询秒杀商品列表的方法
List<SeckillSpu> findSeckillSpus();
}
- 创建SckillSpuMapper.xml
<!-- 秒杀spu表的sql语句字段的片段 -->
<sql id="SimpleField">
<if test="true">
id,
spu_id,
list_price,
end_time,
gmt_create,
gmt_modified
</if>
</sql>
<!-- 查询秒杀商品列表的方法 -->
<select id="findSeckillSpus" resultMap="BaseResultMap">
SELECT
<include refid="SimpleField"/>
FROM
sekill_spu
</select>
业务逻辑层
- 创建包service.impl,再创建类SeckillSpuSerivceImpl实现ISeckillSpuService
@Service
@Slf4j
public class SeckillSpuServiceImpl implements ISeckillSpuService{
// 装配查询秒杀Spu列表的Mapper
@Autowired
private SeckillSpuMapper seckillSpuMapper;
@Dubbo
private IForSeckillSpuService dubboSeckillSpuService;
PageHelper.startPage(page,pageSize);
List<SeckillSpu> seckillSpus=seckillSpuMapper.findSeckillSpus();
List<SeckillSpuVO> seckillSpuVOs=new ArrayList<>();
for(SeckillSpu seckillSpu: seckillSpus){
Long spuId=seckillSpu.getSpuId();
SpuStandardVO spuStandVO=dubboSeckillSpuService.getSpuById(spuId);
SpuStandVO spuStandVO =new SpuStandVO();
BeanUtils.copyProperties(spuStandardVO,secillSpuVO);
secillSpuVO.setSeckillListPrice(seckillSpu.getListPrice());
seckillSpuVO.setStartTime(seckillSpu.getStartTime());
seckillSpuVO.setEndTime(seckillSpu.getEndTime());
seckillSpuVOs.add(seckillSpuVO);
}
return JsonPage.restPage(new PageInfo<>(seckillSpuVOs));
}
@Override
public SeckillSpuVO getSeckillSpu(Long spuId) {
return null;
}
@Override
public SeckillSpuDetailSimpleVO getSeckillSpuDetail(Long spuId) {
return null;
}
}
控制层
- 创建包controller,创建SeckillSpuController类
@RestController
@RequestMapping("/seckill/spu")
@Api(tags="秒杀Spu模块")
public class SeckillSpuController {
@Autowired
private ISeckillSpuService seckillSpuService;
@GetMapping("/list")
@ApiOperation("分页查询秒杀Spu商品列表)
@ApiImplicitParams({
@ApiImplicitParam(value = "页码",name = "page",example = "1"),
@ApiImplicitParam(value = "每页条数",name = "pageSize",example = "10")
})
public JsonResult<JsonPage<SeckillSpuVO>> listSeckillSpus(
Integer page,Integer pageSize){
JsonPage<SeckillSpuVO> jsonPage=seckillSpuService.listSeckillSpus(page,pageSize)l
return JsonResult.ok(jsonPage);
}
}
三、业务实现
在秒杀开始,用户在秒杀商品的规定时间内可以查询秒杀商品详情。
所有秒杀商品spu查询时,都先查询布隆过滤器是否包含这个spuId。如果包含,则允许访问;如果不包含,则抛出异常。同时,也要考虑布隆过滤器误判的情况。
每当业务中查询spu和sku时,都需要先检查Redis中是否包含这个数据。如果包含直接从redis中获得数据;如果不包含,则再从数据库中查。
但同时也要注意,查询完毕后要保存到Redis中,以便之后的查询直接从Redis中获取。在保存到Redis时,为了减少缓存雪崩的几率,我们为每个Spu和Sku对象都添加了过期时间随机数(RandCode)。
查询返回前,可以在判断一下当前时间是否在可秒杀该商品的时间段内,如果不在秒杀时间段内,则抛出异常。
只有返回了完整信息,前端才可能获得包含随机码的提交路径,否则是无法完成正常连接购买的。
(一) 根据SpuId查询秒杀Sku列表信息
将秒杀的商品Spu列表查询出来。当用户选择一个商品时,我们要将这个商品的sku也查询出来,也就是根据SpuId查询Sku的列表。
代码部分:
- 创建SeckillSkuMapper
@Repository
public interface SeckillSkuMapper {
// 根据spuId查询sku列表
List<SeckillSku> findSeckillSkusBySpuId(Long spuId);
}
- 创建SeckillSkuMapper.xml
<sql id="SimpleField">
<if test="true">
id,
sku_id,
spu_id,
seckill_stock,
seckill_price,
gmt_create,
gmt_modified,
seckill_limit
</if>
<!-- 根据spuId查询sku列表 -->
<select id="findSeckillSkusBySpuId" resultMap="BaseResultMap">
select
<include refid="SimpleField" />
from
seckill_sku
where
spu_id=#{spuId}
</select>
(二) 创建流控和降级的处理类
秒杀业务是一个高并发的处理,并发数超过程序设计的限制时,就需要对请求的数量进行限流。
-
创建exception包并新建SeckillBlockHandler
@Slf4j public class SeckillBlockHandler { public static JsonResult seckillBlock(String randCode, SeckillOrderAddDTO seckillOrderAddDTO, BlockException e){ log.error("一个请求被限流!"); return JsonResult.failed(ResponseCode.INTERNAL_SERVICE_ERROR, "服务器繁忙!"); } }
-
在exception包中新建降级类SeckillFallBack
@Slf4j public class SeckillFallBack { public static JsonResult seckillFallback(String randCode, SeckillOrderAddDTO seckillOrderAddDTO, BlockException e){ log.error("一个请求被降级!"); e.printStackTrace(); return JsonResult.failed(ResponseCode.INTERNAL_SERVICE_ERROR, "发生异常,异常信息为:"+e.getMessage()); } }
-
在service包新建业务逻辑层SeckillServiceImpl
@Service @Slf4j public class SeckillServiceImpl implements ISeckillService { @Autowired private StringRedisTemplate stringRedisTemplate; @Autowired private IOmsOrderService dubboOrderService; @Autowired private RabbitTemplate rabbitTemplate; @Override public SeckillCommitVO commitSeckill(SeckillOrderAddDTO seckillOrderAddDTO) { // 获取用户想要购买的skuId Long skuId=seckillOrderAddDTO.getSeckillOrderItemAddDTO().getSkuId(); // 获取登陆用户的id Long userId=getUserId(); // 根据用户(userId)选择购买某个商品(skuId)生成Key,用来检查是否重复购买 String reSeckillCheckKey=SeckillCacheUtils.getReseckillCheckKey(skuId,userId); Long seckillTimes =stringRedisTemplate.boundValueOps(reSckillCheckKey).increment(); //值为1可以购买,大于1则不能重复购买 if(seckillTimes>1){ throw new CoolSharkServiceException(ResponseCode.FORBIDDEN, "您已经购买过这个商品了,谢谢您的支持!"); } String skuStockKey=SeckillCacheUtils.getStockKey(skuId); Long leftStock=stringRedisTemplate.boundValueOps(skuStockKey).decrement(); if(leftStock<0){ stringRedisTemplate.boundValueOps(reSckillCheckKey).decrement(); throw new CoolSharkServiceException(ResponseCode.BAD_REQUEST, "对不起,您要购买的商品暂时售罄"); } OrderAddDTO orderAddDTO=convertSeckilllOrderToOrder(seckillOrderAddDTO); orderAddDTO.setUserId(userId); OrderAddVO orderAddVO=dubboOrderService.addOrder(orderAddDTO); Success success=new Success(); BeanUtils.copyProperties(seckillOrderAddDTO.getSeckillOrderItemAddDTO(), success); success.setUserId(userId); success.setOrderSn(orderAddVO.getSn()); success.setSeckillPrice( seckillOrderAddDTO.getSeckillOrderItemAddDTO().getPrice()); rabbitTemplate.convertAndSend( RabbitMqComponentConfiguration.SECKILL_EX, RabbitMqComponentConfiguration.SECKILL_RK, success); SeckillCommitVO commitVO=new SeckillCommitVO(); BeanUtils.copyProperites(orderAddVO,commitVO); return commitVO; } private OrderAddDTO converSeckillOrderToOrder( SeckillOrderAddDTO seckillOrderAddDTO ){ OrderAddDTO orderAddDTO =new OrderAddDTO(); BeanUtils.copyProperites(seckillOrderAddDTO,orderAddDTO); OrderItemAddDTO orderItemAddDTO =new OrderItemAddDTO(); BeanUtils.copyProperites( seckillOrderAddDTO.getSeckillOrderItemAddDTO(),orderItemAddDTO); List<OrderItemAddDTO> list=new ArrayList<>(); // 赋值完毕的普通订单项对象添加到list集合中 list.add(orderItemAddDTO); // 普通订单项集合赋给OrderAddDTO orderAddDTO.setOrderItems(list); return orderAddDTO; } public CsmallAuthenticationInfo getUserInfo(){ UsernamePasswordAuthenticationToken authenticationToken= (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); if(authenticationToken == null){ throw new CoolSharkServiceException( ResponseCode.UNAUTHORIZED,"请登录!"); } CsmallAuthenticationInfo csmallAuthenticationInfo= (CsmallAuthenticationInfo) authenticationToken.getCredentials(); return csmallAuthenticationInfo; } public Long getUserId(){ return getUserInfo().getId(); } }
-
在controller包中新建SeckillController
@RestController @RequestMapping("/seckill") @Apid(tags="执行秒杀模块") public class SeckillController { @Autowired private ISeckillService seckillService; @Autowired private RedisTemplate redisTemplate; @PostMapping("/{randCode}") @ApiOperation("验证随机码并提交秒杀订单") @ApiImplicitParm(value="随机码",name="randCode",required=true) @PreAuthorize("hasRole('user')") @SentinelResource(value = "seckill", blockHandlerClass = SeckillBlockHandler.class,blockHandler = "seckillBlock", fallbackClass = SeckillFallBack.class,fallback = "seckillFallback") public JsonResult<SeckillCommitVO> commitSeckill( @PathVariable String randCode,@Validated SeckillOrderAddDTO seckillOrderAddDTO){ Long spuId=seckillOrderAddDTO.getSpuId(); String randCodeKey=SeckillCacheUtils.getRandCodeKey(randCode); if(redisTemplate.hasKey(randCodeKey)){ String redisRandCode= redisTemplate.boundValueOps(randCodeKey).get()+""; if(!redisTemplate.equals(randCode)){ throw new CoolSharkServiceException(ResponseCode.NOT_FOUND, "没有找到指定商品(随机码不正确)"); } SeckillCommitVO CommitVO=seckillSevice.commitSeckill(seckillOrderAddDTO); return JsonResult.ok(commitVO); }else{ throw new CoolSharkServiceException(ResponseCode.NOT_FOUND, "没有找到指定商品"); } } }
四、 success秒杀成功信息的处理
在用户购买秒杀商品时,保证用户登录的前提下,验证用户是否重复秒杀(业务要求秒杀相同商品只能购买一次),使用userId和skuId向Redis中保存一个key,如果没有这个key,表示该用户没有参加秒杀过,否则将发生异常提示。
要保证用户进行购买时,该商品有库存。减少库存后,获得剩余库存信息,只要剩余库存不小于0,才能为该用户生成订单,否则将发生异常。生成订单直接Dubbo调用Order模块编写的生成订单的方法。
订单提交后,还需要修改秒杀sku库存数和生成秒杀成功记录保存在数据库。但是这个业务非迫切运行,可以将信息发送给消息队列,进行削峰填谷。然后,再编写接收消息队列的代码,完成修改秒杀库存和生成秒杀成功记录的操作。
在控制层方法上添加注解实现Sentinel的限流,保证这个业务在非常大的并发下,也能稳定运行。控制器方法中还要判断用户请求路径中的随机码,是否和Redis中保存的随机码一致,防止非正常链接购买
代码部分:
-
在SeckillSkuMapper中。
int updateReduceStockBySkuId(@Param("skuId") Long skuId, @Param("quantity") Integer quantity);
-
SeckillSkuMapper.xml
<update id="updateReduceStockBySkuId"> UPDATE seckill_sku SET seckill_stock=seckill_stock-#{quantity} WHERE sku_id=#{skuId} </update>
-
SuccessMapper
@Reposiory public interface SuccessMapper{ int saveSuccess(Success success); }
-
创建编写SuccessMapper.xml
<insert id="saveSuccess">
INSERT INTO
success(
user_id,user_phone,sku_id,title,
main_picture,seckill_price,quantity,bar_code,data,order_sn)
VALUES(
#{userId},#{userPhone},#{skuId},#{title},#{mainPicture},
#{seckillPrice},#{quantity},#{barCode},#{data},#{orderSn}
)
</insert>
-
创建consumer包并新建SeckillQueueConsumer
@Component @RabbitListener(queues=RabbitMqComponentConfiguration.SECKILL_QUEUE) public class SeckillQueueConsumer(){ @Autowired private SeckillSkuMapper skuMapper; @Autowired private SuccessMapper successMapper; @RabbitHandler public void process(Success success){ skuMapper.updateReduceStockBySkuId( success.getSkuId(),success.getQuantity()); successMapper.saveSuccess(success); } }
五、总结
以上就是今天要讲的内容,本文介绍了秒杀业务的产品分析、业务实现。其中包括数据层、业务层、实现层。