文章目录

  • 前言
  • 第一节 入门使用Redisson
  • 第二节 注解形式的分布式锁
  • 1. 分布式锁的注解实现
  • 2. 分析MyRedissonLock注解和使用

前言

并发执行是比较场景的场景,单机情况下,我们可以利用锁机制来实现顺序执行。然而微服务时代,多节点运行,如何让某业务可以同一时刻只允许一个任务运行呢?
Redisson实现分布式锁的用法,可以很容易实现分布式锁的配置。

第一节 入门使用Redisson

  1. 导入依赖
<dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.16.6</version>
        </dependency>
  1. 编写RedissonConfig
package com.it2.springbootredisson.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;

@Configuration
public class RedissonConfig {

    @Value("${spring.redis.host:localhost}")
    private String host;

    @Value("${spring.redis.port:6379}")
    private String port;

    /**
     * RedissonClient,单机模式
     * @return
     * @throws IOException
     */
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redisson() throws IOException {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://" + host + ":" + port);
        return Redisson.create(config);
    }
}
  1. 配置redis

RedissonConfig里已经包含了redis默认的localhost:6379了,如果redis的配置不是默认值,则需要在配置文件中配置。

spring:
  redis:
    host: xxx.xxx.xxx.xxx
    port: 16379
  1. 编写测试的service业务实现
public interface TestService {
     void hello(String name);
}
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class TestServiceImpl implements  TestService {

    @Resource
    RedissonClient redissonClient;

    private int ticket=100;

    private final static String LOCK_KEY = "TICKET_KEY";

    @Override
    public void hello(String name) {
        //定义锁
        RLock lock = redissonClient.getLock(LOCK_KEY);
        try {
            //尝试加锁,最大等待时间2秒(如果还没等到锁,则失败),最大持锁时间10秒(防止意外情况无法释放)
            if (lock.tryLock(2000, 10000, TimeUnit.MILLISECONDS)) {
                log.info("线程:" + Thread.currentThread().getName() + "获得了锁");
                log.info("剩余数量:{}", --ticket);
            }
        } catch (Exception e) {
            log.error("程序执行异常:{}", e);
        } finally {
            log.info("线程:" + Thread.currentThread().getName() + "准备释放锁");
            //释放锁
            lock.unlock();
        }
    }
}
  1. 编写测试用例,模拟并发抢票,并执行
@Autowired
    private TestService testService;
    @Test
    public void testTicket() throws InterruptedException {
        class TicketThread extends Thread{
            private TestService testService;
            public TicketThread(TestService testService,String name){
                super(name);
                this.testService=testService;
            }
            public void run(){
                testService.hello("abc");
            }
        }
        List<TicketThread> ticketThreads=new ArrayList<>();
        for (int i=0;i<30;i++){
            TicketThread ticketThread=new TicketThread(testService,"thread-"+i);
            ticketThreads.add(ticketThread);
        }

        TimeUnit.SECONDS.sleep(2);
        ticketThreads.forEach(t->{
            t.start();
        });

        TimeUnit.HOURS.sleep(1);
    }

可以看到各线程在竞争到锁后才能取得执行资格,保证了同一时刻跨服务也只能有一个线程拥有执行权限,并且是卖票是有序执行的。

springboot redis对象 redisson springboot_spring

第二节 注解形式的分布式锁

前面我们使用redisson做分布式锁,很显然我们需要在方法内进行改造,如何更加简单的实现分布式锁?我们可以利用注解的形式的无侵入实现分布式锁,对于业务的改造也就更加简单。

1. 分布式锁的注解实现

  1. 导入依赖
<dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.16.6</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>
  1. spel解析器(用于支持spel表达式)
package com.it2.springbootredisson.util;


import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

import java.util.Map;

/**
 * spel工具
 */
public class SpELUtil {

    /**
     * spel转换
     * @param spelExpression
     * @param variables
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> T parse(String spelExpression, Map<String, Object> variables, Class<T> clazz) {
        ExpressionParser parser = new SpelExpressionParser();
        StandardEvaluationContext context = new StandardEvaluationContext();
        context.setVariables(variables);
        Expression exp = parser.parseExpression(spelExpression);
        return exp.getValue(context, clazz);
    }
}
  1. 定义注解
package com.it2.springbootredisson.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRedissonLock {

    /**
     * 空间
     * @return
     */
    String cacheName() default "";

    /**
     * 分布式锁key
     */
    String key() default "";

    /**
     * 获取锁等待时间(默认2000毫秒,还没获取到锁即放弃)
     */
    long waitTime() default 2000;

    /**
     * 锁的过期时间,默认60秒,超时自动失效
     */
    long expire() default 60_000;

    /**
     * key的生成器
     *
     * @return
     */
    String keyGenerator() default "";
}
  1. 定义RedissonConfig
package com.it2.springbootredisson.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;

@Configuration
public class RedissonConfig {

    @Value("${spring.redis.host:localhost}")
    private String host;

    @Value("${spring.redis.port:6379}")
    private String port;

    /**
     * RedissonClient,单机模式
     * @return
     * @throws IOException
     */
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redisson() throws IOException {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://" + host + ":" + port);
        return Redisson.create(config);
    }
}
  1. 定义key生成器
package com.it2.springbootredisson.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.lang.reflect.Method;

@Configuration
@Slf4j
public class MyRedissonKeyGenerator {

    /**
     * 默认的key生成器
     */
    public static final String myKeyGeneratorByMethodName = "myKeyGeneratorByMethodName";

    /**
     * 类全名+方法名
     * @return
     */
    @Bean(myKeyGeneratorByMethodName)
    public KeyGenerator myKeyGeneratorByMethodName() {
        KeyGenerator keyGenerator = new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... params) {
                String className = target.getClass().getName();
                String methodName = method.getName();
                StringBuffer cacheKey = new StringBuffer();
                cacheKey.append(className).append(".").append(methodName);
                return cacheKey.toString();
            }
        };
        return keyGenerator;
    }
}
  1. 实现aop的核心
package com.it2.springbootredisson.config;

import com.it2.springbootredisson.annotation.MyRedissonLock;
import com.it2.springbootredisson.util.SpELUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import org.springframework.cache.interceptor.KeyGenerator;

import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

@Component
@Aspect
@Slf4j
public class RedissonLockAspect implements ApplicationContextAware {
    @Resource
    private RedissonClient redissonClient;

    @Around("@annotation(distributedLock)")
    public Object lock(ProceedingJoinPoint proceedingJoinPoint, MyRedissonLock distributedLock) {
        String key = distributedLock.key();//key
        String keyGenerator = distributedLock.keyGenerator();//自定义的keyGenerator
        String cacheName = distributedLock.cacheName();//缓存空间
        long waitTime = distributedLock.waitTime(); //最大等待时间 毫秒
        long expire = distributedLock.expire(); //失效时间(防止线程意外无法释放锁) 毫秒
        waitTime = waitTime < 0 ? 0 : waitTime;
        expire = expire < 0 ? 0 : expire;

        StringBuffer rKey = new StringBuffer();
        try {
            Signature signature = proceedingJoinPoint.getSignature();
            String className = proceedingJoinPoint.getTarget().getClass().getName();
            String methodName = signature.getName();
            Object[] objects = proceedingJoinPoint.getArgs();
            Class[] parameterTypes = new Class[objects.length];
            /**
             * 打印输入的参数
             */
            for (int i = 0; i < objects.length; i++) {
                Object obj = objects[i];
                // System.out.println("type:" + obj.getClass().getSimpleName() + ",   value:" + obj);
                parameterTypes[i] = obj.getClass();
            }

            Method method = signature.getDeclaringType().getMethod(methodName, parameterTypes);
            Parameter[] parameters = method.getParameters();
//            for (Parameter parameter : parameters) {
//                System.out.println(parameter.getName() + "," + parameter.getType() + ",");
//            }

            if (!"".equals(cacheName)) {
                rKey.append(cacheName).append("::");
            }

            if (!key.equals("")) {
                //利用spel表达式转换key
                Map<String, Object> variables = new HashMap<>();
                for (int i = 0; i < parameters.length; i++) {
                    variables.put(parameters[i].getName(), objects[i]);
                }
                rKey.append(SpELUtil.parse(key, variables, String.class));
            } else if (!keyGenerator.equals("")) {
                //未设置key,则按照默认规则生成key
                KeyGenerator kg = applicationContext.getBean(keyGenerator, KeyGenerator.class);
                rKey.append(kg.generate(proceedingJoinPoint.getTarget(), method, objects).toString());
            } else {//如果未设置key,也没有使用keyGenerator,则使用默认的key生成器
                KeyGenerator kg = applicationContext.getBean(MyRedissonKeyGenerator.myKeyGeneratorByMethodName, KeyGenerator.class);
                rKey.append(kg.generate(proceedingJoinPoint.getTarget(), method, objects).toString());
            }
        } catch (Exception e) {
             throw new RuntimeException("运行时异常",e);
        }

        RLock lock = redissonClient.getLock(rKey.toString());
        boolean hasLock = false;
        try {
            hasLock = lock.tryLock(waitTime, expire, TimeUnit.MILLISECONDS);
            if (hasLock) {
                log.info("获取分布式锁成功,key={}", rKey);
                return proceedingJoinPoint.proceed();
            } else {
                log.info("获取锁失败");
            }
        } catch (Throwable e) {
            log.error("切面分布式锁异常:{}", e);
        } finally {
            if (hasLock) {
                lock.unlock();
                log.info("解锁成功:{}", rKey);
            }
        }
        return null;
    }

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}
  1. 使用注解实现redisson分布式锁
默认的分布式锁是方法锁,key为className.method,如果存在方法重载的情况,则对于同类的重载方法用的都是同一把锁。如果你希望对重载也做隔离锁,那么你可以通过修改key的生成器策略来处理,或者使用cacheName进行重载方法的隔离也是一样效果。

springboot redis对象 redisson springboot_redis_02

  1. 运行测试代码(前面的测试用例),可以看到使用注解简单的实现了分布式锁。

2. 分析MyRedissonLock注解和使用

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRedissonLock {

    /**
     * 空间
     * @return
     */
    String cacheName() default "";

    /**
     * 分布式锁key
     */
    String key() default "";

    /**
     * 获取锁等待时间(默认2000毫秒,还没获取到锁即放弃)
     */
    long waitTime() default 2000;

    /**
     * 锁的过期时间,默认60秒,超时自动失效
     */
    long expire() default 60_000;

    /**
     * key的生成器
     *
     * @return
     */
    String keyGenerator() default "";
}

key: 支持spel表达式,例如

@MyRedissonLock(key = "#name") //获取参数
@MyRedissonLock(key = "'lock-'+#name") //拼接参数(下图为示例)
@MyRedissonLock(key = "mylock") //常量参数

springboot redis对象 redisson springboot_spring_03

cacheName: 缓存空间,不同名的cacheName ,相互之间无关联。这样即使key的规则一致,两个不同的cacheName也不会发生锁竞争。与上面的demo中@MyRedissonLock(key = “‘lock-’+#name”)方式相比而言,这里实际只是分离了key而已,但是好处是规则简单了,可以复用key规则。

#下面两个redissonLock,虽然key规则一致,但是cacheName不一致,它们竞争的不是同一把锁。
@MyRedissonLock(cacheName = "hello",key = "#name")
@MyRedissonLock(cacheName = "hello2",key = "#name")

springboot redis对象 redisson springboot_spring_04

waitTime: 最大等待多久,如果到了最大等待时间仍未获取锁则失败。单位毫秒。
expire:锁的失效时间,获取锁后,最大允许持有锁多久,超时会自动释放锁。单位毫秒。锁失效是为了避免因为某些意外锁长期占有无法释放导致线程堵塞。

waitTime和expire需根据业务具体情况设定

keyGenerator: key的生成器,前面的代码里已经包含了一个默认的生成器myKeyGeneratorByMethodName,它生成的规则是全类名+方法名,如果用户既未指定key,也没有指定key生成器,则使用默认的myKeyGeneratorByMethodName。用户可以根据自己的需求,自定义生成器。可参照demo定义自己的生成器策略,并使用。

@MyRedissonLock //默认为@MyRedissonLock(keyGenerator = "myKeyGeneratorByMethodName") 
@MyRedissonLock(keyGenerator = "myKeyGeneratorByXXX")  //自定义

springboot redis对象 redisson springboot_spring boot_05


redis分布式锁官方文档
http://www.redis.cn/topics/distlock.html