商品秒杀并发场景,php如何调用mysql保证数据完整性?
发布于 作者:苏南大叔 来源:程序如此灵动~经过前几篇文章的铺垫,苏南大叔在本文里面描述经典的购物场景中的操作。实际的需求场景必然是比本文描述的要复制的多,并且代码也不会不使用各种框架。本文就是使用php
原生mysqli
扩展来读写mysql
的,只是为了说明事务在购物这种场景下的使用而已。实际的业务场景下,会使用redis
等方案来做更优化的方案处理。
苏南大叔的“程序如此灵动”博客,记录苏南大叔的编程经验总结。本文测试环境:win10
,nginx@1.15.11
,php@8.2.10-nts
,mysql@5.7.26
。因为本文的代码使用的是悲观锁的概念,虽然保证了并发场景下的数据完整性。但是,对于访问量大的秒杀场景下,这样做可能会带来系统上的瓶颈,所以慎用。
前文回顾
本文的代码,使用了原始的mysqli
扩展方式访问mysql
数据库,访问方式可以参考:
- https://newsn.net/say/php-mysqli.html
- https://newsn.net/say/php-mysqli-2.html
- https://newsn.net/say/php-mysqli-fetch.html
在和mysql
代码交互的时候,为了保证数据的完整性,使用了事务以及select ... for update
加行锁的方式对数据表进行了操作。相关代码:
- https://newsn.net/say/mysql-autocommit.html
- https://newsn.net/say/mysql-commit.html
- https://newsn.net/say/mysql-savepoint.html
- https://newsn.net/say/mysql-for-update.html
- https://newsn.net/say/mysql-for-update-2.html
因为使用了事务,所以代码里面的数据表,必然使用的是innodb
存储引擎类型。有关这种innodb
存储类型的数据表,可以参考文章:
在构建订单号的时候,使用了uinqid()
的方式创建了随机数单号。关于这个uinqid()
函数,可以参考:
需要对一些字段做索引优化,以保证数据访问速度。
建表语句
有两个innodb
类型的数据表:产品表、订单表。
DROP TABLE IF EXISTS `goods`;
CREATE TABLE `goods` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`price` varchar(255) DEFAULT NULL,
`count` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `index_name` (`name`),
KEY `index_count` (`count`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
INSERT INTO `goods` VALUES (1,'商品一','50',999),(2,'商品二','100',999);
DROP TABLE IF EXISTS `orders`;
CREATE TABLE `orders` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`sn` varchar(255) DEFAULT NULL,
`uid` varchar(255) DEFAULT NULL,
`gid` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `index_sn` (`sn`),
KEY `index_uid` (`uid`),
KEY `index_gid` (`gid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
注意,为了保证mysql
的查表效率,需要对部分字段做索引。同时,还可以避免在select...for update
语句里面锁表。
完整示意部分
这部分代码非常好理解,不做过多解释。重点就是要理解为啥要加上begin
、commit
和rollback
即可。
$uid = 1; // 用户id
$gid = 1; // 商品id
$buy_cnt = 1; // 购买数量
function get_order_no(){
return date('ymd') . substr(uniqid(),5);
}
$conn = mysqli_connect('127.0.0.1', 'root', 'root', 'test', 3306);
mysqli_query($conn, "BEGIN");
$sql = "select count,price from goods where id='$gid' FOR UPDATE";
$res = mysqli_query($conn, $sql);
$row = $res->fetch_assoc();
$ok = false;
if ($row['count'] > 0) {
$sql = "insert into orders(sn,uid,gid) values('".get_order_no()."','$uid','$gid')";
mysqli_query($conn, $sql);
$sql = "update goods set count=count-{$buy_cnt} where id='$gid' and count>0"; // 尝试减少库存
$rs = mysqli_query($conn, $sql);
if ($rs) {
mysqli_query($conn, "COMMIT"); // 库存减少成功;
$ok = true;
}
}
if(!$ok){
mysqli_query($conn, "ROLLBACK");
}
如果这个秒杀活动并不吸引人的话,使用这样的悲观锁并不会给访问造成太多的压力。
注意:如果访问量高,可以采用其它的乐观锁方案,待后续讨论。但是,乐观锁不能解决脏读的问题。
结语
读取频繁时使用乐观锁,写入频繁时则使用悲观锁。苏南大叔偶尔也写写php
文章,可以参考:
本博客不欢迎:各种镜像采集行为。请尊重原创文章内容,转载请保留作者链接。