ビジネス定期的なタスクを使用する必要がありますが、以下はあなたの参照のためのいくつかの一般的なシナリオを収集して、タスクのタイミングを繰り返し、私たちが望むものではないクラスタ内の同じ時刻に開始することができる考慮に入れる必要があります。
問題
まず、クラスタを必要としている定期的なタスクを解決することです。
クラスタ内の各マシンがスケジュールされたタスクを開始する場合は1、そうなデータの複製処理の問題を引き起こすこと
2、問題を処理するデータの重複を避けるために、タスク切替モード、他のマシンOFFスイッチ、上にのみ1つの機械スイッチのタイミングであれば、しかし、単一障害点の問題があります。
スキーム
①機械の定期的なタスクを実行するために指定された
タスク実行タイミングで複数のマシンを選択すると、すべての実行はマシンに現在のバックと指定されたマシンが同一であるかどうかを判断するために、実行されます。
このアプローチは、何回の実施を回避しますが、タスクは実行されません後に、この指定されたマシンがハングアップしたときに最も明白な欠点は、単一障害点です。
②動的に設定ファイルをロードすることによって達成され
、たとえば、:新Timmerに複数のプロファイルを、タスクが割り当てられました。異なるマシンが異なるタスクを実行する、つまり、異なるマシンに異なるプロファイルをロードします。
このアプローチの欠点は、少なくとも2つの異なるプロファイルを持っているので、メンテナンスが非常に面倒です。
③タイミングタスクは別々に実行しました
メインルーチンから剥離タスクタイミングに関わる手順は、別々に行います。
このアプローチの欠点は、それは一般的には推奨されていないいない大規模なプロジェクトならば、開発とメンテナンスのコストを上昇させることです。
④リスナー
モニタにプログラムは、タスクの重複があるかどうかを監視タイマー、関連するビジネスロジックいずれかによって行われ、実行されません。
⑤タイマタスクは、データベースから読み出した
同一のデータベースに接続されているので、マークされたタスクのタイミング対応するマークのデータベースは、かを区別または時限タスクを繰り返します。
create table timed_task (
type int(3) not null,
status tinyint(1) not null,
exec_timestamp bigint(20) not null,
interval_time int(11) not null,
PRIMARY KEY (type)
);
プロジェクトタスクの多くのタイミングがある場合、このテーブルは、現在のクエリタスクを実行することができるかどうかを示すために使用され、タイマーの異なる複数種類の共通テーブルを達成するために、タイプフィールドで区別することができます。
type # 将多个不同类型的定时器进行区分
status # 状态,是否可以执行定时任务,0为可执行,1为不可执行
exec_timestamp # 上一次定时任务的执行时间
interval_time # 时间阈值,以秒为单位
ここでは主にフィールドの話をinterval_time
中心に定期的なタスクの障害やダウンタイムを実行し、短い時間指定タスクが実行されていた防ぐために存在または不在のノードを検出するために使用し、。
ノード障害やダウンタイムを行うことがタイムリーでないときは、そのフィールドのexec_timestampとフィールドinterval_timeと現在の時刻に基づいて比較することができますので、実行可能な状態で再タイミングタスクという、フィールドの状態が0にリセットされ、チェック機構を設定することができます。現在時刻がフィールドexec_timestampとフィールドinterval_timeの合計よりも大きい場合、障害やダウンタイムを示す場合、ステータスフィールドは常に1であるときに問題がある場合、あなたはチェックプログラムを行くことができます。
タイマタスクは、クラスタで実行、各マシンはそうあってもよく、数秒の差として存在してもよい
ステータスフィールドにリセットされるので、それは機械を通して処理され、処理時間は、わずか数ミリ秒でタイミングタスク、その後、データベースフィールドの状態で見つかった唯一の有効期限が切れる時間の定期的なタスクへのマシンは、0と強制力で間のタスクを時限ので、実行可能な状態ではなく、偏差の数秒間ので再タイミングタスクことを0、ステータスは、それが再びこのタイミング、状況繰り返しデータ処理が発生したタスクを実行します。したがって、このフィールドは、interval_time
現在時刻が以下のフィールド及びフィールドexec_timestamp interval_time和単語とステータスフィールドは短期タスクのタイミングが一度行われたことを示し、0であり、次いで、許容以下である場合に、繰り返し行わ問題を防止するために使用することができます再度実施。
⑥使用Redisのは、ロック機構と分散期限切れ
ボロー分散ロックは、データ処理の重複の問題を解決するためのRedis、ロックを防止するために、長い時間のためにロックされている、またはクライアントがロック跳ね返っ削除されません防ぐために、有効期限を期限切れに追加することができます。
個人練習
私が個人的に第六のプログラムを使用し、それは、分散ロックの使用は、時限タスクの多機械加工の問題を解決するためのRedisあります。そこ次の簡単な実装コードであってもよいが、限られたスペースのために、次のファイルでこれだけのコードが、これはここに参照のために開発中の非合理的な構造であるだろう。
注:次のコードRedisの実施は、単一インスタンスの場合のためのものであり、分散ロックを実装するメインをRedisのクラスタアーキテクチャから別の記事に記載された、またはRedisのであろう
package main
import (
"github.com/robfig/cron/v3"
"github.com/go-redis/redis"
"time"
"fmt"
"math/rand"
"strconv"
"crypto/md5"
)
// 分布式锁的结构体
type redisLock struct {
key string
value string
}
var Cron *cron.Cron // 用到第三方库提供的定时任务,很方便就解决了定时任务的难题, 避免重复造轮子
var client = redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379",
Password: "", // no password set
DB: 0, // use default DB
})
func StartCronJob() {
Cron = cron.New()
cronJobTime := "* * * * *" // 每分钟执行一次
_, err := Cron.AddFunc(cronJobTime, cronJob)
if err != nil {
fmt.Println(err)
return
}
Cron.Start()
}
func cronJob() {
r := redisLock{}
hasLock, err := r.GrabLock(time.Now().Unix(), 101) // 抢锁
if err != nil {
return
}
if !hasLock {
return
}
defer r.ReleaseLock() // 抢到锁的才有资格释放锁
fmt.Println("hello world, ", time.Now())
}
// 抢锁
func (r *redisLock) GrabLock(dateStamp int64, tp int) (hasLock bool, err error) {
// 随机睡眠(0-20ms),在集群中,往往由于存在几ms的时间偏差,所以可能会导致定时任务
// 总是被某几台机器执行,这就很不均衡了,所以随机睡眠,让每台机器都有机会抢到锁去执行定时任务
rand.Seed(time.Now().UnixNano())
time.Sleep(time.Duration(rand.Intn(20)) * time.Millisecond)
// key之所以这样设置就是因为如果存在很多定时任务的时候,可以更好的区别开来不同的定时任务
key := "cron_job:" + strconv.FormatInt(dateStamp, 10) + ":" + strconv.Itoa(tp)
// value之所以这样设置是为了在所有获取锁请求的客户端里保持唯一,就是用来保证能安全地释放锁,这个很重要,因为这可以避免误删其他客户端得到的锁
ds := strconv.FormatInt(dateStamp, 10) + strconv.FormatInt(int64(tp), 10) + strconv.Itoa(rand.Intn(100))
value := fmt.Sprintf("%x", md5.Sum([]byte(ds)))
expireTime := 300
r.key = key
r.value = value
// 上锁
hasLock, err = client.SetNX(key, value, time.Duration(expireTime)*time.Second).Result()
if err != nil {
fmt.Println(err)
return
}
return
}
// 释放锁
func (r *redisLock) ReleaseLock() {
// 为保证原子性,使用lua脚本去删除key
delScript := `if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else return 0
end`
keys := []string{r.key}
result, err := client.Eval(delScript, keys, r.value).Result()
if err != nil || result == 0 {
fmt.Println(err)
return
}
}
func main() {
StartCronJob()
}
まず、によってRedisのsetnx
コマンド操作するsetnx
キーの割り当ては、このキーのRedisの存在が、そこに返した場合-1、そうでない場合は、直接かどうかを決定する時間のために自分自身をset
キー。これsetnx
でコマンドset
任意の差指令?setnx
それは原子であるが、set
原子性を保証することはできません。
タイムアウトはRedisのは、上のロックを解除していない前に、クライアントをつかむためにロックを防ぐためであれば、ロックが解除されることはありませんダウンして、他のクライアントがロックをつかむませんが、結果はデッドロックです。
なぜ実行eval()
方法は、ここで、による特性のRedisに、原子性を保証しの公式サイトであるeval
と解釈されるコマンド:eval
コマンド実行Lua
スクリプトの時間、Lua
スクリプトが実行するコマンドとして扱われ、までeval
コマンドが完了すると、Redisのは、他を行いますコマンド。
最も一般的な方法は、直接使用してロックを解除することでdel()
ロックを解除する方法を、これはロックと直接的な方法を解放するロックの所有者を決定することはない、別のクライアントのロックへのクライアントのリリースにつながります。例:クライアントはロックが長時間操作をブロックされたしまった、その後、タイムアウトRedisの後の後のロックが自動的に解除され、その後、Aクライアントがロックしたり、独自の考えに行きますロックを解除し、ロックが実際にこのように、クライアントのクライアントBのロックの解除につながる、クライアントBを持っている可能性があります。だから、シンプルでDEL
指示ロックの他のクライアントを削除するクライアントを引き起こす可能性があります。
したがって、他の誰かのを防ぐためにロック解除、それがロックを解除する前にチェックする必要があり、現在の値が値に自分自身を設定されていない、そうでない場合、それはロックが自分ではないことを意味し、解放することはできません。
参照
http://blog.sina.com.cn/s/blog_3fba24680102vhb4.html
https://blog.csdn.net/crazy_yinchao/article/details/77837357
https://www.bbsmax.com/A/amd08lBjdg/