作为程序员一定要保持良好的睡眠,才能好编程

秒杀流量肖锋解决方案-redis版

发布时间:2019-03-03

1、数据库建立

create table redis_queue(
	id int(11) unsigned not null auto_increment primary key comment 'id',
	uid int(11) not null,
	time_stamp varchar(20) default '',
	status tinyint(2) not null
)engine=innodb default charset='utf8';


2、建立获取参数、放入到redis消息队列

<?php

$redis=new \Redis();

if(!$redis){
  exit('redis对象初始化失败');
}

$flag=$redis->connect('127.0.0.1',6379);
if(!$flag){
  exit('redis链接失败');
}
$uid=$redis->incr('uid');
$order_queue='redis_queue';
$redis->watch($order_queue);
if($redis->llen($order_queue)<10){
//  $redis->watch($order_queue);
  $redis->multi();
  $redis->lpush($order_queue,$uid.'#'.microtime());  
  $redis->exec();
}else{    
  echo('秒杀程序结束');
}

$redis->close();



使用ab工具压力测试


共计发送了1000个请求:


./ab -n 1000 -c 100 http://192.168.61.100:8069/redis_queue.php >> log.log 2>&1


This is ApacheBench, Version 2.3 <$Revision: 1826891 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 192.168.61.100 (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:        nginx/1.6.3
Server Hostname:        192.168.61.100
Server Port:            8069

Document Path:          /redis_queue.php
Document Length:        0 bytes

Concurrency Level:      100
Time taken for tests:   2.713 seconds
Complete requests:      1000
Failed requests:        988
   (Connect: 0, Receive: 0, Length: 988, Exceptions: 0)
Total transferred:      178784 bytes
HTML transferred:       17784 bytes
Requests per second:    368.60 [#/sec] (mean)
Time per request:       271.299 [ms] (mean)
Time per request:       2.713 [ms] (mean, across all concurrent requests)
Transfer rate:          64.35 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    4  11.5      0      44
Processing:    32  255  43.8    266     305
Waiting:        0  254  44.0    265     303
Total:         35  259  37.4    267     338

Percentage of the requests served within a certain time (ms)
  50%    267
  66%    272
  75%    277
  80%    281
  90%    294
  95%    298
  98%    303
  99%    317
 100%    338 (longest request)


redis客户端查看:

127.0.0.1:6379> llen redis_queue
(integer) 10


刚好10个,一个不多,一个不少




事务锁/watch注意事项

超卖.png



不能写成下面的代码:商品会超卖的。

sw.png




3、在命令行启动消费队列 redis2db.php

<?php
include 'Db.php';

$database = new Medoo([
    'database_type' => 'mysql',
    'database_name' => 'test',
    'server' => 'localhost',
    'username' => 'root',
    'password' => 'song'
]);

$redis=new \Redis();

if(!$redis){
	exit('redis对象初始化失败');
}

$flag=$redis->connect('127.0.0.1',6379);

if(!$flag){
	exit('redis链接失败');
}

$order_queue='redis_queue';
$redis->watch($order_queue);
while(1){
	
	$info=$redis->rpop($order_queue);

	if(!$info){
		sleep(2);
		continue;
	}
	try{
		$data=explode('#',$info);
		$fields=['uid','time_stamp'];

		$data=array_combine($fields,$data);

		$result=$database->insert('redis_queue',$data);

		if(!$result){
			$redis->lpush($order_queue,$info);
		}
		echo "入库成功{$data['uid']}\n";
		sleep(2);
	}catch(Exception $e){
		$redis->lpush($order_queue,$info);
		echo $e->getMessage();
	}
}

$redis->close();


[root@localhost order_queue]# php redis2db.php 
入库成功1001
入库成功1002
入库成功1003
入库成功1004
入库成功1006
入库成功1008
入库成功1009
入库成功1010
入库成功1011
入库成功1012





插入数据结果:

mysql> select * from redis_queue;
+----+------+----------------------+--------+
| id | uid  | time_stamp           | status |
+----+------+----------------------+--------+
|  1 | 1001 | 0.38014100 155161925 |      0 |
|  2 | 1002 | 0.41516900 155161925 |      0 |
|  3 | 1003 | 0.42616100 155161925 |      0 |
|  4 | 1004 | 0.43027300 155161925 |      0 |
|  5 | 1006 | 0.43886400 155161925 |      0 |
|  6 | 1008 | 0.44785000 155161925 |      0 |
|  7 | 1009 | 0.45800200 155161925 |      0 |
|  8 | 1010 | 0.46536400 155161925 |      0 |
|  9 | 1011 | 0.47095100 155161925 |      0 |
| 10 | 1012 | 0.47724800 155161925 |      0 |
+----+------+----------------------+--------+
10 rows in set (0.00 sec)





Db.php

<?php
/*!
 * Medoo database framework
 * https://medoo.in
 * Version 1.6.1
 *
 * Copyright 2018, Angel Lai
 * Released under the MIT license
 */

class Raw {
	public $map;
	public $value;
}
class Medoo
{
	public $pdo;
	protected $type;
	protected $prefix;
	protected $statement;
	protected $dsn;
	protected $logs = [];
	protected $logging = false;
	protected $debug_mode = false;
	protected $guid = 0;
	public function __construct(array $options)
	{
		if (isset($options[ 'database_type' ]))
		{
			$this->type = strtolower($options[ 'database_type' ]);
			if ($this->type === 'mariadb')
			{
				$this->type = 'mysql';
			}
		}
		if (isset($options[ 'prefix' ]))
		{
			$this->prefix = $options[ 'prefix' ];
		}
		if (isset($options[ 'logging' ]) && is_bool($options[ 'logging' ]))
		{
			$this->logging = $options[ 'logging' ];
		}
		$option = isset($options[ 'option' ]) ? $options[ 'option' ] : [];
		$commands = (isset($options[ 'command' ]) && is_array($options[ 'command' ])) ? $options[ 'command' ] : [];
		switch ($this->type)
		{
			case 'mysql':
				// Make MySQL using standard quoted identifier
				$commands[] = 'SET SQL_MODE=ANSI_QUOTES';
				break;
			case 'mssql':
				// Keep MSSQL QUOTED_IDENTIFIER is ON for standard quoting
				$commands[] = 'SET QUOTED_IDENTIFIER ON';
				// Make ANSI_NULLS is ON for NULL value
				$commands[] = 'SET ANSI_NULLS ON';
				break;
		}
		if (isset($options[ 'pdo' ]))
		{
			if (!$options[ 'pdo' ] instanceof PDO)
			{
				throw new InvalidArgumentException('Invalid PDO object supplied');
			}
			$this->pdo = $options[ 'pdo' ];
			foreach ($commands as $value)
			{
				$this->pdo->exec($value);
			}
			return;
		}
		if (isset($options[ 'dsn' ]))
		{
			if (is_array($options[ 'dsn' ]) && isset($options[ 'dsn' ][ 'driver' ]))
			{
				$attr = $options[ 'dsn' ];
			}
			else
			{
				throw new InvalidArgumentException('Invalid DSN option supplied');
			}
		}
		else
		{
			if (
				isset($options[ 'port' ]) &&
				is_int($options[ 'port' ] * 1)
			)
			{
				$port = $options[ 'port' ];
			}
			$is_port = isset($port);
			switch ($this->type)
			{
				case 'mysql':
					$attr = [
						'driver' => 'mysql',
						'dbname' => $options[ 'database_name' ]
					];
					if (isset($options[ 'socket' ]))
					{
						$attr[ 'unix_socket' ] = $options[ 'socket' ];
					}
					else
					{
						$attr[ 'host' ] = $options[ 'server' ];
						if ($is_port)
						{
							$attr[ 'port' ] = $port;
						}
					}
					break;
				case 'pgsql':
					$attr = [
						'driver' => 'pgsql',
						'host' => $options[ 'server' ],
						'dbname' => $options[ 'database_name' ]
					];
					if ($is_port)
					{
						$attr[ 'port' ] = $port;
					}
					break;
				case 'sybase':
					$attr = [
						'driver' => 'dblib',
						'host' => $options[ 'server' ],
						'dbname' => $options[ 'database_name' ]
					];
					if ($is_port)
					{
						$attr[ 'port' ] = $port;
					}
					break;
				case 'oracle':
					$attr = [
						'driver' => 'oci',
						'dbname' => $options[ 'server' ] ?
							'//' . $options[ 'server' ] . ($is_port ? ':' . $port : ':1521') . '/' . $options[ 'database_name' ] :
							$options[ 'database_name' ]
					];
					if (isset($options[ 'charset' ]))
					{
						$attr[ 'charset' ] = $options[ 'charset' ];
					}
					break;
				case 'mssql':
					if (isset($options[ 'driver' ]) && $options[ 'driver' ] === 'dblib')
					{
						$attr = [
							'driver' => 'dblib',
							'host' => $options[ 'server' ] . ($is_port ? ':' . $port : ''),
							'dbname' => $options[ 'database_name' ]
						];
						if (isset($options[ 'appname' ]))
						{
							$attr[ 'appname' ] = $options[ 'appname' ];
						}
						if (isset($options[ 'charset' ]))
						{
							$attr[ 'charset' ] = $options[ 'charset' ];
						}
					}
					else
					{
						$attr = [
							'driver' => 'sqlsrv',
							'Server' => $options[ 'server' ] . ($is_port ? ',' . $port : ''),
							'Database' => $options[ 'database_name' ]
						];
						if (isset($options[ 'appname' ]))
						{
							$attr[ 'APP' ] = $options[ 'appname' ];
						}
						$config = [
							'ApplicationIntent',
							'AttachDBFileName',
							'Authentication',
							'ColumnEncryption',
							'ConnectionPooling',
							'Encrypt',
							'Failover_Partner',
							'KeyStoreAuthentication',
							'KeyStorePrincipalId',
							'KeyStoreSecret',
							'LoginTimeout',
							'MultipleActiveResultSets',
							'MultiSubnetFailover',
							'Scrollable',
							'TraceFile',
							'TraceOn',
							'TransactionIsolation',
							'TransparentNetworkIPResolution',
							'TrustServerCertificate',
							'WSID',
						];
						foreach ($config as $value)
						{
							$keyname = strtolower(preg_replace(['/([a-z\d])([A-Z])/', '/([^_])([A-Z][a-z])/'], '$1_$2', $value));
							if (isset($options[ $keyname ]))
							{
								$attr[ $value ] = $options[ $keyname ];
							}
						}
					}
					break;
				case 'sqlite':
					$attr = [
						'driver' => 'sqlite',
						$options[ 'database_file' ]
					];
					break;
			}
		}
		if (!isset($attr))
		{
			throw new InvalidArgumentException('Incorrect connection options');
		}
		$driver = $attr[ 'driver' ];
		if (!in_array($driver, PDO::getAvailableDrivers()))
		{
			throw new InvalidArgumentException("Unsupported PDO driver: {$driver}");
		}
		unset($attr[ 'driver' ]);
		$stack = [];
		foreach ($attr as $key => $value)
		{
			$stack[] = is_int($key) ? $value : $key . '=' . $value;
		}
		$dsn = $driver . ':' . implode($stack, ';');
		if (
			in_array($this->type, ['mysql', 'pgsql', 'sybase', 'mssql']) &&
			isset($options[ 'charset' ])
		)
		{
			$commands[] = "SET NAMES '{$options[ 'charset' ]}'" . (
				$this->type === 'mysql' && isset($options[ 'collation' ]) ?
				" COLLATE '{$options[ 'collation' ]}'" : ''
			);
		}
		$this->dsn = $dsn;
		try {
			$this->pdo = new PDO(
				$dsn,
				isset($options[ 'username' ]) ? $options[ 'username' ] : null,
				isset($options[ 'password' ]) ? $options[ 'password' ] : null,
				$option
			);
			foreach ($commands as $value)
			{
				$this->pdo->exec($value);
			}
		}
		catch (PDOException $e) {
			throw new PDOException($e->getMessage());
		}
	}
	public function query($query, $map = [])
	{
		$raw = $this->raw($query, $map);
		$query = $this->buildRaw($raw, $map);
		return $this->exec($query, $map);
	}
	public function exec($query, $map = [])
	{
		if ($this->debug_mode)
		{
			echo $this->generate($query, $map);
			$this->debug_mode = false;
			return false;
		}
		if ($this->logging)
		{
			$this->logs[] = [$query, $map];
		}
		else
		{
			$this->logs = [[$query, $map]];
		}
		$statement = $this->pdo->prepare($query);
		if ($statement)
		{
			foreach ($map as $key => $value)
			{
				$statement->bindValue($key, $value[ 0 ], $value[ 1 ]);
			}
			$statement->execute();
			$this->statement = $statement;
			return $statement;
		}
		return false;
	}
	protected function generate($query, $map)
	{
		$identifier = [
			'mysql' => '`$1`',
			'mssql' => '[$1]'
		];
		$query = preg_replace(
			'/"([a-zA-Z0-9_]+)"/i',
			isset($identifier[ $this->type ]) ?  $identifier[ $this->type ] : '"$1"',
			$query
		);
		foreach ($map as $key => $value)
		{
			if ($value[ 1 ] === PDO::PARAM_STR)
			{
				$replace = $this->quote($value[ 0 ]);
			}
			elseif ($value[ 1 ] === PDO::PARAM_NULL)
			{
				$replace = 'NULL';
			}
			elseif ($value[ 1 ] === PDO::PARAM_LOB)
			{
				$replace = '{LOB_DATA}';
			}
			else
			{
				$replace = $value[ 0 ];
			}
			$query = str_replace($key, $replace, $query);
		}
		return $query;
	}
	public static function raw($string, $map = [])
	{
		$raw = new Raw();
		$raw->map = $map;
		$raw->value = $string;
		return $raw;
	}
	protected function isRaw($object)
	{
		return $object instanceof Raw;
	}
	protected function buildRaw($raw, &$map)
	{
		if (!$this->isRaw($raw))
		{
			return false;
		}
		$query = preg_replace_callback(
			'/((FROM|TABLE|INTO|UPDATE)\s*)?\<([a-zA-Z0-9_\.]+)\>/i',
			function ($matches)
			{
				if (!empty($matches[ 2 ]))
				{
					return $matches[ 2 ] . ' ' . $this->tableQuote($matches[ 3 ]);
				}
				return $this->columnQuote($matches[ 3 ]);
			},
			$raw->value);
		$raw_map = $raw->map;
		if (!empty($raw_map))
		{
			foreach ($raw_map as $key => $value)
			{
				$map[ $key ] = $this->typeMap($value, gettype($value));
			}
		}
		return $query;
	}
	public function quote($string)
	{
		return $this->pdo->quote($string);
	}
	protected function tableQuote($table)
	{
		return '"' . $this->prefix . $table . '"';
	}
	protected function mapKey()
	{
		return ':MeDoO_' . $this->guid++ . '_mEdOo';
	}
	protected function typeMap($value, $type)
	{
		$map = [
			'NULL' => PDO::PARAM_NULL,
			'integer' => PDO::PARAM_INT,
			'double' => PDO::PARAM_STR,
			'boolean' => PDO::PARAM_BOOL,
			'string' => PDO::PARAM_STR,
			'object' => PDO::PARAM_STR,
			'resource' => PDO::PARAM_LOB
		];
		if ($type === 'boolean')
		{
			$value = ($value ? '1' : '0');
		}
		elseif ($type === 'NULL')
		{
			$value = null;
		}
		return [$value, $map[ $type ]];
	}
	protected function columnQuote($string)
	{
		if (strpos($string, '.') !== false)
		{
			return '"' . $this->prefix . str_replace('.', '"."', $string) . '"';
		}
		return '"' . $string . '"';
	}
	protected function columnPush(&$columns, &$map)
	{
		if ($columns === '*')
		{
			return $columns;
		}
		$stack = [];
		if (is_string($columns))
		{
			$columns = [$columns];
		}
		foreach ($columns as $key => $value)
		{
			if (is_array($value))
			{
				$stack[] = $this->columnPush($value, $map);
			}
			elseif (!is_int($key) && $raw = $this->buildRaw($value, $map))
			{
				preg_match('/(?<column>[a-zA-Z0-9_\.]+)(\s*\[(?<type>(String|Bool|Int|Number))\])?/i', $key, $match);
				$stack[] = $raw . ' AS ' . $this->columnQuote( $match[ 'column' ] );
			}
			elseif (is_int($key) && is_string($value))
			{
				preg_match('/(?<column>[a-zA-Z0-9_\.]+)(?:\s*\((?<alias>[a-zA-Z0-9_]+)\))?(?:\s*\[(?<type>(?:String|Bool|Int|Number|Object|JSON))\])?/i', $value, $match);
				if (!empty($match[ 'alias' ]))
				{
					$stack[] = $this->columnQuote( $match[ 'column' ] ) . ' AS ' . $this->columnQuote( $match[ 'alias' ] );
					$columns[ $key ] = $match[ 'alias' ];
					if (!empty($match[ 'type' ]))
					{
						$columns[ $key ] .= ' [' . $match[ 'type' ] . ']';
					}
				}
				else
				{
					$stack[] = $this->columnQuote( $match[ 'column' ] );
				}
			}
		}
		return implode($stack, ',');
	}
	protected function arrayQuote($array)
	{
		$stack = [];
		foreach ($array as $value)
		{
			$stack[] = is_int($value) ? $value : $this->pdo->quote($value);
		}
		return implode($stack, ',');
	}
	protected function innerConjunct($data, $map, $conjunctor, $outer_conjunctor)
	{
		$stack = [];
		foreach ($data as $value)
		{
			$stack[] = '(' . $this->dataImplode($value, $map, $conjunctor) . ')';
		}
		return implode($outer_conjunctor . ' ', $stack);
	}
	protected function dataImplode($data, &$map, $conjunctor)
	{
		$stack = [];
		foreach ($data as $key => $value)
		{
			$type = gettype($value);
			if (
				$type === 'array' &&
				preg_match("/^(AND|OR)(\s+#.*)?$/", $key, $relation_match)
			)
			{
				$relationship = $relation_match[ 1 ];
				$stack[] = $value !== array_keys(array_keys($value)) ?
					'(' . $this->dataImplode($value, $map, ' ' . $relationship) . ')' :
					'(' . $this->innerConjunct($value, $map, ' ' . $relationship, $conjunctor) . ')';
				continue;
			}
			$map_key = $this->mapKey();
			if (
				is_int($key) &&
				preg_match('/([a-zA-Z0-9_\.]+)\[(?<operator>\>\=?|\<\=?|\!?\=)\]([a-zA-Z0-9_\.]+)/i', $value, $match)
			)
			{
				$stack[] = $this->columnQuote($match[ 1 ]) . ' ' . $match[ 'operator' ] . ' ' . $this->columnQuote($match[ 3 ]);
			}
			else
			{
				preg_match('/([a-zA-Z0-9_\.]+)(\[(?<operator>\>\=?|\<\=?|\!|\<\>|\>\<|\!?~|REGEXP)\])?/i', $key, $match);
				$column = $this->columnQuote($match[ 1 ]);
				if (isset($match[ 'operator' ]))
				{
					$operator = $match[ 'operator' ];
					if (in_array($operator, ['>', '>=', '<', '<=']))
					{
						$condition = $column . ' ' . $operator . ' ';
						if (is_numeric($value))
						{
							$condition .= $map_key;
							$map[ $map_key ] = [$value, PDO::PARAM_INT];
						}
						elseif ($raw = $this->buildRaw($value, $map))
						{
							$condition .= $raw;
						}
						else
						{
							$condition .= $map_key;
							$map[ $map_key ] = [$value, PDO::PARAM_STR];
						}
						$stack[] = $condition;
					}
					elseif ($operator === '!')
					{
						switch ($type)
						{
							case 'NULL':
								$stack[] = $column . ' IS NOT NULL';
								break;
							case 'array':
								$placeholders = [];
								foreach ($value as $index => $item)
								{
									$placeholders[] = $map_key . $index . '_i';
									$map[ $map_key . $index . '_i' ] = $this->typeMap($item, gettype($item));
								}
								$stack[] = $column . ' NOT IN (' . implode(', ', $placeholders) . ')';
								break;
							case 'object':
								if ($raw = $this->buildRaw($value, $map))
								{
									$stack[] = $column . ' != ' . $raw;
								}
								break;
							case 'integer':
							case 'double':
							case 'boolean':
							case 'string':
								$stack[] = $column . ' != ' . $map_key;
								$map[ $map_key ] = $this->typeMap($value, $type);
								break;
						}
					}
					elseif ($operator === '~' || $operator === '!~')
					{
						if ($type !== 'array')
						{
							$value = [ $value ];
						}
						$connector = ' OR ';
						$data = array_values($value);
						if (is_array($data[ 0 ]))
						{
							if (isset($value[ 'AND' ]) || isset($value[ 'OR' ]))
							{
								$connector = ' ' . array_keys($value)[ 0 ] . ' ';
								$value = $data[ 0 ];
							}
						}
						$like_clauses = [];
						foreach ($value as $index => $item)
						{
							$item = strval($item);
							if (!preg_match('/(\[.+\]|_|%.+|.+%)/', $item))
							{
								$item = '%' . $item . '%';
							}
							$like_clauses[] = $column . ($operator === '!~' ? ' NOT' : '') . ' LIKE ' . $map_key . 'L' . $index;
							$map[ $map_key . 'L' . $index ] = [$item, PDO::PARAM_STR];
						}
						$stack[] = '(' . implode($connector, $like_clauses) . ')';
					}
					elseif ($operator === '<>' || $operator === '><')
					{
						if ($type === 'array')
						{
							if ($operator === '><')
							{
								$column .= ' NOT';
							}
							$stack[] = '(' . $column . ' BETWEEN ' . $map_key . 'a AND ' . $map_key . 'b)';
							$data_type = (is_numeric($value[ 0 ]) && is_numeric($value[ 1 ])) ? PDO::PARAM_INT : PDO::PARAM_STR;
							$map[ $map_key . 'a' ] = [$value[ 0 ], $data_type];
							$map[ $map_key . 'b' ] = [$value[ 1 ], $data_type];
						}
					}
					elseif ($operator === 'REGEXP')
					{
						$stack[] = $column . ' REGEXP ' . $map_key;
						$map[ $map_key ] = [$value, PDO::PARAM_STR];
					}
				}
				else
				{
					switch ($type)
					{
						case 'NULL':
							$stack[] = $column . ' IS NULL';
							break;
						case 'array':
							$placeholders = [];
							foreach ($value as $index => $item)
							{
								$placeholders[] = $map_key . $index . '_i';
								$map[ $map_key . $index . '_i' ] = $this->typeMap($item, gettype($item));
							}
							$stack[] = $column . ' IN (' . implode(', ', $placeholders) . ')';
							break;
						case 'object':
							if ($raw = $this->buildRaw($value, $map))
							{
								$stack[] = $column . ' = ' . $raw;
							}
							break;
						case 'integer':
						case 'double':
						case 'boolean':
						case 'string':
							$stack[] = $column . ' = ' . $map_key;
							$map[ $map_key ] = $this->typeMap($value, $type);
							break;
					}
				}
			}
		}
		return implode($conjunctor . ' ', $stack);
	}
	protected function whereClause($where, &$map)
	{
		$where_clause = '';
		if (is_array($where))
		{
			$where_keys = array_keys($where);
			$conditions = array_diff_key($where, array_flip(
				['GROUP', 'ORDER', 'HAVING', 'LIMIT', 'LIKE', 'MATCH']
			));
			if (!empty($conditions))
			{
				$where_clause = ' WHERE ' . $this->dataImplode($conditions, $map, ' AND');
			}
			if (isset($where[ 'MATCH' ]) && $this->type === 'mysql')
			{
				$MATCH = $where[ 'MATCH' ];
				if (is_array($MATCH) && isset($MATCH[ 'columns' ], $MATCH[ 'keyword' ]))
				{
					$mode = '';
					$mode_array = [
						'natural' => 'IN NATURAL LANGUAGE MODE',
						'natural+query' => 'IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION',
						'boolean' => 'IN BOOLEAN MODE',
						'query' => 'WITH QUERY EXPANSION'
					];
					if (isset($MATCH[ 'mode' ], $mode_array[ $MATCH[ 'mode' ] ]))
					{
						$mode = ' ' . $mode_array[ $MATCH[ 'mode' ] ];
					}
					$columns = implode(array_map([$this, 'columnQuote'], $MATCH[ 'columns' ]), ', ');
					$map_key = $this->mapKey();
					$map[ $map_key ] = [$MATCH[ 'keyword' ], PDO::PARAM_STR];
					$where_clause .= ($where_clause !== '' ? ' AND ' : ' WHERE') . ' MATCH (' . $columns . ') AGAINST (' . $map_key . $mode . ')';
				}
			}
			if (isset($where[ 'GROUP' ]))
			{
				$GROUP = $where[ 'GROUP' ];
				if (is_array($GROUP))
				{
					$stack = [];
					foreach ($GROUP as $column => $value)
					{
						$stack[] = $this->columnQuote($value);
					}
					$where_clause .= ' GROUP BY ' . implode($stack, ',');
				}
				elseif ($raw = $this->buildRaw($GROUP, $map))
				{
					$where_clause .= ' GROUP BY ' . $raw;
				}
				else
				{
					$where_clause .= ' GROUP BY ' . $this->columnQuote($GROUP);
				}
				if (isset($where[ 'HAVING' ]))
				{
					if ($raw = $this->buildRaw($where[ 'HAVING' ], $map))
					{
						$where_clause .= ' HAVING ' . $raw;
					}
					else
					{
						$where_clause .= ' HAVING ' . $this->dataImplode($where[ 'HAVING' ], $map, ' AND');
					}
				}
			}
			if (isset($where[ 'ORDER' ]))
			{
				$ORDER = $where[ 'ORDER' ];
				if (is_array($ORDER))
				{
					$stack = [];
					foreach ($ORDER as $column => $value)
					{
						if (is_array($value))
						{
							$stack[] = 'FIELD(' . $this->columnQuote($column) . ', ' . $this->arrayQuote($value) . ')';
						}
						elseif ($value === 'ASC' || $value === 'DESC')
						{
							$stack[] = $this->columnQuote($column) . ' ' . $value;
						}
						elseif (is_int($column))
						{
							$stack[] = $this->columnQuote($value);
						}
					}
					$where_clause .= ' ORDER BY ' . implode($stack, ',');
				}
				elseif ($raw = $this->buildRaw($ORDER, $map))
				{
					$where_clause .= ' ORDER BY ' . $raw;	
				}
				else
				{
					$where_clause .= ' ORDER BY ' . $this->columnQuote($ORDER);
				}
				if (
					isset($where[ 'LIMIT' ]) &&
					in_array($this->type, ['oracle', 'mssql'])
				)
				{
					$LIMIT = $where[ 'LIMIT' ];
					if (is_numeric($LIMIT))
					{
						$LIMIT = [0, $LIMIT];
					}
					
					if (
						is_array($LIMIT) &&
						is_numeric($LIMIT[ 0 ]) &&
						is_numeric($LIMIT[ 1 ])
					)
					{
						$where_clause .= ' OFFSET ' . $LIMIT[ 0 ] . ' ROWS FETCH NEXT ' . $LIMIT[ 1 ] . ' ROWS ONLY';
					}
				}
			}
			if (isset($where[ 'LIMIT' ]) && !in_array($this->type, ['oracle', 'mssql']))
			{
				$LIMIT = $where[ 'LIMIT' ];
				if (is_numeric($LIMIT))
				{
					$where_clause .= ' LIMIT ' . $LIMIT;
				}
				elseif (
					is_array($LIMIT) &&
					is_numeric($LIMIT[ 0 ]) &&
					is_numeric($LIMIT[ 1 ])
				)
				{
					$where_clause .= ' LIMIT ' . $LIMIT[ 1 ] . ' OFFSET ' . $LIMIT[ 0 ];
				}
			}
		}
		elseif ($raw = $this->buildRaw($where, $map))
		{
			$where_clause .= ' ' . $raw;
		}
		return $where_clause;
	}
	protected function selectContext($table, &$map, $join, &$columns = null, $where = null, $column_fn = null)
	{
		preg_match('/(?<table>[a-zA-Z0-9_]+)\s*\((?<alias>[a-zA-Z0-9_]+)\)/i', $table, $table_match);
		if (isset($table_match[ 'table' ], $table_match[ 'alias' ]))
		{
			$table = $this->tableQuote($table_match[ 'table' ]);
			$table_query = $table . ' AS ' . $this->tableQuote($table_match[ 'alias' ]);
		}
		else
		{
			$table = $this->tableQuote($table);
			$table_query = $table;
		}
		$join_key = is_array($join) ? array_keys($join) : null;
		if (
			isset($join_key[ 0 ]) &&
			strpos($join_key[ 0 ], '[') === 0
		)
		{
			$table_join = [];
			$join_array = [
				'>' => 'LEFT',
				'<' => 'RIGHT',
				'<>' => 'FULL',
				'><' => 'INNER'
			];
			foreach($join as $sub_table => $relation)
			{
				preg_match('/(\[(?<join>\<\>?|\>\<?)\])?(?<table>[a-zA-Z0-9_]+)\s?(\((?<alias>[a-zA-Z0-9_]+)\))?/', $sub_table, $match);
				if ($match[ 'join' ] !== '' && $match[ 'table' ] !== '')
				{
					if (is_string($relation))
					{
						$relation = 'USING ("' . $relation . '")';
					}
					if (is_array($relation))
					{
						// For ['column1', 'column2']
						if (isset($relation[ 0 ]))
						{
							$relation = 'USING ("' . implode($relation, '", "') . '")';
						}
						else
						{
							$joins = [];
							foreach ($relation as $key => $value)
							{
								$joins[] = (
									strpos($key, '.') > 0 ?
										// For ['tableB.column' => 'column']
										$this->columnQuote($key) :
										// For ['column1' => 'column2']
										$table . '."' . $key . '"'
								) .
								' = ' .
								$this->tableQuote(isset($match[ 'alias' ]) ? $match[ 'alias' ] : $match[ 'table' ]) . '."' . $value . '"';
							}
							$relation = 'ON ' . implode($joins, ' AND ');
						}
					}
					$table_name = $this->tableQuote($match[ 'table' ]) . ' ';
					if (isset($match[ 'alias' ]))
					{
						$table_name .= 'AS ' . $this->tableQuote($match[ 'alias' ]) . ' ';
					}
					$table_join[] = $join_array[ $match[ 'join' ] ] . ' JOIN ' . $table_name . $relation;
				}
			}
			$table_query .= ' ' . implode($table_join, ' ');
		}
		else
		{
			if (is_null($columns))
			{
				if (
					!is_null($where) ||
					(is_array($join) && isset($column_fn))
				)
				{
					$where = $join;
					$columns = null;
				}
				else
				{
					$where = null;
					$columns = $join;
				}
			}
			else
			{
				$where = $columns;
				$columns = $join;
			}
		}
		if (isset($column_fn))
		{
			if ($column_fn === 1)
			{
				$column = '1';
				if (is_null($where))
				{
					$where = $columns;
				}
			}
			elseif ($raw = $this->buildRaw($column_fn, $map))
			{
				$column = $raw;
			}
			else
			{
				if (empty($columns) || $this->isRaw($columns))
				{
					$columns = '*';
					$where = $join;
				}
				$column = $column_fn . '(' . $this->columnPush($columns, $map) . ')';
			}
		}
		else
		{
			$column = $this->columnPush($columns, $map);
		}
		return 'SELECT ' . $column . ' FROM ' . $table_query . $this->whereClause($where, $map);
	}
	protected function columnMap($columns, &$stack)
	{
		if ($columns === '*')
		{
			return $stack;
		}
		foreach ($columns as $key => $value)
		{
			if (is_int($key))
			{
				preg_match('/([a-zA-Z0-9_]+\.)?(?<column>[a-zA-Z0-9_]+)(?:\s*\((?<alias>[a-zA-Z0-9_]+)\))?(?:\s*\[(?<type>(?:String|Bool|Int|Number|Object|JSON))\])?/i', $value, $key_match);
				$column_key = !empty($key_match[ 'alias' ]) ?
					$key_match[ 'alias' ] :
					$key_match[ 'column' ];
				if (isset($key_match[ 'type' ]))
				{
					$stack[ $value ] = [$column_key, $key_match[ 'type' ]];
				}
				else
				{
					$stack[ $value ] = [$column_key, 'String'];
				}
			}
			elseif ($this->isRaw($value))
			{
				preg_match('/([a-zA-Z0-9_]+\.)?(?<column>[a-zA-Z0-9_]+)(\s*\[(?<type>(String|Bool|Int|Number))\])?/i', $key, $key_match);
				$column_key = $key_match[ 'column' ];
				if (isset($key_match[ 'type' ]))
				{
					$stack[ $key ] = [$column_key, $key_match[ 'type' ]];
				}
				else
				{
					$stack[ $key ] = [$column_key, 'String'];
				}
			}
			elseif (!is_int($key) && is_array($value))
			{
				$this->columnMap($value, $stack);
			}
		}
		return $stack;
	}
	protected function dataMap($data, $columns, $column_map, &$stack)
	{
		foreach ($columns as $key => $value)
		{
			$isRaw = $this->isRaw($value);
			if (is_int($key) || $isRaw)
			{
				$map = $column_map[ $isRaw ? $key : $value ];
				$column_key = $map[ 0 ];
				$result = $data[ $column_key ];
				if (isset($map[ 1 ]))
				{
					if ($isRaw && in_array($map[ 1 ], ['Object', 'JSON']))
					{
						continue;
					}
					if (is_null($result))
					{
						$stack[ $column_key ] = null;
						continue;
					}
					switch ($map[ 1 ])
					{
						case 'Number':
							$stack[ $column_key ] = (double) $result;
							break;
						case 'Int':
							$stack[ $column_key ] = (int) $result;
							break;
						case 'Bool':
							$stack[ $column_key ] = (bool) $result;
							break;
						case 'Object':
							$stack[ $column_key ] = unserialize($result);
							break;
						case 'JSON':
							$stack[ $column_key ] = json_decode($result, true);
							break;
						case 'String':
							$stack[ $column_key ] = $result;
							break;
					}
				}
				else
				{
					$stack[ $column_key ] = $result;
				}
			}
			else
			{
				$current_stack = [];
				$this->dataMap($data, $value, $column_map, $current_stack);
				$stack[ $key ] = $current_stack;
			}
		}
	}
	public function select($table, $join, $columns = null, $where = null)
	{
		$map = [];
		$stack = [];
		$column_map = [];
		$index = 0;
		$column = $where === null ? $join : $columns;
		$is_single = (is_string($column) && $column !== '*');
		$query = $this->exec($this->selectContext($table, $map, $join, $columns, $where), $map);
		$this->columnMap($columns, $column_map);
		if (!$query)
		{
			return false;
		}
		if ($columns === '*')
		{
			return $query->fetchAll(PDO::FETCH_ASSOC);
		}
		if ($is_single)
		{
			return $query->fetchAll(PDO::FETCH_COLUMN);
		}
		while ($data = $query->fetch(PDO::FETCH_ASSOC))
		{
			$current_stack = [];
			$this->dataMap($data, $columns, $column_map, $current_stack);
			$stack[ $index ] = $current_stack;
			$index++;
		}
		return $stack;
	}
	public function insert($table, $datas)
	{
		$stack = [];
		$columns = [];
		$fields = [];
		$map = [];
		if (!isset($datas[ 0 ]))
		{
			$datas = [$datas];
		}
		foreach ($datas as $data)
		{
			foreach ($data as $key => $value)
			{
				$columns[] = $key;
			}
		}
		$columns = array_unique($columns);
		foreach ($datas as $data)
		{
			$values = [];
			foreach ($columns as $key)
			{
				if ($raw = $this->buildRaw($data[ $key ], $map))
				{
					$values[] = $raw;
					continue;
				}
				$map_key =$this->mapKey();
				$values[] = $map_key;
				if (!isset($data[ $key ]))
				{
					$map[ $map_key ] = [null, PDO::PARAM_NULL];
				}
				else
				{
					$value = $data[ $key ];
					$type = gettype($value);
					switch ($type)
					{
						case 'array':
							$map[ $map_key ] = [
								strpos($key, '[JSON]') === strlen($key) - 6 ?
									json_encode($value) :
									serialize($value),
								PDO::PARAM_STR
							];
							break;
						case 'object':
							$value = serialize($value);
						case 'NULL':
						case 'resource':
						case 'boolean':
						case 'integer':
						case 'double':
						case 'string':
							$map[ $map_key ] = $this->typeMap($value, $type);
							break;
					}
				}
			}
			$stack[] = '(' . implode($values, ', ') . ')';
		}
		foreach ($columns as $key)
		{
			$fields[] = $this->columnQuote(preg_replace("/(\s*\[JSON\]$)/i", '', $key));
		}
		return $this->exec('INSERT INTO ' . $this->tableQuote($table) . ' (' . implode(', ', $fields) . ') VALUES ' . implode(', ', $stack), $map);
	}
	public function update($table, $data, $where = null)
	{
		$fields = [];
		$map = [];
		foreach ($data as $key => $value)
		{
			$column = $this->columnQuote(preg_replace("/(\s*\[(JSON|\+|\-|\*|\/)\]$)/i", '', $key));
			if ($raw = $this->buildRaw($value, $map))
			{
				$fields[] = $column . ' = ' . $raw;
				continue;
			}
			$map_key = $this->mapKey();
			preg_match('/(?<column>[a-zA-Z0-9_]+)(\[(?<operator>\+|\-|\*|\/)\])?/i', $key, $match);
			if (isset($match[ 'operator' ]))
			{
				if (is_numeric($value))
				{
					$fields[] = $column . ' = ' . $column . ' ' . $match[ 'operator' ] . ' ' . $value;
				}
			}
			else
			{
				$fields[] = $column . ' = ' . $map_key;
				$type = gettype($value);
				switch ($type)
				{
					case 'array':
						$map[ $map_key ] = [
							strpos($key, '[JSON]') === strlen($key) - 6 ?
								json_encode($value) :
								serialize($value),
							PDO::PARAM_STR
						];
						break;
					case 'object':
						$value = serialize($value);
					case 'NULL':
					case 'resource':
					case 'boolean':
					case 'integer':
					case 'double':
					case 'string':
						$map[ $map_key ] = $this->typeMap($value, $type);
						break;
				}
			}
		}
		return $this->exec('UPDATE ' . $this->tableQuote($table) . ' SET ' . implode(', ', $fields) . $this->whereClause($where, $map), $map);
	}
	public function delete($table, $where)
	{
		$map = [];
		return $this->exec('DELETE FROM ' . $this->tableQuote($table) . $this->whereClause($where, $map), $map);
	}
	public function replace($table, $columns, $where = null)
	{
		if (!is_array($columns) || empty($columns))
		{
			return false;
		}
		$map = [];
		$stack = [];
		foreach ($columns as $column => $replacements)
		{
			if (is_array($replacements))
			{
				foreach ($replacements as $old => $new)
				{
					$map_key = $this->mapKey();
					$stack[] = $this->columnQuote($column) . ' = REPLACE(' . $this->columnQuote($column) . ', ' . $map_key . 'a, ' . $map_key . 'b)';
					$map[ $map_key . 'a' ] = [$old, PDO::PARAM_STR];
					$map[ $map_key . 'b' ] = [$new, PDO::PARAM_STR];
				}
			}
		}
		if (!empty($stack))
		{
			return $this->exec('UPDATE ' . $this->tableQuote($table) . ' SET ' . implode(', ', $stack) . $this->whereClause($where, $map), $map);
		}
		return false;
	}
	public function get($table, $join = null, $columns = null, $where = null)
	{
		$map = [];
		$stack = [];
		$column_map = [];
		if ($where === null)
		{
			$column = $join;
			unset($columns[ 'LIMIT' ]);
		}
		else
		{
			$column = $columns;
			unset($where[ 'LIMIT' ]);
		}
		$is_single = (is_string($column) && $column !== '*');
		$query = $this->exec($this->selectContext($table, $map, $join, $columns, $where) . ' LIMIT 1', $map);
		if ($query)
		{
			$data = $query->fetchAll(PDO::FETCH_ASSOC);
			if (isset($data[ 0 ]))
			{
				if ($column === '*')
				{
					return $data[ 0 ];
				}
				$this->columnMap($columns, $column_map);
				$this->dataMap($data[ 0 ], $columns, $column_map, $stack);
				if ($is_single)
				{
					return $stack[ $column_map[ $column ][ 0 ] ];
				}
				return $stack;
			}
		}
	}
	public function has($table, $join, $where = null)
	{
		$map = [];
		$column = null;
		if ($this->type === 'mssql')
		{
			$query = $this->exec($this->selectContext($table, $map, $join, $column, $where, Medoo::raw('TOP 1 1')), $map);
		}
		else
		{
			$query = $this->exec('SELECT EXISTS(' . $this->selectContext($table, $map, $join, $column, $where, 1) . ')', $map);
		}
		if ($query)
		{
			$result = $query->fetchColumn();
			return $result === '1' || $result === 1 || $result === true;
		}
		return false;
	}
	public function rand($table, $join = null, $columns = null, $where = null)
	{
		$type = $this->type;
		$order = 'RANDOM()';
		if ($type === 'mysql')
		{
			$order = 'RAND()';
		}
		elseif ($type === 'mssql')
		{
			$order = 'NEWID()';
		}
		$order_raw = $this->raw($order);
		if ($where === null)
		{
			if ($columns === null)
			{
				$columns = [
					'ORDER'  => $order_raw
				];
			}
			else
			{
				$column = $join;
				unset($columns[ 'ORDER' ]);
				$columns[ 'ORDER' ] = $order_raw;
			}
		}
		else
		{
			unset($where[ 'ORDER' ]);
			$where[ 'ORDER' ] = $order_raw;
		}
		return $this->select($table, $join, $columns, $where);
	}
	private function aggregate($type, $table, $join = null, $column = null, $where = null)
	{
		$map = [];
		$query = $this->exec($this->selectContext($table, $map, $join, $column, $where, strtoupper($type)), $map);
		if ($query)
		{
			$number = $query->fetchColumn();
			return is_numeric($number) ? $number + 0 : $number;
		}
		return false;
	}
	public function count($table, $join = null, $column = null, $where = null)
	{
		return $this->aggregate('count', $table, $join, $column, $where);
	}
	public function avg($table, $join, $column = null, $where = null)
	{
		return $this->aggregate('avg', $table, $join, $column, $where);
	}
	public function max($table, $join, $column = null, $where = null)
	{
		return $this->aggregate('max', $table, $join, $column, $where);
	}
	public function min($table, $join, $column = null, $where = null)
	{
		return $this->aggregate('min', $table, $join, $column, $where);
	}
	public function sum($table, $join, $column = null, $where = null)
	{
		return $this->aggregate('sum', $table, $join, $column, $where);
	}
	public function action($actions)
	{
		if (is_callable($actions))
		{
			$this->pdo->beginTransaction();
			try {
				$result = $actions($this);
				if ($result === false)
				{
					$this->pdo->rollBack();
				}
				else
				{
					$this->pdo->commit();
				}
			}
			catch (Exception $e) {
				$this->pdo->rollBack();
				throw $e;
			}
			return $result;
		}
		return false;
	}
	public function id()
	{
		$type = $this->type;
		if ($type === 'oracle')
		{
			return 0;
		}
		elseif ($type === 'pgsql')
		{
			return $this->pdo->query('SELECT LASTVAL()')->fetchColumn();
		}
		return $this->pdo->lastInsertId();
	}
	public function debug()
	{
		$this->debug_mode = true;
		return $this;
	}
	public function error()
	{
		return $this->statement ? $this->statement->errorInfo() : null;
	}
	public function last()
	{
		$log = end($this->logs);
		return $this->generate($log[ 0 ], $log[ 1 ]);
	}
	public function log()
	{
		return array_map(function ($log)
			{
				return $this->generate($log[ 0 ], $log[ 1 ]);
			},
			$this->logs
		);
	}
	public function info()
	{
		$output = [
			'server' => 'SERVER_INFO',
			'driver' => 'DRIVER_NAME',
			'client' => 'CLIENT_VERSION',
			'version' => 'SERVER_VERSION',
			'connection' => 'CONNECTION_STATUS'
		];
		foreach ($output as $key => $value)
		{
			$output[ $key ] = @$this->pdo->getAttribute(constant('PDO::ATTR_' . $value));
		}
		$output[ 'dsn' ] = $this->dsn;
		return $output;
	}
}