Spring 基于 Lettuce Reactive API 实现 Redis 分布式锁

Spring 基于 Lettuce Reactive API 实现 Redis 分布式锁

前言

通常都是基于 Redissetnx 操作来实现分布式锁,思想不难理解:

  • 获取锁资源,在一定时间内试图获取锁资源,即试图基于 setnx 设置锁标识,若设置失败说明锁资源已被其他对象持有。锁资源一定要有过期时间,否则持有锁资源的对象如果出于各种原因没有及时释放,会造成其他对象获取不到锁资源
  • 释放锁资源,释放锁资源时要确认当前对象确实持有锁资源,可以通过锁资源的值进行匹配判断

如果基于 Redisson 整合 Redis,有现成的 API 可以直接调用,因为个人习惯使用 Lettuce 客户端,且整个 分布式锁 的实现思想较简单,因此基于 Lettuce 实现 分布式锁,同时也方便进行一定程度上的抽象

实现细节

Lock

/**
 * 资源锁
 */
public interface Lock {

    /**
     * 获取一个 10s 过期的锁资源
     * @param lockName
     * @return
     */
    default String acquire(String lockName) throws InterruptedException {

        return acquire(lockName, 10);
    }

    /**
     * 获取超时时长 expireTime(单位:s)的锁资源
     *      默认 1s 内未获取到则返回 null
     * @param lockName
     * @param expireTime
     * @return
     */
    default String acquire(String lockName, int expireTime) throws InterruptedException {

        return acquireWithWait(lockName, expireTime, 1);
    }

    /**
     * 获取超时时长 expireTime(单位:s)的锁资源
     *      waitTime 秒内若未获取到则返回 null
     * @param lockName
     * @param expireTime
     * @param waitTime
     * @return
     */
    String acquireWithWait(String lockName, int expireTime, int waitTime) throws InterruptedException;

    /**
     * 释放 lockName 的锁资源,以 lock 值匹配确保
     *      释放锁的当前应用持有该锁资源
     * @param lockName
     * @param lock
     * @return
     */
    boolean release(String lockName, String lock);
    
}

定义顶层接口 Lock,提供以下方法:

  • String acquire(String lockName),获取 lockName 的锁资源,默认过期时长 10s,即 10s 后无论释放锁资源都会过期,获取过程默认持续 1s
  • String acquire(String lockName, int expireTime),可以指定锁资源的过期时长,获取过程默认持续 1s
  • String acquireWithWait(String lockName, int expireTime, int waitTime),可以指定锁资源的过期时长,可以指定获取等待时长
  • boolean release(String lockName, String lock),释放锁资源,必须对锁资源的值 lock 进行匹配,以判断当前对象是否持有锁资源(而不是锁资源过期而导致释放掉其他对象持有的锁)

AbstractLock

public abstract class AbstractLock implements Lock {

    @Override
    public String acquireWithWait(String lockName, int expireTime, int waitTime) throws InterruptedException {

        String lock = null;

        /**
         * 试图在规定时间内获取锁资源
         */
        long endTime = System.currentTimeMillis() + waitTime * 1000;
        while (System.currentTimeMillis() < endTime) {
            if ((lock = doAcquire(lockName, expireTime)) != null) {
                break;
            }
            TimeUnit.MILLISECONDS.sleep(100);
        }

        return lock;
    }

    protected abstract String doAcquire(String lockName, int expireTime);
}

方法 acquireWithWait 的实现基调:

  • 在规定等待时长中多次尝试获取,每次尝试间隔 100ms
  • 核心逻辑委托给 doAcquire 方法,交给子类来实现锁资源的获取,比如:基于 Lettuce 整合 Redis 实现

LettuceConfig

@Configuration
@EnableConfigurationProperties(RedisProperties.class)
// @ConditionalOnClass(RedisClient.class)
public class LettuceConfig {

    @Bean
    public RedisURI redisURI(RedisProperties redisProperties) {

        RedisURI.Builder builder = RedisURI.builder()
                .withHost(redisProperties.getHost())
                .withPort(redisProperties.getPort())
                .withDatabase(redisProperties.getDatabase())
                .withSsl(redisProperties.isSsl());

        Optional.ofNullable(redisProperties.getClientName())
                .ifPresent(clientName -> builder.withClientName(clientName));

        Optional.ofNullable(redisProperties.getTimeout())
                .ifPresent(timeout -> builder.withTimeout(timeout));

        if (StringUtils.hasText(redisProperties.getUsername())
                && StringUtils.hasText(redisProperties.getPassword())) {

            builder.withAuthentication(
                    redisProperties.getUsername()
                    , redisProperties.getPassword()
            );
        }

        return builder.build();
    }

    @Bean
    public RedisClient redisClient(RedisURI redisURI) {

        return RedisClient.create(redisURI);
    }

    // ...其他 Lettuce 组件声明

    @Bean
    public RedisReactiveCommands<String, String> redisReactiveCommands(
            RedisClient redisClient
    ) {

        return redisClient.connect().reactive();
    }
}

Lettuce 配置类,本文打算基于 Reactive API 来实现,因此注册的 Bean 组件为 RedisReactiveCommands<String, String>

RedisLock

public abstract class RedisLock extends AbstractLock {
}

中间类,主要是方便拓展不同客户端的实现,比如 Redssion Jedis Lettuce

LettuceRedisLock

@Component
public class LettuceRedisLock extends RedisLock {

    @Autowired
    RedisReactiveCommands<String, String> redisClient;

    AlternativeJdkIdGenerator idGenerator = new AlternativeJdkIdGenerator();

    Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    public String doAcquire(String lockName, int expireTime) {

        // 生成锁资源值
        String lock = idGenerator.generateId().toString();
        
        // 基于 setnx 设置锁资源
        Boolean block = redisClient.setnx(lockName, lock)
                .block(Duration.ofSeconds(3));

        /**
         * 获取锁资源成功,则指定超时时间并返回
         * 获取失败则说明锁已被其他对象持有,此时如果该锁资源并未
         *      指定超时时间,则此处为了确保锁资源保证释放,未其
         *      指定超时时间
         */
        if (block) {

            doExpire(lockName, expireTime);
            return lock;

        } else {
            redisClient.ttl(lockName)
                    .subscribe(time -> {
                        if (time == -1) {
                            doExpire(lockName, expireTime);
                        }
                    });
        }

        return null;
    }

    /**
     * 基于 expire 命令指定锁的超时时间
     * @param lockName
     * @param expireTime
     */
    private void doExpire(String lockName, int expireTime) {
        redisClient.expire(lockName, expireTime)
                .doOnError(e -> logger.error(
                        "error occurred when set expire time for lock: {}", lockName
                ))
                .subscribe();
    }

    /**
     * 释放锁资源
     * @param lockName
     * @param lock
     * @return
     */
    @Override
    public boolean release(String lockName, String lock) {
        redisClient.get(lockName)
                .subscribe(l -> {
                    if (lock.equals(l)) {
                        redisClient.del(lockName)
                                .doOnError(e -> release(lockName, lock))
                                .subscribe();
                    }
                });
        return true;
    }
}
  • String doAcquire(String lockName, int expireTime),基于 AlternativeJdkIdGenerator 生成 UUID 唯一资源,基于 setnx 命令获取锁资源,无论获取成功失败都要指定 expire 超时时间,防止锁资源得不到正确释放
  • doExpire(String lockName, int expireTime),基于 expire 命令指定超时时间
  • boolean release(String lockName, String lock),锁资源的释放,释放之前会把锁资源与当前对象持有的锁对象进行对比,以避免释放到其他对象持有的锁

测试

@Component
public class LettuceRedisLockTest implements CommandLineRunner {

    @Autowired
    LettuceRedisLock lettuceRedisLock;

    Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    public void run(String... args) throws Exception {
        for (int i = 0; i < 5; i++) {
            new Thread(new TestRunner(i)).start();
        }

        Thread.currentThread().join();
    }

    private void handle(int i) throws InterruptedException {
        String test = lettuceRedisLock.acquireWithWait("test", 10, 3);
        if (test != null) {
            logger.info("start:" + i);
            TimeUnit.SECONDS.sleep(1);
            logger.info("end:" + i);
            lettuceRedisLock.release("test", test);
        }
    }

    class TestRunner implements Runnable {

        int i;

        TestRunner(int i) {
           this.i = i;
        }

        @Override
        public void run() {
            try {
                handle(i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

此处以 5 个线程模拟五个分布式应用,每个应用处理逻辑都需要 1s,而锁的等待时长指定为 3s,因此最后只有 3 个应用可以获取到锁执行代码,可以自己尝试下

总结

整体实现相对简单,也忽略了部分细节,比如参数的鉴定等,但大体上实现了分布式锁的思想,在个人或小团队内的开发使用问题应该不大

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring提供了对Redis分布式的支持,可以通过SpringRedisTemplate或LettuceConnectionFactory来实现。下面是一个简单的实现示例: 1. 首先,在Spring配置文件中配置Redis连接池: ``` <bean id="redisConnectionFactory" class="org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory"> <constructor-arg name="hostName" value="${redis.host}"/> <constructor-arg name="port" value="${redis.port}"/> <constructor-arg name="password" value="${redis.password}"/> </bean> <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"> <property name="connectionFactory" ref="redisConnectionFactory"/> <property name="keySerializer"> <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/> </property> <property name="valueSerializer"> <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/> </property> </bean> ``` 2. 然后,定义一个Redis分布式的工具类: ``` @Component public class RedisLockUtil { private static final long LOCK_EXPIRE_TIME = 30000; // 过期时间,单位毫秒 private static final String LOCK_PREFIX = "lock:"; // 前缀 @Autowired private RedisTemplate<String, String> redisTemplate; public boolean lock(String key) { String lockKey = LOCK_PREFIX + key; long now = System.currentTimeMillis(); long expireTime = now + LOCK_EXPIRE_TIME; Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, String.valueOf(expireTime)); if (result != null && result) { redisTemplate.expire(lockKey, LOCK_EXPIRE_TIME, TimeUnit.MILLISECONDS); return true; } String oldExpireTime = redisTemplate.opsForValue().get(lockKey); if (oldExpireTime != null && Long.parseLong(oldExpireTime) < now) { String newExpireTime = redisTemplate.opsForValue().getAndSet(lockKey, String.valueOf(expireTime)); if (newExpireTime != null && newExpireTime.equals(oldExpireTime)) { redisTemplate.expire(lockKey, LOCK_EXPIRE_TIME, TimeUnit.MILLISECONDS); return true; } } return false; } public void unlock(String key) { String lockKey = LOCK_PREFIX + key; redisTemplate.delete(lockKey); } } ``` 该工具类定义了两个方法:lock和unlock。其中,lock方法实现Redis分布式的获取,unlock方法实现Redis分布式的释放。在lock方法中,首先将的过期时间设置为当前时间+30秒,并通过Redis的setIfAbsent方法尝试获取;如果获取成功,则返回true;否则,通过get方法获取的过期时间,如果已经过期,则通过getAndSet方法更新的过期时间并获取旧的过期时间,然后判断旧的过期时间是否等于获取到的过期时间,如果相等,则说明获取到了,返回true。在unlock方法中,直接通过delete方法删除。 3. 最后,在需要使用分布式的地方,注入RedisLockUtil即可使用分布式: ``` @Autowired private RedisLockUtil redisLockUtil; public void doSomethingWithLock(String key) { if (redisLockUtil.lock(key)) { try { // 获取成功后执行业务逻辑 // ... } finally { redisLockUtil.unlock(key); } } else { // 获取失败后的处理 // ... } } ``` 上面是一个简单的Spring实现Redis分布式的示例,如果您有更多的问题,请继续提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值