基于Redis SETNX 实现分布式锁

  • 时间:
  • 浏览:0
  • 来源:跟我学网络

环境与配置

  • Redis 任意版本即可
  • SpringBoot 任意版本即可,但是需要依赖 spring-boot-starter-data-redis
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

看一看 Redis 社区对 SETNX 的解释

Redis SETNX

Set key to hold string value if key does not exist. In that case, it is equal to SET. When key already holds a value, no operation is performed. SETNX is short for “SET if Not eXists”.

Return value: Integer reply, specifically:

  • 1 if the key was set
  • 0 if the key was not set

由于当某个 key 不存在的时候,SETNX 才会设置该 key。且由于 Redis 采用单进程单线程模型,所以,不需要担心并发的问题。那么,就可以利用 SETNX 的特性维护一个 key,存在的时候,即锁被某个线程持有;不存在的时候,没有线程持有锁。

关于实现的解释

由于只涉及到 Redis 的操作,所以,代码实现比较简单。只对外提供两个接口:获取锁、释放锁。

  • IDistributedLock: 操作接口定义
  • RedisLock: IDistributedLock 的实现类
  • DistributedLockUtil: 分布式锁工具类
  • SpringContextUtil: 获取当前 classpath 中的 Bean

SETNX 命令对应到 StringRedisTemplate 的 api 是 setIfAbsent,如下所示


Boolean setIfAbsent(K key, V value);

源码和注释信息


public interface IDistributedLock {
    
    boolean acquire();
    
    void release();
}
import SpringContextUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;


@Slf4j
public class RedisLock implements IDistributedLock {

    
    private static StringRedisTemplate redisTemplate;

    private String lockKey;                 
    private int expireMsecs = 15 * 1000;    
    private int timeoutMsecs = 15 * 1000;   
    private boolean locked = false;         

    RedisLock(String lockKey) {
        this.lockKey = lockKey;
    }

    RedisLock(String lockKey, int timeoutMsecs) {
        this.lockKey = lockKey;
        this.timeoutMsecs = timeoutMsecs;
    }

    RedisLock(String lockKey, int expireMsecs, int timeoutMsecs) {
        this.lockKey = lockKey;
        this.expireMsecs = expireMsecs;
        this.timeoutMsecs = timeoutMsecs;
    }

    public String getLockKey() {
        return this.lockKey;
    }

    @Override
    public synchronized boolean acquire() {

        int timeout = timeoutMsecs;

        if (redisTemplate == null) {
            redisTemplate = SpringContextUtil.getBean(StringRedisTemplate.class);
        }

        try {

            while (timeout >= 0) {

                long expires = System.currentTimeMillis() + expireMsecs + 1;
                String expiresStr = String.valueOf(expires); 

                if (redisTemplate.opsForValue().setIfAbsent(lockKey, expiresStr)) {
                    locked = true;
                    log.info("[1] 成功获取分布式锁!");
                    return true;
                }
                String currentValueStr = redisTemplate.opsForValue().get(lockKey); 

                
                if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {

                    String oldValueStr = redisTemplate.opsForValue().getAndSet(lockKey, expiresStr);

                    
                    
                    
                    if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
                        locked = true;
                        log.info("[2] 成功获取分布式锁!");
                        return true;
                    }
                }

                timeout -= 100;
                Thread.sleep(100);
            }
        } catch (Exception e) {
            log.error("获取锁出现异常, 必须释放: {}", e.getMessage());
        }

        return false;
    }

    @Override
    public synchronized void release() {

        if (redisTemplate == null) {
            redisTemplate = SpringContextUtil.getBean(StringRedisTemplate.class);
        }

        try {
            if (locked) {

                String currentValueStr = redisTemplate.opsForValue().get(lockKey); 

                
                if (currentValueStr != null && Long.parseLong(currentValueStr) > System.currentTimeMillis()) {
                    redisTemplate.delete(lockKey);
                    locked = false;
                    log.info("[3] 成功释放分布式锁!");
                }
            }
        } catch (Exception e) {
            log.error("释放锁出现异常, 必须释放: {}", e.getMessage());
        }
    }
}

public class DistributedLockUtil {

    
    public static IDistributedLock getDistributedLock(String lockKey) {
        lockKey = assembleKey(lockKey);
        return new RedisLock(lockKey);
    }

    
    public static IDistributedLock getDistributedLock(String lockKey, int timeoutMsecs) {
        lockKey = assembleKey(lockKey);
        return new RedisLock(lockKey, timeoutMsecs);
    }

    
    public static IDistributedLock getDistributedLock(String lockKey, int timeoutMsecs, int expireMsecs) {
        lockKey = assembleKey(lockKey);
        return new RedisLock(lockKey, expireMsecs, timeoutMsecs);
    }

    
    private static String assembleKey(String lockKey) {
        return String.format("imooc_analyze_%s", lockKey);
    }
}
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;


@Component
public class SpringContextUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContextUtil.applicationContext = applicationContext;
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    @SuppressWarnings("unchecked")
    public static <T> T getBean(Class c) throws BeansException {
        return (T) applicationContext.getBean(c);
    }
}

欢迎关注课程:

基于 SpringCloud 微服务架构下 广告系统设计与实现

JAVA分布式优惠券系统后台 手把手实战开发