自从上次整顿了秒杀零碎的文章(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长久化

长久化两种模式都开启: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...