• 高并发、死锁、幂等性问题


    一.介绍

    在短时间之内对数据表(库)的集中访问,就称为“高并发”。
    高并发 在使用的时候容易出现问题,
    在短时间之内对数据表有大量的集中修改操作,如果不做控制,数据表的修改容易出现重复。
    比如:
    多个人操作获得的剩余量(95)是一致的(操作的时间点是同一个)
    操作完毕对剩余量做减少操作,多人减少的数额(94)也是一样的
    这样数据库剩余的数据量就不准确

    新建数据表:

    create table gbf_hot_goods(
        id MEDIUMINT UNSIGNED not null auto_increment comment '主键id',
        number TINYINT UNSIGNED not null default 0 comment '库存',
        name varchar(32) not null default '' comment '名称',
        price decimal(10,2) not null default 0 comment '价格',
        primary key (id)
    )engine=Innodb charset=utf8;
    
    insert into  gbf_hot_goods values(null,100,'HUAWEI手机',5450);

    正常整理库存:

    <?php 
    
    $pdo = new PDO('mysql:host=127.0.0.1;dbname=demo','root','');
    $pdo->query('set names utf8');//设置字符集
    
    //模拟购买商品,购买前先判断库存,购买后库存做减少操作
    $sql = 'select number from gbf_hot_goods';
    $qry = $pdo->query($sql);
    $rst = $qry->fetch();//取出值
    $num = $rst['number'];
    $num = $num - 3;//每次购买3个
    
    $sql2 = 'update gbf_hot_goods set number='.$num;
    $pdo->exec($sql2);
    
    ?>

      结果:

    二.模拟高并发处理

    使用apache自带的小工具ab.exe进行模拟:

    windows的cmd下apache/bin下> ab.exe -c 人数  -n 总请求数目 地址

    ab.exe -c 10 -n 10 http://local-demo.cn/note/php/demo/high/demo.php

      执行

    This is ApacheBench, Version 2.3 <$Revision: 1748469 $>
    Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
    Licensed to The Apache Software Foundation, http://www.apache.org/
    
    Benchmarking local-demo.cn (be patient).....done
    
    
    Server Software:        Apache/2.4.23
    Server Hostname:        local-demo.cn
    Server Port:            80
    
    Document Path:          /note/php/demo/high/demo.php
    Document Length:        0 bytes
    
    Concurrency Level:      10
    Time taken for tests:   2.030 seconds
    Complete requests:      10
    Failed requests:        0
    Total transferred:      2220 bytes
    HTML transferred:       0 bytes
    Requests per second:    4.93 [#/sec] (mean)
    Time per request:       2030.309 [ms] (mean)
    Time per request:       203.031 [ms] (mean, across all concurrent requests)
    Transfer rate:          1.07 [Kbytes/sec] received
    
    Connection Times (ms)
                  min  mean[+/-sd] median   max
    Connect:        1    1   0.5      1       2
    Processing:    15  524 698.3     44    1989
    Waiting:       13  523 698.2     43    1988
    Total:         16  526 698.2     45    1990
    
    Percentage of the requests served within a certain time (ms)
      50%     45
      66%   1025
      75%   1031
      80%   1034
      90%   1990
      95%   1990
      98%   1990
      99%   1990
     100%   1990 (longest request)

    请求10次后,number=73.正确应该等于70,所以已经出现了并发性问题。

    注意,若等于70的话,可以多试几次,本地环境有可能受性能影响造成数据不准确。

    高并发容易造成多个客户端同时处理程序,这样获得的数据结果有重复(错误)

    解决:虽然是高并发,但是程序文件必须一个一个执行。

      设置锁即可。

    锁有两种类型,php的锁、mysql的锁,

    定义描述: 

      共享锁:又叫做读锁,所有人可以读同一个资源,但只有获取锁的人可以对其进行写操作。

      排它锁:只有获得锁的对象可以操作资源,在其释放锁之前其他人不能进行任何操作,读都不可以。

    策略描述:

      悲观锁:在读取数据时锁住那几行,其他对这几行的更新需要等到悲观锁结束时才能继续 。
      乐观锁:读取数据时不锁,更新时检查是否数据已经被更新过,如果是则取消当前更新,一般在悲观锁的等待时间过长而不能接受时我们才会选择乐观锁。

    mysql表锁:

    <?php 
    
    $pdo = new PDO('mysql:host=127.0.0.1;dbname=demo','root','');
    $pdo->query('set names utf8');//设置字符集
    
    //查询前先上锁:
    $pdo->query('LOCK TABLES `gbf_hot_goods` WRITE');//也可以锁多个表,以,间隔,如:a READ,b WRITE,c READ,d WRITE
    
    //模拟购买商品,购买前先判断库存,购买后库存做减少操作
    $sql = 'select number from gbf_hot_goods';
    $qry = $pdo->query($sql);
    $rst = $qry->fetch();//取出值
    $num = $rst['number'];
    $num = $num - 3;//每次购买3个
    
    $sql2 = 'update gbf_hot_goods set number='.$num;
    $pdo->exec($sql2);
    
    //执行完后释放锁,即解锁
    $pdo->query('UNLOCK TABLES');
    
    ?>

    表锁的缺点是:因为同一时间只能有一人对表进行操作,所以会出现阻塞,如果同时锁多张表的话,还会影响整个网站相关表的加载。

    mysql行级锁:

      共享锁:SELECT `id` FROM  table WHERE id in(1,2)   LOCK IN SHARE MODE。//读锁,不让他人写,一般之后有update操作时用写锁较好

      排它锁:SELECT `id` FROM mk_user WHERE id=1 FOR UPDATE。//写锁,不让他人读,须开启事务才有效,且要是innodb类型引擎,明确主键id,并有数据

    <?php 
    
    $pdo = new PDO('mysql:host=127.0.0.1;dbname=demo','root','');
    $pdo->query('set names utf8');//设置字符集
    
    //mysql行级锁,必须开启事务才有效
    $pdo->beginTransaction();//开启事务
    $sql = 'select number from gbf_hot_goods id = 1 FOR UPDATE';
    $qry = $pdo->query($sql);
    $rst = $qry->fetch();//取出值
    $num = $rst['number'];
    if($num == 997){
        $pdo->rollback();//回滚的同时也会释放锁
    }else{
        $num = $num - 3;//每次购买3个
    
    $sql2 = 'update gbf_hot_goods set number='.$num.' where id = 1';
    $pdo->exec($sql2);
    $pdo->commit();//提交的同时也会释放锁
    }
    
    ?>

    php锁:

      特点:当调用flock锁一个文件时,如果没有获取锁,直接返回FALSE,不会出现阻塞。

    故,flock时判断是否等于false

      排它锁:flock($fp,LOCK_EX);

      共享锁:flock($fp,LOCK_SH);

      释放锁:flock($fp,LOCK_UN);

    <?php 
    
    $pdo = new PDO('mysql:host=127.0.0.1;dbname=demo','root','');
    $pdo->query('set names utf8');//设置字符集
    
    //查询前先上锁:
    //$pdo->query('LOCK TABLES `gbf_hot_goods` WRITE');
    
    //php的锁
    $fp = fopen('01.php','r');//打开一个实际存在的物理文件,文件可以是空的
    flock($fp,LOCK_EX);//上锁
    
    //模拟购买商品,购买前先判断库存,购买后库存做减少操作
    $sql = 'select number from gbf_hot_goods';
    $qry = $pdo->query($sql);
    $rst = $qry->fetch();//取出值
    $num = $rst['number'];
    $num = $num - 3;//每次购买3个
    
    $sql2 = 'update gbf_hot_goods set number='.$num;
    $pdo->exec($sql2);
    
    //执行完后释放锁,即解锁
    //$pdo->query('UNLOCK TABLES');
    
    //释放php锁
    flock($fp,LOCK_UN);//解锁
    fclose($fp);//释放锁的资源
    
    ?>

    高并发进行上锁,可以确保所有人都是排队依次对程序进行访问。

    避免负数:

    $pdo->query('UPDATE warehouse SET `number` = `number` -1 WHERE `number` > 0');  //可以避免库存为负数

    三.死锁

        `id`  主键索引

        `name` index 索引

        `age`  普通字段

    死锁产生的根本原因:

      是两个以上的进程都要求对方释放资源,以至于进程都一直等待。在代码上是因为两个或者以上的事务都要求另一个释放资源。

    死锁产生的四个必要条件:

      互斥条件、环路条件、请求保持、不可剥夺,缺一不可,相对应的只要破坏其中一种条件死锁就不会产生。

    例如下面两条语句:

      第一条语句会优先使用`name`索引,因为name不是主键索引,还会用到主键索引。

      第二条语句是首先使用主键索引,再使用name索引。

      如果两条语句同时执行,第一条语句执行了name索引等待第二条释放主键索引,第二条执行了主键索引等待第一条的name索引,这样就造成了死锁。

      解决方法:改造第一条语句 使其根据主键值进行更新

    #①
    update user set name ='1' where `name`='tom';
    #②
    update user set name='12'  where id=12;
    //改造后
    update user set name='1' where id=(select id from user where name='tom' );

     四.幂等

      在日常开发中很容易碰到这类问题,如用户重复点击按钮请求、重复请求接口等。这样的后果,就是会生成多条数据或者造成脏数据。

    我在日常开发中习惯使用的方法就是使用token加锁、或者判断下是否存在再添加。当然判断是否存在再添加再核心高并发下不建议。所以,使用数据去主键索引或者唯一索引就很必要了。

    如下,就是利用第三方工具redis来进行加锁排队判断:

        public function execute()
        {
            $this->time = 	ime();
            declare(ticks=1);
            pcntl_signal(SIGTERM, [$this, 'termHandle']);
            pcntl_signal(SIGINT, [$this, 'termHandle']);
            $status = true;
            while ($status) {
                if ($this->termFlag === true) {
                    self::showMsg('进程重启退出');
                    die;// 进程重启退出
                }
                $lock = self::lock('lock:mnw:task');
                if ($lock) {
                    $message = '';
                    try {
                        $this->start();
                    } catch (Throwable $e) {
                        $message =   'Error line ' . $e->getLine().' in ' . $e->getFile()
                            .': <b>' . $e->getMessage();
                    }
                    $status = false;
                    self::unlock('lock:mnw:task');
                    self::showMsg(empty($message) ? '进程正常退出' : ('进程异常退出:' . $message));
                    die;
                }
                self::showMsg('已有其他进程运行等待30秒后再次尝试运行');
                sleep(30);
    
    
            }
    
        }
        //加锁
        public static function lock($key, $expire = 1800){
            $isLock = CashRedis::setnx($key,time()+$expire);
            if(!$isLock){
                $lockTime = CashRedis::get($key);
                //锁已过期,重置
                if($lockTime < time()){
                    self::unlock($key);
                    $isLock = CashRedis::setnx($key,time()+$expire);
                }
            }
    
            if ($isLock) {
                CashRedis::expire($key, $expire);
                return true;
            }
    
            return false;
        }
    
        // 释放锁
        private static function unlock($key){
            CashRedis::delete($key);
        }
        public function termHandle()
        {
            $this->termFlag = true;
        }

    上面就是利用redis的setnx方法,有就不操作来进行进程的幂等性处理。

    推荐一篇博文,对于幂等性问题的讲解就很详细了

    https://blog.csdn.net/u011635492/article/details/81058153

  • 相关阅读:
    ORACLE PL/SQL编程总结(二)
    ORACLE PL/SQL基础编程
    Linux centos7环境下安装Nginx
    namespace 实例命名空间 及 应用命名空间 问题
    python 2.7 的django项目
    django项目 导出 和 安装 依赖包
    windows 2012 安装apache
    FX-玩列表
    记录pycharm快捷键出错的其中一个原因
    Django 安装配置
  • 原文地址:https://www.cnblogs.com/two-bees/p/10662257.html
Copyright © 2020-2023  润新知