声明:本文仅为个人观点,偏向于实现,如有不当还请指出。
简介
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);
}

我们将项目终止。
日志打印成功!

锁也已经释放。

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

724

被折叠的 条评论
为什么被折叠?



