项⽬实战,⽤Redis实现分布式锁,⽀持重试。
背景
对于锁⼤家肯定不会陌⽣,在单体系统中, Java 提供的 synchronized 关键字和 ReentrantLock 可重⼊锁基本能满⾜我们的需求。
但是随着分布式的快速发展,本地的加锁往往不能满⾜我们的需要。因为分布式与单机情况下最⼤的不同在于其不是多线程⽽是多进程。多线程由于可以共享堆内存,因此可以简单的采取内存作为标记存储位置。⽽进程之间甚⾄可能都不在同⼀台物理机上,因此需要将标记存储在⼀个所有进程都能看到的地⽅。(即在单机系统中锁对象可以存在内存中共享,⽽分布式系统往往是隔离的)
说到底,分布式锁实现就是将锁放在中间件中维护,这样⽆论多少台机器,锁都是共享的。
分布式锁的特性
可以保证在分布式部署的应⽤集中,同⼀个⽅法在同⼀时间只能被⼀台机器上的⼀个线程执⾏。
这把锁要是⼀把可重⼊锁(避免死锁)
这把锁最好是⼀把阻塞锁(根据业务需求考虑要不要这条)
这把锁最好是⼀把公平锁(根据业务需求考虑要不要这条)
有⾼可⽤的获取锁和释放锁功能
获取锁和释放锁的性能要好
何谓可重⼊锁?
简单来说,就是同⼀个线程可以对同⼀把锁可以多次获得,当然,释放锁也需要多次释放
贴下代码
何谓阻塞锁?
从侧⾯来说,⾮阻塞的锁指的是 没有获得锁的线程并不会进⼊排队队列,要想再次获得锁就要再次触发获得锁操作。
简单粗暴的话,实现搞⼀个 while 循环,直到获取锁成功为⽌。
何谓公平锁?
表⽰线程获取锁的顺序是按照加锁的顺序来分配的,及先来先得,先进先出的顺序,或者线程多次获取锁不成功,可以增加权重,提⾼获取锁的机会。
⾼可⽤的释放锁功能:
锁要设置失效时间,因为⼀旦释放锁操作失败,其他线程⽆法再获得到锁
实现
通过注解结合AOP来实现分布式锁。
好处:灵活设置需要锁定的字段。
⽅案⼀:基于 REDIS 的 SETNX()、EXPIRE() ⽅法做分布式锁
1、setnx(lockkey, 1) 如果返回 0,则说明占位失败;如果返回 1,则说明占位成功
2、expire() 命令对 lockkey 设置超时时间,为的是避免死锁问题。
3、执⾏完业务代码后,可以通过 delete 命令删除 key。
⽅案⼀有缺陷,如果在第⼀步 setnx 执⾏成功后,在 expire() 命令执⾏成功前,发⽣了宕机的现象,那么就依然会出现死锁的问题
⽅案⼆: 在⽅案⼀基础上进⾏改善,利⽤redisTemplate提供的setIfAbsent(),即设置值的时候同是设置过期时间
下⾯的Coding是基于⽅案⼆实现的。
1、引⼊依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
2、属性配置
3、注解
3.1、创建⼀个 DistributedLock注解,作⽤于⽅法上,属性配置如下
/**
* 锁的注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DistributedLock {
/
**
* redis 锁key的前缀
*
* @return redis 锁key的前缀
*/
String prefix() default "distributedLock";
/**
* 过期秒数,默认为10秒
*
*/
int expire() default 10;
/
**
* 重试时间,默认为30秒
*
*/
int retry() default 10;
/**
* 超时时间单位
*
* @return 秒
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/
**
* Key的分隔符(默认 :)
* @return String
*/
String delimiter() default ":";
/**
* 当不使⽤@LockKey注解时或者注解失效,是否锁整个⽅法
* @return
*/
boolean isLockMethod() default false;
}
3.2、创建⼀个 LockKey注解,作⽤于⽅法参数上,即具体要锁住的属性字段,属性配置如下
/**
* 要锁住的属性字段
* 注意:只⽀持⽅法形参(基本类型、String或者⾃定义实体的第⼀层属性)加注解,⽤法可参考第6⽰例 * 例如:test(@LockKey int stuNo);
* test(Student input);
* Student:{
* @LockKey int stuNo;
* }
*
springboot aop*/
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface LockKey {
}
4、Lock(AOP)
@Aspect
@Configuration
@Slf4j
public class LockMethodAspect {
@Autowired
public LockMethodAspect(StringRedisTemplate lockRedisTemplate) {
this.lockRedisTemplate = lockRedisTemplate;
}
private final StringRedisTemplate lockRedisTemplate;
@Around("execution(public * *(..)) && @st.DistributedLock)")
public Object interceptor(ProceedingJoinPoint pjp) throws Throwable{
MethodSignature signature = (MethodSignature) Signature();
Method method = Method();
DistributedLock lock = Annotation(DistributedLock.class);
if (StringUtils.isEmpty(lock.prefix())) {
throw new RuntimeException("lock key prefix can't ");
}
final String lockKey = LockKey(pjp);
boolean flag = false;
try {
//key不存在才能设置成功
flag = RedisLockUtils.lock(lockRedisTemplate, lockKey, pire(), (), lock.timeUnit()); } catch (Exception e) {
<("redis 异常", e);
//TODO ⾃⾏处理
}
if (flag) {
try {
return pjp.proceed();
} catch (Exception e) {
<("分布式锁的执⾏异常", e);
//TODO ⾃⾏处理
}finally {
RedisLockUtils.unLock(lockRedisTemplate,lockKey);
}
} else {
//按理来说我们应该抛出⼀个⾃定义的 CacheLockException 异常;
throw new RuntimeException("获取分布式锁失败");
}
}
}
5、Lock的⼯具类
@Slf4j
public class RedisLockUtils {
/**
* 加锁
*
* @param key redis key
* @param expire 过期时间,单位秒
* @param retry 重试时间,单位秒
* @return true:加锁成功,false,加锁失败
*/
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论