5

竞拍系统设计(秒杀系统知识迁移)

 3 years ago
source link: https://segmentfault.com/a/1190000039796542
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

竞拍系统设计(秒杀系统知识迁移)

发布于 4 月 8 日

自从上次整理了秒杀系统的文章(php+golang商品秒杀)后,知识迁移一新项目,商品竞拍。

技术:php、mysql、redis、laravel业务对象:商品、场次、订单竞拍过程:

一、实现商品、竞拍场次和订单的CRUD;
二、定时将秒杀场次、商品、库存等信息提前写入redis;
三、配置Redis持久化;
四、实现秒杀下单逻辑;
五、秒杀过程redis优化;
六、使用golang并发编程模拟秒杀。

一、实现商品、竞拍场次和订单的CRUD;

 CREATE TABLE `goods` (
  `id` int(12) unsigned NOT NULL AUTO_INCREMENT COMMENT 'pk',
  `num` varchar(64) NOT NULL COMMENT '商品编号',
  `users_id` int(12) unsigned NOT NULL COMMENT '拥有者',
  `create_users_id` int(12) unsigned NOT NULL COMMENT '商品创建人',
  `contract_roles_id` int(10) unsigned NOT NULL COMMENT '商品合约级别外键',
  `name` varchar(255) NOT NULL COMMENT '商品名称',
  `img` int(11) NOT NULL COMMENT '封面图',
  `price` decimal(10,2) unsigned NOT NULL COMMENT '当前价格',
  `area_id` int(11) NOT NULL COMMENT '区域id',
  `trade_num` int(11) unsigned NOT NULL COMMENT '交易次数',
  `user_name` varchar(100) DEFAULT NULL COMMENT '收货人名称',
  `user_phone` varchar(11) DEFAULT NULL COMMENT '收货人联系电话',
  `user_address` varchar(255) DEFAULT NULL COMMENT '收货人地址',
  `express_id` int(11) DEFAULT NULL COMMENT '物流ID',
  `express_no` varchar(255) DEFAULT NULL COMMENT '物流单号',
  `is_auction` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否可竞拍,1=》可 2=》不可',
  `status` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '状态1=>可交易 2=>待支付 3=>交易完成 4=>待发货 5=》配送中 6=>完成 7 =>待收款',
  `next_time` timestamp NULL DEFAULT NULL COMMENT '下次最早显示时间',
  `trade_time` timestamp NULL DEFAULT NULL COMMENT '下次可交易时间',
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_at` timestamp NULL DEFAULT NULL COMMENT '更新时间',
  `deleted_at` timestamp NULL DEFAULT NULL COMMENT '删除时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=111 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

竞拍场次表:

CREATE TABLE `auctions` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `area` tinyint(4) NOT NULL COMMENT '拍卖区域,1=>新手区,2=>竞拍区,3=>星级区',
  `name` varchar(64) DEFAULT NULL COMMENT '场次名称',
  `start` time NOT NULL COMMENT '开始时间',
  `end` time NOT NULL COMMENT '结束时间',
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_at` timestamp NULL DEFAULT NULL COMMENT '更新时间',
  `deleted_at` timestamp NULL DEFAULT NULL COMMENT '删除时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='拍卖场次表';
CREATE TABLE `orders` (
  `id` int(12) unsigned NOT NULL AUTO_INCREMENT COMMENT 'pk',
  `serial_num` varchar(32) DEFAULT NULL COMMENT '流水号,没交易前为空',
  `goods_id` int(12) unsigned NOT NULL COMMENT '商品id',
  `sell_users_id` int(12) unsigned NOT NULL COMMENT '竞拍商品拥有者id',
  `buy_users_id` int(12) unsigned DEFAULT NULL COMMENT '购买商品用户id',
  `buy_price` decimal(10,2) NOT NULL COMMENT '购买价格-成本价格',
  `pay_time` datetime DEFAULT NULL COMMENT '支付时间',
  `status` char(5) NOT NULL COMMENT '状态10000=>待支付  20000=>支付超时 30000=>确认支付 30001=>确认收款 40000=>卖家申诉中 40001=>买家申诉中 45000=>申诉完成 50000=>完成',
  `contract_roles_id` int(10) NOT NULL COMMENT '购买时商品合约外键',
  `charge_rate` decimal(10,4) unsigned DEFAULT NULL COMMENT '手续费',
  `remark` varchar(255) DEFAULT NULL COMMENT '备注-可以填写申诉结果',
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_at` timestamp NULL DEFAULT NULL COMMENT '更新时间',
  `deleted_at` timestamp NULL DEFAULT NULL COMMENT '删除时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=23 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

二、定时提前写入redis

1、竞拍场次时间是为每天固定的三个时间,定时提前写入并设置过期时间。

2、缓存数据结构设计有两个版本:

a、第一个版本的数据结构设计在商品列表查询时,无法排除自身商品信息并且分页。

  • 不同区域可秒杀的用户set (判断用户所属竞拍区)

      key: prefix + area_id + start + end + auctions_id, 
      value:uid 
    
  • 不同区域下的商品信息zset (可支持分页)

      key: prefix + area_id + start + end + auctions_id, 
      score:goods_id, 
      member:goods_detail
    
  • 库存字面量

    key: random  
    value:1 
    
  • 是否购买占位

    key: prefix + area_id + start + end  
    value:1 
    

为了满足排除自身的商品功能和分页,思考了一些实现方案:
(1) 完全放弃从缓存中获取竞拍商品信息,这样增加数据库压力,同时无法使用竞拍随机码。
(2)为每个用户单独存放一个排除自身商品信息的集合,这样会存放重复数据造成增加内容空间。
(3)查询到redis有一个SCAN命令来迭代获取数据,并可利用glob模式匹配,但是获取数量无法确定而无法分页。
以上(1)(3)点都被排除,我们从第(2)出发重新设计第二版数据结构,单独存放商品数据和用户可查询的商品id集合来减少重复,但又会出现keys过多的情况,需要进行优化。

b.第二个版本的数据结构设计

  • 用户可查询的商品id的zset (判断用户是否有可竞拍商品)
 key: prefix + area_id +users_id + auction_id+ start + end, 
 score:goods_id, 
 member:goods_id 
  • 商品信息string (可支持分页)
 key: prefix + area_id + auction_id + goods_id + start + end, 
 value:goods
  • 库存字面量
 key: random  
 value:1 
  • 是否购买占位
  key: prefix + area_id + start + end  
  value:1 

redis缓存数据

三、配置Redis持久化

持久化两种模式都开启:RDB(快照模式)+ AOF(日志模式)
配置文件:save/append_only
区别:两者数据保存间隔周期不同,RDB存储间隔大于AOF存储间隔

四、实现秒杀下单逻辑

1、查询场次和当前秒杀商品
查询redis中的缓存数据,当并发量大时可能出现:
缓存穿透:key值不存在,重复请求压垮数据库 => 布隆过滤器或设置缓存为空。
缓存击穿:key值存在但是失效,需重新请求数据库造成并发问题 => SETNX锁
缓存雪崩:缓存重启或集中失效,则都请求往DB => 过期时间设置分散

2、正式竞拍是单独的秒杀下单功能。

3、具体的下单逻辑:
登录校验 => 秒杀过程校验 => 通过队列进行异步下单同时返回订单号orderSN
秒杀过程中校验点如下:

秒杀时间:是否在秒杀时间内;
用户是否在该区有可竞拍商品
随机码:商品是否可秒杀;
是否已购买过:通过redis的SETNX设置Key=场次id_商品id_用户id来判断是否购买过。
秒杀库存数量:在获取对应库存信息前,将随机码作为key设置SETNX来实现并发锁,设置超时时间,秒杀成功或失败都释放该锁。

五、秒杀过程redis优化

因缓存数据结构的设计,可能会在redis存储大量的key,若通过keys命令查询会是O(n)复杂度,查询会卡顿而缓慢,redis有提供scan迭代来代替keys,但是根据本项目无需使用它。
优化大致有两个方面:
1、在提前将竞拍信息写入redis时,因key数量大,可采用redis的pipeline管道来提高写入效率
2、尽可能将场次和开始结束时间返回前端让其在查询或竞拍时传给后端,后端拼接key值获取数据的时间复杂度是O(1)。

六、使用golang并发编程模拟秒杀

图片请参考另外一篇文章:PHP+Golang 商品秒杀功能
================================================================

golang并发调度项目码云:

https://gitee.com/jasonlxs/se...


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK