Files

147 lines
4.2 KiB
PHP
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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
*
* @param string $poolKey 选号池标识(如 preferred / deferred),用于双层选号独立轮转
*/
public function nextIndex(int $splitLinkId, int $numberCount, string $poolKey = 'main'): int
{
if ($splitLinkId <= 0 || $numberCount <= 0) {
return 0;
}
$poolKey = $this->sanitizePoolKey($poolKey);
if ($this->ensureRedis()) {
$key = self::KEY_PREFIX . $splitLinkId . ':' . $poolKey;
$seq = (int) $this->redis->incr($key);
if ($seq <= 0) {
$seq = 1;
}
return ($seq - 1) % $numberCount;
}
return $this->nextIndexFallback($splitLinkId, $numberCount, $poolKey);
}
/**
* 选号池 key 仅允许字母数字与下划线,避免 Redis key 注入
*/
private function sanitizePoolKey(string $poolKey): string
{
$poolKey = preg_replace('/[^a-zA-Z0-9_]/', '', $poolKey) ?? '';
return $poolKey !== '' ? $poolKey : 'main';
}
/**
* 尝试连接 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, string $poolKey = 'main'): 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 . '_' . $poolKey . '.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;
}
}