import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; import java.time.Duration; import java.util.function.Supplier; /** * A simple implementation for Distributed Lock based on Redis. *

* The implementation relies on Redis SETNX (SET if Not eXists). * if one process acquires the lock, the other processes won't wait, and simply an exception will be thrown. *

* Usually, you would use the Database as a lock mechanism (using unique keys for example). * However, sometimes, you cannot use the database (for any reason), or you need a quick solution, hence you can use this class. * * @see Redis Lock */ @Slf4j public class Lock { private static final String KEY_PREFIX = "idempotent-"; private static final Options DEFAULT_OPTIONS = new Options(); private static RedisTemplate redisTemplate; /** * Run with default options */ public static void run(String key, Runnable action) { run(key, DEFAULT_OPTIONS, action); } public static void run(String key, Options options, Runnable action) { run(key, options, runnableToSupplier(action)); } /** * Run with default options */ public static T run(String key, Supplier action) { return run(key, DEFAULT_OPTIONS, action); } public static T run(String key, Options options, Supplier action) { initRedisTemplate(); writeKey(key, options.duration, options.errorKey); try { return action.get(); } finally { if (!options.keepLock) { removeKey(key); } } } /** * Use {@link Lock#options()} to create default {@link Options} object. */ @NoArgsConstructor(access = AccessLevel.PRIVATE) public static class Options { private Duration duration = Duration.ofMinutes(5); private boolean keepLock = false; private String errorKey = "operation_in_progress"; /** * the maximum duration to lock, default is 5 minutes */ public Options duration(Duration d) { this.duration = d; return this; } /** * keep the lock for the whole duration regardless of whether the execution is done or not, default is false. */ public Options keepLock(boolean b) { this.keepLock = b; return this; } /** * error key to thrown in case of operation is in progress, default is "operation_in_progress". */ public Options errorKey(String key) { this.errorKey = key; return this; } } public static Options options() { return new Options(); } @SuppressWarnings({"unchecked"}) private static void initRedisTemplate() { if (redisTemplate == null) { redisTemplate = AppContextUtil.getBean(RedisTemplate.class, String.class, Object.class); } } private static void writeKey(String key, Duration duration, String errorMessage) { log.trace("write key: {}", key); var ok = redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + key, "1", duration); if (ok == null) { throw new RuntimeException("Lock cannot used inside Redis pipeline/transaction"); } if (!ok) { throw new BusinessException(errorMessage, "key", key); } } private static void removeKey(String key) { log.trace("remove key: {}", key); redisTemplate.opsForValue().getAndDelete(KEY_PREFIX + key); } private static Supplier runnableToSupplier(Runnable action) { return () -> { action.run(); return null; }; } }