Redis结合AQS实现分布式锁

本文介绍了如何结合AQS(AbstractQueuedSynchronizer)和Redis的setnx命令来实现分布式锁。通过分析关键代码,展示了加锁和释放锁的逻辑,包括使用Spring的retry机制处理网络波动,确保锁的正确获取。此外,还讨论了服务异常终止时如何避免死锁,提出了锁失效时间的优化策略,并通过并发测试验证了分布式锁的原子性。

声明:本文仅为个人观点,偏向于实现,如有不当还请指出。

简介

AQS抽象队列锁+redis setnx来进行加锁和释放锁

环境构建

练习采用Gradle构建,如果使用Maven仅限参考。

plugins {
    id 'org.springframework.boot' version '2.3.7.RELEASE'
    id 'io.spring.dependency-management' version '1.0.10.RELEASE'
    id 'java'
}

ext {
    set('springCloudVersion', "Hoxton.SR9")
    set('springCloudAlibabaVersion', "2.2.2.RELEASE")
}

dependencies {
    //lombok 支持
    compile 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    //hutool工具包
    implementation 'cn.hutool:hutool-all:5.6.2'
    //redis
    compile 'org.springframework.boot:spring-boot-starter-data-redis'
    compile 'org.apache.commons:commons-pool2'
    //springBoot
    implementation 'org.springframework.boot:spring-boot-starter'
    //springCloud
    implementation 'org.springframework.cloud:spring-cloud-starter'
    //spring retry机制
    implementation 'org.springframework.retry:spring-retry'
    //aspectj
    implementation 'org.aspectj:aspectjweaver:1.8.6'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
        mavenBom "com.alibaba.cloud:spring-cloud-alibaba-dependencies:${springCloudAlibabaVersion}"
    }
}

注意!加入依赖这里有一个小坑。使用spirng的retry机制必须要导入aspectj的依赖否则使用会报错!

原因:我们看@EnableRetry注解的源码可以得知它是基于CGLIB的代理机制实现的。


/**
 * Global enabler for <code>@Retryable</code> annotations in Spring beans. If this is
 * declared on any <code>@Configuration</code> in the context then beans that have
 * retryable methods will be proxied and the retry handled according to the metadata in
 * the annotations.
 * 
 * @author Dave Syer
 * @since 2.0
 *
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@EnableAspectJAutoProxy(proxyTargetClass = false)
@Import(RetryConfiguration.class)
@Documented
public @interface EnableRetry {

	/**
	 * Indicate whether subclass-based (CGLIB) proxies are to be created as opposed
	 * to standard Java interface-based proxies. The default is {@code false}.
	 *
	 * @return whether to proxy or not to proxy the class
	 */
	boolean proxyTargetClass() default false;

}

正文

这里先贴出实现分布式锁的代码,我们逐一分析。

@Slf4j
@Component
public class RedisLock extends AbstractQueuedSynchronizer {
    private final static String LOCK = "redis:lock";

    @Autowired
    private StringRedisTemplate redisTemplate;

    public RedisLock() {
        //注册一个新的虚拟机关闭钩子
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            log.info("JVM关闭钩子触发!!!");
            dispose();
        }));
    }

    @Override
    protected boolean tryAcquire(int arg) {
        Thread thread = Thread.currentThread();
        //设置当前拥有独占访问权限的线程。 null参数表示没有线程拥有访问权限。 此方法不会以其他方式强加任何同步或volatile字段访问。
        setExclusiveOwnerThread(thread);
        return redisTemplate.opsForValue().setIfAbsent(LOCK, NetUtil.getLocalhostStr());
    }

    @Override
    @Retryable(value = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 5000, multiplier = 1))
    protected boolean tryRelease(int arg) {
        if (!redisTemplate.hasKey(LOCK)) {
            log.info("前往加锁!!!");
            return true;
        }
        log.info("线程" + Thread.currentThread().getName() + "执行完毕!!!");
        return redisTemplate.delete(LOCK);
    }

    /**
     * 容器关闭触发
     */
    public void dispose() {
        String ip = redisTemplate.opsForValue().get(LOCK);
        if (StringUtils.isNotBlank(ip) && ip.equals(NetUtil.getLocalhostStr())) {
            tryRelease(10);
        }
    }
}

我们继承的这个类这就是我们常说的AQS(抽象队列同步器),我们主要使用的是它下面的两个方法。

看源码注释我们可以得知,它的功能是控制当前锁是否释放,如果此对象现在处于完全释放状态,则为true ,以便任何等待的线程都可以尝试获取; 否则为false 。我们将锁判断逻辑写在这里。

    /**
     * Attempts to set the state to reflect a release in exclusive
     * mode.
     *
     * <p>This method is always invoked by the thread performing release.
     *
     * <p>The default implementation throws
     * {@link UnsupportedOperationException}.
     *
     * @param arg the release argument. This value is always the one
     *        passed to a release method, or the current state value upon
     *        entry to a condition wait.  The value is otherwise
     *        uninterpreted and can represent anything you like.
     * @return {@code true} if this object is now in a fully released
     *         state, so that any waiting threads may attempt to acquire;
     *         and {@code false} otherwise.
     * @throws IllegalMonitorStateException if releasing would place this
     *         synchronizer in an illegal state. This exception must be
     *         thrown in a consistent fashion for synchronization to work
     *         correctly.
     * @throws UnsupportedOperationException if exclusive mode is not supported
     */
    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }

进行释放锁操作,如果key不存在就证明锁已被释放我们可以加锁,如果否则启用释放锁操作。

使用retry重试机制,重试3次,防止因为网络波动导致抢锁失败的状况。

   @Override
   @Retryable(value = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 5000, multiplier = 1))
    protected boolean tryRelease(int arg) {
        if (!redisTemplate.hasKey(LOCK)) {
            return true;
        }
        log.info("线程" + Thread.currentThread().getName() + "执行完毕!!!");
        return redisTemplate.delete(LOCK);
    }

第二个方法我们看源码注释

尝试以独占模式获取。 该方法应该查询对象的状态是否允许以独占模式获取它,如果允许则获取它。
此方法始终由执行获取的线程调用。 如果此方法报告失败,acquire 方法可能会将线程排队(如果它尚未排队),直到收到来自某个其他线程的释放信号。 

如果成功则为true 。 成功后,该对象已获得。

    /**
     * Attempts to acquire in exclusive mode. This method should query
     * if the state of the object permits it to be acquired in the
     * exclusive mode, and if so to acquire it.
     *
     * <p>This method is always invoked by the thread performing
     * acquire.  If this method reports failure, the acquire method
     * may queue the thread, if it is not already queued, until it is
     * signalled by a release from some other thread. This can be used
     * to implement method {@link Lock#tryLock()}.
     *
     * <p>The default
     * implementation throws {@link UnsupportedOperationException}.
     *
     * @param arg the acquire argument. This value is always the one
     *        passed to an acquire method, or is the value saved on entry
     *        to a condition wait.  The value is otherwise uninterpreted
     *        and can represent anything you like.
     * @return {@code true} if successful. Upon success, this object has
     *         been acquired.
     * @throws IllegalMonitorStateException if acquiring would place this
     *         synchronizer in an illegal state. This exception must be
     *         thrown in a consistent fashion for synchronization to work
     *         correctly.
     * @throws UnsupportedOperationException if exclusive mode is not supported
     */
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

进行上锁操作。我们通过工具包拿到当前ip作为value,为了防止其他ip做释放锁动作,而导致锁被释放。(本ip的锁只有本ip可以释放)

    @Override
    protected boolean tryAcquire(int arg) {
        Thread thread = Thread.currentThread();
        //设置当前拥有独占访问权限的线程。 null参数表示没有线程拥有访问权限。 此方法不会以其他方式强加任何同步或volatile字段访问。
        setExclusiveOwnerThread(thread);
        return redisTemplate.opsForValue().setIfAbsent(LOCK, NetUtil.getLocalhostStr(),2L, TimeUnit.MINUTES);
    }

最后我们来看最后两个方法,主要是为了解决锁加上了,但是业务没有执行完毕(没有释放锁),服务被终止了,其他服务一直无法拿到锁。

我们在无参构造中注册一个钩子线程,在服务终止时触发。释放锁操作。或是使用@PreDestroy注解进行控制。但是此方法暴力终止无法触发(kill -9)。所以我们就要根据业务场景来设置锁失效时间来进行优化。

    public RedisLock() {
        //注册一个新的虚拟机关闭钩子
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            log.info("JVM关闭钩子触发!!!");
            dispose();
        }));
    }

    /**
     * 容器关闭触发
     */
    public void dispose() {
        String ip = redisTemplate.opsForValue().get(LOCK);
        if (StringUtils.isNotBlank(ip) && ip.equals(NetUtil.getLocalhostStr())) {
            tryRelease(10);
        }
    }

测试

我们写一个计数功能的操作,来测试一下并发下的原子性。

    /**
     * 计数操作
     *
     * @return
     */
    @GetMapping("/hincr")
    public void hincr() throws InterruptedException {
        redisLock.acquire(10);
        log.info("正在执行:"+Thread.currentThread().getName()+"线程");
        redisUtils.hincr("num", "hincr", 1);
        Thread.sleep(10000);
        redisLock.release(10);
    }

 我们通过日志打印可以看到,线程都在逐步执行。

我们模拟一个上锁不解锁的动作。

  /**
     * 上锁
     */
    @GetMapping("/lock")
    public void lock(){
        redisLock.acquire(10);
    }

 ​​​​​​​

我们将项目终止。 

日志打印成功!

锁也已经释放。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值