项目实战,用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、属性配置

spring.redis.host=192.168.68.110
spring.redis.port=6379
spring.redis.password=123456

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;
 * }
 *
 */
@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 * *(..)) && @annotation(com.test.DistributedLock)")
    public Object interceptor(ProceedingJoinPoint pjp) throws Throwable{
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        DistributedLock lock = method.getAnnotation(DistributedLock.class);
        if (StringUtils.isEmpty(lock.prefix())) {
            throw new RuntimeException("lock key prefix can't be null...");
        }
        final String lockKey = RedisLockUtils.getLockKey(pjp);
        boolean flag = false;
        try {
            //key不存在才能设置成功
            flag = RedisLockUtils.lock(lockRedisTemplate, lockKey, lock.expire(), lock.retry(), lock.timeUnit());
        } catch (Exception e) {
            log.error("redis 异常", e);
            //TODO  自行处理
        }
        if (flag) {
            try {
                return pjp.proceed();
            } catch (Exception e) {
                log.error("分布式锁的执行异常", 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,加锁失败
     */
    public static boolean lock(RedisTemplate redisTemplate, String key, int expire, int retry, TimeUnit timeUnit) {
         long currentTimeMillis = System.currentTimeMillis();
        long value = currentTimeMillis + expire * 1000;
        long end = currentTimeMillis+ retry * 1000;

        while ( System.currentTimeMillis() <= end) {
            boolean status = false;
            //long status = redisService.setnx(key, String.valueOf(value));
            try {
                status = redisTemplate.opsForValue().setIfAbsent(key, String.valueOf(value), expire, timeUnit);
            } catch (Exception e) {
                log.error("resdis 异常", e);
                break;
            }
            if (status) {
                return true;
            }
            //重试
            if (!status && retry>0){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }


        }

        return false;
    }

    public static void unLock(RedisTemplate redisTemplate, String key) {
        String value = (String) redisTemplate.opsForValue().get(key);
        if (StringUtils.isNotBlank(value)) {
            try {
                redisTemplate.delete(key);
            } catch (Exception e) {
                log.error("resdis 异常", e);
            }
        }
    }

    public static String getLockKey(ProceedingJoinPoint pjp) {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        DistributedLock lockAnnotation = method.getAnnotation(DistributedLock.class);
         Object[] args = pjp.getArgs();
         Parameter[] parameters = method.getParameters();
        StringBuilder builder = new StringBuilder();
        //默认解析方法里面带 LockKey 注解的属性,如果没有尝试着解析实体对象中的
        for (int i = 0; i < parameters.length; i++) {
             LockKey annotation = parameters[i].getAnnotation(LockKey.class);
            if (annotation == null) {
                continue;
            }
            builder.append(lockAnnotation.delimiter()).append(args[i]);
        }
        if (StringUtils.isEmpty(builder.toString())) {
             Annotation[][] parameterAnnotations = method.getParameterAnnotations();
            for (int i = 0; i < parameterAnnotations.length; i++) {
                 Object object = args[i];
                 Field[] fields = object.getClass().getDeclaredFields();
                for (Field field : fields) {
                     LockKey annotation = field.getAnnotation(LockKey.class);
                    if (annotation == null) {
                        continue;
                    }
                    field.setAccessible(true);
                    builder.append(lockAnnotation.delimiter()).append(ReflectionUtils.getField(field, object));
                }
            }
        }

        //如果属性和实体对象都没有LockKey注解,则锁住整个方法
        if (StringUtils.isEmpty(builder.toString()) && lockAnnotation.isLockMethod()) {
            builder.append(lockAnnotation.delimiter()).append(method.getName());
        }

        return lockAnnotation.prefix() + builder.toString();
    }

}

6、用法示例


@RequestMapping(value="/")
@RestController
public class IndexController {
    @DistributedLock(prefix = "testLock")
    @RequestMapping(value = "/testLock", method = RequestMethod.GET)
    public AjaxResponse testLock(String request)  {
        return  new AjaxResponse();
    }

   @DistributedLock(prefix = "testLock2")
    @PostMapping(value = "/testLock2")
    public AjaxResponse testLock2(@LockKey int id,  Object  yy, Object xxxx)  {
        return  new AjaxResponse();
    }
   @DistributedLock(prefix = "testLock3")
    @PostMapping(value = "/testLock3")
    public AjaxResponse testLock3(@RequestBody Student input)  {
        return  new AjaxResponse();
    }
}

@Setter
@Getter
public class Studentimplements Serializable {

    /**
     * 学生编号
     */
     @LockKey
    private String stuNo;

    /**
     * 学生姓名
     */
    private String name;

    }

以上是自己学习过程的理解,如有不妥,谢谢指正~
点个赞再走哦,哈哈哈

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值