Skip to content

Commit 7d2373d

Browse files
committed
redis session handler
0 parents  commit 7d2373d

File tree

8 files changed

+385
-0
lines changed

8 files changed

+385
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.idea
2+

LICENSE

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Permission is hereby granted, free of charge, to any person
2+
obtaining a copy of this software and associated documentation
3+
files (the "Software"), to deal in the Software without
4+
restriction, including without limitation the rights to use,
5+
copy, modify, merge, publish, distribute, sublicense, and/or sell
6+
copies of the Software, and to permit persons to whom the
7+
Software is furnished to do so, subject to the following
8+
conditions:
9+
10+
The above copyright notice and this permission notice shall be
11+
included in all copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
14+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
15+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
16+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
17+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
18+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
20+
OTHER DEALINGS IN THE SOFTWARE.

Readme.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
Обработчик сессий через Redis с механизмом блокировки
2+
=====================================================
3+
4+
5+
Описание
6+
---------
7+
Используется для хранения сессий php в редисе.
8+
9+
Добавлен механизм блокировок: пока один процесс работает с сессией, второй процесс ожидает.
10+
11+
Установка
12+
---------
13+
14+
```
15+
composer require dmitry-suffi/RedisSessionHandler
16+
```
17+
18+
Использование
19+
-------------
20+
21+
```php
22+
23+
24+
$redis = new Redis();
25+
if ($redis->pconnect('11.111.111.11', 6379') && $redis->select(0)) {
26+
$handler = new \suffi\RedisSessionHandler\RedisSessionHandler($redis);
27+
session_set_save_handler($handler);
28+
}
29+
30+
session_start();
31+
32+
```
33+

composer.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "dmitry-suffi/RedisSessionHandler",
3+
"description": "Обработчик сессий через Redis с механизмом блокировки",
4+
"license": "MIT",
5+
"keywords": ["redis", "session"],
6+
"homepage": "https://github.com/dmitry-suffi/RedisSessionHandler",
7+
"type": "project",
8+
"autoload": {
9+
"psr-4": {
10+
"suffi\\RedisSessionHandler\\": "src\\"
11+
}
12+
}
13+
}

src/RedisSessionHandler.php

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
<?php
2+
3+
namespace suffi\RedisSessionHandler;
4+
5+
/**
6+
* Class RedisSessionHandler
7+
* @package suffi\RedisSessionHandler
8+
*/
9+
class RedisSessionHandler implements \SessionHandlerInterface
10+
{
11+
/**
12+
* Инстанс редиса
13+
* @var \Redis
14+
*/
15+
protected $redis;
16+
17+
/**
18+
* Время жизни сессии
19+
* @var int
20+
*/
21+
protected $ttl;
22+
23+
/**
24+
* Префикс
25+
* @var string
26+
*/
27+
protected $prefix;
28+
29+
/**
30+
* Флаг блокировки
31+
* @var bool
32+
*/
33+
protected $locked;
34+
35+
/**
36+
* Ключ блокировки
37+
* @var string
38+
*/
39+
private $lockKey;
40+
41+
/**
42+
* Токен блокировки
43+
* @var string
44+
*/
45+
private $token;
46+
47+
/**
48+
* Время между попытками разблокировки
49+
* @var int
50+
*/
51+
private $spinLockWait;
52+
53+
/**
54+
* Максимальное время ожидания разблокировки
55+
* @var int
56+
*/
57+
private $lockMaxWait;
58+
59+
/**
60+
* RedisSessionHandler constructor.
61+
* @param \Redis $redis
62+
* @param string $prefix
63+
* @param int $spinLockWait
64+
*/
65+
public function __construct(\Redis $redis, $prefix = 'session_key', $spinLockWait = 200000)
66+
{
67+
$this->redis = $redis;
68+
69+
$this->ttl = ini_get('gc_maxlifetime');
70+
$iniMaxExecutionTime = ini_get('max_execution_time');
71+
$this->lockMaxWait = $iniMaxExecutionTime ? $iniMaxExecutionTime * 0.7 : 20;
72+
73+
$this->prefix = $prefix;
74+
$this->locked = false;
75+
$this->lockKey = null;
76+
$this->spinLockWait = $spinLockWait;
77+
78+
}
79+
80+
/**
81+
* @inheritdoc
82+
*/
83+
public function open($savePath, $sessionName)
84+
{
85+
return true;
86+
}
87+
88+
/**
89+
* Попытка разблокировать сессию
90+
*/
91+
protected function lockSession($sessionId)
92+
{
93+
$attempts = (1000000 * $this->lockMaxWait) / $this->spinLockWait;
94+
95+
$this->token = uniqid();
96+
$this->lockKey = $sessionId . '.lock';
97+
for ($i = 0; $i < $attempts; ++$i) {
98+
$success = $this->redis->set(
99+
$this->getRedisKey($this->lockKey),
100+
$this->token,
101+
[
102+
'NX', //Установить ключ только, если он уже не существует.
103+
]
104+
);
105+
if ($success) {
106+
$this->locked = true;
107+
return true;
108+
}
109+
usleep($this->spinLockWait);
110+
}
111+
return false;
112+
}
113+
114+
/**
115+
* Снятие блокировки сессии
116+
*/
117+
private function unlockSession()
118+
{
119+
$script = <<<LUA
120+
if redis.call("GET", KEYS[1]) == ARGV[1] then
121+
return redis.call("DEL", KEYS[1])
122+
else
123+
return 0
124+
end
125+
LUA;
126+
127+
$this->redis->eval($script, array($this->getRedisKey($this->lockKey), $this->token), 1);
128+
129+
$this->locked = false;
130+
$this->token = null;
131+
}
132+
133+
/**
134+
* @inheritdoc
135+
*/
136+
public function close()
137+
{
138+
139+
if ($this->locked) {
140+
$this->unlockSession();
141+
}
142+
143+
return true;
144+
}
145+
146+
/**
147+
* @inheritdoc
148+
*/
149+
public function read($sessionId)
150+
{
151+
152+
if (!$this->locked) {
153+
if (!$this->lockSession($sessionId)) {
154+
return false;
155+
}
156+
}
157+
158+
return $this->redis->get($this->getRedisKey($sessionId)) ?: '';
159+
}
160+
161+
/**
162+
* @inheritdoc
163+
*/
164+
public function write($sessionId, $data)
165+
{
166+
if ($this->ttl > 0) {
167+
$this->redis->setex($this->getRedisKey($sessionId), $this->ttl, $data);
168+
} else {
169+
$this->redis->set($this->getRedisKey($sessionId), $data);
170+
}
171+
return true;
172+
}
173+
174+
/**
175+
* @inheritdoc
176+
*/
177+
public function destroy($sessionId)
178+
{
179+
$this->redis->del($this->getRedisKey($sessionId));
180+
$this->close();
181+
return true;
182+
}
183+
184+
/**
185+
* @inheritdoc
186+
*/
187+
public function gc($lifetime)
188+
{
189+
return true;
190+
}
191+
192+
/**
193+
* Установка времени жизни сессии
194+
* @param int $ttl
195+
*/
196+
public function setTtl($ttl)
197+
{
198+
$this->ttl = $ttl;
199+
}
200+
201+
/**
202+
* Максимальное время ожидания разблокировки
203+
* @return int
204+
*/
205+
public function getLockMaxWait()
206+
{
207+
return $this->lockMaxWait;
208+
}
209+
210+
/**
211+
* Максимальное время ожидания разблокировки
212+
* @param int $lockMaxWait
213+
*/
214+
public function setLockMaxWait($lockMaxWait)
215+
{
216+
$this->lockMaxWait = $lockMaxWait;
217+
}
218+
219+
/**
220+
* Подготовка ключа
221+
* @param string $key key
222+
* @return string prefixed key
223+
*/
224+
protected function getRedisKey($key)
225+
{
226+
if (empty($this->prefix)) {
227+
return $key;
228+
}
229+
return $this->prefix . $key;
230+
}
231+
232+
public function __destruct()
233+
{
234+
$this->close();
235+
}
236+
}

tests/Readme.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Запуск тестов
2+
=============
3+
4+
```
5+
phpunit --colors=always --bootstrap=tests/autoload.php tests/
6+
```
7+

tests/RedisSessionHandlerTest.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
use \suffi\RedisSessionHandler\RedisSessionHandler;
4+
5+
class RedisSessionHandlerTest extends \PHPUnit\Framework\TestCase
6+
{
7+
8+
public function getRedis()
9+
{
10+
$savePath = ini_get('session.save_path');
11+
$redis = new Redis();
12+
$redis->pconnect($savePath);
13+
$redis->select(963);
14+
return $redis;
15+
}
16+
17+
public function testReadWrite()
18+
{
19+
$handler = new RedisSessionHandler($this->getRedis());
20+
21+
$sessionId = 'ses' . uniqid();
22+
$handler->write($sessionId, 'session data');
23+
24+
$this->assertEquals($handler->read($sessionId), 'session data');
25+
}
26+
27+
public function testLock()
28+
{
29+
$handler1 = new RedisSessionHandler($this->getRedis());
30+
$handler2 = new RedisSessionHandler($this->getRedis());
31+
32+
$sessionId = 'ses' . uniqid();
33+
$handler1->write($sessionId, 'session data');
34+
$this->assertEquals($handler1->read($sessionId), 'session data');
35+
36+
$handler2->setLockMaxWait(5);
37+
$this->assertNotEquals($handler2->read($sessionId), 'session data');
38+
$this->assertFalse($handler2->read($sessionId));
39+
40+
$handler1->close();
41+
$this->assertEquals($handler2->read($sessionId), 'session data');
42+
}
43+
44+
}

tests/autoload.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
spl_autoload_register(function ($class) {
4+
5+
// project-specific namespace prefix
6+
$prefix = 'suffi\RedisSessionHandler';
7+
8+
// base directory for the namespace prefix
9+
$base_dir = __DIR__ . '/../src';
10+
11+
// does the class use the namespace prefix?
12+
$len = strlen($prefix);
13+
if (strncmp($prefix, $class, $len) !== 0) {
14+
// no, move to the next registered autoloader
15+
return;
16+
}
17+
18+
// get the relative class name
19+
$relative_class = substr($class, $len);
20+
21+
// replace the namespace prefix with the base directory, replace namespace
22+
// separators with directory separators in the relative class name, append
23+
// with .php
24+
$file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';
25+
26+
// if the file exists, require it
27+
if (file_exists($file)) {
28+
require $file;
29+
}
30+
});

0 commit comments

Comments
 (0)