1. The necessity of distributed locks
In the era of single-body applications, we useReentrantLock
orsynchronized
It can solve thread safety problems. But when the system is split into a distributed architecture (most companies should not just be single-piece applications at present).Cross-process shared resource competitionIt becomes a problem that must be solved.
Distributed locks came into being, but three core issues must be solved:
- Race conditions: Multiple people operate shared resources, the order is uncontrollable
- Lock invalid: The lock expires automatically but the business has not been completed, and other clients seize resources/Locking is successful but expiration time is not set, service downtime leads to deadlock
- Lock error delete: Client A releases the lock held by Client B.
2. Core implementation analysis (with source code)
2.1 Atomic locking
local lockKey = KEYS[1] -- The key name of the lock, such as "order_lock_123"
local lockSecret = ARGV[1] -- The unique identifier of the lock (UUID is recommended)
local expireTime = tonumber(ARGV[2]) -- Expiry time (unit: seconds)
-- Parameter validity verification
if not expireTime or expireTime <= 0 then
return "0" -- The parameter failed to return directly
end
-- Atomic operation: SET lockKey lockSecret NX EX expireTime
local result = ("set", lockKey, lockSecret, "NX", "EX", expireTime)
return result and "1" or "0" -- return "1" successfully, return "0" if failed
Design ideas:
- value uses a client-only identifier (SnowflakeID is recommended)
- Parameter verification: prevent incoming illegal expiration time
- Atomicity: Single command completes "judgment + setting + expiration" operation
2.2 Watchdog renewal mechanism
local lockKey = KEYS[1] -- the key name of the lock
local lockSecret = ARGV[1] -- lock identifier
local expireTime = tonumber(ARGV[2]) -- New expiration time
-- Parameter verification
if not expireTime or expireTime <= 0 then
return "0"
end
-- Get the value of the current lock
local storedSecret = ("get", lockKey)
-- Renewal logic
if storedSecret == lockSecret then
-- Value matching will extend the expiration time
local result = ("expire", lockKey, expireTime)
return result == 1 and "1" or "0" -- Renewal successfully returns "1"
else
-- The lock does not exist or the value does not match
return "0"
end
// Timed renewal thread
(() -> {
().removeIf(entry -> ().isCancelled());
for (Entry<String, Lock> entry : ()) {
if (!().isCancelled()) {
String result = (RENEWAL_SCRIPT,
(key),
, "30");
if ("0".equals(result)) ();
}
}
}, 0, 10, );
Design ideas:
- Renewal interval = expiration time/3 (if 30s expire, 10s renewal)
- Asynchronous thread pool needs to be configured separately
- Double check lock status (memory mark + Redis actual value)
2.3 Safe release lock
local lockKey = KEYS[1] -- the key name of the lock
local lockSecret = ARGV[1] -- The lock identifier to be released
-- Get the value of the current lock
local storedSecret = ("get", lockKey)
-- Verify lock attribute
if storedSecret == lockSecret then
-- When the value matches, delete the key
return ("del", lockKey) == 1 and "1" or "0"
else
-- Values do not match
return "0"
end
Design ideas:
- Verify the value to avoid accidentally deleting locks of other threads
3. Source code
package ;
import .;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
public class RedisUtils {
static class Lock {
private final String value;
private volatile boolean isCancelled = false;
public Lock(String value) {
= value;
}
public boolean isCancelled() {
return isCancelled;
}
public void cancel() {
isCancelled = true;
}
}
private static final String LOCK_LUA = "local lockKey = KEYS[1]\n" + "local lockSecret = ARGV[1]\n" + "local expireTime = tonumber(ARGV[2]) -- Dynamic Expiry Time\n" + "if not expireTime or expireTime <= 0 then\n" + "return \"0\"\n" + "end\n" + "local result = (\"set\", lockKey, lockSecret, \"NX\", \"EX\", expireTime)\n" + "return result and \"1\" or \"0\"";
private static final String RELEASE_LOCK_LUA = "local lockKey = KEYS[1]\n" + "local lockSecret = ARGV[1]\n" + "local storedSecret = (\"get\", lockKey)\n" + "if storedSecret == lockSecret then\n" + "return (\"del\", lockKey) == 1 and \"1\" or \"0\"\n" + "else\n" + "return \"0\"\n" + "end";
private static final String RENEWAL_LUA = "local lockKey = KEYS[1]\n" + "local lockSecret = ARGV[1]\n" + "local expireTime = tonumber(ARGV[2])\n" + "if not expireTime or expireTime <= 0 then\n" + "return \"0\"\n" + "end\n" + "local storedSecret = (\"get\", lockKey)\n" + "if storedSecret == lockSecret then\n" + "local result = (\"expire\", lockKey, expireTime)\n" + " return result == 1 and \"1\" or \"0\"\n" + "else\n" + "return \"0\"\n" + "end";
private final String defaultExpireTime = "30";
private final RedisTemplate<String, String> redisTemplate;
private final Map<String, Lock> locks = new ConcurrentHashMap<>();
private final ScheduledExecutorService watchdogExecutor = (1);
public RedisUtils(RedisTemplate<String, String> redisTemplate) {
= redisTemplate;
(() -> {
try {
("watchdogExecutor is executing... locks => " + (locks));
().removeIf(entry -> ().isCancelled());
for (<String, Lock> entry : ()) {
String key = ();
Lock lock = ();
if (!()) {
RedisScript<String> redisScript = (RENEWAL_LUA, );
String result = (redisScript, (key), , defaultExpireTime);
if ((result, "0")) {
(); // Remove the released lock
}
}
}
} catch (Exception e) {
("Watchdog task execution failed: " + ());
}
}, 0, 10, );
}
public boolean acquireLock(String key, String value) {
RedisScript<String> redisScript = (LOCK_LUA, );
String result = (redisScript, (key), value, defaultExpireTime);
if ((result, "1")) {
(key, new Lock(value));
return true;
}
return false;
}
public boolean acquireLockWithRetry(String key, String value, int maxRetries, long retryIntervalMillis) {
int retryCount = 0;
while (retryCount < maxRetries) {
boolean result = (key, value);
if (result) {
(key, new Lock(value));
return true;
}
retryCount++;
try {
(retryIntervalMillis);
} catch (InterruptedException e) {
().interrupt();
return false;
}
}
return false;
}
public boolean releaseLock(String key, String value) {
RedisScript<String> redisScript = (RELEASE_LOCK_LUA, );
String result = (redisScript, (key), value);
if ((result, "1")) {
Lock lock = (key);
if (lock != null) {
();
}
return true;
}
return false;
}
@PreDestroy
public void shutdown() {
();
try {
if (!(60, )) {
();
}
} catch (InterruptedException e) {
();
}
}
}
4. How to use
4.1 Configuration class
@Configuration
public class AppConfig {
@Resource
private RedisTemplate<String, String> redisTemplate;
@Bean
public RedisUtils init() {
return new RedisUtils(redisTemplate);
}
}
4.2 Use
@RestController
@RequestMapping("/users")
public class UserController {
@Resource
private RedisTemplate<String, String> redisTemplate;
@PostMapping("/test2")
public Boolean test2(@RequestBody Map<String, String> map) {
boolean res;
if ((("lockFlag"), "true")) {
res = (("key"), ("value"));
} else {
res = (("key"), ("value"));
}
return res;
}
}
postscript
Or a disclaimer, for learning reference only