SpringBoot+AOP+Redission combat distributed lock


foreword

Problems in non-monolithic applications in a cluster environment: JVM locks can only control access to local resources, but cannot control resource access between multiple JVMs, so third-party middleware is needed to control overall resource access. Redis is a To implement distributed locks and ensure AP middleware, you can use the setnx command to implement, but there are many points that need to be paid attention to in the implementation details, such as the timing of acquiring locks, releasing locks, and lock renewal issues, and the redission tool can effectively reduce To achieve the complexity of distributed locks, the watchdog mechanism effectively solves the problem of lock renewal.


1. What is Redission?

Redisson is a Redis client for Java, which provides many distributed objects and services, making it easier to use Redis in Java applications. Redisson provides complete support for Redis, and comes with a series of functions, such as distributed locks, distributed collections, distributed objects, distributed queues, etc.


2. Usage scenarios

  1. Distributed locks: Redisson provides a powerful distributed lock mechanism, which can ensure data consistency in a distributed environment through Redis-based distributed locks. Common usage scenarios include distributed task scheduling, prevention of repeated operations, resource competition control, etc.
  2. Distributed collection: Redisson provides a series of distributed collection implementations, such as Set, List, Queue, Deque, etc. The characteristic of these collections is that the data is distributed on multiple machines and supports concurrent access, which is suitable for scenarios where data needs to be shared among multiple nodes.
  3. Distributed objects: Redisson supports converting ordinary Java objects into distributed objects that can be stored and manipulated in Redis. These objects can be transferred across JVMs and remain consistent. Using distributed objects, functions such as distributed caching and session management can be easily implemented.
  4. Distributed queue: Redisson provides an advanced distributed queue implementation that supports fair lock and unfair lock queuing. Distributed queues can transmit data between multiple threads and multiple JVMs, and are suitable for scenarios such as message queues and asynchronous task processing.
  5. Distributed semaphores, locks, and locks: Redisson provides mechanisms such as distributed semaphores, reentrant locks, and locks to achieve more complex concurrency control requirements. These tools can effectively manage concurrent access and ensure the correctness of data operations in a distributed environment.

In addition to the functions mentioned above, Redisson also provides many other functional components required by distributed application scenarios, such as distributed BitSet, distributed geographic location, distributed publish/subscribe, etc.


3. Code combat

Through aop aspect programming, the coupling degree with business code can be reduced, which is convenient for expansion and maintenance

1. Project structure

insert image description here


2. Class Diagram

insert image description here


3. maven dependency

<dependencies>
   <!-- Spring Boot -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <!-- Redisson -->
    <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson</artifactId>
        <version>3.20.0</version>
    </dependency>

    <!-- Spring AOP -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>

    <!-- Spring Expression Language (SpEL) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
    </dependency>
</dependencies>

4.yml

dserver:
  port: 8081

spring:
  redis:
    host: localhost
    port: 6379
    # 如果需要密码认证,请使用以下配置
    # password: your_password

5.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;

/**
 * @author 28382
 */
@Configuration
public class RedissonConfig {
    
    

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

    @Value("${spring.redis.port}")
    private int redisPort;

    // 如果有密码认证,请添加以下注解,并修改相应的配置:
    //@Value("${spring.redis.password}")
    //private String redisPassword;

    @Bean(destroyMethod = "shutdown")
    public RedissonClient redissonClient() {
    
    
        Config config = new Config();
        config.useSingleServer().setAddress("redis://" + redisHost + ":" + redisPort);
        // 如果有密码认证,请添加以下配置:
        //config.useSingleServer().setPassword(redisPassword);
        return Redisson.create(config);
    }
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * @author 28382
 */
@Configuration
public class RedisTemplateConfig {
    
    

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
    
    
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }
}

6.annotation

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

/**
 * @author 28382
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {
    
    

    // 自定义业务keys
    String[] keys() default {
    
    };

    // 租赁时间 单位:毫秒
    long leaseTime() default 30000;

    // 等待时间 单位:毫秒
    long waitTime() default 3000;

}

7.aop

Support for parsing SpEL

import com.mxf.code.annotation.DistributedLock;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

@Aspect
@Component
@Slf4j
public class DistributedLockAspect {
    
    
    @Autowired
    private RedissonClient redissonClient;

    @Around("@annotation(distributedLock)")
    public Object lockMethod(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) throws Throwable {
    
    
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();

        // 获取自定义业务keys
        String[] keys = distributedLock.keys();
        // 租赁时间
        long leaseTime = distributedLock.leaseTime();
        // 等待时间
        long waitTime = distributedLock.waitTime();

        // 创建参数名发现器
        ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

        // 获取方法参数名
        String[] parameterNames = parameterNameDiscoverer.getParameterNames(method);

        // 创建 SpEL 解析器
        ExpressionParser expressionParser = new SpelExpressionParser();

        // 创建 SpEL 上下文
        EvaluationContext evaluationContext = new StandardEvaluationContext();

        // 设置方法参数值到 SpEL 上下文中
        Object[] args = joinPoint.getArgs();
        if (parameterNames != null && args != null && parameterNames.length == args.length) {
    
    
            for (int i = 0; i < parameterNames.length; i++) {
    
    
                evaluationContext.setVariable(parameterNames[i], args[i]);
            }
        }

        // 构建完整的锁键名
        StringBuilder lockKeyBuilder = new StringBuilder();
        if (keys.length > 0) {
    
    
            for (String key : keys) {
    
    
                if (StringUtils.hasText(key)) {
    
    
                    try {
    
    
                        // 解析 SpEL 表达式获取属性值
                        Object value = expressionParser.parseExpression(key).getValue(evaluationContext);
                        lockKeyBuilder.append(value).append(":");
                    } catch (SpelEvaluationException ex) {
    
    
                        // 如果解析失败,则使用原始字符串作为属性值
                        LiteralExpression expression = new LiteralExpression(key);
                        lockKeyBuilder.append(expression.getValue()).append(":");
                    }
                }
            }
        }
        // 使用方法名作为最后一部分键名
        lockKeyBuilder.append(methodSignature.getName());
        String fullLockKey = lockKeyBuilder.toString();
        // 获取 Redisson 锁对象
        RLock lock = redissonClient.getLock(fullLockKey);
        // 尝试获取分布式锁
        // boolean tryLock(long waitTime, long leaseTime, TimeUnit unit)
        boolean success = lock.tryLock(waitTime, leaseTime, TimeUnit.MILLISECONDS);

        if (success) {
    
    
            try {
    
    
                // 执行被拦截的方法
                return joinPoint.proceed();
            } finally {
    
    
                // 释放锁
                lock.unlock();
            }
        } else {
    
    
            log.error("Failed to acquire distributed lock");
            // 获取锁超时,抛出异常
            throw new RuntimeException("Failed to acquire distributed lock");
        }
    }

}

8.model


import lombok.Data;

/**
 * @author 28382
 */
@Data
public class User {
    
    
    private Long id;
    private String name;
    private String address;

    public User(Long id, String name) {
    
    
        this.id = id;
        this.name = name;
    }
}

9.service

import com.mxf.code.annotation.DistributedLock;
import com.mxf.code.model.User;
import org.springframework.stereotype.Service;

/**
 * @author 28382
 */
@Service
public class UserService {
    
    
    int i = 0;

    @DistributedLock
    public void test01() {
    
    
        System.out.println("执行方法1 , 当前线程:" + Thread.currentThread().getName() + "执行的结果是:" + ++i);
        sleep();
    }

    @DistributedLock(keys = "myKey",leaseTime = 30L)
    public void test02() {
    
    
        System.out.println("执行方法2 , 当前线程:" + Thread.currentThread().getName() + "执行的结果是:" + ++i);
        sleep();
    }

    @DistributedLock(keys = "#user.id")
    public User test03(User user) {
    
    
        System.out.println("执行方法3 , 当前线程:" + Thread.currentThread().getName() + "执行的结果是:" + ++i);
        sleep();
        return user;
    }

    @DistributedLock(keys = {
    
    "#user.id", "#user.name"}, leaseTime = 5000, waitTime = 5000)
    public User test04(User user) {
    
    
        System.out.println("执行方法4 , 当前线程:" + Thread.currentThread().getName() + "执行的结果是:" + ++i);
        sleep();
        return user;
    }

    private void sleep() {
    
    
        // 模拟业务耗时
        try {
    
    
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
    
    
            throw new RuntimeException(e);
        }
    }

}

4. Unit testing

import com.mxf.code.model.User;
import com.mxf.code.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;


@SpringBootTest(classes = SpringBootLockTest.class)
@SpringBootApplication
public class SpringBootLockTest {
    
    
    @Autowired
    UserService userService;

    private static final Random RANDOM = new Random();

    public static void main(String[] args) {
    
    
        SpringApplication.run(SpringBootLockTest.class, args);
    }

    @Test
    public void test01() throws Exception {
    
    
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        Runnable task = () -> userService.test01();
        for (int i = 0; i < 100; i++) {
    
    
            executorService.submit(task);
        }
        Thread.sleep(10000);
    }

    @Test
    public void test02() throws Exception {
    
    
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        Runnable task = () -> userService.test02();
        for (int i = 0; i < 100; i++) {
    
    
            executorService.submit(task);
        }
        Thread.sleep(10000L);
    }

    @Test
    public void test03() throws Exception {
    
    
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        Runnable task = () -> userService.test03(new User(1L, "name"));
        for (int i = 0; i < 100; i++) {
    
    
            executorService.submit(task);
        }
        Thread.sleep(10000L);
    }

    @Test
    public void test04() throws Exception {
    
    
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        Runnable task = () -> userService.test04(new User(1L, "name"));
        for (int i = 0; i < 100; i++) {
    
    
            executorService.submit(task);
        }
        Thread.sleep(100000L);
    }
}

test01

insert image description here

test02

insert image description here

test03

insert image description here

test04

insert image description here

Summarize

You can create a Module separately in the project, directly import the required subsystems, and add annotations and attribute values ​​to the business code methods that need to be distributed. There is a potential problem that if redis uses a master-slave architecture, in When the master node and the slave node synchronize the lock information, the master node hangs up. At this time, when a slave node that has not yet synchronized the complete node information is selected as the master node, there is a certain lock failure problem. This can be considered to implement strong consistency.

Guess you like

Origin blog.csdn.net/weixin_44063083/article/details/132073657