前端分流页功能
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user