0%

我愿意舍弃一切,以想念你终此一生。

在 Redis 里,所谓 SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果,不过很多人没有意识到 SETNX 有陷阱!

比如说:某个查询数据库的接口,因为调用量比较大,所以加了缓存,并设定缓存过期后刷新,问题是当并发量比较大的时候,如果没有锁机制,那么缓存过期的瞬间,大量并发请求会穿透缓存直接查询数据库,造成雪崩效应,如果有锁机制,那么就可以控制只有一个请求去更新缓存,其它的请求视情况要么等待,要么使用过期的缓存。

下面以目前 PHP 社区里最流行的 PHPRedis 扩展为例,实现一段演示代码:

1
2
3
4
5
6
7
8
9
10
<?php

$ok = $redis->setNX($key, $value);

if ($ok) {
$cache->update();
$redis->del($key);
}

?>

缓存过期时,通过 SetNX 获取锁,如果成功了,那么更新缓存,然后删除锁。看上去逻辑非常简单,可惜有问题:如果请求执行因为某些原因意外退出了,导致创建了锁但是没有删除锁,那么这个锁将一直存在,以至于以后缓存再也得不到更新。于是乎我们需要给锁加一个过期时间以防不测:

1
2
3
4
5
6
7
8
<?php

$redis->multi();
$redis->setNX($key, $value);
$redis->expire($key, $ttl);
$redis->exec();

?>

因为 SetNX 不具备设置过期时间的功能,所以我们需要借助 Expire 来设置,同时我们需要把两者用 Multi/Exec 包裹起来以确保请求的原子性,以免 SetNX 成功了 Expire 却失败了。 可惜还有问题:当多个请求到达时,虽然只有一个请求的 SetNX 可以成功,但是任何一个请求的 Expire 却都可以成功,如此就意味着即便获取不到锁,也可以刷新过期时间,如果请求比较密集的话,那么过期时间会一直被刷新,导致锁一直有效。于是乎我们需要在保证原子性的同时,有条件的执行 Expire,接着便有了如下 Lua 代码:

1
2
3
4
5
6
7
8
9
10
11
local key   = KEYS[1]
local value = KEYS[2]
local ttl = KEYS[3]

local ok = redis.call('setnx', key, value)

if ok == 1 then
redis.call('expire', key, ttl)
end

return ok

想到实现一个看起来很简单的功能还要用到 Lua 脚本,着实有些麻烦。其实 Redis 已经考虑到了大家的疾苦,从 2.6.12 起,SET 涵盖了 SETEX 的功能,并且 SET 本身已经包含了设置过期时间的功能,也就是说,我们前面需要的功能只用 SET 就可以实现。

1
2
3
4
5
6
7
8
9
10
<?php

$ok = $redis->set($key, $value, array('nx', 'ex' => $ttl));

if ($ok) {
$cache->update();
$redis->del($key);
}

?>

如上代码是完美的吗?答案是还差一点!设想一下,如果一个请求更新缓存的时间比较长,甚至比锁的有效期还要长,导致在缓存更新过程中,锁就失效了,此时另一个请求会获取锁,但前一个请求在缓存更新完毕的时候,如果不加以判断直接删除锁,就会出现误删除其它请求创建的锁的情况,所以我们在创建锁的时候需要引入一个随机值:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
# 这个方法我自己没有试验通过, 会报错
$ok = $redis->set($key, $random, array('nx', 'ex' => $ttl));

if ($ok) {
$cache->update();

if ($redis->get($key) == $random) {
$redis->del($key);
}
}

?>

补充:本文在删除锁的时候,实际上是有问题的,没有考虑到 GC pause 之类的问题造成的影响,比如 A 请求在 DEL 之前卡住了,然后锁过期了,这时候 B 请求又成功获取到了锁,此时 A 请求缓过来了,就会 DEL 掉 B 请求创建的锁,此问题远比想象的要复杂,具体解决方案参见本文最后关于锁的若干个参考链接。

https://huoding.com/2015/09/14/463

我不辛苦,我命苦!
针对websocket中常见的几个问题做一个详细的总结说明,具体要说的重点大概有下面3个: - 心跳检测的必要性 - 校验客户端连接的有效性 - 客户端的重连机制

心跳检测

swoole内置了心跳检测机制,我们只需要做如下简单的配置即可

1
2
3
4
$serv->set([
'heartbeat_check_interval' => N,
'heartbeat_idle_time' => M,
]);

如上,分别配置heartbeat_check_interval和heartbeat_idle_time参数,二者配合使用,其含义就是N秒检查一次,看看哪些连接M内没有活动的,就认为这个连接是无效的,server就会主动关闭这个无效的连接。
是不是说N秒server会主动向客户端发一个心跳包,没有收到客户端响应的才认为这个连接是死连接呢?那还要heartbeat_idle_time做什么,对吧?
swoole的实现原理是这样的:server每次收到客户端的数据包都会记录一个时间戳,N秒内循环检测下所有的连接,如果M秒内该连接还没有活动,才断开这个连接。

校验客户端连接的有效性

实际项目上线后,如果你的websocket server是对外开放的,就需要把ip修改为服务器外网的ip地址或者修改为0.0.0.0。
如此,也便带来了新的问题:任意客户端都可以连接到我们的server了,这个“任意”可不止我们自己认为有效的客户端,还包括你的我的所有的非有效或者恶意的连接,这可不是我们想要的。

如何避免这一问题呢?方法有很多种,比如我们可以在连接的时候认为只有get传递的参数valid=1才允许连接;或者我们只允许登录用户才可以连接server;再或者我们可以校验客户端每次send所携带的token,server对该值校验通过后才认为当前是有效连接等等。与此同时,server开启心跳检测,对于恶意无效的连接,直接干掉!

server的代码实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<?php

class WebSocketServerValid
{
private $_serv;
public $key = '^manks.top&swoole$';

public function __construct()
{
$this->_serv = new swoole_websocket_server("127.0.0.1", 9501);
$this->_serv->set([
'worker_num' => 1,
'heartbeat_check_interval' => 30,
'heartbeat_idle_time' => 62,
]);
$this->_serv->on('open', [$this, 'onOpen']);
$this->_serv->on('message', [$this, 'onMessage']);
$this->_serv->on('close', [$this, 'onClose']);
}

/**
* @param $serv
* @param $request
*/
public function onOpen($serv, $request)
{
$this->checkAccess($serv, $request);
}

/**
* @param $serv
* @param $frame
*/
public function onMessage($serv, $frame)
{
$this->_serv->push($frame->fd, 'Server: ' . $frame->data);
}
public function onClose($serv, $fd)
{
echo "client {$fd} closed.\n";
}

/**
* 校验客户端连接的合法性,无效的连接不允许连接
* @param $serv
* @param $request
* @return mixed
*/
public function checkAccess($serv, $request)
{
// get不存在或者uid和token有一项不存在,关闭当前连接
if (!isset($request->get) || !isset($request->get['uid']) || !isset($request->get['token'])) {
$this->_serv->close($request->fd);
return false;
}
$uid = $request->get['uid'];
$token = $request->get['token'];
// 校验token是否正确,无效关闭连接
if (md5(md5($uid) . $this->key) != $token) {
$this->_serv->close($request->fd);
return false;
}
}

public function start()
{
$this->_serv->start();
}
}

$server = new WebSocketServerValid;
$server->start();

可以看到,checkAccess是授权方法,我们在onOpen回调内对uid以及token进行了校验,无效则关闭连接。

客户端重连机制

客户端重连机制又可以理解为一种保活机制,你也可以跟服务端的心跳检测在一起理解为双向心跳。即我们有一种需求是,如何能保证客户端和服务端的连接一直是有效的,不断开的。

其实很简单,对客户端而言,只要触发error或者close再或者连接失败,就主动重连server,这便是我们的目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<script>
var ws;//websocket实例
var lockReconnect = false;//避免重复连接
var wsUrl = 'ws://127.0.0.1:9501';

function createWebSocket(url) {
try {
ws = new WebSocket(url);
initEventHandle();
} catch (e) {
reconnect(url);
}
}

function initEventHandle() {
ws.onclose = function () {
reconnect(wsUrl);
};
ws.onerror = function () {
reconnect(wsUrl);
};
ws.onopen = function () {
//心跳检测重置
heartCheck.reset().start();
};
ws.onmessage = function (event) {
//如果获取到消息,心跳检测重置
//拿到任何消息都说明当前连接是正常的
heartCheck.reset().start();
}
}

function reconnect(url) {
if(lockReconnect) return;
lockReconnect = true;
//没连接上会一直重连,设置延迟避免请求过多
setTimeout(function () {
createWebSocket(url);
lockReconnect = false;
}, 2000);
}

//心跳检测
var heartCheck = {
timeout: 60000,//60秒
timeoutObj: null,
serverTimeoutObj: null,
reset: function(){
clearTimeout(this.timeoutObj);
clearTimeout(this.serverTimeoutObj);
return this;
},
start: function(){
var self = this;
this.timeoutObj = setTimeout(function(){
//这里发送一个心跳,后端收到后,返回一个心跳消息,
//onmessage拿到返回的心跳就说明连接正常
ws.send("");
self.serverTimeoutObj = setTimeout(function(){//如果超过一定时间还没重置,说明后端主动断开了
ws.close();//如果onclose会执行reconnect,我们执行ws.close()就行了.如果直接执行reconnect 会触发onclose导致重连两次
}, self.timeout);
}, this.timeout);
}
}

createWebSocket(wsUrl);

</script>

从不说硬话; 从不做软事

我们在设计接口的时候,最怕一个接口被用户截取用于重放攻击。重放攻击是什么呢?就是把你的请求原封不动地再发送一次,两次…n次,一般正常的请求都会通过验证进入到正常逻辑中,如果这个正常逻辑是插入数据库操作,那么一旦插入数据库的语句写的不好,就有可能出现多条重复的数据。一旦是比较慢的查询操作,就可能导致数据库堵住等情况。

timestamp+nonce

我们常用的防止重放的机制是使用timestampnonce来做的重放机制。

timestamp用来表示请求的当前时间戳,这个时间戳当然要和服务器时间戳进行校正过的。我们预期正常请求带的timestamp参数会是不同的(预期是正常的人每秒至多只会做一个操作)。每个请求带的时间戳不能和当前时间超过一定规定的时间。比如60s。这样,这个请求即使被截取了,你也只能在60s内进行重放攻击。过期失效。

但是这样也是不够的,还有给攻击者60s的时间。所以我们就需要使用一个nonce,随机数。

nonce是由客户端根据足够随机的情况生成的,比如 md5(timestamp+rand(0, 1000)); 它就有一个要求,正常情况下,在短时间内(比如60s)连续生成两个相同nonce的情况几乎为0。

服务端

服务端第一次在接收到这个nonce的时候做下面行为:

  • 去redis中查找是否有keynonce:{nonce}的string
  • 如果没有,则创建这个key,把这个key失效的时间和验证timestamp失效的时间一致,比如是60s。
  • 如果有,说明这个key在60s内已经被使用了,那么这个请求就可以判断为重放请求。

示例

那么比如,下面这个请求:
http://a.com?uid=123&timestamp=1480556543&nonce=43f34f33&sign=80b886d71449cb33355d017893720666
这个请求中国的uid是我们真正需要传递的有意义的参数
timestampnoncesign都是为了签名和防重放使用。
timestamp是发送接口的时间,nonce是随机串,sign是对uid,timestamp,nonce(对于一些rest风格的api,我建议也把url放入sign签名)。签名的方法可以是md5({秘要}key1=val1&key2=val2&key3=val3…)
服务端接到这个请求:

  • 先验证sign签名是否合理,证明请求参数没有被中途篡改
  • 再验证timestamp是否过期,证明请求是在最近60s被发出的
  • 最后验证nonce是否已经有了,证明这个请求不是60s内的重放请求

https://www.cnblogs.com/yjf512/p/6590890.html

不自量力的还手 直至死方休

最近项目中遇到了一个问题, 问题出在env函数上,在Middleware中读取环境变量时取到的竟然是null

1
dump(env('APP_ENV') === null); // true

查看env 的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
function env($key, $default = null)
{
$value = getenv($key);

if ($value === false) {
return value($default);
}

// 此处省略部分代码

return $value;
}

可以发现,实际上调用的就是php自带的getenv函数,如果取不到值,那必定是某段执行putenv或者操作$_ENV全局变量的代码没有执行,而且这些环境变量设置操作一定是在初始化的时候进行的
如果对Laravel的Kernel执行顺序比较了解的话,应该知道,在Kernel启动阶段会首先执行一组bootstrappers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Illuminate/Foundation/Http/Kernel.php
class Kernel implements KernelContract
{
/**
* The bootstrap classes for the application.
*
* @var array
*/
protected $bootstrappers = [
'Illuminate\Foundation\Bootstrap\DetectEnvironment',
'Illuminate\Foundation\Bootstrap\LoadConfiguration',
'Illuminate\Foundation\Bootstrap\ConfigureLogging',
'Illuminate\Foundation\Bootstrap\HandleExceptions',
'Illuminate\Foundation\Bootstrap\RegisterFacades',
'Illuminate\Foundation\Bootstrap\RegisterProviders',
'Illuminate\Foundation\Bootstrap\BootProviders',
];

其中一个bootstrapper名为DetectEnvironment,而它的嫌疑是最大的,检查这个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//Illuminate/Foundation/Bootstrap/DetectEnvironment.php
class DetectEnvironment
{
/**
* Bootstrap the given application.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function bootstrap(Application $app)
{
if (! $app->configurationIsCached()) {
$this->checkForSpecificEnvironmentFile($app);

try {
(new Dotenv($app->environmentPath(), $app->environmentFile()))->load();
} catch (InvalidPathException $e) {
//
}
}
}

// ……
}

在代码中,我们并没有发现任何执行putenv或者操作$_ENV全局变量的地方,实际上在整个Laravel的框架代码里也没有发现蛛丝马迹,那么这些环境变量是如何保存进来的呢?
如果Laravel框架里没有执行这些代码,我们应该可以推测是框架使用的第三方组件做了这些事情
而事实也确实如此,可以看到在DetectEnvironment类的bootstrap方法里实例化了一个Dotenv类的对象,而Dotenv\Loader正是执行了读取.env文件,执行putenv的操作。
那么env获取不到值只能说明Dotenv的实例化并没有执行

再仔细看一下DetectEnvironment类,在实例化Dotenv前,先进行了一个 if 判断

1
if (! $app->configurationIsCached())

而这个方法的作用就是判断 bootstrap/cache/config.php 缓存文件是否存在,如果文件存在则不会执行Dotenv的实例化操作,也就不会往$_ENV里存数据,自然env函数是取不到值的

bootstrap/cache/config.php是执行下面的命令生成的

1
php artisan config:cache

清除使用命令

1
php artisan config:clear

那么,真相大白了,之前在开发时并没有出现问题,是因为没有执行 config:cache 命令,而项目上线后为了优化访问速度,生成了缓存文件,导致env取值失败

如果使用了config:cache,env函数只能在config目录下的配置文件的php里使用,不可以在其他地方使用。

一个非常简单的办法就是将

1
env('APP_ENV')

改为

1
config('app.env')

config函数会优先读取 bootstrap/cache/config.php 中缓存的配置,如果没有缓存文件,则会直接读取 config 目录下的所有配置文件

https://www.jianshu.com/p/83f9cd407751

All is well

如果你想在当前目录下 查找”hello,world!”字符串,可以这样:

1
grep -rn "hello,world!" *
1
2
3
4
5
* : 表示当前目录所有文件,也可以是某个文件名
-r 是递归查找
-n 是显示行号
-R 查找所有文件包含子目录
-i 忽略大小写

下面是一些有意思的命令行参数:

1
2
3
4
5
6
7
grep -i pattern files :不区分大小写地搜索。默认情况区分大小写, 
grep -l pattern files :只列出匹配的文件名,
grep -L pattern files :列出不匹配的文件名,
grep -w pattern files :只匹配整个单词,而不是字符串的一部分(如匹配‘magic’,而不是‘magical’),
grep -C number pattern files :匹配的上下文分别显示[number]行,
grep pattern1 | pattern2 files :显示匹配 pattern1 或 pattern2 的行,
grep pattern1 files | grep pattern2 :显示既匹配 pattern1 又匹配 pattern2 的行。

这里还有些用于搜索的特殊符号:

1
2
3
4
5
6
7
\< 和 \> 分别标注单词的开始与结尾。
例如:
grep man * 会匹配 ‘Batman’、‘manic’、‘man’等,
grep '\<man' * 匹配‘manic’和‘man’,但不是‘Batman’,
grep '\<man\>' 只匹配‘man’,而不是‘Batman’或‘manic’等其他的字符串。
'^':指匹配的字符串在行首,
'$':指匹配的字符串在行尾

http://blog.51cto.com/151wqooo/1162118

昔日寒山问拾得曰:世间有人谤我、欺我、辱我、笑我、轻我、贱我、恶我、骗我、如何处治乎?拾得曰:只要忍他、让他、由他、避他、耐他、敬他、不要理他,再待几年你且看他。

查询构造器也包含一些可以帮助你在 select 语法上实现 「悲观锁定」的函数。

sharedLock

共享锁可防止选中的数据列被篡改,直到事务被提交为止

1
DB::table('users')->where('votes', '>', 100)->sharedLock()->get();

lockForUpdate

使用「更新」锁可避免行被其它共享锁修改或选取

1
DB::table('users')->where('votes', '>', 100)->lockForUpdate()->get();

相同和不同

  • 相同的地方是都能避免同一行数据被其他 transaction 进行 update。
  • 不同的地方是:
    sharedLock 不会阻止其他 transaction 读取同一行
    lockForUpdate 会阻止其他 transaction 读取同一行(需要特别注意的是,普通的非锁定读取读取依然可以读取到该行,只有 sharedLock 和 lockForUpdate 的读取会被阻止。)

试验

事务本身并不会锁表或者锁行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 
Route::get('/haha', function () {
\DB::beginTransaction();
\App\Models\User::query()->where('id', 13)->first();
sleep(5);
\DB::commit();
});

// hehe的查询操作并不会受到影响
Route::get('/hehe', function () {
$user = \App\Models\User::query()->where('id', 13)->first();
return $user;
});
// 更新也没有任何影响

使用共享锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// sharedLock
Route::get('/haha', function () {
\DB::beginTransaction();
\App\Models\User::query()->where('id', 13)->sharedLock()->first();
sleep(5);
\DB::commit();
});

// 普通的查询没有影响
Route::get('/hehe', function () {
$user = \App\Models\User::query()->where('id', 13)->first();
return $user;
});
// 同样使用共享锁的查询没有影响
Route::get('/hehe', function () {
$user = \App\Models\User::query()->where('id', 13)->sharedLock()->first();
return $user;
});
// 使用lockForUpdate的查询会有影响

Route::get('/hehe', function () {
$user = \App\Models\User::query()->where('id', 13)->lockForUpdate()->first();
return $user;
});

使用排他锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Route::get('/haha', function () {
\DB::beginTransaction();
\App\Models\User::query()->where('id', 13)->lockForUpdate()->first();
sleep(5);
\DB::commit();
});

//普通的查询并没有影响
Route::get('/hehe', function () {
$user = \App\Models\User::query()->where('id', 13)->first();
return $user;
});

// 使用了共享锁或者排他锁的查询才会有影响
Route::get('/hehe', function () {
$user = \App\Models\User::query()->where('id', 13)->sharedLock()->first();
return $user;
});
// 对更新有影响
Route::get('/hehe', function () {
$user = \App\Models\User::query()->where('id', 13)->update(['name' => 'yangzie']);
return $user;
});

为天地立心,为生民立命,为前圣继绝学,为万世开太平。
今天遇到了一个问题
1
2
Predis \ Response \ ServerException
ERR Error running script (call to f_a08c9c69b7c07ae2485190873b90d128a23e502d): @user_script:1: @user_script: 1: -OOM command not allowed when used memory > 'maxmemory'.
谷歌了一下, 发现是Redis内存不够导致的, 遂开始我的修复之旅
  • 连接上redis

    1
    /usr/local/redis/bin/redis-cli
  • 查看内存信息

    1
    info memory
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    used_memory:461709952
    used_memory_human:440.32M
    used_memory_rss:521863168
    used_memory_rss_human:497.69M
    used_memory_peak:461915512
    used_memory_peak_human:440.52M
    used_memory_peak_perc:99.96%
    used_memory_overhead:2811662
    used_memory_startup:786656
    used_memory_dataset:458898290
    used_memory_dataset_perc:99.56%
    total_system_memory:3722543104
    total_system_memory_human:3.47G
    used_memory_lua:47104
    used_memory_lua_human:46.00K
    maxmemory:843000000
    maxmemory_human:803.95M
    maxmemory_policy:noeviction
    mem_fragmentation_ratio:1.13
    mem_allocator:jemalloc-4.0.3
    active_defrag_running:0
    lazyfree_pending_objects:0
  • 编辑配置文件

    1
    vi /usr/local/redis/etc/redis.conf
  • 根据你机器的配置适当的修改

    1
    maxmemory 2gb
  • 修改redis存储策略
    默认的redis设置是非常保守的,即内存超限后就不在存储,可以把策略修改为LRU算法(最近最少使用算法)——新存储的信息会替换掉旧的信息,从而不会是内存越线,修改redis.conf。这个必须结合业务场景,如果没有自动加载数据到redis的缓存机制,会造成数据缺少。我觉得可以往这方面靠,能最大利用资源。

    1
    maxmemory-policy volatile-lru
  • 重启redis

    1
    2
    3
    4
    5
    6
    7
    redis-cli -h 127.0.0.1 -p 6379 shutdown

    #加上`&`号使redis以后台程序方式运行
    ./redis-server &

    #启动时指定配置文件
    redis-server ./redis.conf

小巷 又弯又长 没有门 没有窗 我拿把旧钥匙 敲着厚厚的墙 —— 顾城《小巷》

在websocket实例化的时候,我们会绑定一些事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var ws = new WebSocket(url);
ws.onclose = function () {
//something
};
ws.onerror = function () {
//something
};

ws.onopen = function () {
//something
};
ws.onmessage = function (event) {
//something
}

如果希望websocket连接一直保持,我们会在close或者error上绑定重新连接方法。

1
2
3
4
5
6
ws.onclose = function () {
reconnect();
};
ws.onerror = function () {
reconnect();
};

这样一般正常情况下失去连接时,触发onclose方法,我们就能执行重连了。

stackoverflow上的一种做法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function connect() {
var ws = new WebSocket('ws://localhost:8080');
ws.onopen = function() {
// subscribe to some channels
ws.send(JSON.stringify({
//.... some message the I must send when I connect ....
}));
};

ws.onmessage = function(e) {
console.log('Message:', e.data);
};

ws.onclose = function(e) {
console.log('Socket is closed. Reconnect will be attempted in 1 second.', e.reason);
setTimeout(function() {
connect();
}, 1000);
};

ws.onerror = function(err) {
console.error('Socket encountered error: ', err.message, 'Closing socket');
ws.close();
};
}

connect();

使用第三方包 reconnecting-websocket

1
var ws = new ReconnectingWebSocket('ws://....');

一些选项

1
var socket = new ReconnectingWebSocket(url, null, {debug: true, reconnectInterval: 3000});

或者

1
2
3
var socket = new ReconnectingWebSocket(url);
socket.debug = true;
socket.timeoutInterval = 5400;

https://stackoverflow.com/questions/22431751/websocket-how-to-automatically-reconnect-after-it-dies
https://www.jianshu.com/p/dfde99d46ef4
https://github.com/joewalnes/reconnecting-websocket

就算终有一散,也别辜负相遇。好好相遇,好好告别。

linux下可以通过配置crontab来定时执行任务,执行体可以是一条系统命令或自己写的一个脚本,同时可以指派用户来执行。配置crontab有两种方法。

使用crontab命令,例如添加一个新的或编辑已有的,使用:

1
crontab -e

就可以进入配置文件。此时配置crontab的执行者是当前登入用户,如果当前用户是root,需要为其他用户配置,可以使用

1
2
3
4
5
crontab -e -u 用户名


su 用户名
crontab -e

这种方法有一个缺点,就是当前系统中配置的crontab不在一个配置文件中,让管理员不方便查询系统到底有多少个crontab。

直接在/etc/crontab文件中添加,不过需要是root身份。打开文件,应该会看到类似下面的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root
HOME=/

# For details see man 4 crontabs

# Example of job definition:
# .---------------- minute (0 - 59)
# | .------------- hour (0 - 23)
# | | .---------- day of month (1 - 31)
# | | | .------- month (1 - 12) OR jan,feb,mar,apr ...
# | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# | | | | |
# * * * * * user-name command to be executed

要添加新的crontab,只需要在文件最后增加即可。注意这里面需要指定用户名;而方法1中则不需要,如果指定了,它会认为是命令的一部分,从而可能导致crontab执行失败。

如果服务器都是有root来管理,建议添加crontab使用方法2,这样系统中的所有计划任务都在一起,一目了然。

注意问题

我在配置Laravel的计划任务的时候有这样一条配置

1
2
3
vi /etc/crontab

* * * * www /bin/php /data/wwwroot/51ito.io/ethgo/artisan schedule:run >> /dev/null 2>&1

正常保存以后发现计划任务并没有顺利执行, 随想这去查看日志

1
tail -10 /var/log/cron

发现

1
2
3
4
5
6
7
8
9
10
11
Jul 25 12:35:01 ip-172-31-45-45 CROND[24729]: (www) CMD (/bin/php /data/wwwroot/51ito.io/ethgo/artisan schedule:run >> /dev/null 2>&1)
Jul 25 12:35:01 ip-172-31-45-45 CROND[24729]: (CRON) ERROR chdir failed (/home/www): No such file or directory
Jul 25 12:36:01 ip-172-31-45-45 CROND[24782]: (www) CMD (/bin/php /data/wwwroot/51ito.io/ethgo/artisan schedule:run >> /dev/null 2>&1)
Jul 25 12:36:01 ip-172-31-45-45 CROND[24782]: (CRON) ERROR chdir failed (/home/www): No such file or directory
Jul 25 12:37:01 ip-172-31-45-45 CROND[24834]: (www) CMD (/bin/php /data/wwwroot/51ito.io/ethgo/artisan schedule:run >> /dev/null 2>&1)
Jul 25 12:37:01 ip-172-31-45-45 CROND[24834]: (CRON) ERROR chdir failed (/home/www): No such file or directory
Jul 25 12:38:01 ip-172-31-45-45 CROND[24887]: (www) CMD (/bin/php /data/wwwroot/51ito.io/ethgo/artisan schedule:run >> /dev/null 2>&1)
Jul 25 12:38:01 ip-172-31-45-45 CROND[24887]: (CRON) ERROR chdir failed (/home/www): No such file or directory
Jul 25 12:39:01 ip-172-31-45-45 CROND[24940]: (www) CMD (/bin/php /data/wwwroot/51ito.io/ethgo/artisan schedule:run >> /dev/null 2>&1)
Jul 25 12:39:01 ip-172-31-45-45 CROND[24940]: (CRON) ERROR chdir failed (/home/www): No such file or directory
Jul 25 12:40:01 ip-172-31-45-45 CROND[24995]: (www) CMD (/bin/php /data/wwwroot/51ito.io/ethgo/artisan schedule:run >> /dev/null 2>&1)

意思就是指定某一个执行计划任务的时候需要这个用户必须有家目录, 那么进行以下操作

1
2
3
cd /home
mkdir www
chown -R www:www www

经过以上的一番操作, 正常运行

ubuntu没有计划任务日志解决办法

修改rsyslog

1
vim /etc/rsyslog.d/50-default.conf

搜索cron 把如下行之前的注释”#”去掉

1
#cron.*   /var/log/cron.log 

重启rsyslog

1
sudo  service rsyslog  restart

现在看看定时任务的日志

1
tail -f /var/log/cron.log

Debian系统

重启

1
/etc/init.d/cron restart

http://www.netingcn.com/crontab-designate.html

讲真话的最大好处就是:不必记得自己讲过什么。

Eloquent has a convenient feature called Accessors – you can define your own custom fields on top of existing in the database table. But then there’s an Eloquent property $appends – should we use it or not? And what’s the difference?

First – how Accessors work

For those who don’t know or have forgotten: for example, if you have User model and fields first_name and last_name in the DB table, then you can create a function in app\User.php:

1
2
3
function getFullNameAttribute() {
return $this->first_name . ' ' . $this->last_name;
}

Then you have access to property full_name (in the function name it’s CamelCase, and the property name is with underscoresd, for example like this:

1
ho User::find(1)->full_name;

But here’s the thing – if you just return User object, it won’t contain full_name:

1
dd(User::find(1)->toJSON());

The result would look something like this:

1
2
3
4
5
6
7
8
9
{
"id":1,
"first_name":"Povilas",
"last_name":"Korop",
"email":"povilas@webcoderpro.com",
"created_at":"2015-06-19 08:16:58",
"updated_at":"2015-06-19 19:48:09"
}

Here’s where $appends comes in

Now this is the trick – in your User model you can add $appends attribute and list the fields that would automatically be appended:

1
2
3
4
class User extends Model
{
// ...
protected $appends = ['full_name'];

Now that attribute will be automatically added to the previous JSON:

1
2
3
4
5
6
7
8
9
{
"id":1,
"first_name":"Povilas",
"last_name":"Korop",
"email":"povilas@webcoderpro.com",
"created_at":"2015-06-19 08:16:58",
"updated_at":"2015-06-19 19:48:09",
"full_name":"Povilas Korop"
}

https://laraveldaily.com/why-use-appends-with-accessors-in-eloquent/