Swoole 实战:MySQL 查询器的实现(协程连接池)

需求分析

本篇我们将通过 Swoole 实现一个自带连接池的 MySQL 查询器:

1. 支持通过链式调用构造并执行 SQL 语句;

2. 支持连接池技术;

3. 支持多协程事务并发执行(协程安全性);

4. 支持连接对象的健康检测;

5. 支持连接对象断线重连;

6. 程序需要可扩展,为未来的改造留好扩展点;

使用示例

查询:

$query->select(['uid', 'name'])

->from('users u')

->join('auth_users au', "u.uid=au.uid")

->where(['uid' => $uid])

->groupBy("u.phone")

->having("count(u.phone)>1")

->orderBy("u.uid desc")

->limit(10, 0)

->list();

插入:

$query->insert('users')

->values(

[

[

'name' => 'linvanda',

'phone' => '18687664562',

'nickname' => '林子',

],

[

'name' => 'xiake',

'phone' => '18989876543',

'nickname' => '侠客',

],

])->execute();// 这里是批量插入,不需要批量插入的话,传入一维数组即可

// 延迟插入$query->insert('users')

->delayed()

->values(

[

'name' => 'linvanda',

'phone' => '18687664562',

'nickname' => '林子',

])->execute();

更新:

$query->update('users u')

->join('auth_users au', "u.uid=au.uid")

->set(['u.name' => '粽子'])

->where("u.uid=:uid", ['uid' => 123])

->execute();

删除:

$query->delete('users')

->where("uid=:uid", ['uid' => 123])

->execute();

事务:

$query->begin();$query->update('users u')

->join('auth_users au', "u.uid=au.uid")

->set(['u.name' => '粽子'])

->where("u.uid=:uid", ['uid' => 123])

->execute();...$query->commit();

模块设计

1. 查询模块:

查询器(Query,入口)

SQL构造器(Builder)

2. 事务模块:

事务接口(ITransaction)

协程版事务类(CoTransaction)

协程上下文(TContext)

3. 连接池模块:

连接池接口(IPool)

协程连接池类(CoPool)

4. 数据库连接(驱动)模块:

连接接口(IConnector)

连接生成器接口(IConnectorBuilder)

协程连接类(CoConnector)

协程连接生成器(CoConnectorBuilder)

数据库连接配置类(DBConfig)

数据库连接(统计)信息类(ConnectorInfo)

我们希望通过统一的入口对外提供服务,将复杂性隐藏在内部。该统一入口由查询模块提供。该模块由查询器SQL 构造器构成,其中查询器作为外界唯一入口,而构造器是一个 Trait,因为这样可以让外界通过查询器入口直接使用构造器提供的 SQL 组装功能。

查询器通过事务模块执行 SQL。这里的事务有两个层面含义:数据库操作的事务性(显式或隐式事务,由 CoTransaction 类保障),以及多协程下的执行环境隔离性(由 TContext 类保障)。

事务模块需要通过数据库连接对象执行具体的 SQL。连接对象由连接池模块提供。

连接池模块维护(创建、回收、销毁)数据库连接对象,具体是通过数据库连接模块的连接生成器生成新数据库连接。

模块之间依赖于接口而非具体实现:查询模块依赖事务模块的 ITransaction 接口;事务模块依赖连接池模块的 IPool 接口和数据库连接模块的 IConnector 接口;连接池模块依赖数据库连接模块的 IConnectorBuilder 接口。

UML 类图

下面,我们分模块具体讲解。

入口

由查询模块对外提供统一的使用入口。查询模块由两部分构成:查询器和 SQL 构造器。为了让调用方可以直接通过查询器来构造 SQL(而不用先实例化一个构造器构造 SQL 然后传给查询器),我将构造器设计成 Trait 供查询器 Query 使用。

该入口类做了以下几件事情:

· 提供 list()、one()、page()、execute() 等方法执行 SQL 语句,其内部是通过 transaction 实现的;

· 通过 Builder 这个 Trait 对外提供 SQL 构造功能;

· 委托 transaction 实现事务功能;

构造器主要提供和 SQL 子句对应的方法来构造和编译 SQL,并提供对原生 SQL 的支持。
该构造器并未对所有的 SQL 语句做方法上的实现(比如子查询),只对最常用的功能提供了支持,复杂的 SQL 建议直接写 SQL 语句(一些框架对复杂 SQL 构造也提供了方法级别的支持,但这其实会带来使用和维护上的复杂性,它导致 SQL 不够直观)。

完整的查询模块代码

事务

事务是集中管理 SQL 执行上下文的地方,所有的 SQL 都是在事务中执行的(没有调 begin() 则是隐式事务)。

我们的查询器是协程安全的,即一个 Query 实例可以在多个协程中并发执行事务而不会相互影响。协程安全性是通过事务模块保证的,这里需要处理两个维度的“事务”:数据库维度和协程维度。不但需要保证数据库事务的完整执行,还要保证多个协程间的 SQL 执行不会相互影响。

我们先看一个多协程并发执行事务的例子(在两个子协程中使用同一个 Query 实例执行事务:先从数据库查询用户信息,然后更新姓名):

$query = new Query(...);

for ($i = 0; $i < 2; $i++) {

go(function () use ($query) {

$query->begin();

$user = $query->select("uid,name")->from("users")->where("phone=:phone", ["phone" => "13908987654"])->one();

$query->update('users')->set(['name' => "李四"])->where("uid=:uid", ['uid' => $user['uid']])->execute();

$query->commit();

});}

上面代码执行步骤如图:

在上图两个协程不断切换过程中,各自的事务是在独立执行的,互不影响。

现实中,我们会在仓储中使用查询器,每个仓储持有一个查询器实例,而仓储是单例模式,多协程共享的,因而查询器也是多协程共享的。如下:

/**

* MySQL 仓储基类

* 仓储是单例模式(通过容器实现单例),多协程会共享同一个仓储实例

*/abstract class MySQLRepository extends Repository implements ITransactional{

/**

* 查询器

*/

protected $query;

public function __construct()

{

if (!$this->dbAlias()) {

throw new Exception('dbName can not be null');

}

// 通过工厂创建查询器实例

$this->query = MySQLFactory::build($this->dbAlias());

}

...}

事务模块是如何实现协程并发事务的隔离性呢?我们用协程上下文 TContext 类实现协程间数据的隔离,事务类 CoTransaction 持有 TContext 实例,事务中所有的状态信息都通过 TContext 存取,以实现协程间状态数据互不影响。

我们先看看协程上下文类:

class TContext implements ArrayAccess{

private $container = [];

...

public function offsetGet($offset)

{

if (!isset($this->container[Co::getuid()])) {

return null;

}

return $this->container[Co::getuid()][$offset] ?? null;

}

public function offsetSet($offset, $value)

{

$cuid = Co::getuid();

if (!isset($this->container[$cuid])) {

$this->init();

}

$this->container[$cuid][$offset] = $value;

}

private function init()

{

$this->container[Co::getuid()] = [];

// 协程退出时需要清理当前协程上下文

Co::defer(function () {

unset($this->container[Co::getuid()]);

});

}}

协程上下文内部通过 $container 数组维护每个协程的数据。该类实现了 ArrayAccess 接口,可以通过下标访问,如:

// 创建上下文实例$context = new TContext();// 设置当前协程的数据$context["model"] = "write";// 访问当前协程的数据$context["model"];

再看看事务。

事务接口定义:

interface ITransaction{

public function begin(string $model = 'write', bool $isImplicit = false): bool;

/**

* 发送 SQL 指令

*/

public function command(string $preSql, array $params = []);

/**

* 提交事务

* @param bool $isImplicit 是否隐式事务,隐式事务不会向 MySQL 提交 commit (要求数据库服务器开启了自动提交的配置)

* @return bool

* @throws Exception

*/

public function commit(bool $isImplicit = false): bool;

public function rollback(): bool;

/**

* 获取或设置当前事务执行模式

* @param string 读/写模式 read/write

* @return string 当前事务执行模式

*/

public function model(?string $model = null): string;

...

/**

* 获取一次事务中执行的 SQL 列表

* @return array

*/

public function sql():array;}

上面接口定义了事务管理器的主要工作:开启事务、执行 SQL、提交/回滚事务以及和本次事务执行相关的信息。

我们再来看看它的实现类 CoTransaction,该类是整个查询器中最重要的类,我们把整个类的代码完整贴出来:

/**

* 协程版事务管理器

* 注意:事务开启直到提交/回滚的过程中会一直占用某个 IConnector 实例,如果有很多长事务,则会很快耗完连接池资源

*/class CoTransaction implements ITransaction{

private $pool;

// 事务的所有状态信息(运行状态、SQL、运行模式、运行结果等)都是存储在上下文中

private $context;

/**

* 创建事务实例时需要提供连接池,并在内部创建该事物的协程上下文实例

*/

public function __construct(IPool $pool)

{

$this->pool = $pool;

$this->context = new TContext();

}

public function __destruct()

{

// 如果事务没有结束,则回滚

if ($this->isRunning()) {

$this->rollback();

}

}

/**

* 开启事务

*/

public function begin(string $model = 'write', bool $isImplicit = false): bool

{

// 如果事务已经开启了,则直接返回

if ($this->isRunning()) {

return true;

}

// 事务模式(决定从读连接池还是写连接池拿连接对象)

$this->model($model);

// 设置事务运行状态

$this->isRunning(true);

// 获取数据库连接

try {

if (!($connector = $this->connector())) {

throw new ConnectException("获取连接失败");

}

} catch (Exception $exception) {

$this->isRunning(false);

throw new TransactionException($exception->getMessage(), $exception->getCode());

}

// 开启新事务前,需要清除上一次事务的数据

$this->resetLastExecInfo();

$this->clearSQL();

// 调用数据库连接对象的 begin 方法开始事务(如果是隐式事务则不调用)

return $isImplicit || $connector->begin();

}

/**

* 执行 SQL 指令

* 如果是隐式事务,则在该方法中自动调用 begin 和 commit 方法

*/

public function command(string $preSql, array $params = [])

{

if (!$preSql) {

return false;

}

// 是否隐式事务:外界没有调用 begin 而是直接调用 command 则为隐式事务

$isImplicit = !$this->isRunning();

// 如果是隐式事务,则需要自动开启事务

if ($isImplicit && !$this->begin($this->calcModelFromSQL($preSql), true)) {

return false;

}

// 执行 SQL

$result = $this->exec([$preSql, $params]);

// 隐式事务需要及时提交

if ($isImplicit && !$this->commit($isImplicit)) {

return false;

}

return $result;

}

/**

* 提交事务

*/

public function commit(bool $isImplicit = false): bool

{

if (!$this->isRunning()) {

return true;

}

$result = true;

if (!$isImplicit) {

// 显式事务才需要真正提交到 MySQL 服务器

if ($conn = $this->connector(false)) {

$result = $conn->commit();

if ($result === false) {

// 执行失败,试图回滚

$this->rollback();

return false;

}

} else {

return false;

}

}

// 释放事务占用的资源

$this->releaseTransResource();

return $result;

}

/**

* 回滚事务

* 无论是提交还是回滚,都需要释放本次事务占用的资源

*/

public function rollback(): bool

{

if (!$this->isRunning()) {

return true;

}

if ($conn = $this->connector(false)) {

$conn->rollback();

}

$this->releaseTransResource();

return true;

}

/**

* 获取或设置当前事务执行模式

*/

public function model(?string $model = null): string

{

// 事务处于开启状态时不允许切换运行模式

if (!isset($model) || $this->isRunning()) {

return $this->context['model'];

}

$this->context['model'] = $model === 'read' ? 'read' : 'write';

return $model;

}

public function lastInsertId()

{

return $this->getLastExecInfo('insert_id');

}

public function affectedRows()

{

return $this->getLastExecInfo('affected_rows');

}

public function lastError()

{

return $this->getLastExecInfo('error');

}

public function lastErrorNo()

{

return $this->getLastExecInfo('error_no');

}

/**

* 本次事务执行的所有 SQL

* 该版本并没有做记录

*/

public function sql(): array

{

return $this->context['sql'] ?? [];

}

/**

* 释放当前事务占用的资源

*/

private function releaseTransResource()

{

// 保存本次事务相关执行结果供外界查询使用

$this->saveLastExecInfo();

// 归还连接资源

$this->giveBackConnector();

unset($this->context['model']);

$this->isRunning(false);

}

/**

* 保存事务最终执行的一些信息

*/

private function saveLastExecInfo()

{

if ($conn = $this->connector(false)) {

$this->context['last_exec_info'] = [

'insert_id' => $conn->insertId(),

'error' => $conn->lastError(),

'error_no' => $conn->lastErrorNo(),

'affected_rows' => $conn->affectedRows(),

];

} else {

$this->context['last_exec_info'] = [];

}

}

private function resetLastExecInfo()

{

unset($this->context['last_exec_info']);

}

private function getLastExecInfo(string $key)

{

return isset($this->context['last_exec_info']) ? $this->context['last_exec_info'][$key] : '';

}

/**

* 执行指令池中的指令

* @param $sqlInfo

* @return mixed

* @throws

*/

private function exec(array $sqlInfo)

{

if (!$sqlInfo || !$this->isRunning()) {

return true;

}

return $this->connector()->query($sqlInfo[0], $sqlInfo[1]);

}

private function clearSQL()

{

unset($this->context['sql']);

}

private function calcModelFromSQL(string $sql): string

{

if (preg_match('/^(update|replace|delete|insert|drop|grant|truncate|alter|create)s/i', trim($sql))) {

return 'write';

}

return 'read';

}

/**

* 获取连接资源

*/

private function connector(bool $usePool = true)

{

if ($connector = $this->context['connector']) {

return $connector;

}

if (!$usePool) {

return null;

}

$this->context['connector'] = $this->pool->getConnector($this->model());

return $this->context['connector'];

}

/**

* 归还连接资源

*/

private function giveBackConnector()

{

if ($this->context['connector']) {

$this->pool->pushConnector($this->context['connector']);

}

unset($this->context['connector']);

}

private function isRunning(?bool $val = null)

{

if (isset($val)) {

$this->context['is_running'] = $val;

} else {

return $this->context['is_running'] ?? false;

}

}}

该类中,一次 SQL 执行(无论是显式事务还是隐式事务)的步骤:

begin -> exec -> commit/rollback

1. begin:

判断是否可开启新事务(如果已有事务在运行,则不可开启);

设置事务执行模式(read/write);

将当前事务状态设置为 running;

获取连接对象;

清理本事务实例中上次事务的痕迹(上下文、SQL);

调连接对象的 begin 启动数据库事务;

2. exec:

调用连接对象的 query 方法执行 SQL(prepare 模式);

3. commit:

判断当前状态是否可提交(running 状态才可以提交);

调用连接对象的 commit 方法提交数据库事务(如果失败则走回滚);

释放本次事务占用的资源(保存本次事务执行的相关信息、归还连接对象、清除上下文里面相关信息)

4. rollback:

判断当前状态是否可回滚;

调用连接对象的 rollback 回滚数据库事务;

释放本次事务占用的资源(同上);

优化:

类 CoTransaction 依赖 IPool 连接池,这种设计并不合理(违反了迪米特法则)。从逻辑上说,事务管理类真正依赖的是连接对象,而非连接池对象,因而事务模块应该依赖连接模块而不是连接池模块。让事务管理类依赖连接池,一方面向事务模块暴露了连接管理的细节, 另一方面意味着如果使用该事务管理类,就必须使用连接池技术。

一种优化方案是,在连接模块提供一个连接管理类供外部(事务模块)取还连接:

interface IConnectorManager{

public function getConnector() IConnector;

public function giveBackConnector(IConnector $conn);}

将 IConnectorManager 注入到 CoTransaction 中:

class CoTransaction implements ITransaction{

...

public function __construct(IConnectorManager $connMgr)

{

...

}}

连接管理器 IConnectorManager 承担了工厂方法角色,至此,事务模块仅依赖连接模块,而不用依赖连接池。

连接池

连接池模块由 IPool 接口和 CoPool 实现类组成。

连接池模块和连接模块之间的关系比较巧妙(上面优化后的方案)。从高层(接口层面)来说,连接池模块依赖连接模块:连接池操作(取还)IConnector 的实例;从实现上来说,连接模块同时又依赖连接池模块:PoolConnectorManager(使用连接池技术的连接管理器)依赖连接池模块来操作连接对象(由于该依赖是实现层面的而非接口层面,因而它不是必然的,如果连接管理器不使用连接池技术则不需要依赖连接池模块)。“连接管理器”这个角色很重要:它对外(事务模块)屏蔽了连接池模块的存在,代价是在内部引入了对连接池模块的依赖(也就是用内部依赖换外部依赖)。

经过上面的分析我们得出,连接池模块和连接模块具有较强的耦合性,连接模块可以对外屏蔽掉连接池模块的存在,因而在设计上我们可以将这两个模块看成一个大模块放在一个目录下面,在该目录下再细分成两个内部模块即可。

我们可以先去看看连接池接口:

这里有几点需要注意:

1. 连接池使用的是伪单例模式,同一个生成器对应的是同一个连接池实例;

2. 连接池内部维护了读写两个池子,生成器生成的读写连接对象分别放入对应的池子里面;

3. 从连接池取连接对象的时候,如果连接池为空,则根据情况决定是创建新连接还是等待。此处并非是在池子满了的情况下就等待,而是会超额创建,为的是应对峰值等异常情况。当然一个优化点是,将溢出比例做成可配置的,由具体的项目决定溢出多少。另外,如果创建新连接的时候数据库服务器报连接过多的错误,也需要转为等待连接归还;

4. 如果多次等待连接失败(超时),则后面的请求会直接抛出异常(直到池子不为空)。这里有个优化点:目前的实现没有区分是读池子超时还是写池子超时;

5. 归还连接时,如果池子满了,或者连接寿命到期了,则直接关闭连接;

后面在连接模块会讲解连接生成器,到时我们会知道一个连接池实例到底维护的是哪些连接对象。

连接

连接模块负责和数据库建立连接并发出 SQL 请求,其底层使用 Swoole 的 MySQL 驱动。连接模块由连接对象连接生成器构成,对外暴露 IConnector 和 IConnectorBuilder 接口。

(在我们的优化版本中,一方面引入了连接管理器 IConnectorManager,另一方面将连接模块连接池模块合并成一个大模块,因而整个连接模块对外暴露的是 IConnectorManager 和 IConnector 两个接口。)

连接对象的实现比较简单,我们重点看下 CoConnector 里面查询的处理:

该生成器是针对一主多从数据库架构的(包括未走读写分离的),如果使用是是其他数据库架构(如多主架构),则创建其他生成器即可。

同一套读写配置使用同一个生成器,对应的连接池也是同一个。

DBConfig 是一个 DTO 对象,不再阐述。

查询器的组装

使用工厂组装查询器实例:

class MySQLFactory{

/**

* @param string $dbAlias 数据库配置别名,对应配置文件中数据库配置的 key

*/

public static function build(string $dbAlias): Query

{

// 从配置文件获取数据库配置

$dbConf = Config::getInstance()->getConf("mysql.$dbAlias");

if (!$dbConf) {

throw new ConfigNotFoundException("mysql." . $dbAlias);

}

if (!isset($dbConf['read']) && !isset($dbConf['write'])) {

$writeConf = $dbConf;

$readConfs = [$writeConf];

} else {

$writeConf = $dbConf['write'] ?? [];

$readConfs = $dbConf['read'] ?? [$writeConf];

}

$writeConfObj = self::createConfObj($writeConf);

$readConfObjs = [];

foreach ($readConfs as $readConf) {

$readConfObjs[] = self::createConfObj($readConf);

}

// 创建生成器、连接池、事务管理器

// 在优化后版本中,用连接管理器代替连接池的位置即可

$mySQLBuilder = CoConnectorBuilder::instance($writeConfObj, $readConfObjs);

$pool = CoPool::instance($mySQLBuilder, $dbConf['pool']['size'] ?? 30);

$transaction = new CoTransaction($pool);

return new Query($transaction);

}

private static function createConfObj(array $config): DBConfig

{

if (!$config) {

throw new Exception("config is null");

}

return new DBConfig(

$config['host'],

$config['user'],

$config['password'],

$config['database'],

$config['port'] ?? 3306,

$config['timeout'] ?? 3,

$config['charset'] ?? 'utf8'

);

}}

至此,整个查询器的编写、创建和使用就完成了。

总结

1. 项目的开发需要划分模块,模块之间尽量减少耦合,通过接口通信(模块之间依赖接口而不是实现);

2. 如果两个模块之间具有强耦合性,则往往意味着两者本身应该归并到同一个模块中,在其内部划分子模块,对外屏蔽内部细节,如本项目的连接模块和连接池模块;

3. 如果模块之间存在不合常理的依赖关系,则意味着模块划分有问题,如本项目中的事务模块依赖连接池模块;

4. 有问题的模块划分往往违反第一点(也就是迪米特法则),会造成模块暴露细节、过多的依赖关系,影响设计的灵活性、可扩展性,如本项目中事务模块依赖连接池模块(虽然是实现层面的依赖而非接口层面),造成要使用 CoTransaction 时必须同时使用连接池;

5. 编写生产可用的项目时需要注意处理异常场景,如本项目中从连接池获取连接对象,以及在连接对象上执行 SQL 时的断线重连;

6. 设计本身是迭代式的,并非一蹴而就、一次性设计即可完成的,本项目在开发过程中已经经历过几次小重构,在本次分析时仍然发现一些设计上的缺陷。重构属于项目开发的一部分;

优化版 UML 图:

以上内容希望帮助到大家,很多PHPer在进阶的时候总会遇到一些问题和瓶颈,业务代码写多了没有方向感,不知道该从那里入手去提升,对此我整理了一些资料,包括但不限于:分布式架构、高可扩展、高性能、高并发、服务器性能调优、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql优化、shell脚本、Docker、微服务、Nginx等多个知识点高级进阶干货需要的可以免费分享给大家PHP进阶学习交流群

关注:架构师学习路线图,每日更新互联网最新技术文章与你不断前行,实战资料,笔试面试。

mysql 连接查询_Swoole 实战:MySQL 查询器的实现(协程连接池)相关推荐

  1. swoole mysql 协程_swoole-orm: 基于swoole的mysql协程连接池,简单封装。实现多个协程间共用同一个协程客户端。参考thinkphp-orm...

    swoole-orm 基于swoole的mysql协程连接池,简单封装. 实现多个协程间共用同一个协程客户端 感谢完善 [1]:nowbe -> 新增数据返回insert_id 版本 v0.0. ...

  2. 二进制安装mysql集群_实战mysql集群搭建(一)--centos7下二进制安装mysql-5.6

    在超哥的帮助下,完成了基于InnoDb数据引擎的mysql数据库集群搭建,实现了主从复制的功能,本篇博文介绍如何使用二进制安装mysql的方法,具体实现步骤如下: 软件使用说明: Liunx系统:ce ...

  3. python的装饰器、迭代器、yield_python装饰器,迭代器,生成器,协程

    python装饰器[1] 首先先明白以下两点 #嵌套函数 defout1():definner1():print(1234) inner1()#当没有加入inner时out()不会打印输出1234,当 ...

  4. day27-迭代器协议,协程,同步异步

    day27 总结 review """ !./env python -*- coding: utf-8 -*- @Time: 2021/6/3 9:21 @Author: ...

  5. mysql逻辑架构连接池_GitHub - zzjzzb/ycsocket: 基于swoole的socket框架,支持协程版MySQL、Redis连接池、Actor模型...

    ycsocket 基于 swoole 和 swoole_orm 的 websocket 框架,各位可以自己扩展到 TCP/UDP,HTTP. 在ycsocket 中,采用的是全协程化,全池化的数据库. ...

  6. php协程实现mysql异步_swoole与php协程实现异步非阻塞IO开发

    "协程可以在遇到阻塞的时候中断主动让渡资源,调度程序选择其他的协程运行.从而实现非阻塞IO" 然而php是不支持原生协程的,遇到阻塞时如不交由异步进程来执行是没有任何意义的,代码还 ...

  7. python协程池操作mysql_在python中使用aiomysql异步操作mysql

    之前一直在使用mongo与redis,最近在项目中开始使用mysql数据库,由于现在的项目是全程异步的操作,所以在在网上查了下关于在python中异步的操作mysql,找来找去最后发现aiomysql ...

  8. Kotlin中协程理解与实战(一)

    Kotlin中协程理解与实战(一) 什么是协程 在Android中协程用来解决什么问题 协程是: suspend -也称为挂起或暂停,用于暂停执行当前协程,并保存所有局部变量: resume -用于让 ...

  9. Android kotlin实战之协程suspend详解与使用

    前言 Kotlin 是一门仅在标准库中提供最基本底层 API 以便各种其他库能够利用协程的语言.与许多其他具有类似功能的语言不同,async 与 await 在 Kotlin 中并不是关键字,甚至都不 ...

最新文章

  1. 独家揭秘!阿里大规模数据中心的性能分析
  2. Asp.net MVC4.0(net4.5) 部署到window server 2003上的解决方案
  3. Node.js webpack-dev-server配置命令的两种方式
  4. 018_SpringBoot异常处理方式-ExceptionHandle注解处理异常
  5. 机器学习-数据科学库(第五天)
  6. Google App Engine平台下JDOQL查询报异常的问题解决方案
  7. 肖婧医生直播讲稿整理
  8. arduino的esp32程序无法上传_原装正版arduino uno R3无法上传程序
  9. 跟着内核学框架-从misc子系统到3+2+1设备识别驱动框架
  10. 中台架构与实现:基于ddd和微服务 下载_为什么在做微服务设计的时候需要DDD?...
  11. 增强幸福感的五种方法
  12. tp3.2 分析打印查询语句sql
  13. 手机ram内存测试软件,RAMTester(内存检测工具)
  14. 模块[camera]_摄影基础知识: 曝光补偿完全指南
  15. django 文档参考模型
  16. pytest框架笔记(十三) : Pytest+Allure定制报告
  17. 移动安全-Frida hook安卓So层函数实战
  18. Gateway网关简介以及使用
  19. 第3章 最简单的C程序设计——顺序程序设计
  20. 计算机系统安全之利用操作系统自带命令杀毒

热门文章

  1. linux shell sed d删除指定行并更换分隔符为#
  2. Android Studio3.5.2离线安装gradle
  3. 阿里DataV可视化大屏介绍
  4. 图解Hadoop hdfs写数据流程
  5. 【Java面试题视频讲解】提取不重复的整数
  6. android入门程序源代码,安卓程序开发入门
  7. texstudio 使用方法_简单说说LaTex(TexStudio中的使用)
  8. 性能测试(02)-HttpSampler
  9. win7笔记本电脑如何分割和重命名磁盘
  10. android studio异步单元测试,在Android Studio中可以进行单元测试