blog

cache2goソースコードの解釈

cache2goは、オープンソースのアプリケーション内キャッシュライブラリです。 キーと値は任意のデータ型にすることができます。ソースコードでは、ライブラリがデータを維持する方法、同時実行のセキュリテ...

Apr 29, 2020 · 8 min. read
シェア

cache2go ソースコードの解釈

cache2goとは?

cache2go は、オープンソースのアプリケーション内キャッシング・ライブラリで、同時実行のセキュリティを確保し、キー・バリュー・ストレージを提供し、有効期限を制御します。 キーと値はどのようなデータ型でもかまいません。ソースコードでは、このライブラリがデータを維持する方法、並行性のセキュリティを確保するためにロックを使用する方法、どのようにデータ処理を期限切れにする方法を学ぶことができます。

主なAPIの説明

  • Cache() : *CacheTable 型の、指定された名前の既存のキャッシュ・テーブルを返します。
  • Add() : 新しいキー/値のペアをキャッシュ・テーブルに追加します。キーの有効期限を指定することもできます。値 0 は、常に有効であることを意味します。
  • Value() : *CacheItem 型のキーに従って、キャッシュ内の対応する値を返します。キーが存在する場合は、アクセス・カウントと最終アクセス時刻を同時に更新します。キーが存在しない場合は、事前登録済みのコールバック関数 loadData を呼び出して、キャッシュ・テーブルにデータを初期化しようとします。
  • Delete():指定されたキーをキャッシュ・テーブルから削除します。
  • Exists() : キーがキャッシュに存在するかどうかを調べ、存在する場合は true を返します。キャッシュされた項目の最終アクセス時刻は更新されません。
  • Foreach():キャッシュを走査し、各キャッシュ項目に対してカスタムコールバック関数を呼び出して処理します。
  • これは、存在しないキーをキャッシュから取得し、データベースやファイルなどの他の場所からキーと値のペアをキャッシュに自動的にロードするために使用されます。
  • SetAddedItemCallback(): 新しいキー/値のペアがキャッシュに追加されたときに自動的にトリガされるコールバック関数です。
  • SetAboutToExpireCallback() : 特定のキーの期限切れコールバックを設定します。

ソースコード解析

データ構造はキー・バリュー形式です。redisと同様に、まずテーブルを作成し、n個のキー値をテーブルに格納します。

cache.go には、CacheTable オブジェクトを作成するメソッドが 1 つしかありません。
var (
	cache = make(map[string]*CacheTable)
	mutex sync.RWMutex
)
//指定された名前の既存のキャッシュ・テーブルを返すか、存在しない場合は新しいキャッシュ・テーブルを作成する。
func Cache(table string) *CacheTable {
	mutex.RLock()
	t, ok := cache[table]
	mutex.RUnlock()
	if !ok {
		mutex.Lock()
		t, ok = cache[table]
		// テーブルが存在するか再度チェックする
		if !ok {
			t = &CacheTable{
				name: table,
				items: make(map[interface{}]*CacheItem),
			}
			cache[table] = t
		}
		mutex.Unlock()
	}
	return t
}

cache=map[string]*CacheTableソース・コードからわかるように、cache.go はグローバル・プライベート変数を定義します。このオブジェクトは、オブジェクトを取得するために CacheTable の name 属性を使用し、同時実行の安全性を確保するためにロックを使用して、CacheTable へのすべてのポインタを保持します。Cache メソッドでは、最初にオブジェクトが存在するかどうかを判断し、存在しない場合は新しいオブジェクトを作成することを理解するのは簡単です。

cacheitem.go は、保存される特定のデータ項目の内容を定義します。
// CacheItem別のキャッシュ・アイテムである
// パラメータ・データには、ユーザーが設定した値がキャッシュされている
type CacheItem struct {
	sync.RWMutex
	// The item's key.
	key interface{}
	
	// The item's data.
	data interface{}
	
	// How long will the item live in the cache when not being accessed/kept alive.
	// この項目の保存期間
	lifeSpan time.Duration
	// Creation timestamp.
	// で作成された。
	createdOn time.Time
	
	// Last access timestamp.
	// 最終アクセス
	accessedOn time.Time
	
	// How often the item was accessed.
	// への訪問回数は、1,000回を超えた。
	accessCount int64
	// Callback method triggered right before removing the item from the cache
	// 削除時にトリガーされるメソッド一覧
	aboutToExpire []func(key interface{})
}

各オブジェクト、そのキーとデータは、任意の値のインターフェイス{}にすることができ、そのデータ項目は、独自のライフサイクルとコールバック関数を定義することができます。

その機能は比較的単純で、あまり紹介することはありません。

// key、生存時間lifeSpan、dataを渡すと、CacheItemオブジェクトが生成され、lifeSpanが0のときは、有効期限が切れていないことを意味する。
func NewCacheItem(key interface{}, lifeSpan time.Duration, data interface{}) *CacheItem {
	t := time.Now()
	return &CacheItem{
		key: key,
		lifeSpan: lifeSpan,
		createdOn: t,
		accessedOn: t,
		accessCount: 0,
		aboutToExpire: nil,
		data: data,
	}
}
// データ・アイテムの状態をリフレッシュする、accessedOnを変更して有効期限を遅らせる(有効期限が切れるかどうかは、現在時刻-accessedOnによって決まる>lifeSpan 判断を下すために)
// そして、アクセスできる回数を増やす
func (item *CacheItem) KeepAlive() {
	item.Lock()
	defer item.Unlock()
	item.accessedOn = time.Now()
	item.accessCount++
}
// lifeSpanを返す。これはイミュータブルなので、ロックする必要はない。
func (item *CacheItem) LifeSpan() time.Duration {
	// immutable
	return item.lifeSpan
}
// 最後にアクセスされた時刻であるaccessedOnを返す。
func (item *CacheItem) AccessedOn() time.Time {
	item.RLock()
	defer item.RUnlock()
	return item.accessedOn
}
// 作成時にcreatedOnを返すことも、ロックを必要としない。
func (item *CacheItem) CreatedOn() time.Time {
	// immutable
	return item.createdOn
}
// accessCountを返す。
func (item *CacheItem) AccessCount() int64 {
	item.RLock()
	defer item.RUnlock()
	return item.accessCount
}
// キーを返す
func (item *CacheItem) Key() interface{} {
	// immutable
	return item.key
}
// データを返す
func (item *CacheItem) Data() interface{} {
	// immutable
	return item.data
}
// このデータが削除されたときにトリガーされるメソッド、存在すればクリアされるメソッドを設定する
func (item *CacheItem) SetAboutToExpireCallback(f func(interface{})) {
	if len(item.aboutToExpire) > 0 {
		item.RemoveAboutToExpireCallback()
	}
	item.Lock()
	defer item.Unlock()
	item.aboutToExpire = append(item.aboutToExpire, f)
}
// このデータが削除されたときにトリガーとなるメソッドを追加する
func (item *CacheItem) AddAboutToExpireCallback(f func(interface{})) {
	item.Lock()
	defer item.Unlock()
	item.aboutToExpire = append(item.aboutToExpire, f)
}
// コールバック関数をクリアする
func (item *CacheItem) RemoveAboutToExpireCallback() {
	item.Lock()
	defer item.Unlock()
	item.aboutToExpire = nil
}

全体として、このファイルは、CacheItem のデータ構造を定義し、いくつかのアクセス・メソッドを設定するだけで、複雑なロジックはありません。

cachetable.go

cachetable.goはこのライブラリの中核であり、データの有効期限切れを処理することに重点を置いています。

まず、各テーブルのデータ構造を見てみましょう。

// CacheTable is a table within the cache
type CacheTable struct {
	//  
	sync.RWMutex
	//  
	name string
	// キャッシュされたデータ項目
	items map[interface{}]*CacheItem
	// データ項目の削除のためのタイマー、タイマーがトリガーされると、データ項目をスキャンし、期限切れのデータを削除する。
	cleanupTimer *time.Timer
	// タイマーのトリガーは
	cleanupInterval time.Duration
	// logger 
	logger *log.Logger
	
	// この関数は、存在しない関数がアクセスされたときに呼び出される。通常は、キャッシュを更新するため、つまり、データベースにクエリを実行して結果をキャッシュに追加するためである。
	loadData func(key interface{}, args ...interface{}) *CacheItem
	// このリストにあるメソッドは、オブジェクトが追加されたときに呼び出される
	addedItem []func(item *CacheItem)
	// このリストにあるメソッドは、オブジェクトが削除されたときに呼び出される
	aboutToDeleteItem []func(item *CacheItem)
}

cachetable.goには、データの有効期限切れの処理に重点を置いたメソッドがいくつかあります。これを行う簡単な方法は、データ項目ごとにタイマーを持つことですが、これではパフォーマンスが高すぎるため、cache2goでは1つのタイマーのみを使用して維持します。expirationCheckメソッドでは、このメソッドを呼び出すたびにオブジェクトを繰り返し、有効期限が切れたかどうかをチェックし、次のチェックがいつ行われるかを決定します。now.Sub(accessedOn) >= lifeSpanexpirationCheckでは、まずタイマーを停止し、次にデータ・アイテムをトラバースし、 , を使用してデータが削除されるかどうかを判断し、削除される最先端データの削除時間であるsmallestDurationの最小のトリガー時間を記録し、最後にsmallestDuration秒後に再び関数をトリガーするようにタイマーを設定します。秒後に再び関数が起動するようにタイマーを設定します.

// 自己調整タイマーをトリガーとする期限切れチェックループ。
func (table *CacheTable) expirationCheck() {
	table.Lock()
	if table.cleanupTimer != nil {
		table.cleanupTimer.Stop()
	}
	if table.cleanupInterval > 0 {
		table.log("Expiration check triggered after", table.cleanupInterval, "for table", table.name)
	} else {
		table.log("Expiration check installed for table", table.name)
	}
	// To be more accurate with timers, we would need to update 'now' on every
	// loop iteration. Not sure it's really efficient though.
	now := time.Now()
	smallestDuration := 0 * time.Second
	for key, item := range table.items {
		// Cache values so we don't keep blocking the mutex.
		item.RLock()
		lifeSpan := item.lifeSpan
		accessedOn := item.accessedOn
		item.RUnlock()
		if lifeSpan == 0 {
			continue
		}
		if now.Sub(accessedOn) >= lifeSpan {
			// Item has excessed its lifespan.
			table.deleteInternal(key)
		} else {
			// Find the item chronologically closest to its end-of-lifespan.
			if smallestDuration == 0 || lifeSpan-now.Sub(accessedOn) < smallestDuration {
				smallestDuration = lifeSpan - now.Sub(accessedOn)
			}
		}
	}
	// Setup the interval for the next cleanup run.
	table.cleanupInterval = smallestDuration
	if smallestDuration > 0 {
		// smallestDurationの後にトリガーするタイマーをスタートさせ、それが来たら関数をトリガーする。
		table.cleanupTimer = time.AfterFunc(smallestDuration, func() {
			go table.expirationCheck()
		})
	}
	table.Unlock()
}

あなたはaddInternal関数でもexpirationCheckを呼び出すで見つけることができる、これは新しいデータが有効期限に追加される可能性があるため、次の時間を削除する時間よりも小さい、更新されていない場合、それはデータ項目がタイムリーに削除することはできませんにつながる良いです。item.lifeSpan > 0 && (expDur == 0 || item.lifeSpan < expDur) トリガ条件は、新しく追加されたデータ項目の有効期限が0でない場合、および、タイマーの有効期限が0であるか、その有効期限がタイマーのトリガイベントよりも小さい場合は、タイマーのトリガ時間を更新するexpirationCheckメソッドを呼び出します。

func (table *CacheTable) addInternal(item *CacheItem) {
	// Careful: do not run this method unless the table-mutex is locked!
	// 注意:テーブルをロックせずにこのメソッドを呼び出してはいけない!
	// It will unlock it for the caller before running the callbacks and checks
	// を実行すると、テーブルのロックが解除される。
	table.log("Adding item with key", item.key, "and lifespan of", item.lifeSpan, "to table", table.name)
	table.items[item.key] = item
	// Cache values so we don't keep blocking the mutex.
	expDur := table.cleanupInterval
	addedItem := table.addedItem
	table.Unlock()
	// Trigger callback after adding an item to cache.
	if addedItem != nil {
		for _, callback := range addedItem {
			callback(item)
		}
	}
	// If we haven't set up any expiration check timer or found a more imminent item.
	if item.lifeSpan > 0 && (expDur == 0 || item.lifeSpan < expDur) {
		table.expirationCheck()
	}
}

追記

他の人のコードを読んで理解することを通して、ロックの理解と使用を強化します。ロックが多すぎるとパフォーマンスが低下するため、データを取得または変更した後はロックを解除する必要があります。同時に、一定の間隔でデータを維持する良い方法を理解し、最小トリガー時間を更新することにより、一定の間隔で更新をトリガーし、少量のリソースで大量のデータを維持します。同時に、拡張可能なキャッシュライブラリとして、データ項目のキーとデータの型がinterface{}であること、データのメンテナンスを容易にするために多くのコールバックメソッドを設計したこと、ロギングの導入......など、拡張性のために多くの側面を設計しています。これらの側面はすべて学ぶ価値があります。

Read next

これらのフロントエンドの問題をすべてご存知だろうか?

1.アロー関数と通常の関数の違い 関数がnewキーワードで呼び出された場合、関数内では完全に新しいオブジェクトとなります。 applyメソッド、callメソッド、bindメソッドを使用して関数を呼び出し、作成する場合、関数内のこれは、これらのメソッドの引数として渡されるオブジェクトです。 関数がオブジェクトの中のメソッドとして呼び出された場合、そのオブジェクトのthis...

Apr 29, 2020 · 8 min read