本实例使用到了curl模拟并发操作与悲观锁和乐观锁的方法
在分享本实例前让我们先引入以下场景
数据库中有两张表,一张为秒杀活动的商品库存表,一张为用户订单表,此次秒杀的商品为iPhone X手机,库存为10部, 在一个并发量的秒杀场景中,当数据库中库存值为1时,可能同时会有多个进程读取到库存数的值,他们在同一时间点读到的库存值都为1,程序判断符合条件,抢购成功,库存数减一。这样会导致商品超发的情况,本来只有10件可以抢购的商品,可能会有超过10个人抢到,此时库存在抢购完成之后为负值。
在秒杀前,数据表示例如下

秒杀后的库存结果如下所示,并且此时有16人秒杀到商品

很明显,当库存秒杀到0时,照常理说,是停止秒杀活动,但是在此处,它直接将库存数量降低为负数,那么为什么会产生这种情况呢?让我们看看以下的错误的代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
<?php //不加锁的错误实例 $conn = mysqli_connect("localhost", "root", "root", "miaosha"); //链接数据库配置 if (mysqli_connect_errno($conn)) { die; } mysqli_query($conn, "set names utf8"); //设置字符集编码 $user_id = rand(100000, 9999999);//生成用户id $order_num = rand(10000, 99999);//生成订单号 $time = time();//生成下单时间 $sql = "select storage from goods where id=1"; $res = mysqli_query($conn, $sql); $assoc = mysqli_fetch_assoc($res); if ($assoc['storage'] > 0) { //当库存数>0时 usleep(100); $sql = "update goods set storage=storage-1 where id=1"; //更新库存,使库存数量-1 $query = mysqli_query($conn, $sql); if ($query) { //如果更新库存执行成功 $sql = "insert into order_num (user_id,order_num,time) values ('$user_id','$order_num','$time')"; //写入订单表 $query = mysqli_query($conn, $sql); if ($query) { //如果用户订单写入成功 echo "秒杀成功"; } } } else { echo "秒杀结束"; } |
假设单位时间t1内, 商品库存数量为1时,此时有7个用户同时访问,他们都读到了库存数量等于1,都执行了更新库存操作.使库存数量-1,所以就造成了这种结果。
那么如何解决这个问题呢?其实方法有很多,以mysql为例我们可以为此数据表或者记录上锁。
解决方案一:悲观锁
悲观锁的方案采用的是排他读,也就是同时只能有一个进程读取到库存的值。事务在提交或回滚之后,锁会释放,其他的进程才能读取。该方案最简单易懂,在对性能要求不高时,可以直接采用该方案。要注意的是,SELECT … FOR UPDATE要尽可能的使用索引,以便锁定尽可能少的行数;排他锁是在事务执行结束之后才释放的,不是读取完成之后就释放,因此使用的事务应该尽可能的早些提交或回滚,以便早些释放排它锁。
具体实现代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
<?php //悲观锁实例 $conn = mysqli_connect("localhost", "root", "root", "miaosha"); //链接数据库配置 if (mysqli_connect_errno($conn)) { die; } mysqli_query($conn, "set names utf8"); //设置字符集编码 mysqli_query($conn, 'begin');//开启事务 $user_id = rand(100000, 9999999);//生成用户id $order_num = rand(10000, 99999);//生成订单号 $time = time();//生成下单时间 $sql = "select storage from goods where id=1 for update"; //从goods表中查询id编号为1的库存数,并且加上排它锁(这里对id=1的记录进行加锁(排它锁)注意这里还没有结束事务,会一直占用着id=1记录的锁) /* 使用for update 为所有查询select的记录加上独占锁。 独占锁又叫写锁,意思在锁的期间,不允许其他任何尝试获取锁(包括读锁和写锁)的请求,只有这个锁被释放掉才能被另外一个事务获取锁。 */ $res = mysqli_query($conn, $sql); $assoc = mysqli_fetch_assoc($res); if ($assoc['storage'] > 0) { //当库存数>0时 usleep(100); $sql = "update goods set storage=storage-1 where id=1"; //更新库存,使库存数量-1 $query = mysqli_query($conn, $sql); if ($query) { //如果更新库存执行成功 $sql = "insert into order_num (user_id,order_num,time) values ('$user_id','$order_num','$time')"; //写入订单表 $query = mysqli_query($conn, $sql); if ($query) { //如果用户订单写入成功 mysqli_query($conn, "commit"); //提交事务 echo "秒杀成功"; } else { mysqli_query($conn, "rollback"); } } else { mysqli_query($conn, "rollback"); } } else { echo "秒杀结束"; } |
悲观锁简单的理解就是给一条记录加上行级锁,当用户秒杀商品时,对该记录进行锁定,当一个用户执行完抢购秒杀活动时,下一个用户才能进来,否则只能进行等待。它的缺点显而易见,如果在大并发下,用户体验会很差,因为该记录在执行的情况下,很多用户都要进行阻塞等待。
解决方案一:乐观锁
乐观锁的方案在读取数据是并没有加锁,而是通过一个每次更新都会自增的version字段来解决多个进程读取到相同库存,然后都能更新成功的问题。在每个进程读取库存的同时,也读取version的值,并且在更新库存的同时也更新version,并在更新时加上对version的等值判断。假设有10个进程都读取到了库存的值为1,version值为9,则这10个进程执行的更新语句都如下:
UPDATE goods SET storage=storage-1,version=version+1 WHERE version=9
然而当其中一个进程执行成功之后,数据库中version的值就会变为10,剩余的9个进程都不会执行成功,这样保证了商品不会超发,并且库存的值不会小于0,但这也导致了一个问题,那就是发出抢购请求较早的用户可能抢不到,反而被后来的请求抢到了。
数据表设计如下所示

具体代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
<?php //乐观锁实例 $conn = mysqli_connect("localhost", "root", "root", "miaosha"); //链接数据库配置 if (mysqli_connect_errno($conn)) { die; } mysqli_query($conn, "set names utf8"); //设置字符集编码 $user_id = rand(100000, 9999999);//生成用户id $order_num = rand(10000, 99999);//生成订单号 $time = time();//生成下单时间 $sql = "select storage,version from goods where id=1"; $res = mysqli_query($conn, $sql); $assoc = mysqli_fetch_assoc($res); $version=$assoc['version'];//查询当前version字段的值 if ($assoc['storage'] > 0) { //当库存数>0时 mysqli_query($conn,"begin"); //开启事务 usleep(100); $sql = "update goods set storage=storage-1,version=version+1 where version='$version'"; //更新库存,使库存数量-1,并且使version字段自增 $query = mysqli_query($conn, $sql); $affected_rows=mysqli_affected_rows($conn); if ($affected_rows==1) { //如果更新库存执行成功 $sql = "insert into order_num (user_id,order_num,time) values ('$user_id','$order_num','$time')"; //写入订单表 $query = mysqli_query($conn, $sql); $affected_rows=mysqli_affected_rows($conn); if ($affected_rows==1) { //如果用户订单写入成功 mysqli_query($conn,"commit"); //提交事务 echo "秒杀成功"; }else{ mysqli_query($conn,"rollback"); //回滚 } }else{ //mysqli_query($conn,"rollback"); //此处逻辑不能回滚 echo "秒杀结束"; } } else { echo "秒杀结束"; } |
在此,我们就对乐观锁以及悲观锁的实现先告一段落。
接下来就是模拟curl多线程并发的操作
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
<?php //用户只需关注$url与$num这两个变量即可 ini_set("display_errors", "off"); header("Content-type: text/html; charset=utf-8"); $url = 'http://localhost/ceshi/client_1.php'; //请求路径 $num = 150; //设置并发访问量 $mh = curl_multi_init(); for ($i = 0; $i <= $num; $i++) { $conn[$i] = curl_init($url); curl_setopt($conn[$i], CURLOPT_RETURNTRANSFER, 1); curl_multi_add_handle($mh, $conn[$i]); } do { $n = curl_multi_exec($mh, $active); } while ($active); for ($i = 0; $i <= $num; $i++) { $res[$i] = curl_multi_getcontent($conn[$i]); curl_close($conn[$i]); } echo "<pre>"; print_r($res); echo "</pre>"; |
本实例就是使用这个curl多线程来模拟本地的并发操作的
代码中$num这个变量看情况设置大小 ,因为设置太大,可能本地服务器环境吃不消
还有些redis的秒杀队列的方法暂时就不在此分享了,具体解决方案请查看该链接
文章评论(0)