关于主从数据库,有两种方式。
1、自己搭建主从数据库,然后在代码层面上将请求定向到主或从。
2、使用类似阿里云的服务,一种是把主从连接都配置上,这就和普通的主从一样了。另一种就是你可以在代码里只配置一个数据库(主库)的连接,然后阿里云根据你的SQL判断将请求定向到主或从,在代码层面你看不出是主从配置,这种操作使你可以很灵活的上下从库,而代码不需要做任何改动,但是弊端就是不能从代码层面决定去主或从(当然,你可以通过开启事务的方式强制到主库)。
无论是哪种方式,开启了主从之后,在编码过程中需要注意的是主从延迟是必然会出现的,不可能消除!!所以,对于查询操作,如果没查到,并不是真的不存在,有可能是没同步过来。
1、在做数据一致性校验的时候,请读主库;
2、在同一个会话中先写再读,请读主库;
3、实时计算或统计的时候,请读主库;
。。。。。。
强调以上三点是因为我最近踩过的坑,公司用的就是阿里云的主从,而在Laravel的配置中是看不到的(只配了一个连接),所以我没意识到是主从,在统计的时候偶尔会出现偏差,在排查完代码之后才怀疑到可能是数据没同步的原因,继而才知道是配置了主从,并且无法将读定向到主库(除非开启事务),所以不得不重写该功能。这也提醒了我在编码的时候要以主从来考虑问题,即使项目暂时没上主从,如果以后业务需要上了主从,你要能确保自己的功能不会出问题。
Laravel配置主从
文档地址:https://learnku.com/docs/laravel/5.7/database/2288
有时候你希望 SELECT 语句使用一个数据库连接,而 INSERT,UPDATE,和 DELETE 语句使用另一个数据库连接。在 Laravel 中,无论你是使用原生查询,查询构造器,或者是 Eloquent ORM,都能轻松的实现。
配置文件 config/database.php
1、一主一从配置
'mysql' => [
'read' => [
'host' => env('DB_READ1_HOST', ''),
'username'=>env('DB_READ1_USERNAME', ''),
'password'=>env('DB_READ1_PASSWORD', ''),
],
'write' => [
'host' => env('DB_WRITE1_HOST', ''),
'username'=>env('DB_WRITE1_USERNAME', ''),
'password'=>env('DB_WRITE1_PASSWORD', ''),
],
'sticky' => true,
'driver' => 'mysql',
'database' => 'database',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
],
2、一主多从
'mysql' => [
'read' => [
[
'host' => env('DB_READ1_HOST', ''),
'username'=>env('DB_READ1_USERNAME', ''),
'password'=>env('DB_READ1_PASSWORD', ''),
],
[
'host' => env('DB_READ2_HOST', ''),
'username'=>env('DB_READ2_USERNAME', ''),
'password'=>env('DB_READ2_PASSWORD', ''),
],
],
'write' => [
'host' => env('DB_WRITE1_HOST', ''),
'username'=>env('DB_WRITE1_USERNAME', ''),
'password'=>env('DB_WRITE1_PASSWORD', ''),
],
'sticky' => true,
'driver' => 'mysql',
'database' => 'database',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
],
3、多主多从
'mysql' => [
'read' => [
[
'host' => env('DB_READ1_HOST', ''),
'username'=>env('DB_READ1_USERNAME', ''),
'password'=>env('DB_READ1_PASSWORD', ''),
],
[
'host' => env('DB_READ2_HOST', ''),
'username'=>env('DB_READ2_USERNAME', ''),
'password'=>env('DB_READ2_PASSWORD', ''),
],
],
'write' => [
[
'host' => env('DB_WRITE1_HOST', ''),
'username'=>env('DB_WRITE1_USERNAME', ''),
'password'=>env('DB_WRITE1_PASSWORD', ''),
],
[
'host' => env('DB_WRITE2_HOST', ''),
'username'=>env('DB_WRITE2_USERNAME', ''),
'password'=>env('DB_WRITE2_PASSWORD', ''),
],
],
'sticky' => true,
'driver' => 'mysql',
'database' => 'database',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
],
4、关于 sticky 配置项
sticky 是一个 可选值,它可用于立即读取在当前请求周期内已写入数据库的记录。若 sticky 选项被启用,并且当前请求周期内执行过 「写」 操作,那么任何 「读」 操作都将使用 「写」 连接。这样可确保同一个请求周期内写入的数据可以被立即读取到,从而避免主从延迟导致数据不一致的问题。不过是否启用它,取决于应用程序的需求。
意思就是,如果 sticky 为 true ,那么只要当前连接发生了“写”操作,那么此连接后续的所有SQL都定向到主库了,其实就是为了防止同一个连接中先写再读数据延迟的问题。
如果你的项目刚开始没用使用主从,现在才加上主从,那么应该设置 sticky 为 true ,因为你无法知道项目中是否存在“同一个连接中先写再读
”的情况;实际上 sticky 为 true 会增加主库的压力,使主从发挥的效果降低了。
5、读操作定向到主库
很多时候为了保证数据及时性,你需要将读操作定向到主库。
Laravel5读写分离主要有两个过程:
第一步,根据database.php配置,创建写库和读库的连接connection。
第二步,调用select
时先判断使用读库还是写库,而insert / update / delete / statement
统一使用写库。
第一步分析:Illuminate/Database/Connectors/ConnectionFactory.php
public function make(array $config, $name = null)
{
$config = $this->parseConfig($config, $name);
if (isset($config['read'])) {
// 开启了读写分离
return $this->createReadWriteConnection($config);
}
return $this->createSingleConnection($config);
}
// 创建read连接,write连接
protected function createReadWriteConnection(array $config)
{
// 创建write连接
$connection = $this->createSingleConnection($this->getWriteConfig($config));
// 创建read连接
return $connection->setReadPdo($this->createReadPdo($config));
}
// 创建write连接,因为默认就是非主从的,只有一个库,切实主库
protected function createSingleConnection(array $config)
{
$pdo = $this->createPdoResolver($config);
return $this->createConnection(
$config['driver'], $pdo, $config['database'], $config['prefix'], $config
);
}
// 随机取一个连接配置
protected function getReadWriteConfig(array $config, $type)
{
return isset($config[$type][0])
? Arr::random($config[$type])
: $config[$type];
}
由以上源码分析可知,开启了主从之后,laravel会预先创建一个read连接
和一个write连接
,如果是多主多从,则从多主中随机选择
一个配置,从多从中随机选择
一个配置来建立连接。
第二部分析:Illuminate/Database/Connection.php
一、对于select操作
1、可根据第三个输入参数判断使用读库还是写库,默认读库。
2、开启了事务,则一律去写库。
3、开启了 sticky 且发生了写操作,则一律去写库。
public function select($query, $bindings = [], $useReadPdo = true)
{
return $this->run($query, $bindings, function ($query, $bindings) use ($useReadPdo) {
if ($this->pretending()) {
return [];
}
// For select statements, we'll simply execute the query and return an array
// of the database result set. Each element in the array will be a single
// row from the database table, and will either be an array or objects.
$statement = $this->prepared($this->getPdoForSelect($useReadPdo)
->prepare($query));
$this->bindValues($statement, $this->prepareBindings($bindings));
$statement->execute();
return $statement->fetchAll();
});
}
protected function getPdoForSelect($useReadPdo = true)
{
return $useReadPdo ? $this->getReadPdo() : $this->getPdo();
}
/* 获取读库连接
1、开启了事务,则一律走写库
2、sticky 为 true,并且发生了写操作,则一律走写库
*/
public function getReadPdo()
{
if ($this->transactions > 0) {
return $this->getPdo();
}
if ($this->recordsModified && $this->getConfig('sticky')) {
return $this->getPdo();
}
if ($this->readPdo instanceof Closure) {
return $this->readPdo = call_user_func($this->readPdo);
}
return $this->readPdo ?: $this->getPdo();
}
// 获取写库连接
public function getPdo()
{
if ($this->pdo instanceof Closure) {
return $this->pdo = call_user_func($this->pdo);
}
return $this->pdo;
}
那么如何设置 select 的第三个参数呢?
1、使用原生SQL时
DB::select('select * from users where active = ?', [1], false);
或者
DB::selectFromWriteConnection('select * from users where active = ?', [1]);
源码文件:Illuminate/Database/Connection.php
public function selectFromWriteConnection($query, $bindings = [])
{
return $this->select($query, $bindings, false);
}
2、使用模型时
User::onWriteConnection()->find($id);
源码文件:Illuminate/Database/Eloquent/Model
public static function onWriteConnection()
{
$instance = new static;
// query builder 指定使用写库
return $instance->newQuery()->useWritePdo();
}
看看query builder如何指定使用写库 主要文件:Illuminate/Database/Query/Builder
/**
* Use the write pdo for query.
*
* @return $this
*/
public function useWritePdo()
{
$this->useWritePdo = true;
return $this;
}
/**
* Run the query as a "select" statement against the connection.
*
* @return array
*/
protected function runSelect()
{
return $this->connection->select($this->toSql(), $this->getBindings(), ! $this->useWritePdo);
}
二、对于 insert / delete / update / statement 操作,一律走写库
public function insert($query, $bindings = [])
{
return $this->statement($query, $bindings);
}
public function update($query, $bindings = [])
{
return $this->affectingStatement($query, $bindings);
}
public function delete($query, $bindings = [])
{
return $this->affectingStatement($query, $bindings);
}
public function statement($query, $bindings = [])
{
return $this->run($query, $bindings, function ($query, $bindings) {
if ($this->pretending()) {
return true;
}
$statement = $this->getPdo()->prepare($query);
$this->bindValues($statement, $this->prepareBindings($bindings));
$this->recordsHaveBeenModified();
return $statement->execute();
});
}
public function affectingStatement($query, $bindings = [])
{
return $this->run($query, $bindings, function ($query, $bindings) {
if ($this->pretending()) {
return 0;
}
// For update or delete statements, we want to get the number of rows affected
// by the statement and return that back to the developer. We'll first need
// to execute the statement and then we'll use PDO to fetch the affected.
$statement = $this->getPdo()->prepare($query);
$this->bindValues($statement, $this->prepareBindings($bindings));
$statement->execute();
$this->recordsHaveBeenModified(
($count = $statement->rowCount()) > 0
);
return $count;
});
}