Mastering Distributed Locks with Redis: From Basic SetNX to Redisson’s Atomic Solutions
This article walks through the evolution of Redis-based distributed locking—from simple SETNX placeholders and manual expiration, through atomic SETNX EX, UUID‑based lock ownership, Lua‑scripted unlocks, and finally Redisson’s high‑level lock API—highlighting common pitfalls and robust solutions for backend developers.
Hello, I'm Feng Ge.
Distributed Lock Evolution
Basic Principle
We can "reserve a spot" in a shared place; if successful, we execute the logic, otherwise we wait until the lock is released. The reservation can be done in Redis, a database, or any accessible location, and waiting can be implemented via spinning.
Stage One
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {<br> // Stage one<br> Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");<br> // If lock acquired, execute business<br> if (lock) {<br> Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();<br> // Delete lock; if an exception or crash occurs before this, a deadlock may happen<br> stringRedisTemplate.delete("lock");<br> return categoriesDb;<br> } else {<br> // No lock, wait 100ms and retry<br> try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }<br> return getCatalogJsonDbWithRedisLock();<br> }<br>}<br><br>public Map<String, List<Catalog2Vo>> getCategoryMap() {<br> ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();<br> String catalogJson = ops.get("catalogJson");<br> if (StringUtils.isEmpty(catalogJson)) {<br> System.out.println("Cache miss, querying database…");<br> Map<String, List<Catalog2Vo>> categoriesDb = getCategoriesDb();<br> String toJSONString = JSON.toJSONString(categoriesDb);<br> ops.set("catalogJson", toJSONString);<br> return categoriesDb;<br> }<br> System.out.println("Cache hit…");<br> Map<String, List<Catalog2Vo>> listMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() {});<br> return listMap;<br>}<br>Problem: setnx reserves the spot, but if business code throws an exception or the process crashes, the lock is never released, causing a deadlock.
Solution: Set an automatic expiration for the lock so it is removed even if delete is not executed.
Stage Two
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {<br> Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");<br> if (lock) {<br> // Set expiration time<br> stringRedisTemplate.expire("lock", 30, TimeUnit.SECONDS);<br> Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();<br> stringRedisTemplate.delete("lock");<br> return categoriesDb;<br> } else {<br> try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }<br> return getCatalogJsonDbWithRedisLock();<br> }<br>}<br>Problem: setnx succeeds, but before setting the expiration the process crashes, leading to a deadlock.
Solution: The reservation and expiration must be atomic; Redis supports the SET command with NX and EX options.
Stage Three
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {<br> // Acquire lock and set expiration atomically<br> Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "1111", 5, TimeUnit.SECONDS);<br> if (lock) {<br> Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();<br> // Simulate long business execution<br> try { Thread.sleep(6000); } catch (InterruptedException e) { e.printStackTrace(); }<br> stringRedisTemplate.delete("lock");<br> return categoriesDb;<br> } else {<br> try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }<br> return getCatalogJsonDbWithRedisLock();<br> }<br>}<br>Problem: Deleting the lock directly can be unsafe if the business takes longer than the lock’s TTL; the lock may expire and be acquired by another process, causing the original process to delete someone else’s lock.
Solution: When acquiring the lock, store a unique UUID as the value and delete the lock only if the stored value matches the UUID.
Stage Four
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {<br> String uuid = UUID.randomUUID().toString();<br> ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();<br> Boolean lock = ops.setIfAbsent("lock", uuid, 5, TimeUnit.SECONDS);<br> if (lock) {<br> Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();<br> String lockValue = ops.get("lock");<br> if (lockValue.equals(uuid)) {<br> try { Thread.sleep(6000); } catch (InterruptedException e) { e.printStackTrace(); }<br> stringRedisTemplate.delete("lock");<br> }<br> return categoriesDb;<br> } else {<br> try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }<br> return getCatalogJsonDbWithRedisLock();<br> }<br>}<br>Problem: If the lock expires just before the owner deletes it, another process may have already set a new lock, and the delete operation would remove the new lock.
Solution: Deleting the lock must be atomic; use a Redis Lua script to compare the stored value and delete only when they match.
Stage Five – Final Form
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {<br> String uuid = UUID.randomUUID().toString();<br> ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();<br> Boolean lock = ops.setIfAbsent("lock", uuid, 5, TimeUnit.SECONDS);<br> if (lock) {<br> Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();<br> String lockValue = ops.get("lock");<br> String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then
" +<br> " return redis.call(\"del\",KEYS[1])
" +<br> "else
" +<br> " return 0
" +<br> "end";<br> stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), lockValue);<br> return categoriesDb;<br> } else {<br> try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }<br> return getCatalogJsonDbWithRedisLock();<br> }<br>}<br>This ensures atomicity for both acquiring (reservation + expiration) and releasing (check + delete) the lock. The next challenge is automatic lock renewal.
Redisson
Redisson is a Java in‑memory data grid built on top of Redis. It provides distributed implementations of many common Java objects and services, such as BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish/Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, and Scheduler service.
BitSet Set Multimap SortedSet Map List Queue BlockingQueue Deque BlockingDeque Semaphore Lock AtomicLong CountDownLatch Publish/Subscribe Bloom filter Remote service Spring cache Executor service Live Object service Scheduler service
Redisson offers the simplest and most convenient way to use Redis, aiming to separate concerns so developers can focus on business logic.
https://github.com/redisson/redisson/wiki
Source: https://blog.csdn.net/zhangkaixuan456/article/details/110679617
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java High-Performance Architecture
Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
