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.

Java Captain
Java Captain
Java Captain
Implementing a Simple Seckill (Flash Sale) Using Redis Distributed Locks in Java

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 KEY

Key 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Javaredisdistributed-lockannotationSeckill
Java Captain
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.