秒杀产品的产品分析及业务实现

前言

分析设计当前各平台的秒杀活动。

实现商品秒杀功能(缓存预热库存\随机码\布隆过滤器,检查重复购买和防止超卖,生成订单,消息队列,流控)

一、秒杀业务分析

(一) 秒杀模块

  • 背景:用少量低价商品进行引流(低价引流)。

  • 核心问题:

    • 业务问题:抢购流程
      • 进入商品列表
      • 点击抢购按钮
      • 进入商品详情
      • 选择商品规格
      • 添加购物车或者直接购买
      • 生成订单
      • 跳转支付页面
      • 发送消息至配送系统
  • 技术问题:

    • 架构设计
    • 技术栈设计

(二) 业务需求分析

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

五、总结

以上就是今天要讲的内容,本文介绍了秒杀业务的产品分析、业务实现。其中包括数据层、业务层、实现层。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值