1+ // src/redis-lock.ts
2+ /**
3+ * Redis Lock Manager
4+ *
5+ * 實現分散式 lock,用於解決高並發寫入時的 lock contention 問題
6+ */
7+
8+ import Redis from 'ioredis' ;
9+
10+ // 生成唯一 token
11+ function generateToken ( ) : string {
12+ return `${ Date . now ( ) } -${ Math . random ( ) . toString ( 36 ) . substring ( 2 , 10 ) } ` ;
13+ }
14+
15+ export interface LockConfig {
16+ redisUrl ?: string ;
17+ ttl ?: number ; // lock 過期時間(毫秒)
18+ maxWait ?: number ; // 最大等待時間(毫秒)
19+ retryDelay ?: number ; // 重試延遲(毫秒)
20+ }
21+
22+ export class RedisLockManager {
23+ private redis : Redis ;
24+ private defaultTTL = 60000 ; // 60 秒
25+ private maxWait = 60000 ; // 最多等 60 秒
26+ private retryDelay = 100 ; // 初始重試延遲
27+
28+ constructor ( config ?: LockConfig ) {
29+ const redisUrl = config ?. redisUrl || process . env . REDIS_URL || 'redis://localhost:6379' ;
30+ this . redis = new Redis ( redisUrl . replace ( 'redis://' , '' ) , {
31+ lazyConnect : true ,
32+ retryStrategy : ( times ) => {
33+ if ( times > 3 ) return null ; // 放棄重連
34+ return Math . min ( times * 200 , 2000 ) ;
35+ } ,
36+ } ) ;
37+
38+ if ( config ?. ttl ) this . defaultTTL = config . ttl ;
39+ if ( config ?. maxWait ) this . maxWait = config . maxWait ;
40+ }
41+
42+ async connect ( ) : Promise < void > {
43+ try {
44+ await this . redis . connect ( ) ;
45+ } catch ( err ) {
46+ // 如果連不上,尝试不連接(lazy connect)
47+ console . warn ( `[RedisLock] Could not connect to Redis: ${ err } ` ) ;
48+ }
49+ }
50+
51+ async acquire ( key : string , ttl ?: number ) : Promise < ( ) => Promise < void > > {
52+ const lockKey = `memory-lock:${ key } ` ;
53+ const token = generateToken ( ) ;
54+ const startTime = Date . now ( ) ;
55+ const lockTTL = ttl || this . defaultTTL ;
56+
57+ // 嘗試連接
58+ try {
59+ await this . redis . ping ( ) ;
60+ } catch {
61+ throw new Error ( 'Redis not available' ) ;
62+ }
63+
64+ let attempts = 0 ;
65+ while ( true ) {
66+ attempts ++ ;
67+
68+ try {
69+ // 使用 SET NX + token (原子操作)
70+ const result = await this . redis . set ( lockKey , token , 'PX' , lockTTL , 'NX' ) ;
71+
72+ if ( result === 'OK' ) {
73+ // 成功取得 lock
74+ console . log ( `[RedisLock] Acquired lock ${ key } after ${ attempts } attempts` ) ;
75+
76+ // 回傳帶 token 的 release function
77+ return async ( ) => {
78+ // 用 Lua script 確保只刪除自己的 lock
79+ const script = `
80+ if redis.call("get", KEYS[1]) == ARGV[1] then
81+ return redis.call("del", KEYS[1])
82+ else
83+ return 0
84+ end
85+ ` ;
86+ try {
87+ await this . redis . eval ( script , 1 , lockKey , token ) ;
88+ console . log ( `[RedisLock] Released lock ${ key } ` ) ;
89+ } catch ( err ) {
90+ console . warn ( `[RedisLock] Failed to release lock: ${ err } ` ) ;
91+ }
92+ } ;
93+ }
94+ } catch ( err ) {
95+ // Redis error,繼續重試
96+ }
97+
98+ // 檢查是否超時
99+ if ( Date . now ( ) - startTime > this . maxWait ) {
100+ throw new Error ( `Lock acquisition timeout: ${ key } after ${ attempts } attempts` ) ;
101+ }
102+
103+ // 指數退避等待
104+ const delay = Math . min ( this . retryDelay * Math . pow ( 1.5 , Math . min ( attempts , 10 ) ) , 2000 ) ;
105+ await this . sleep ( delay + Math . random ( ) * 100 ) ;
106+ }
107+ }
108+
109+ async isHealthy ( ) : Promise < boolean > {
110+ try {
111+ await this . redis . ping ( ) ;
112+ return true ;
113+ } catch {
114+ return false ;
115+ }
116+ }
117+
118+ async disconnect ( ) : Promise < void > {
119+ await this . redis . quit ( ) ;
120+ }
121+
122+ private sleep ( ms : number ) : Promise < void > {
123+ return new Promise ( resolve => setTimeout ( resolve , ms ) ) ;
124+ }
125+ }
126+
127+ /**
128+ * 建立 RedisLockManager 工廠
129+ */
130+ export async function createRedisLockManager ( config ?: LockConfig ) : Promise < RedisLockManager | null > {
131+ const manager = new RedisLockManager ( config ) ;
132+
133+ try {
134+ await manager . connect ( ) ;
135+ const isHealthy = await manager . isHealthy ( ) ;
136+ if ( isHealthy ) {
137+ console . log ( '[RedisLock] Redis lock manager initialized' ) ;
138+ return manager ;
139+ } else {
140+ console . warn ( '[RedisLock] Redis not healthy, will use file lock fallback' ) ;
141+ await manager . disconnect ( ) ;
142+ return null ;
143+ }
144+ } catch ( err ) {
145+ console . warn ( `[RedisLock] Failed to initialize: ${ err } ` ) ;
146+ return null ;
147+ }
148+ }
0 commit comments