我们相信:世界是美好的,你是我也是。平行空间的世界里面,不同版本的生活也在继续...

经过前几篇文章的铺垫,苏南大叔在本文里面描述经典的购物场景中的操作。实际的需求场景必然是比本文描述的要复制的多,并且代码也不会不使用各种框架。本文就是使用php原生mysqli扩展来读写mysql的,只是为了说明事务在购物这种场景下的使用而已。实际的业务场景下,会使用redis等方案来做更优化的方案处理。

苏南大叔:商品秒杀并发场景,php如何调用mysql保证数据完整性? - 商品秒杀并发场景
商品秒杀并发场景,php如何调用mysql保证数据完整性?(图1-1)

苏南大叔的“程序如此灵动”博客,记录苏南大叔的编程经验总结。本文测试环境:win10nginx@1.15.11php@8.2.10-ntsmysql@5.7.26。因为本文的代码使用的是悲观锁的概念,虽然保证了并发场景下的数据完整性。但是,对于访问量大的秒杀场景下,这样做可能会带来系统上的瓶颈,所以慎用。

前文回顾

本文的代码,使用了原始的mysqli扩展方式访问mysql数据库,访问方式可以参考:

在和mysql代码交互的时候,为了保证数据的完整性,使用了事务以及select ... for update加行锁的方式对数据表进行了操作。相关代码:

因为使用了事务,所以代码里面的数据表,必然使用的是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语句里面锁表。

完整示意部分

这部分代码非常好理解,不做过多解释。重点就是要理解为啥要加上begincommitrollback即可。

$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文章,可以参考:

如果本文对您有帮助,或者节约了您的时间,欢迎打赏瓶饮料,建立下友谊关系。
本博客不欢迎:各种镜像采集行为。请尊重原创文章内容,转载请保留作者链接。

 【福利】 腾讯云最新爆款活动!1核2G云服务器首年50元!

 【源码】本文代码片段及相关软件,请点此获取更多信息

 【绝密】秘籍文章入口,仅传授于有缘之人   php