บทนำ
ในบทความนี้ เราจะอภิปรายวิธีสร้าง Distributed Lock ด้วย Redis ใน .NET Core
เมื่อเราสร้าง Distributed Systems เราจะเจอ Multiple Processes ที่จัดการ Shared Resource ร่วมกัน จะทำให้เกิด Unexpected Problems เนื่องจากเพียงหนึ่งในพวกเขาเท่านั้นที่สามารถใช้ Shared Resource ได้ในเวลาเดียวกัน!
เราสามารถใช้ Distributed Lock เพื่อแก้ปัญหานี้
ทำไมต้อง Distributed Lock?
เหมือนปกติ เราจะใช้ Lock เพื่อจัดการปัญหานี้
Code ต่อไปแสดงตัวอย่างการใช้ Lock
public void SomeMethod() {
// Do something...
lock (obj) {
// Do ....
}
// Do something...
}
อย่างไรก็ตาม Lock ประเภทนี้ไม่ช่วยแก้ปัญหาได้ดี! นี่คือ In-process Lock ที่แก้ได้แค่ One Process กับ Shared Resources
นี่คือเหตุผลหลักว่าทำไมเราต้องการ Distributed Lock!
ผมจะใช้ Redis สร้าง Simple Distributed Lock ที่นี่
ทำไมใช้ Redis? เพราะ Redis มี Single-threaded Nature และสามารถทำ Atomic Operations ได้
วิธีสร้าง Lock
ผมจะสร้าง .NET Core Console Application เพื่อแสดง
ก่อนขั้นตอนต่อไป เราควร Run Redis Server ขึ้นมา!
StackExchange.Redis เป็น Redis Client ยอดนิยมที่สุดบน .NET และไม่มีข้อสงสัยเราจะใช้มัน
สร้าง Connection กับ Redis ก่อน
/// <summary>
/// The lazy connection.
/// </summary>
private static Lazy<ConnectionMultiplexer> lazyConnection = new Lazy<ConnectionMultiplexer>(() => {
ConfigurationOptions configuration = new ConfigurationOptions {
AbortOnConnectFail = false,
ConnectTimeout = 5000,
};
configuration.EndPoints.Add("localhost", 6379);
return ConnectionMultiplexer.Connect(configuration.ToString());
});
/// <summary>
/// Gets the connection.
/// </summary>
/// <value>The connection.</value>
public static ConnectionMultiplexer Connection => lazyConnection.Value;
เพื่อ Request Lock บน Shared Resource เราทำดังนี้
SET resource_name unique_value NX PX duration
resource_name คือค่าที่ Application Instances ทั้งหมด Share กัน
unique_value คือค่าที่ Unique ต่อแต่ละ Instance ของ Application และ Purpose ของ Unique Value นี้คือเพื่อ Remove Lock (Unlock)
สุดท้าย เราให้ Duration (ใน Milliseconds) ซึ่งหลังจากนั้น Lock จะถูก Remove โดย Redis อัตโนมัติ
นี่คือ Implementation ใน C# Code
/// <summary>
/// Acquires the lock.
/// </summary>
/// <returns><c>true</c>, if lock was acquired, <c>false</c> otherwise.</returns>
/// <param name="key">Key.</param>
/// <param name="value">Value.</param>
/// <param name="expiration">Expiration.</param>
static bool AcquireLock(string key, string value, TimeSpan expiration) {
bool flag = false;
try {
flag = Connection.GetDatabase().StringSet(key, value, expiration, When.NotExists);
} catch (Exception ex) {
Console.WriteLine($"Acquire lock fail...{ex.Message}");
flag = true;
}
return flag;
}
นี่คือ Code ทดสอบ Acquire Lock
static void Main(string[] args) {
string lockKey = "lock:eat";
TimeSpan expiration = TimeSpan.FromSeconds(5);
// 5 person eat something...
Parallel.For(0, 5, x => {
string person = $"person:{x}";
bool isLocked = AcquireLock(lockKey, person, expiration);
if (isLocked) {
Console.WriteLine($"{person} begin eat food (with lock) at {DateTimeOffset.Now.ToUnixTimeMilliseconds()}.");
} else {
Console.WriteLine($"{person} cannot eat food due to not getting the lock.");
}
});
Console.WriteLine("end");
Console.Read();
}
หลัง Run Code เราอาจได้ Result ดังนี้

มีเพียง 1 Person ได้ Lock! อื่นๆ รออยู่
แม้ Lock จะ Auto-removed โดย Redis แต่ก็ไม่ทำให้ Shared Resource ใช้งานได้อย่างมีประสิทธิภาพ!
เพราะเมื่อ Process จบงาน มันควรปล่อยให้คนอื่นใช้ Resource ไม่ใช่รอจนกว่าจะ Expire!
ดังนั้นเราต้อง Release Lock ด้วย
วิธี Release Lock
เพื่อ Release Lock เราแค่ Remove Item ใน Redis!
สำหรับสิ่งที่เราใส่ตอนสร้าง Lock เราต้อง Match Unique Value ของ Resource นี่จะทำให้ปลอดภัยกว่าในการ Release Lock ที่ถูกต้อง
เมื่อ Match แล้ว เรา Delete Lock หมายถึง Unlock สำเร็จ ไม่ Match = Unlock ไม่สำเร็จ
เราต้อง Execute Get และ Del Commands พร้อมกัน จึงใช้ Lua Script
/// <summary>
/// Releases the lock.
/// </summary>
/// <returns><c>true</c>, if lock was released, <c>false</c> otherwise.</returns>
/// <param name="key">Key.</param>
/// <param name="value">Value.</param>
static bool ReleaseLock(string key, string value) {
string lua_script = @"
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
redis.call('DEL', KEYS[1])
return true
else
return false
end
";
try {
var res = Connection.GetDatabase().ScriptEvaluate(lua_script, new RedisKey[] { key }, new RedisValue[] { value });
return (bool)res;
} catch (Exception ex) {
Console.WriteLine($"ReleaseLock lock fail...{ex.Message}");
return false;
}
}
เราควร Call Method นี้เมื่อ Process จบงาน
เมื่อ Process ได้ Lock แต่ไม่ Release Lock ด้วยบางเหตุผล Process อื่นไม่ควรรอจนกว่าจะ Release ควรไปต่อ
นี่คือ Sample จัดการ Scene นี้
Parallel.For(0, 5, x => {
string person = $"person:{x}";
var val = 0;
bool isLocked = AcquireLock(lockKey, person, expiration);
while (!isLocked && val <= 5000) {
val += 250;
System.Threading.Thread.Sleep(250);
isLocked = AcquireLock(lockKey, person, expiration);
}
if (isLocked) {
Console.WriteLine($"{person} begin eat food (with lock) at {DateTimeOffset.Now.ToUnixTimeMilliseconds()}.");
if (new Random().NextDouble() < 0.6) {
Console.WriteLine($"{person} release lock {ReleaseLock(lockKey, person)} {DateTimeOffset.Now.ToUnixTimeMilliseconds()}");
} else {
Console.WriteLine($"{person} do not release lock ....");
}
} else {
Console.WriteLine($"{person} begin eat food (without lock) at {DateTimeOffset.Now.ToUnixTimeMilliseconds()}.");
}
});
หลัง Run Sample คุณอาจได้ Result ดังนี้
เห็นได้ว่า Person 3 และ 4 จะไปต่อโดยไม่มี Lock
Source Code หาได้ที่ GitHub Page ของผม
- RedisLockDemo
สรุป
บทความนี้แนะนำวิธีสร้าง Distributed Locks ด้วย Redis ใน .NET Core และเป็น Basic Version คุณสามารถ Improve ตาม Business ของคุณได้
หวังว่าจะช่วยคุณได้