为啥协程文章里面的数据库连接类会用单例?s

bobshipwood

问题描述

<?php
use Workerman\Connection\TcpConnection;
use Workerman\Coroutine\Context;
use Workerman\Coroutine;
use Workerman\Coroutine\Pool;
use Workerman\Events\Swoole;
use Workerman\Protocols\Http\Request;
use Workerman\Worker;
require_once __DIR__ . '/vendor/autoload.php';

class Db
{
    private static ?Pool $pool = null;

    public static function __callStatic($name, $arguments)
    {
        if (self::$pool === null) {
            self::initializePool();
        }
        // 从协程上下文中获取连接,保证同一个协程使用同一个连接
        $pdo = Context::get('pdo');
        if (!$pdo) {
            // 从连接池中获取连接
            $pdo = self::$pool->get();
            Context::set('pdo', $pdo);
            // 当协程结束时,自动归还连接
            Coroutine::defer(function () use ($pdo) {
                self::$pool->put($pdo);
            });
        }
        return call_user_func_array([$pdo, $name], $arguments);
    }

    private static function initializePool(): void
    {
        self::$pool = new Pool(10);
        self::$pool->setConnectionCreator(function () {
            return new \PDO('mysql:host=127.0.0.1;dbname=your_database', 'your_username', 'your_password');
        });
        self::$pool->setConnectionCloser(function ($pdo) {
            $pdo = null;
        });
        self::$pool->setHeartbeatChecker(function ($pdo) {
            $pdo->query('SELECT 1');
        });
    }

}

// Http Server
$worker = new Worker('http://0.0.0.0:8001');
$worker->eventLoop = Swoole::class; // Or Swow::class or Fiber::class
$worker->onMessage = function (TcpConnection $connection, Request $request) {
    $value = Db::query('SELECT NOW() as now')->fetchAll();
    $connection->send(json_encode($value));
};

Worker::runAll();

如上,我想不通为啥会用单例模式设计,他不是有这一行保证在单个协程内使用吗?$pdo = Context::get('pdo');
self::initializePool()应该是进程级别的吧,这样做相当于单个进程内,无论多少个协程,只能申请一个链接?

515 8 0
8个回答

morris

这明显是个连接池啊 。 连接池管理多个数据库连接。
每个协程先从连接池申请。 没有可用的则创建 。 并且在每个协程生命周期 通过Context:保证获取到的都是同一个链接。 避免数据错误

  • morris 9天前

    单例的作用就是 管理连接池

  • bobshipwood 9天前

    我理解这个进程池由于单例,导致整个进程下所有协程都共享一个进程池。
    我的问题就在于,需要用单例模式来保证上诉吗?它不是提供了个协程的pool类吗?

nitron

Pool需要单例

  • bobshipwood 9天前

    为啥?redis又不见他用单例?

  • nitron 9天前

    Redis是进程内共享一个连接

  • bobshipwood 9天前

    都是单个进程内多个协程共享吧?pool不就是提供单个进程多协程内共享吗?

  • nitron 9天前

    是的

  • bobshipwood 9天前

    那我的问题是,单例模式是不是保证进程内独享一个pool?为啥要mysql要这样做?
    这样做的原因是什么?
    按我的理解,pool类应该是本身就提供单进程内多协程的共享的,否则为啥叫pool?

  • nitron 9天前

    进程内只有一个pool,mysql有连接上限,不做限制迟早会把连接耗光,pool内连接是进程的协程内共享

  • bobshipwood 9天前

    1 连接数的限制不是通过一个参数max_connections来控制吗?
    2 我的问题是,为啥mysql要设计成单例,而不像redis那样?
    3 如果确实是单例,那起到的作用是什么?

nitron

1.max_connections是单个pool(也可以说是进程)内的连接数上限
2.redis单进程进程共享一个连接,在没引入协程支持之前,mysql也是单进程共享一个连接
3.管理进程内mysql连接,毕竟mysql有事务

这是早期关于连接池的讨论
https://www.workerman.net/q/5389

  • bobshipwood 9天前

    看了,真的很早期。
    但是,还是没有解答我的疑惑啊,
    1 如果数据库有事务,那应该在单个协程内处理完毕吧,不会跨协程吧?
    2 如果保证事务在单个协程内处理完毕(通过Context::get('')和set()),那请问pool类的编写是否一定要写成单例?如果不写成单例,那会造成什么样的后果?

  • bobshipwood 9天前

    看了,真的很早期。
    但是,还是没有解答我的疑惑啊,
    1 如果数据库有事务,那应该在单个协程内处理完毕吧,不会跨协程吧?
    2 如果保证事务在单个协程内处理完毕(通过Context::get('')和set()),那请问pool类的编写是否一定要写成单例?如果不写成单例,那会造成什么样的后果?

  • bobshipwood 9天前

    因为我的理解哈,在单个进程下,写单例模式只能保证每个协程请求的时候,请求的是这个链接。
    但是为啥不像redis一样,不写成单例会不会有啥影响?

  • nitron 9天前

    我先说结论,
    进程内,Redis是共享一个连接,Db也是共享一个连接,因为协程的引入,Db引入了pool,但Db依旧是单例
    举个例子,应用开8进程
    未支持协程之前,应用维护8个redis连接,8个mysql连接
    支持协程后,max_conn=5,应用维护8个redis连接,8 x 5个mysql连接(最高)
    至于协程
    https://www.workerman.net/doc/webman/coroutine/coroutine.html

  • bobshipwood 9天前

    redis的情况,如果是8个进程,maxconnection设置为5,理应是8X5个链接,如果需要做测试的话,那我也可以贴个测试的图上来。

  • nitron 9天前

    关于redis,我的表述有误,许久未看文档,redis也支持连接池,所以上面应该是redis 8x5, redis 8x5

  • bobshipwood 9天前

    所以,为啥需要单例模式?这个是我心中的疑问?

  • nitron 9天前

    常驻内存,非FPM用完即销毁,这就是本质原因

  • bobshipwood 9天前

    常驻内存,非FPM用完即销毁和他是单例有啥区别?
    纯fpm模式单例也是有用的,最起码能保证单次请求下,只能用一个单例

    但在workerman下面,每个进程如你所说,是常驻的,又因为在多个协程下都用到这个链接,所以才会有pool这个概念,因为单纯的用完就扔掉可能会来回创建多次tcp三次握手,所以才需要有pool来完成吧?
    那问题来了,既然pool保证在单个workerman进程下创建mysql的数据库链接,比方说5个链接吧,那协程来使用他的话,理论上加个context类彻底保证协程隔离就行了,为啥这个pool类要单例?

  • nitron 9天前

    减少每次创建/连接/销毁mysql连接的消耗

  • bobshipwood 9天前

    我觉得你是错误的,这个是pool完成的操作。

  • nitron 9天前

    我觉得我从项目历史,项目背景一直到原理都说的差不多了,如果你还是不能理解,那恕我才疏学浅,我能给你的答案就只有:"因为代码是这么写的"

  • bobshipwood 9天前

    别介意。只是纯探讨而已,谢谢你

JustForFun

需要恶补的知识:

  1. 连接池的作用;
  2. 什么是阻塞非阻塞操作;
  3. 进程、线程、协程是什么,用于什么场景;

这些都需要不断学习再学习,疑问很多的话最好系统性看看“操作系统原理”相关书籍

  • bobshipwood 9天前

    问题就在于pool,pool维持着连接池(理论上包括创建,销毁等操作)。供所有协程调用连接池的链接。
    单例在此是个啥意思,不得而知。我认为不用单例也行

  • JustForFun 9天前

    那你思考实践一下以下代码将注释去掉和不去掉的区别:

    <?php
    
    class Test {
        private static $instance;
    
        private function __construct() {
            echo "Hello world\n";
        }
    
        public static function getInstance(): Test {
    //        if (self::$instance) {
    //            return self::$instance;
    //        }
    
            self::$instance = new self();
            return self::$instance;
        }
    }
    
    Test::getInstance();
    Test::getInstance();

    有问题多问 AI,在论坛一直追问一整天不如追问 AI。上面那些基础多补补

  • bobshipwood 9天前

    这个这么简单的我懂。。

  • JustForFun 8天前

    你上面的代码也没复杂到哪里去。还是系统学习下吧,东学一块西学一块的理解不深

  • bobshipwood 8天前

    想问下“操作系统原理”有没有推荐的书或者视频?

agaegha

按照我的理解,如果pool不是单例,每次执行Db::query的时候就会创建一个pool,一个pool里面有多个连接,但是一次Db::query只需要一个连接,其他的不就浪费掉了吗?单例为了不浪费资源吧

  • bobshipwood 9天前

    我保证每个协程内,调用pool啊

  • agaegha 9天前

    为什么要每个协程调用一个pool,pool是连接池,不是连接,连接池一般一个进程只需要一个

  • bobshipwood 9天前

    所以,我有点明白了,要用单例模式保证每个进程内,使用pool

  • agaegha 9天前

    按照你的想法,每个协程单独创建一个连接的话,你需要手动管理每个连接的生命周期。忘记关闭的话可能会导致MySQL超出最大连接时出现异常

  • bobshipwood 9天前

    也不是不行,每个链接设置maxconexction=1

  • agaegha 9天前

    pool是连接池,PDO才是真正的MySQL连接,连接是没有maxconexction这个设置的。你每次都创建一个maxconexction=1的pool还是没有解决MySQL连接数量会耗尽的问题啊,这个和每次创建一个新连接有什么区别

  • bobshipwood 8天前

    假设我的pool是设置了maxconexction为1,那这么一来,pool的实例在多,也不会超出连接数啊?

  • agaegha 8天前

    如果maxconexction为1那一个pool实例就有一条连接,多个pool实例为什么不会超出连接数???

  • bobshipwood 8天前

    因为每个pool只维护1个链接啊,如果真的超出数据库类的链接能力,比方说这个数据库最大的连接数就是5,那new 5个pool实例,每个实例就1条链接也可以啊

  • agaegha 8天前

    但是怎么保证不会有第六个协程创建pool实例呢。再创建一个管理连接池的连接池吗

  • bobshipwood 8天前

    你说的的有道理,但是如果单例连接池的话,如果前面5个再用没有归还,那申请这个连接池的话,不一样会出问题?

  • agaegha 8天前
    /**
     * Get connection.
     *
     * @return object
     * @throws Throwable
     */
    public function get(): object
    {
        if (!Coroutine::isCoroutine()) {
            if (!$this->nonCoroutineConnection) {
                $this->nonCoroutineConnection = $this->createConnection();
            }
            return $this->nonCoroutineConnection;
        }
        $num = $this->channel->length();
        if ($num === 0 && $this->getConnectionCount() < $this->maxConnections) {
            return $this->createConnection();
        }
        $connection = $this->channel->pop($this->waitTimeout);
        if (!$connection) {
            throw new RuntimeException("Failed to get a connection from the pool within the wait timeout ($this->waitTimeout seconds). The connection pool is exhausted.");
        }
        $this->lastUsedTimes[$connection] = time();
        return $connection;
    }

    你可以看下源码,申请连接会等待一段时间,超时就会异常。这个就要根据业务调整连接池的大小和考虑扩大数据库规模了

  • bobshipwood 8天前

    谢谢,暂时能力还没到看源码的地步。

morris

假如有 1-5个协程。
1号协程 从db连接池 获取一个链接 。 开启事务 。 修改了表数据

假如你不用单例 。 在次查询这条数据的时候 。 获取的链接 是不是 有可能不是上一次修改数据的链接 ?
那根据mysql 的隔离特性 。 你是不是看不到 你上次修改后的数据 。

redis 不用单例。 是因为 redis是单进程对客户端进行服务 。 一个请求 一个响应。 请求是排队处理
mysql 是多进程处理 客户端请求。

  • bobshipwood 9天前

    协程中,每次使用链接的时候,首先从context::get(),::set()获得pdo链接的。。你可以看下他的文档

nitron

@walkor, 如果有空的话,你来讲解吧

  • bobshipwood 8天前

    不用了,我大概理解了,经过n轮的大战。。。

  • nitron 8天前

    其实我上面已经说的很清楚了,早期没有协程,一个进程只有一个Db实例,维持一个mysql连接(为了保持连接,会用一个timer,每55秒做一次select 1 维持连接),没记错是在onWorkerStart的时候初始化,可以连接复用,重复创建的消耗,有了协程,一个进程一个Db实例不变,Db::$pool当成该进程的连接池,该进程内所有的协程使用数据库时,从Db::$ppol内获取连接,所有就有了上面说的,8和8x5的结论

关于连接池里的文档只是示例,没有说必须怎样些,没有规定哪里必须是单例。

🔝