共计 4615 个字符,预计需要花费 12 分钟才能阅读完成。
自从上次整顿了秒杀零碎的文章(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…