作者 zed

优化数据库插件。

@@ -6,6 +6,7 @@ use Swoole\Database\MysqliConfig; @@ -6,6 +6,7 @@ use Swoole\Database\MysqliConfig;
6 use Swoole\Database\MysqliPool; 6 use Swoole\Database\MysqliPool;
7 use Swoole\Coroutine; 7 use Swoole\Coroutine;
8 use Swoole\Coroutine\Channel; 8 use Swoole\Coroutine\Channel;
  9 +use Jiaoyin\StreamLogger;
9 10
10 class MysqlCli 11 class MysqlCli
11 { 12 {
@@ -18,6 +19,7 @@ class MysqlCli @@ -18,6 +19,7 @@ class MysqlCli
18 private int $lastReconnectTime = 0; 19 private int $lastReconnectTime = 0;
19 private $connectConfig = null; 20 private $connectConfig = null;
20 private $database = null; 21 private $database = null;
  22 + private $logger = null;
21 23
22 public function __construct($host, $port, $database, $username, $password, $charset = 'utf8', $prefix = '', $connectCount = 20) 24 public function __construct($host, $port, $database, $username, $password, $charset = 'utf8', $prefix = '', $connectCount = 20)
23 { 25 {
@@ -34,6 +36,7 @@ class MysqlCli @@ -34,6 +36,7 @@ class MysqlCli
34 'connectCount' => $connectCount, 36 'connectCount' => $connectCount,
35 ]; 37 ];
36 $this->poolConnect(); 38 $this->poolConnect();
  39 + $this->logger = new StreamLogger(); //默认路径
37 } 40 }
38 41
39 private function poolConnect() 42 private function poolConnect()
@@ -53,12 +56,17 @@ class MysqlCli @@ -53,12 +56,17 @@ class MysqlCli
53 $this->prefix = $this->connectConfig['prefix']; 56 $this->prefix = $this->connectConfig['prefix'];
54 } 57 }
55 58
56 - //执行sql  
57 - public function execute($action, $sql, float $timeout = 5.0, int $retry = 1) 59 + public function execute($action, $sql, float $timeout = 5.0, int $retry = 1, bool $assoc = false)
58 { 60 {
59 - $ret = false;  
60 - $db = $this->pool->get(); 61 + retry_label:
  62 + $db = null;
  63 + $start_time = microtime(true);
61 try { 64 try {
  65 + $db = $this->pool->get();
  66 + if (!$db) {
  67 + $this->logger->error('mysql', "数据库连接池耗尽,请检查数据库连接配置...");
  68 + throw new \RuntimeException("数据库连接池耗尽");
  69 + }
62 $chan = new Channel(1); 70 $chan = new Channel(1);
63 Coroutine::create(function () use ($chan, $db, $sql) { 71 Coroutine::create(function () use ($chan, $db, $sql) {
64 try { 72 try {
@@ -69,34 +77,103 @@ class MysqlCli @@ -69,34 +77,103 @@ class MysqlCli
69 } 77 }
70 }); 78 });
71 79
72 - $result = $chan->pop($timeout); // 超时控制 80 + /** 等待执行结果(支持超时) */
  81 + $result = $chan->pop($timeout);
73 82
  83 + /** ---------- 处理超时 ---------- */
74 if ($result === false) { 84 if ($result === false) {
75 - // 超时  
76 - output("SQL超时: {$sql}, 超时时间 {$timeout}s"); 85 + $this->logger->error('mysql', "SQL超时: {$timeout}s, SQL: {$sql}");
77 if ($retry > 0) { 86 if ($retry > 0) {
78 - output("超时后自动重试一次: {$sql}");  
79 - $this->pool->put($db);  
80 - return $this->execute($action, $sql, $timeout, $retry - 1); 87 + $this->logger->error('mysql', "执行超时,SQL:{$sql} 正在销毁连接并重试...");
  88 + // 1. 永远不要把超时的连接放回池
  89 + if ($db) {
  90 + @$db->close(); // 强制关闭
  91 + // 重要:告诉连接池不要再使用这个连接
  92 + $db = null;
81 } 93 }
82 - } elseif ($result instanceof \Throwable) {  
83 - output("执行SQL异常: {$sql}");  
84 - output("异常信息: " . $result->getMessage());  
85 - } else { 94 + // . 重建一个新连接补入池中
  95 + $retry--;
  96 + goto retry_label;
  97 + }
  98 +
  99 + return false;
  100 + }
  101 + /** ---------- 处理异常(断开 / gone away / server has gone away) ---------- */
  102 + if ($result instanceof \Throwable) {
  103 + $msg = $result->getMessage();
  104 + $this->logger->error('mysql', "执行异常: {$msg}; SQL: {$sql}");
  105 + $needReconnect = false;
  106 + if (
  107 + stripos($msg, 'gone away') !== false ||
  108 + stripos($msg, 'Lost connection') !== false ||
  109 + stripos($msg, 'server has gone away') !== false ||
  110 + stripos($msg, 'server closed') !== false ||
  111 + stripos($msg, 'Connection reset') !== false
  112 + ) {
  113 + $needReconnect = true;
  114 + }
  115 +
  116 + if ($needReconnect && $retry > 0) {
  117 + $this->logger->error('mysql', "数据库连接断开,SQL:{$sql}自动重连并重试 SQL...");
  118 + if ($db) {
  119 + try {
  120 + $db->close(); // 强制关闭
  121 + // 重要:告诉连接池不要再使用这个连接
  122 + $db = null;
  123 + } catch (\Throwable $e) {
  124 + $this->logger->error('mysql', "db close 失败,SQL:{$sql}忽略");
  125 + }
  126 + }
  127 + $this->poolConnect();
  128 +
  129 + $retry--;
  130 + goto retry_label;
  131 + }
  132 +
  133 + return false;
  134 + }
  135 +
  136 + /** ---------- 成功执行 ---------- */
86 if ($action === 'SELECT') { 137 if ($action === 'SELECT') {
87 - $ret = $result->fetch_all(); 138 + if ($assoc) {
  139 + $rows = $result->fetch_all(MYSQLI_ASSOC);
88 } else { 140 } else {
89 - $ret = $result; 141 + $rows = $result->fetch_all();
90 } 142 }
  143 + $result->free();
  144 + return $rows;
  145 + }
  146 +
  147 + return $result;
  148 + } catch (\Throwable $e) {
  149 + $this->logger->error('mysql', "execute() SQL:{$sql}致命异常: " . $e->getMessage());
  150 + if ($retry > 0) {
  151 + $this->logger->error('mysql', "致命异常:SQL:{$sql}尝试重连并重试");
  152 + if ($db) {
  153 + @$db->close(); // 强制关闭
  154 + $db = null;
91 } 155 }
  156 + $retry--;
  157 + goto retry_label;
  158 + }
  159 +
  160 + return false;
92 } finally { 161 } finally {
  162 + $end_time = microtime(true);
  163 + $count_time = $end_time - $start_time;
  164 + if ($count_time > 0.5) {
  165 + $this->logger->log('mysql', "SQL执行时间: " . $count_time . "s, SQL: {$sql}");
  166 + }
93 if ($db) { 167 if ($db) {
  168 + try {
94 $this->pool->put($db); 169 $this->pool->put($db);
  170 + } catch (\Throwable $e) {
  171 + $this->logger->error('mysql', "连接 put 回池失败(可能已断开),SQL:{$sql}忽略");
95 } 172 }
96 } 173 }
97 -  
98 - return $ret;  
99 } 174 }
  175 + }
  176 +
100 //插入数据 177 //插入数据
101 public function insert($table, $data) 178 public function insert($table, $data)
102 { 179 {
@@ -114,14 +191,7 @@ class MysqlCli @@ -114,14 +191,7 @@ class MysqlCli
114 if (!$sql) { 191 if (!$sql) {
115 return false; 192 return false;
116 } 193 }
117 - $data = $this->execute('SELECT', $sql);  
118 -  
119 - // 如果没有数据  
120 - if (!$data) return [];  
121 -  
122 - $newData = [];  
123 -  
124 - // ✅ 判断是否包含聚合函数 194 + // 判断是否包含聚合函数
125 $isAggregate = false; 195 $isAggregate = false;
126 foreach ($col as $c) { 196 foreach ($col as $c) {
127 if (stripos($c, 'sum(') !== false || stripos($c, 'count(') !== false || stripos($c, 'avg(') !== false || stripos($c, 'max(') !== false || stripos($c, 'min(') !== false) { 197 if (stripos($c, 'sum(') !== false || stripos($c, 'count(') !== false || stripos($c, 'avg(') !== false || stripos($c, 'max(') !== false || stripos($c, 'min(') !== false) {
@@ -133,23 +203,37 @@ class MysqlCli @@ -133,23 +203,37 @@ class MysqlCli
133 if ($isAggregate && empty($groupBy)) { 203 if ($isAggregate && empty($groupBy)) {
134 // 没有 group by 的单行聚合查询 204 // 没有 group by 的单行聚合查询
135 $db = $this->pool->get(); 205 $db = $this->pool->get();
  206 + try {
136 $stmt = $db->query($sql); 207 $stmt = $db->query($sql);
137 $result = $stmt->fetch_assoc(); 208 $result = $stmt->fetch_assoc();
138 - $this->pool->put($db); 209 + $stmt->free(); // 释放结果集,防止Commands out of sync错误
139 return [$result]; 210 return [$result];
  211 + } finally {
  212 + $this->pool->put($db);
  213 + }
140 } elseif ($isAggregate && !empty($groupBy)) { 214 } elseif ($isAggregate && !empty($groupBy)) {
141 // 有 group by 的聚合查询,多行返回 215 // 有 group by 的聚合查询,多行返回
142 $db = $this->pool->get(); 216 $db = $this->pool->get();
  217 + try {
143 $stmt = $db->query($sql); 218 $stmt = $db->query($sql);
144 $results = []; 219 $results = [];
145 while ($row = $stmt->fetch_assoc()) { 220 while ($row = $stmt->fetch_assoc()) {
146 $results[] = $row; 221 $results[] = $row;
147 } 222 }
148 - $this->pool->put($db); 223 + $stmt->free(); // 释放结果集,防止Commands out of sync错误
149 return $results; 224 return $results;
  225 + } finally {
  226 + $this->pool->put($db);
150 } 227 }
  228 + }
  229 +
  230 + // 非聚合查询,使用execute方法获取数据
  231 + $data = $this->execute('SELECT', $sql);
  232 +
  233 + // 如果没有数据
  234 + if (!$data) return [];
151 235
152 - // 普通查询才查字段结构 236 + // 普通查询才查字段结构
153 if (empty($col)) { 237 if (empty($col)) {
154 $newsql = 'SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = "' . $this->database . '" AND TABLE_NAME="' . $this->parseTable($table) . '" ORDER BY `ORDINAL_POSITION` ASC'; 238 $newsql = 'SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = "' . $this->database . '" AND TABLE_NAME="' . $this->parseTable($table) . '" ORDER BY `ORDINAL_POSITION` ASC';
155 $coldata = $this->execute('SELECT', $newsql); 239 $coldata = $this->execute('SELECT', $newsql);
@@ -196,7 +280,7 @@ class MysqlCli @@ -196,7 +280,7 @@ class MysqlCli
196 { 280 {
197 $table = $this->parseTable($table); 281 $table = $this->parseTable($table);
198 if (!is_array($data)) { 282 if (!is_array($data)) {
199 - output('insert data 不合法'); 283 + $this->logger->error('mysql', 'insert data 不合法');
200 return false; 284 return false;
201 } 285 }
202 $keys = array_keys($data); 286 $keys = array_keys($data);
@@ -216,7 +300,7 @@ class MysqlCli @@ -216,7 +300,7 @@ class MysqlCli
216 $table = $this->parseTable($table); 300 $table = $this->parseTable($table);
217 301
218 if (!empty($col) && !is_array($col)) { 302 if (!empty($col) && !is_array($col)) {
219 - output('select where col 不合法'); 303 + $this->logger->error('mysql', 'select where col 不合法');
220 return false; 304 return false;
221 } 305 }
222 306
@@ -246,7 +330,7 @@ class MysqlCli @@ -246,7 +330,7 @@ class MysqlCli
246 $tmp = ''; 330 $tmp = '';
247 foreach ($where as $key => $value) { 331 foreach ($where as $key => $value) {
248 if (!is_array($value) && !isset($value[1])) { 332 if (!is_array($value) && !isset($value[1])) {
249 - output('where 不合法'); 333 + $this->logger->error('mysql', 'where 不合法');
250 var_dump($where); 334 var_dump($where);
251 return false; 335 return false;
252 } 336 }
@@ -265,7 +349,8 @@ class MysqlCli @@ -265,7 +349,8 @@ class MysqlCli
265 } 349 }
266 $whereTxt .= $tmp; 350 $whereTxt .= $tmp;
267 } else { 351 } else {
268 - output('where 不合法'); 352 + $this->logger->error('mysql', 'where 不合法');
  353 +
269 var_dump($where); 354 var_dump($where);
270 return false; 355 return false;
271 } 356 }
@@ -277,7 +362,8 @@ class MysqlCli @@ -277,7 +362,8 @@ class MysqlCli
277 { 362 {
278 $table = $this->parseTable($table); 363 $table = $this->parseTable($table);
279 if (!is_array($data) || empty($data)) { 364 if (!is_array($data) || empty($data)) {
280 - output('更新数据不能为空'); 365 + $this->logger->error('mysql', '更新数据不能为空');
  366 +
281 return false; 367 return false;
282 } 368 }
283 $setTxt = 'set '; 369 $setTxt = 'set ';
@@ -319,4 +405,28 @@ class MysqlCli @@ -319,4 +405,28 @@ class MysqlCli
319 } 405 }
320 return $valueNew; 406 return $valueNew;
321 } 407 }
  408 + // 删除数据
  409 + public function delete($table, $where = [])
  410 + {
  411 + $sql = $this->parseDelete($table, $where);
  412 + if (!$sql) {
  413 + return false;
  414 + }
  415 + return $this->execute('DELETE', $sql);
  416 + }
  417 +
  418 + // 解析 delete
  419 + private function parseDelete($table, $where)
  420 + {
  421 + $table = $this->parseTable($table);
  422 + $whereTxt = $this->parseWhere($where);
  423 +
  424 + if (empty($whereTxt)) {
  425 + $this->logger->error('mysql', 'delete 必须指定 where 条件,防止全表删除');
  426 + return false;
  427 + }
  428 +
  429 + $sql = "delete from {$table} {$whereTxt}";
  430 + return $sql;
  431 + }
322 } 432 }