134 lines
3.7 KiB
PHP
134 lines
3.7 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace app\common\service;
|
||
|
||
use think\Config;
|
||
|
||
/**
|
||
* 分流链接号码严格轮转计数(Redis INCR,不可用时降级为 runtime 文件锁)
|
||
*/
|
||
class SplitRoundRobinStore
|
||
{
|
||
private const KEY_PREFIX = 'split:rr:';
|
||
|
||
/** @var \Redis|null */
|
||
private $redis = null;
|
||
|
||
private bool $redisReady = false;
|
||
|
||
private bool $redisAttempted = false;
|
||
|
||
/**
|
||
* 获取本次访问应使用的号码下标(0 .. count-1)
|
||
*/
|
||
public function nextIndex(int $splitLinkId, int $numberCount): int
|
||
{
|
||
if ($splitLinkId <= 0 || $numberCount <= 0) {
|
||
return 0;
|
||
}
|
||
|
||
if ($this->ensureRedis()) {
|
||
$key = self::KEY_PREFIX . $splitLinkId;
|
||
$seq = (int) $this->redis->incr($key);
|
||
if ($seq <= 0) {
|
||
$seq = 1;
|
||
}
|
||
|
||
return ($seq - 1) % $numberCount;
|
||
}
|
||
|
||
return $this->nextIndexFallback($splitLinkId, $numberCount);
|
||
}
|
||
|
||
/**
|
||
* 尝试连接 Redis(配置来自 queue 扩展)
|
||
*/
|
||
private function ensureRedis(): bool
|
||
{
|
||
if ($this->redisAttempted) {
|
||
return $this->redisReady;
|
||
}
|
||
$this->redisAttempted = true;
|
||
|
||
if (!extension_loaded('redis')) {
|
||
return false;
|
||
}
|
||
|
||
$config = Config::get('queue');
|
||
if (!is_array($config) || ($config['connector'] ?? '') !== 'Redis') {
|
||
$appPath = defined('APP_PATH') ? APP_PATH : (defined('ROOT_PATH') ? ROOT_PATH . 'application/' : '');
|
||
$file = $appPath . 'extra/queue.php';
|
||
if (is_file($file)) {
|
||
$loaded = include $file;
|
||
$config = is_array($loaded) ? $loaded : [];
|
||
} else {
|
||
$config = [];
|
||
}
|
||
}
|
||
|
||
$host = (string) ($config['host'] ?? '127.0.0.1');
|
||
$port = (int) ($config['port'] ?? 6379);
|
||
$password = (string) ($config['password'] ?? '');
|
||
$select = (int) ($config['select'] ?? 0);
|
||
$timeout = (float) ($config['timeout'] ?? 1.0);
|
||
|
||
try {
|
||
$redis = new \Redis();
|
||
$connected = $timeout > 0
|
||
? @$redis->connect($host, $port, $timeout)
|
||
: @$redis->connect($host, $port);
|
||
if (!$connected) {
|
||
return false;
|
||
}
|
||
if ($password !== '') {
|
||
if (!$redis->auth($password)) {
|
||
return false;
|
||
}
|
||
}
|
||
if ($select > 0) {
|
||
$redis->select($select);
|
||
}
|
||
$this->redis = $redis;
|
||
$this->redisReady = true;
|
||
} catch (\Throwable $e) {
|
||
$this->redisReady = false;
|
||
}
|
||
|
||
return $this->redisReady;
|
||
}
|
||
|
||
/**
|
||
* 无 Redis 时使用 runtime 文件锁递增(开发/单机可用;生产请启用 Redis)
|
||
*/
|
||
private function nextIndexFallback(int $splitLinkId, int $numberCount): int
|
||
{
|
||
$runtime = defined('RUNTIME_PATH') ? RUNTIME_PATH : (dirname(__DIR__, 3) . '/runtime/');
|
||
$dir = $runtime . 'split_rr/';
|
||
if (!is_dir($dir) && !@mkdir($dir, 0755, true)) {
|
||
return 0;
|
||
}
|
||
$file = $dir . $splitLinkId . '.cnt';
|
||
$fp = @fopen($file, 'c+');
|
||
if ($fp === false) {
|
||
return 0;
|
||
}
|
||
if (!flock($fp, LOCK_EX)) {
|
||
fclose($fp);
|
||
return 0;
|
||
}
|
||
$raw = stream_get_contents($fp);
|
||
$seq = (int) $raw;
|
||
$seq++;
|
||
ftruncate($fp, 0);
|
||
rewind($fp);
|
||
fwrite($fp, (string) $seq);
|
||
fflush($fp);
|
||
flock($fp, LOCK_UN);
|
||
fclose($fp);
|
||
|
||
return ($seq - 1) % $numberCount;
|
||
}
|
||
}
|