Implementing a Simple Seckill (Flash Sale) Using Redis Distributed Locks in Java
This article explains the business background of a flash‑sale (seckill) scenario, analyzes naive locking approaches, introduces Redis‑based distributed locks, and provides a complete Java demo with custom annotations, dynamic proxies, and a multithreaded test to ensure correct inventory decrement under high concurrency.
Business Scenario
In a seckill (flash‑sale) situation many users compete for limited resources (usually products) within a very short time, which translates to multiple threads contending for the same data; the system must ensure high‑throughput concurrency while preserving correctness.
Possible Implementations
Simple ideas include synchronizing the whole method, synchronizing only the critical block, or serializing all requests via a queue, but these either lock too broadly or do not solve the problem of high contention across different products.
A finer‑grained lock can be achieved by assigning a mutex per product, which is exactly what a distributed lock can provide.
What Is a Distributed Lock
A distributed lock coordinates access to a shared resource across multiple machines, ensuring mutual exclusion to maintain consistency.
In a typical seckill, the product inventory stored in a database is the shared resource, and thousands of concurrent requests from different nodes must be synchronized.
Specific Implementation
Redis is used as the lock store because of its atomic commands. The essential commands are:
SETNX key value expire KEY seconds del KEYKey Considerations
Use Jedis client to interact with Redis.
Lock is represented by a key whose existence means the product is locked.
Unlocking is simply deleting the key.
Blocking vs non‑blocking: a blocking approach polls the lock within a timeout.
Exception handling: a lock timeout (via Redis expire) automatically releases stale locks.
Code Overview
Annotations define which methods and parameters should be locked:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheLock {
String lockedPrefix() default ""; // lock key prefix
long timeOut() default 2000; // poll timeout
int expireTime() default 1000; // lock expiration seconds
} @Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LockedObject {} @Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LockedComplexObject {
String field() default ""; // field name inside complex object
}The interceptor obtains the annotations, builds a Redis lock key, attempts to acquire the lock, invokes the target method, and finally releases the lock:
public class CacheLockInterceptor implements InvocationHandler {
public static int ERROR_COUNT = 0;
private Object proxied;
public CacheLockInterceptor(Object proxied) { this.proxied = proxied; }
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
CacheLock cacheLock = method.getAnnotation(CacheLock.class);
if (cacheLock == null) {
System.out.println("no cacheLock annotation");
return method.invoke(proxied, args);
}
// find the locked parameter
Object lockedObject = getLockedObject(method.getParameterAnnotations(), args);
String objectValue = lockedObject.toString();
RedisLock lock = new RedisLock(cacheLock.lockedPrefix(), objectValue);
boolean result = lock.lock(cacheLock.timeOut(), cacheLock.expireTime());
if (!result) {
ERROR_COUNT++;
throw new CacheLockException("get lock fail");
}
try {
return method.invoke(proxied, args);
} finally {
lock.unlock();
}
}
// ... getLockedObject implementation omitted for brevity ...
}The RedisLock class implements the actual lock/unlock logic using SETNX and expire:
public boolean lock(long timeout, int expire) {
long nanoTime = System.nanoTime();
timeout *= MILLI_NANO_TIME;
while (System.nanoTime() - nanoTime < timeout) {
if (redisClient.setnx(key, LOCKED) == 1) {
redisClient.expire(key, expire);
lock = true;
return true;
}
System.out.println("出现锁等待");
Thread.sleep(3, RANDOM.nextInt(30));
}
return false;
}
public void unlock() {
if (lock) {
redisClient.delKey(key);
}
}Using the Framework
Define a service interface with a method annotated for locking:
public interface SeckillInterface {
@CacheLock(lockedPrefix="TEST_PREFIX")
void secKill(String userID, @LockedObject Long commodityID);
}Implement the interface:
public class SecKillImpl implements SeckillInterface {
static Map<Long, Long> inventory = new HashMap<>();
static {
inventory.put(10000001L, 10000L);
inventory.put(10000002L, 10000L);
}
@Override
public void secKill(String arg1, Long arg2) {
reduceInventory(arg2);
}
public Long reduceInventory(Long commodityId) {
inventory.put(commodityId, inventory.get(commodityId) - 1);
return inventory.get(commodityId);
}
}A multithreaded test creates 1000 threads, half targeting each product, synchronizes their start with a latch, and verifies that each inventory is reduced by 500:
@Test
public void testSecKill() throws Exception {
int threadCount = 1000;
int splitPoint = 500;
CountDownLatch end = new CountDownLatch(threadCount);
CountDownLatch begin = new CountDownLatch(1);
SecKillImpl target = new SecKillImpl();
Thread[] threads = new Thread[threadCount];
// create threads for product 1
for (int i = 0; i < splitPoint; i++) {
threads[i] = new Thread(() -> {
try { begin.await();
SeckillInterface proxy = (SeckillInterface) Proxy.newProxyInstance(
SeckillInterface.class.getClassLoader(),
new Class[]{SeckillInterface.class},
new CacheLockInterceptor(target));
proxy.secKill("test", 10000001L);
end.countDown();
} catch (InterruptedException e) { e.printStackTrace(); }
});
threads[i].start();
}
// create threads for product 2 (similar)
// ... omitted for brevity ...
long start = System.currentTimeMillis();
begin.countDown();
end.await();
System.out.println(SecKillImpl.inventory.get(10000001L));
System.out.println(SecKillImpl.inventory.get(10000002L));
System.out.println("error count" + CacheLockInterceptor.ERROR_COUNT);
System.out.println("total cost " + (System.currentTimeMillis() - start));
}The test shows both inventories drop from 10000 to 9500, confirming that the distributed lock prevents lost updates that would occur without locking.
Conclusion
The article walks from the business requirement of a seckill, through the abstraction of a distributed lock, to a concrete Java implementation using Redis, custom annotations, and dynamic proxies, providing a reusable framework for high‑concurrency scenarios.
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 Captain
Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.
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.
