• php 高并发防止超卖


    PHP高并发情况下防止商品库存超卖 | PHP 技术论坛
    https://learnku.com/articles/66811

    php 防止库存超卖之并发测试 | PHP 技术论坛
    https://learnku.com/articles/66812

    商城系统中,抢购和秒杀是很常见的营销场景,在一定时间内有大量的用户访问商场下单,主要需要解决的问题有两个:

    1. 高并发对数据库产生的压力;

    2. 竞争状态下如何解决商品库存超卖;

    高并发对数据库产生的压力

    对于第一个问题,使用缓存来处理,避免直接操作数据库,例如使用 Redis。

    竞争状态下如何解决商品库存超卖

    对于第二个问题,需要重点说明。

    常规写法:查询出对应商品的库存,判断库存数量否大于 0,然后执行生成订单等操作,但是在判断库存是否大于 0 处,如果在高并发下就会有问题,导致库存量出现负数。

    测试表 sql
    把如下表数据导入到数据库中

    1. /*
    2. Navicat MySQL Data Transfer
    3. Source Server : 01 本地localhost
    4. Source Server Version : 50553
    5. Source Host : localhost:3306
    6. Source Database : test
    7. Target Server Type : MYSQL
    8. Target Server Version : 50553
    9. File Encoding : 65001
    10. Date: 2020-11-06 14:31:35
    11. */
    12. SET FOREIGN_KEY_CHECKS=0;
    13. -- ----------------------------
    14. -- Table structure for products
    15. -- ----------------------------
    16. DROP TABLE IF EXISTS `products`;
    17. CREATE TABLE `products` (
    18. `id` int(10) NOT NULL AUTO_INCREMENT COMMENT 'ID',
    19. `title` varchar(50) DEFAULT NULL COMMENT '货品名称',
    20. `store` int(11) DEFAULT '0' COMMENT '货品库存',
    21. PRIMARY KEY (`id`)
    22. ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='货品表';
    23. -- ----------------------------
    24. -- Records of products
    25. -- ----------------------------
    26. INSERT INTO `products` VALUES ('1', '稻花香大米', '20');
    27. -- ----------------------------
    28. -- Table structure for order_log
    29. -- ----------------------------
    30. DROP TABLE IF EXISTS `order_log`;
    31. CREATE TABLE `order_log` (
    32. `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    33. `content` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '日志内容',
    34. `c_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
    35. PRIMARY KEY (`id`)
    36. ) ENGINE=MyISAM DEFAULT CHARSET=latin1;
    37. -- ----------------------------
    38. -- Table structure for order
    39. -- ----------------------------
    40. DROP TABLE IF EXISTS `order`;
    41. CREATE TABLE `order` (
    42. `oid` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '订单号',
    43. `product_id` int(11) DEFAULT '0' COMMENT '商品ID',
    44. `number` int(11) DEFAULT '0' COMMENT '购买数量',
    45. `c_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
    46. PRIMARY KEY (`oid`)
    47. ) ENGINE=MyISAM DEFAULT CHARSET=latin1 COMMENT='订单表';

    下单处理代码

    1. <?php
    2. db();
    3. global $con;
    4. //step1 接收下单参数
    5. $product_id = 1;// 商品ID
    6. $buy_num = 1;// 购买数量
    7. //step2 查询商品信息
    8. $sql = "select * from products where id={$product_id}";
    9. $result = mysqli_query($con, $sql);
    10. $row = mysqli_fetch_assoc($result);
    11. //step3 判断商品下单数量是否大于商品库存数量
    12. //此处在高并发下,可能出现上一个下单后还没来得及更新库存,下一个下单判断库存数不是最新的库存
    13. if ($row['store'] > 0) {
    14. sleep(1);
    15. //step4 更新商品库存数量(减去下单数量)
    16. $sql = "update products set store=store-{$buy_num} where id={$product_id}";
    17. if (mysqli_query($con, $sql)) {
    18. echo "更新成功";
    19. //step5 生成订单号创建订单
    20. $oid = build_order_no();
    21. create_order($oid, $product_id, $buy_num);
    22. insertLog('库存减少成功,下单成功');
    23. } else {
    24. echo "更新失败";
    25. insertLog('库存减少失败');
    26. }
    27. } else {
    28. echo "没有库存";
    29. insertLog('库存不够');
    30. }
    31. function db()
    32. {
    33. global $con;
    34. $con = new mysqli('localhost','root','root','test');
    35. if (!$con) {
    36. echo "数据库连接失败";
    37. }
    38. }
    39. /**
    40. * 生成唯一订单号
    41. */
    42. function build_order_no()
    43. {
    44. return date('Ymd') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
    45. }
    46. function create_order($oid, $product_id, $number)
    47. {
    48. global $con;
    49. $sql = "INSERT INTO `order` (oid, product_id, number) values('$oid', '$product_id', '$number')";
    50. mysqli_query($con, $sql);
    51. }
    52. /**
    53. * 记录日志
    54. */
    55. function insertLog($content)
    56. {
    57. global $con;
    58. $sql = "INSERT INTO `order_log` (content) values('$content')";
    59. mysqli_query($con, $sql);
    60. }

     将库存字段字段设为 unsigned

    因为库存字段不能为负数,在下单后更新商品库存时,如果出现负数将返回 false

    1. <?php
    2. db();
    3. global $con;
    4. //step1 接收下单参数
    5. $product_id = 1;// 商品ID
    6. $buy_num = 1;// 购买数量
    7. //step2 查询商品信息
    8. $sql = "select * from products where id={$product_id} for UPDATE";//利用for update 开启行锁
    9. $result = mysqli_query($con, $sql);
    10. $row = mysqli_fetch_assoc($result);
    11. //step3 判断商品下单数量是否大于商品库存数量
    12. if ($row['store'] > 0) {
    13. sleep(1);
    14. //step4 更新商品库存数量(减去下单数量)
    15. $sql = "update products set store=store-{$buy_num} where id={$product_id}";
    16. if (mysqli_query($con, $sql)) {
    17. echo "更新成功";
    18. //step5 生成订单号创建订单
    19. $oid = build_order_no();
    20. create_order($oid, $product_id, $buy_num);
    21. insertLog('库存减少成功,下单成功');
    22. } else {
    23. // 如果出现负数将返回false
    24. echo "更新失败";
    25. insertLog('库存减少失败');
    26. }
    27. } else {
    28. //商品已经抢购完
    29. echo "没有库存";
    30. insertLog('库存不够');
    31. }
    32. function db()
    33. {
    34. global $con;
    35. $con = new mysqli('localhost','root','root','test');
    36. if (!$con) {
    37. echo "数据库连接失败";
    38. }
    39. }
    40. /**
    41. * 生成唯一订单号
    42. */
    43. function build_order_no()
    44. {
    45. return date('Ymd') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
    46. }
    47. function create_order($oid, $product_id, $number)
    48. {
    49. global $con;
    50. $sql = "INSERT INTO `order` (oid, product_id, number) values('$oid', '$product_id', '$number')";
    51. mysqli_query($con, $sql);
    52. }
    53. /**
    54. * 记录日志
    55. */
    56. function insertLog($content)
    57. {
    58. global $con;
    59. $sql = "INSERT INTO `order_log` (content) values('$content')";
    60. mysqli_query($con, $sql);
    61. }

    使用 mysql 的事务,锁住操作的行

    在下单处理过程中,使用 mysql 的事务将正在下单商品行数据锁定

    1. <?php
    2. db();
    3. global $con;
    4. //step1 接收下单参数
    5. $product_id = 1;// 商品ID
    6. $buy_num = 1;// 购买数量
    7. mysqli_query($con, "BEGIN"); //开始事务
    8. //step2 查询商品信息
    9. $sql = "select * from products where id={$product_id} for UPDATE";//利用for update 开启行锁
    10. $result = mysqli_query($con, $sql);
    11. $row = mysqli_fetch_assoc($result);
    12. //step3 判断商品下单数量是否大于商品库存数量
    13. if ($row['store'] > 0) {
    14. sleep(1);
    15. //step4 更新商品库存数量(减去下单数量)
    16. $sql = "update products set store=store-{$buy_num} where id={$product_id}";
    17. if (mysqli_query($con, $sql)) {
    18. echo "更新成功";
    19. //step5 生成订单号创建订单
    20. $oid = build_order_no();
    21. create_order($oid, $product_id, $buy_num);
    22. insertLog('库存减少成功,下单成功');
    23. mysqli_query($con, "COMMIT");//事务提交即解锁
    24. } else {
    25. echo "更新失败";
    26. insertLog('库存减少失败');
    27. mysqli_query($con, "ROLLBACK");//事务回滚即解锁
    28. }
    29. } else {
    30. //商品已经抢购完
    31. echo "没有库存";
    32. insertLog('库存不够');
    33. mysqli_query($con, "ROLLBACK");//事务回滚即解锁
    34. }
    35. function db()
    36. {
    37. global $con;
    38. $con = new mysqli('localhost','root','root','test');
    39. if (!$con) {
    40. echo "数据库连接失败";
    41. }
    42. }
    43. /**
    44. * 生成唯一订单号
    45. */
    46. function build_order_no()
    47. {
    48. return date('Ymd') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
    49. }
    50. function create_order($oid, $product_id, $number)
    51. {
    52. global $con;
    53. $sql = "INSERT INTO `order` (oid, product_id, number) values('$oid', '$product_id', '$number')";
    54. mysqli_query($con, $sql);
    55. }
    56. /**
    57. * 记录日志
    58. */
    59. function insertLog($content)
    60. {
    61. global $con;
    62. $sql = "INSERT INTO `order_log` (content) values('$content')";
    63. mysqli_query($con, $sql);
    64. }

    使用非阻塞的文件排他锁
    在处理下单请求的时候,用 flock 锁定一个文件,如果锁定失败说明有其他订单正在处理,此时要么等待要么直接提示用户” 服务器繁忙”,计数器存储抢购的商品数量,避免查询数据库。

    阻塞 (等待) 模式:并发时,当有第二个用户请求时,会等待第一个用户请求完成、释放锁,获得文件锁之后,程序才会继续运行下去。

    1. <?php
    2. db();
    3. global $con;
    4. //step1 接收下单参数
    5. $product_id = 1;// 商品ID
    6. $buy_num = 1;// 购买数量
    7. $fp = fopen('lock.txt', 'w');
    8. if (flock($fp, LOCK_EX)) { //文件独占锁,阻塞
    9. //step2 查询商品信息
    10. $sql = "select * from products where id={$product_id}";
    11. $result = mysqli_query($con, $sql);
    12. $row = mysqli_fetch_assoc($result);
    13. //step3 判断商品下单数量是否大于商品库存数量
    14. if ($row['store'] > 0) {
    15. //处理订单
    16. sleep(1);
    17. //step4 更新商品库存数量(减去下单数量)
    18. $sql = "update products set store=store-{$buy_num} where id={$product_id}";
    19. if (mysqli_query($con, $sql)) {
    20. echo "更新成功";
    21. //step5 生成订单号创建订单
    22. $oid = build_order_no();
    23. create_order($oid, $product_id, $buy_num);
    24. insertLog('库存减少成功,下单成功');
    25. } else {
    26. echo "更新失败";
    27. insertLog('库存减少失败');
    28. }
    29. } else {
    30. //商品已经抢购完
    31. echo "没有库存";
    32. insertLog('库存不够');
    33. }
    34. flock($fp, LOCK_UN); //释放锁
    35. }
    36. fclose($fp);
    37. function db()
    38. {
    39. global $con;
    40. $con = new mysqli('localhost','root','root','test');
    41. if (!$con) {
    42. echo "数据库连接失败";
    43. }
    44. }
    45. /**
    46. * 生成唯一订单号
    47. */
    48. function build_order_no()
    49. {
    50. return date('Ymd') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
    51. }
    52. function create_order($oid, $product_id, $number)
    53. {
    54. global $con;
    55. $sql = "INSERT INTO `order` (oid, product_id, number) values('$oid', '$product_id', '$number')";
    56. mysqli_query($con, $sql);
    57. }
    58. /**
    59. * 记录日志
    60. */
    61. function insertLog($content)
    62. {
    63. global $con;
    64. $sql = "INSERT INTO `order_log` (content) values('$content')";
    65. mysqli_query($con, $sql);
    66. }

     非阻塞模式:并发时,第一个用户请求,拿得文件锁之后。后面请求的用户直接返回系统繁忙,请稍后再试

    1. <?php
    2. db();
    3. global $con;
    4. //step1 接收下单参数
    5. $product_id = 1;// 商品ID
    6. $buy_num = 1;// 购买数量
    7. $fp = fopen('lock.txt', 'w');
    8. if (flock($fp, LOCK_EX|LOCK_NB)) { //文件独占锁,非阻塞
    9. //step2 查询商品信息
    10. $sql = "select * from products where id={$product_id}";
    11. $result = mysqli_query($con, $sql);
    12. $row = mysqli_fetch_assoc($result);
    13. //step3 判断商品下单数量是否大于商品库存数量
    14. if ($row['store'] > 0) {
    15. //处理订单
    16. sleep(1);
    17. //step4 更新商品库存数量(减去下单数量)
    18. $sql = "update products set store=store-{$buy_num} where id={$product_id}";
    19. if (mysqli_query($con, $sql)) {
    20. echo "更新成功";
    21. //step5 生成订单号创建订单
    22. $oid = build_order_no();
    23. create_order($oid, $product_id, $buy_num);
    24. insertLog('库存减少成功,下单成功');
    25. } else {
    26. echo "更新失败";
    27. insertLog('库存减少失败');
    28. }
    29. } else {
    30. //商品已经抢购完
    31. echo "没有库存";
    32. insertLog('库存不够');
    33. }
    34. flock($fp, LOCK_UN); //释放锁
    35. } else {
    36. //系统繁忙,请稍后再试
    37. echo "系统繁忙,请稍后再试";
    38. insertLog('系统繁忙,请稍后再试');
    39. }
    40. fclose($fp);
    41. function db()
    42. {
    43. global $con;
    44. $con = new mysqli('localhost','root','root','test');
    45. if (!$con) {
    46. echo "数据库连接失败";
    47. }
    48. }
    49. /**
    50. * 生成唯一订单号
    51. */
    52. function build_order_no()
    53. {
    54. return date('Ymd') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
    55. }
    56. function create_order($oid, $product_id, $number)
    57. {
    58. global $con;
    59. $sql = "INSERT INTO `order` (oid, product_id, number) values('$oid', '$product_id', '$number')";
    60. mysqli_query($con, $sql);
    61. }
    62. /**
    63. * 记录日志
    64. */
    65. function insertLog($content)
    66. {
    67. global $con;
    68. $sql = "INSERT INTO `order_log` (content) values('$content')";
    69. mysqli_query($con, $sql);
    70. }

    使用 redis 队列

    • 因为 pop 操作是原子的,即使有很多用户同时到达,也是依次执行,推荐使用
    • mysql 事务在高并发下性能下降很厉害,文件锁的方式也是
    1. 先将商品库存到 redis 队列

    1. <?php
    2. db();
    3. global $con;
    4. // 查询商品信息
    5. $product_id = 1;
    6. $sql = "select * from products where id={$product_id}";
    7. $result = mysqli_query($con, $sql);
    8. $row = mysqli_fetch_assoc($result);
    9. $store = $row['store'];
    10. // 获取商品在redis缓存的库存
    11. $redis = new Redis();
    12. $result = $redis->connect('127.0.0.1', 6379);
    13. $key = 'goods_store_' . $product_id;
    14. $res = $redis->llen($key);
    15. $count = $store - $res;
    16. for ($i=0; $i<$count; $i++) {
    17. $redis->lpush($key, 1);
    18. }
    19. echo $redis->llen($key);
    20. function db()
    21. {
    22. global $con;
    23. $con = new mysqli('localhost','root','root','test');
    24. if (!$con) {
    25. echo "数据库连接失败";
    26. }
    27. }

    2. 抢购、秒杀逻辑

    1. <?php
    2. db();
    3. global $con;
    4. //step1 接收下单参数
    5. $product_id = 1;// 商品ID
    6. $buy_num = 1;// 购买数量
    7. //step2 下单前判断redis队列库存量
    8. $redis = new Redis();
    9. $result = $redis->connect('127.0.0.1',6379);
    10. $count = $redis->lpop('goods_store_' . $product_id);
    11. if (!$count) {
    12. insertLog('error:no store redis');
    13. return '秒杀结束,没有商品库存了';
    14. }
    15. sleep(1);
    16. //step3 更新商品库存数量(减去下单数量)
    17. $sql = "update products set store=store-{$buy_num} where id={$product_id}";
    18. if (mysqli_query($con, $sql)) {
    19. echo "更新成功";
    20. //step4 生成订单号创建订单
    21. $oid = build_order_no();
    22. create_order($oid, $product_id, $buy_num);
    23. insertLog('库存减少成功,下单成功');
    24. } else {
    25. echo "更新失败";
    26. insertLog('库存减少失败');
    27. }
    28. function db()
    29. {
    30. global $con;
    31. $con = new mysqli('localhost','root','root','test');
    32. if (!$con) {
    33. echo "数据库连接失败";
    34. }
    35. }
    36. /**
    37. * 生成唯一订单号
    38. */
    39. function build_order_no()
    40. {
    41. return date('Ymd') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
    42. }
    43. function create_order($oid, $product_id, $number)
    44. {
    45. global $con;
    46. $sql = "INSERT INTO `order` (oid, product_id, number) values('$oid', '$product_id', '$number')";
    47. mysqli_query($con, $sql);
    48. }
    49. /**
    50. * 记录日志
    51. */
    52. function insertLog($content)
    53. {
    54. global $con;
    55. $sql = "INSERT INTO `order_log` (content) values('$content')";
    56. mysqli_query($con, $sql);
    57. }

    3.redis 乐观锁防止超卖

    1. <?php
    2. $redis =new Redis();
    3. $redis->connect("127.0.0.1", 6379);
    4. $redis->watch('sales');//乐观锁 监视作用 set() 初始值0
    5. $sales = $redis->get('sales');
    6. $n = 20;// 库存
    7. if ($sales >= $n) {
    8. exit('秒杀结束');
    9. }
    10. //redis开启事务
    11. $redis->multi();
    12. $redis->incr('sales'); //将 key 中储存的数字值增一 ,如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。
    13. $res = $redis->exec(); //成功1 失败0
    14. if ($res) {
    15. //秒杀成功
    16. $con = new mysqli('localhost','root','root','test');
    17. if (!$con) {
    18. echo "数据库连接失败";
    19. }
    20. $product_id = 1;// 商品ID
    21. $buy_num = 1;// 购买数量
    22. sleep(1);
    23. $sql = "update products set store=store-{$buy_num} where id={$product_id}";
    24. if (mysqli_query($con, $sql)) {
    25. echo "秒杀完成";
    26. }
    27. } else {
    28. exit('抢购失败');
    29. }

  • 相关阅读:
    kvm添加磁盘
    python学习1
    ubuntu使sudo不需要密码
    磁盘挂载
    github/gitlab添加多个ssh key
    生成SSH key
    git 删除追踪状态
    angular2+ 初理解
    本地项目上传到GitHub
    new Date()之参数传递
  • 原文地址:https://www.cnblogs.com/ianlab/p/16357749.html
Copyright © 2020-2023  润新知