Web
Web

并行和并发

并行:多个任务同一时间执行。

    两个人同时去挖坑,1小时内挖了直径2米的坑

并发:同一时间执行多个任务。

    两个人交替去挖坑,1小时内挖了直径1米的坑

关键:同时、交替、效率。 

即:并行是同时发生,并发是交替发生。并发的效率小于并行。

高并发:在极短的时间内,有极多的任务。

高并行:不存在,极多任务也是同时去执行。

产生高并发的问题:交替。在计算机微观里,CPU是无法同时执行很多个程序的,虽然在宏观上感觉是同时执行。比如同时开两个音乐播放器,可以听到两首歌。但是实际上CPU是通过不断的切换(时间片)来达到这个目的的。播放器A在播放的时候播放器B是停止的。然后播放器B播放时播放器A是停止的,不过由于CPU切换的速度非常快,所以人感受上是分辨不出来的,就像是显示屏的刷新频率60HZ,屏幕一直在刷新,只是人感官上是感受不到的。由于CPU交替切换的,所以会产生一个资源竞争的问题。这个问题的产生是因为IO读写,IO分为两种:文件IO网络IO

例如:Mysql

Mysql是存储在磁盘上的,每次读取的时候相当于从磁盘去读取,所以产生了一个文件IO。A用户正在读X字段,但是B用户正在写X字段,本来是B先执行写操作,但是在还没有写入的时候CPU切换了执行的程序,切换到A,那么A去读的数据就是B未写入前的数据,但是我们理想状态是B先写A在读。由于存在切换,并且是操作同一个字段,所以会产生资源竞争。即高并发问题。本身高并发并不会造成资源竞争,但是如果你的资源是同一个资源的话,那么就会产生高并发。不过如果你每个链接都是访问的不同地址的话,那么也就不是并发而是并行了。

总结:并发:宏观上感觉是同时执行,但是微观上是交替进行。并行:宏观微观都是同时执行。

微擎 Redis Mysql 高并发

1、宝塔安装

软件管理-PHP-设置-安装扩展-redis

2、微擎配置

文件:data/config.php

$config['setting']['cache'] = 'redis'; [35行左右] 缓存改为redis。

最下面加上

// --------------------------  CONFIG REDIS  --------------------------- //

$config['setting']['redis']['server'] = '127.0.0.1';//如果redis服务器在别的机器,请填写机器的IP地址。

$config['setting']['redis']['port'] = 6379;

$config['setting']['redis']['pconnect'] = 0;

$config['setting']['redis']['timeout'] = 1;

$config['setting']['redis']['auth'] = 'iwonmo'; //微擎

$config['setting']['redis']['requirepass'] = 'iwonmo';//人人用这个


auth 配置需要到redis的配置文档里进行配置,在微擎里如上定义只是方便引用。

3、更新微擎缓存

微擎后台-系统-更新缓存

4、查看是否开启成功

微擎后台-站点-性能优化

微信截图_20190213150825.png

5、测试代码

    public function build_sn(){
        $yCode = array('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J');
        $orderSn = $yCode[rand(0,9)] . strtoupper(dechex(date('m'))) . date('d') . substr(time(), -5) . substr(microtime(), 2, 5) . sprintf('%02d', rand(0, 99));
        return $orderSn;
    }
    public function doPageRedis_setRedPack(){
        global  $_W;
        $redisConfig = $_W["config"]['setting']['redis'];
        $redis = new Redis();
        $redis->pconnect($redisConfig['server'], $redisConfig['port'],$redisConfig['timeout']);
//        随机一个红包Key
        $key = $this->build_sn();
        for ($i = 1; $i <= 10; $i++)
        {
            $redis->lpush($key, $i);
        }
//        写入到数据库
        $RedPack_data = array(
            'num' => 10,
        );
        $result = pdo_insert('redis_test', $RedPack_data);
//        返回前端信息 - 抢红包
        if (!empty($result)) {
            echo "id:".pdo_insertid()."  RadKey:".$key;
        }else{
            echo "RadPack:Error";
        }
    }
    public function doPageRedis_test(){
        global  $_W,$_GPC;
        $redisConfig = $_W["config"]['setting']['redis'];
        $redis = new Redis();
        $redis->pconnect($redisConfig['server'], $redisConfig['port'],$redisConfig['timeout']);
//        返回列表的长度  key 被解释为一个空列表,返回 0
        if($redis->llen($_GPC['RadKey']) == 0) return $this->result($errno = 0, $message = 'No Pack');
//        移除并返回列表的第一个元素
        $List = $redis->lpop($_GPC['RadKey']);
        if(intval($List) > 0){
            $result = pdo_update('redis_test', array('num' => $List-1), array('id' => $_GPC['id']));
            if (!empty($result)) return $this->result($errno = 0, $message = 'Get Ok:'.$List); else return $this->result($errno = 0, $message = 'Get Error:'.$List);
        }else
            return $this->result($errno = 1, $message = 'Error:High incidence of complications','llen(RadKey) == 0');
    }

生成红包:Redis_setRedPack

拆开红包:Redis_test

    POST:id=[红包ID]&RadKey=[RdisListKey]

使用:访问Redis_setRedPack创建红包,会返回ID和RdisListKey。继而访问Redis_test拆开红包。

6、高并发测试

软件会测 [H.Test]

并发30个链接结果:

会测.png

7、缓存数据库情况

微信截图_20190213152109.png

随机抢占高并发问题

需求:地图上随机位置点亮。

问题:多个用户随机的时候抢占同一个位置

表结构 (a.b.c.d.e 为需要抢占的位置)

CREATE TABLE `fa_seize` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `a` int(11) DEFAULT NULL,
  `b` int(11) DEFAULT NULL,
  `c` int(11) DEFAULT NULL,
  `d` int(11) DEFAULT NULL,
  `e` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=latin1 COMMENT='抢占表';

后端

    /* 解决:随机的时候会有多个用户随机到一个位置的可能 */
    public function test_seize(){
        $map_id="";
        //需要抢占的位置
        $m_arr=['a','b','c','d','e'];
        //打乱数组 - 造成随机
        shuffle($m_arr);
        //去更新每一个需要抢占的位置
        foreach ($m_arr as $m){
            //字段初始的值为null
            //加上null判断是为了防止用户更新成自己的id
            //把抢占的位置置为 1 这样别的用户在置 1 的时候会更新失败
            //利用更新失败 避免抢占
            $ret=Db::name("seize")->where(['id'=>1,$m=>null])->update([$m=>1]);
            //影响行数大于0的时候为更新成功
            if($ret>0){
                //把抢占的位置更新为自己的id 这里假设为111
                Db::name("seize")->where('id',1)->update([$m=>111]);
                //返回前端抢占信息
                $map_id=$m;
                //跳出循环
                break;
            }
        }
        //返回给前端
        $map_id = $map_id == "" ?  "已点满": "位置:".$m;
        echo $map_id;
    }

并发结果

屏幕快照 2019-01-22 下午9.04.36.png


可以看到,在同一秒的请求的几个用户,只有在位置为空的时候才可以抢占到。

http协议Mysql的高并发抢红包处理

1、Mysql 事物处理的问题


假设字段S-RedNum是红包数量,有三个客户端S-A,S-B,S-C。下面是正常的流程。


微信图片_20181230172859.png


上图是三个客户端请求不在一个时间线上,所以服务端可以分别处理,不会造成并发现象。


假设:三个客户的同时请求,这种情况是很多的。然后会造成什么情况?即使加了事物,当S-A读取完红包数走处理加钱逻辑的时候,注意这个时候事物并没有提交给数据库,所以红包数还是3,这时S-B请求过来了,由于事物的特性,S-B获取的红包数还是3。这样就造成了“脏读”,即S-B读取的数据是S-A事物还没有提交的数据。所以事物并不是解决高并发的,而是确保你的执行代码块是有效的,即要么全部成功,要么回滚到初始状态。


2、Mysql 加锁问题

这种情况应该是加一个X锁,即排它锁。它是一个有效的方法,但是并不是一个合理的方法。排它锁是在写入的时候不可读。这会造成一个问题。比方说S-A正在写入,这个时候由于是排它锁,所以S-B,S-C过来后无法读取,那么可能会返给前端说已经没有红包,其实红包还有,只是S-A在写数据,其余的请求无法获取对应的红包数而已。如果说S-B在请求的时候如果判断为没有红包而进行死循环等待,其实也是不行的。在并发数大的情况下,S-B,S-C...S-N都会进行等待,这个等待时间如何控制?要知道http是需要设置超时时间的。超过了时间还是要返回给前端。


加锁会造成的情况:S-A在写入,S-B返回无红包,S-C却是正好在S-A写完后进来的,所以S-C又可以拆的红包。S-B就会漏掉红包,但是它确实是抢红包较早的用户。


  • InnoDB的锁配合事务使用

  • MySQL有共享锁和排它锁

  • 使用共享锁时,其他线程(连接)可以查询数据,但是不能更新和删除数据,使用排它锁时,不能查询数据不能更新数据,不能删除数据

  • MySQL的InnoDB引擎支持行级锁和表级锁,行级锁

  • InnoDB的行级锁是基于索引的,加锁是对索引加锁,加锁时没有索引时会锁住整张表

  • 修改锁等待的时间:innodb_lock_wait_timeout=500 单位秒


死锁情况:

1、A用户锁住了x行,B用户锁住了n行。这个时候A、B均为提交事物,所以锁还都保持着。然后A要访问n行,由于B用户锁住了n行,所以A用户等待。这时B用户访问x行,由于A用户锁住了x行,没有提交事物,所以锁还在。B只能等待A释放x行的锁,而A又等待B释放n行的锁。所以等待。


2、间隙锁造成的死锁:

number1222566611
id135
7910111223

A:select * from t where number=6;那么间隙锁锁定的间隙为:(5,11),也就是id 10 11 12 都会被加上锁

B:select * from t where number=2;那么间隙锁锁定的间隙为:(1,5),也就是id 3 5 7 都会被加上锁

注意这个时候A、B都没有提交事务。

A:更新id为3的数据,由于B锁住了3-7所以A等待

B:更新id为10的数据,由于A锁住了10-12所以B等待


死锁造成的情况还有很多,所以写的时候又要特别注意。


查询存在死锁的事务:SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;

结束存在死锁的事务:kill trx_mysql_thread_id(线程id)




3、临界

临界可以理解为一种加锁,只有你申请到了临界牌才可以进入临界进行操作,但是这也会造成标题2的问题。


4、redis对高并发

redis提供的指令都是具有原子性的,即不会被线程调度机制所打断,也就是不会被时间切片。lpush llen 这些操作同样具备原子性,但是在业务处理中,需要的是一系列的指令集合,这些指令集合可以理解为代码块,如果将具备原子性的指令组成代码块,那么这个代码块是不具备原子性的,有被打断的可能,如果被打断,其他线程的指令又过来操作数据,那么会造成标题2中提到的加锁问题。造成的结果是一样样的。即使你用setnx这类加锁指令也是一样的。


大多数的处理:使用pop命令,即预先把红包数存入栈中,然后每次pop出来,因为这个pop是具备原子性的,所以可以有效的解决高并发抢红包问题,并且它也是合理的。




5、高并发的解决思路:队列方式

注意这里说的一种思路是队列的方式,即把用户请求排队,尝试利用php来实现队列的操作,但是发现这种操作无法保证原子性,因为php的指令绝大部分是不支持原子性的,后来想自己实现原子性函数,但是发现原子性函数的实现php是无法完成的,需要用汇编语言直接编写。汇编我只学习了8086处理器的编写方法,Linux还并没有技术积累,而且即使写了具备原子性的函数,如何和php进行沟通,这又是一个问题。所以用php或者汇编来编写具备原子性的函数的方法搁浅~~~


6、高并发的解决思路:Socket

在看了redis和memcached的使用方法,看到它们都是做了一个tcp的监听,于是发现是不是可以利用socket的特性来做高并发处理,但是php要实现常驻socket需要cli模式运行,所以大多数情况下,php不作为socket服务端,而是只作为客户端。


变通思路:利用c++语言编写php扩展,实现socket服务端监听,而php作为客户端与服务端进行通讯,好处是可以实现分布式部署。


实现:


这里我用c++语言做了一个服务端的监听,并且利用php来做请求,实际php代码和监听服务端是在同一服务器内。注意,我这里的模式是阻塞模式,最好是像redis那样利用epoll来做一个非阻塞模式的socket。


SOCKET connSocket = accept(serverSocket, (SOCKADDR*)&clientAddr, &len);
char recvBuf[14]={}; //注意对你申请的栈进行置空,不然会有乱码
recv(connSocket, recvBuf, 14, 0);
if(redPack>0)
{
   sprintf_s(sendBuf, "成功:%s", inet_ntoa(clientAddr.sin_addr));
   redPack--;
   printf("当前红包数:%d | 请求时间戳:",redPack);
}
else
{
    sprintf_s(sendBuf, "失败:%s", inet_ntoa(clientAddr.sin_addr));
    printf("红包已拆完 | 请求时间戳:");
}

然后php端

if(!socket_write($socket, $in, strlen($in))) {
    return json(['r'=>0,'m'=>'抢红包成功']);
}


执行结果,这里我并发了200个请求,对于数量较多的请求需要看你服务器的吞吐量。


微信截图_20181230194557.png


可以看到相同的时间戳下,抢红包还是正确的。并且现在已经精确到了毫秒。



socket 阻塞模式下,socket并不会同时处理多个请求,而会一个一个请求,其余多的请求会放在缓存中等待,这是socket的内部实现机制。



对待真实的请求,我觉得还是要具体问题具体分析,并不是一种方式能够解决的,socket还有很多问题,比如多个红包,你要怎么做处理,怎么和php进行协作,这就涉及到了内存操作,而内存操作实际更为复杂,因为内存里是没有变量类型这一说法的,所以你要自己实现内存的操作模块,这里就涉及到了你的存储方法,如果是链表的话,那你要完全实现一套链表的增删改查,而且是否还需要处理“原子性”问题,这些都是要考虑进去的,并且阻塞模式是非常不推荐的,所以你要实现非阻塞模式,而这种方式你试后会发现......... 。



写这篇文章和demo主要是记录查阅资料过程中学习到的知识点。不保证正确性,因为好多概念还比较懵懂。如有错误还望指出。

Fastadmin 自定义请求参数

Fastadmin的table数据是通过Bootstrap Table来实现的,所以需要覆盖它的方法。这里是直接修改的require-table.js来实现的全局管理,如果不需要全局管理,可直接按照同样的方法修改html对应的js文件。


修改require-table.js里面的defaults新增一个对象queryParams,queryParams是一个函数对象,其中有一个参数,这个参数是Bootstrap Table本身的一些请求参数,可以在后面追加自己的自定义参数,然后返回就可以了。完整代码如下:

        defaults: {
            url: '',
            sidePagination: 'server',
            method: 'get', //请求方法
            toolbar: ".toolbar", //工具栏
            search: true, //是否启用快速搜索
            cache: false,
            commonSearch: true, //是否启用通用搜索
            searchFormVisible: false, //是否始终显示搜索表单
            titleForm: '', //为空则不显示标题,不定义默认显示:普通搜索
            idTable: 'commonTable',
            showExport: true,
            exportDataType: "all",
            exportTypes: ['json', 'xml', 'csv', 'txt', 'doc', 'excel'],
            pageSize: 10,
            pageList: [10, 25, 50, 'All'],
            pagination: true,
            clickToSelect: true, //是否启用点击选中
            dblClickToEdit: true, //是否启用双击编辑
            singleSelect: false, //是否启用单选
            showRefresh: false,
            locale: 'zh-CN',
            showToggle: true,
            showColumns: true,
            pk: 'id',
            sortName: 'id',
            sortOrder: 'desc',
            paginationFirstText: __("First"),
            paginationPreText: __("Previous"),
            paginationNextText: __("Next"),
            paginationLastText: __("Last"),
            cardView: false, //卡片视图
            checkOnInit: true, //是否在初始化时判断
            escape: true, //是否对内容进行转义
            extend: {
                index_url: '',
                add_url: '',
                edit_url: '',
                del_url: '',
                import_url: '',
                multi_url: '',
                dragsort_url: 'ajax/weigh',
            }, queryParams: function (params) {
                console.log(window.location.href);
                var url_http=window.location.href;
                var url=url_http.match(/ids(.*?)[1-9]\d*/g);
                if(url != null){
                    if(url.length>0){
                        params.ids= url[0].replace('ids/',"");
                    }
                }
                return params;
            }
        },