13

Since the last time I compiled the article on the spike system (php+golang commodity spike), a new project of knowledge migration, commodity auctions.

Technology: php, mysql, redis, laravel
Business objects: commodities, sessions, orders
Auction process:

1. Realize CRUD of commodities, auctions and orders;
2. Write the information such as the number of spikes, merchandise, and inventory into redis in advance;
Three, configure Redis persistence;
4. Realize the logic of placing an order in a spike;
5. Redis optimization during the spike process;
Sixth, use golang concurrent programming to simulate spikes.

1. Realize CRUD of commodities, auctions and orders;

Commodity table:

 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 '商品创建人',
  `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',
  `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;

Auction schedule:

CREATE TABLE `auctions` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `area` tinyint(4) NOT NULL,
  `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='拍卖场次表';

Order form:

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,
  `buy_users_id` int(12) unsigned DEFAULT NULL,
  `buy_price` decimal(10,2) NOT NULL COMMENT '购买价格',
  `pay_time` datetime DEFAULT NULL COMMENT '支付时间',
  `status` char(5) NOT NULL COMMENT '状态10000=>待支付  20000=>支付超时 30000=>支付完成 30001=>配送中 50000=>完成',
  `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;

2. Write redis in advance

1. The bidding times are written in advance and the expiration time is set.

2. There are two versions of cache data structure design:

a. The data structure design of the first version cannot exclude its own product information and pagination when querying the product list.

  • Set of users that can be killed in different areas (determine the auction area that the user belongs to)

      key: prefix + area_id + start + end + auctions_id, 
      value:uid 
    
  • Product information zset under different regions (support pagination)

      key: prefix + area_id + start + end + auctions_id, 
      score:goods_id, 
      member:goods_detail
    
  • Inventory literal

      key: random  
 * 是否购买占位  
 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缓存数据](/img/bVcQ83R)

**三、配置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 商品秒杀功能](https://segmentfault.com/a/1190000039349297)
##### ================================================================
golang并发调度项目码云:

sengerlion
55 声望401 粉丝

了解自己到细胞粒度。