Spring Boot 自定义注解实现Redis分布式锁

Java小咖2019-02-13 12:35:43


一、什么是分布式锁?

       要介绍分布式锁,首先要提到与分布式锁相对应的线程锁、进程锁。

线程锁:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如synchronized是共享对象头,显示锁Lock是共享某个变量(state)。

进程锁:为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized等线程锁实现进程锁。

分布式锁:当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。

二、分布式锁的使用场景。

      线程间并发问题和进程间并发问题都是可以通过分布式锁解决的,但是强烈不建议这样做!因为采用分布式锁解决这些小问题是非常消耗资源的!分布式锁应该用来解决分布式情况下的多进程并发问题才是最合适的。有这样一个情境,线程A和线程B都共享某个变量X。如果是单机情况下(单JVM),线程之间共享内存,只要使用线程锁就可以解决并发问题。如果是分布式情况下(多JVM),线程A和线程B很可能不是在同一JVM中,这样线程锁就无法起到作用了,这时候就要用到分布式锁来解决。

三、分布式锁的实现(Redis)

      分布式锁实现的关键是在分布式的应用服务器外,搭建一个存储服务器,存储锁信息,这时候我们很容易就想到了Redis。首先我们要搭建一个Redis服务器,用Redis服务器来存储锁信息。在实现的时候要注意的几个关键点:1、锁信息必须是会过期超时的,不能让一个线程长期占有一个锁而导致死锁;2、同一时刻只能有一个线程获取到锁。

      需要了解redis命令: 

      setnx(key, value):“set if not exits”,若该key-value不存在,则成功加入缓存并且返回1,否则返回0。

      get(key):获得key对应的value值,若不存在则返回nil。

      getset(key, value):先获取key对应的value值,若不存在则返回nil,然后将旧的value更新为新的value。

      expire(key, seconds):设置key-value的有效期为seconds秒。


下面使用的是工具类RedisTemplate,集成了上面的方法;

流程图:

核心代码:

1、首先自定义注解:@RedisLock

 <!-- springboot项目需要aop的包-->
<dependency>
    <groupId>
org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
 </dependency>

2、定义切面,使用环绕通知来实现加锁,解锁


import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

@Documented
@Retention
(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RedisLock{
   long timeOut() default 30;
  
TimeUnit timeUnit() default TimeUnit.SECONDS;
}


@Aspect
@Component
public class
RedisDistributeLockAspect{
   
   @Autowired

   private
RedisTemplate<String, Object> redisTemplate;

   private static final
Logger logger = LoggerFactory.getLogger(RedisDistributeLockAspect.class);

   @Pointcut
("@annotation(RedisLock)")
   
void pointCut() {
    }
 
   
@Around(value = "pointCut() && @annotation(lock)")
   
Object around(ProceedingJoinPoint joinPoint, RedisLock lock) throws Throwable {

       
// 锁名称
       
String lockKey = RedisKeyUtil.redisLockKey(joinPoint.getSignature().getName());
      // 锁值
       
String lockValue = UUID.randomUUID().toString();
      // 获取锁
       
if(!redisTemplate.opsForValue().setIfAbsent(lockKey,lockValue)){

           
if(redisTemplate.getExpire(lockKey, lock.timeUnit()) > 0) {
               
throw new LockException("服务器打了个盹!请稍后再试");
           }

           
// 删除死锁,重新获取锁
           
redisTemplate.delete(lockKey);
           if
(!redisTemplate.opsForValue().setIfAbsent(lockKey,lockValue)){
               
throw new LockException("服务器打了个盹!请稍后再试");
             }
       }
       logger
.info("==========获取到锁 ===========");
   //设置超时时间
   redisTemplate.expire(lockKey, lock.timeOut(), lock.timeUnit()
);
   Object proceed = joinPoint.proceed();

   logger.info("==========使用完毕,正在释放锁==========");

   if
(!redisTemplate.hasKey(lockKey) || !redisTemplate.opsForValue().get(lockKey).equals(lockValue)){
     redisTemplate
.delete(lockKey);
     logger.info("==========锁出现异常==========");
     redisTemplate.delete(lockKey);
     throw new
LockException("服务器打了个盹!请稍后再试");
     }

     redisTemplate
.delete(lockKey);
     return
proceed;
    }
}

    ☆   在  joinPoint.proceed( ) 方法处有异常,谨慎catch;(catch异常会将目标 方法的异常捕获)

      分布式场景中数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance),最多只能同时满足两项。所以很多系统在设计之初就要对这三者作出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证"最终一致性",只要对最终时间是在用户可以接受的范围内即可。

需要的可以联系我提供源码;

Copyright © 古田计算器虚拟社区@2017