Redisson

Redis 作为分布式锁的思路

  • 因为 Redis 有 setnx k v 这个命令,即 set if not exist,如果 key 已存在,就无法成功设置值。那么就可以通过判断谁在 Redis 中设置相同 key 的值成功,谁就获得了锁。 但是存在问题,如果某个服务卡住了导致这个锁不会被删除,那这个锁就会一直存在,那服务链路就崩了。

  • 为了防止锁一直存在的情况,可以给这个 kv键值对加上一个过期时间,这样的话就可以防止这把锁无法释放的问题了。set k v EX time NX,NX 表示使用 setnx模式,时间单位是 秒。 但是也存在问题,加了锁有固定的过期时间,假如微服务A 的工作时间大于了 kv键值对的过期时间,那么就会出现问题。

  • 为了防止删除别人的锁,那就保证锁只能被加锁的人删,就让锁的值设置成该任务专属的值,然后我删除锁的时候会判断该锁的值是不是对应我的任务。 但是由于高并发,会存在这种情况:A 在超时前进入释放锁的代码块,然后获取到的锁的值还是属于 A 的,然后在 A 进行释放锁的时候刚刚好超时,锁自动失效,B 加上了锁,然后 A 又刚好在 B 上锁之后删除锁,那 B 又是还没干活锁就没了。

Redisson框架

简介

自己去做分布式锁,因为锁的超时时间不太好衡量,所以很容易出现各种问题。

我们可以借助一下 Redisson框架,它是 Redis 官方推荐的 Java版的 Redis客户端。

Redisson 内部提供了一个监控锁的看门狗(前提是不设置超时时间),它的作用是在 Redisson实例被关闭前,不断的延长锁的有效期,它为我们提供了很多种分布式锁的实现。Redisson 提供的分布式锁是支持锁自动续期的,也就是说,如果线程仍旧没有执行完,那么 redisson 会自动给 redis 中的目标 key 延长超时时间,这在 Redisson 中称之为 Watch Dog 机制。同时 redisson 还有公平锁、读写锁的实现。

注意:如果 Redis 没有密码,有可能会报错,所以报错可以试试给 Redis 加上密码验证。

实例

实践1

SpringBoot 集成 Redisson。

引入依赖

注意版本,我的 SpringBoot 版本是 2.2.1.RELEASE,所以使用 3.12。

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.12.5</version>
</dependency>

配置文件

server:
  # 服务端口
  port: 8001
spring:
  application:
    # 服务名称
    name: daily-test
  redis:
    host: 192.168.174.128
    database: 0
    timeout: 1800000
    lettuce:
      pool:
        max-active: 20
        max-wait: -1
        max-idle: 5
        min-idle: 0

使用 RLock.lock() 加锁

多线程下对 Redis 修改,如果不加锁,数值很可能会出错。

学习时直接 new Thread 了,应该是要使用 ThreadPoolExecutor。

除了 RLock,还有 RedLock,防止因为 Redis服务器挂了而崩溃。

自定义 Client
import com.chw.service.TestService;
import java.util.concurrent.TimeUnit;
import javax.annotation.Resource;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
​
@Resource
private StringRedisTemplate stringRedisTemplate;
public void testLock() throws InterruptedException {
    System.out.println("进来了");
    Config config = new Config();
    // 配置连接的Redis服务器,也可以指定集群
    config.useSingleServer().setAddress("redis://192.168.174.128:6379");
    // 创建RedissonClient客户端
    RedissonClient client = Redisson.create(config);
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            System.out.println("开始!");
            // 指定锁的名称,拿到锁对象
            RLock lock = client.getLock("testLock");
            for (int j = 0; j < 10; j++) {
                // 加锁
                lock.lock();
                int num = Integer.parseInt(stringRedisTemplate.opsForValue().get("test")) + 1;
                System.out.println("---" + num);
                stringRedisTemplate.opsForValue().set("test", String.valueOf(num));
                // 解锁
                lock.unlock();
            }
            System.out.println("结束!");
        }).start();
    }
}
使用自动注入的 RedissonClient
@Resource
private RedissonClient redissonClient;
@Resource
private StringRedisTemplate stringRedisTemplate;
​
@GetMapping("test1")
public void testLock() throws InterruptedException {
    System.out.println("进来了");
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            System.out.println("开始!");
            // 指定锁的名称,拿到锁对象
            RLock lock = redissonClient.getLock("testLock");
            for (int j = 0; j < 10; j++) {
                // 加锁
                lock.lock();
                int num = Integer.parseInt(stringRedisTemplate.opsForValue().get("test")) + 1;
                System.out.println("---" + num);
                stringRedisTemplate.opsForValue().set("test", String.valueOf(num));
                // 解锁
                lock.unlock();
            }
            System.out.println("结束!");
        }).start();
    }
}

实践2

使用注解 + 切面的方式。

但这种方式有一个注意点,因为是使用切面去自动加锁,所以你这个加锁的方法的调用者必须是一个 bean,就是说必须是 service 的 impl方法,而不能是 impl 里面的私有方法,这会使得其使用不到 Spring 的代理。

自定义加锁注解

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
/**
 * The interface Distributed lock.
 *
 * @author xxw
 * @date 2022.08.11 14:43
 */
@Documented
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {
    /**
     * 锁的名称
     *
     * @return the string
     */
    String value() default "";
​
    /**
     * Key string.
     *
     * @return the string
     */
    String key() default "";
    /**
     * 等待获取时间
     *
     * @return the long
     */
    long waitTime() default 10;
    /**
     * 获得锁有效时间,到时间自动释放
     *
     * @return the long
     */
    long leaseTime() default 10;
    /**
     * Time unit time unit.
     *
     * @return the time unit
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;
}

定义一个切面去处理注解

import com.chw.annotations.DistributedLock;
import java.lang.reflect.Method;
import javax.annotation.Resource;
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.junit.jupiter.api.Assertions;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
@Aspect
@Component
@Slf4j
public class DistributedLockAspect {
    /**
     * The Redisson client.
     */
    @Resource
    private RedissonClient redissonClient;
    /**
     * Lock object.
     *
     * @param pjp the pjp
     * @return the object
     * @throws Throwable the throwable
     */
    @Around("@annotation(com.chw.locks.DistributedLock)")
    public Object lock(ProceedingJoinPoint pjp) throws Throwable {
        MethodSignature ms = (MethodSignature) pjp.getSignature();
        Method method = ms.getMethod();
        DistributedLock distributedLock = method.getAnnotation(DistributedLock.class);
        StringBuffer lockKey = new StringBuffer("distributed:lock");
        String value = distributedLock.value();
        Assertions.assertNotNull(value);
        lockKey.append(":" + value);
        // 获取key
        String spel = distributedLock.key();
        Assertions.assertNotNull(spel);
        lockKey.append(":" + generatorKey(spel, method, pjp.getArgs()));
        RLock lock = redissonClient.getLock(lockKey.toString());
        try {
            boolean isSuccess = lock.tryLock(distributedLock.waitTime(), distributedLock.leaseTime(), distributedLock.timeUnit());
            if (!isSuccess) {
                log.warn("distributed lock fail key: {}", lockKey);
                throw new RuntimeException("分布式锁获取锁失败");
            }
            return pjp.proceed();
        } catch (Exception e) {
            log.error("distributed lock error", e);
            throw e;
        } finally {
            lock.unlock();
        }
    }
    
    private String generatorKey(String key, Method method, Object[] args) {
        LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
        String[] paraNameArr = u.getParameterNames(method);
        ExpressionParser parser = new SpelExpressionParser();
        StandardEvaluationContext context = new StandardEvaluationContext();
        for (int i = 0; i < paraNameArr.length; i++) {
            context.setVariable(paraNameArr[i], args[i]);
        }
        return parser.parseExpression(key).getValue(context, String.class);
    }
}

加锁测试

我们这里用两个 controller,先请求 test2,让他拿到锁,然后再请求 test3,那么他就会等待锁。

@DistributedLock(value = "testLock", key = "#request.id+'-'+#request.name")
@PostMapping("test2")
public void testLock2(@RequestBody TestRequest request) throws InterruptedException {
    System.out.println("开始!");
    for (int j = 0; j < 10; j++) {
        int num = Integer.parseInt(stringRedisTemplate.opsForValue().get("test")) + 1;
        System.out.println(Thread.currentThread().getId() + "--" + num);
        stringRedisTemplate.opsForValue().set("test", String.valueOf(num));
        Thread.sleep(5 * 1000);
    }
    System.out.println(Thread.currentThread().getId() + "结束!");
}
​
​
​
@DistributedLock(value = "testLock", key = "#request.id+'-'+#request.name")
@PostMapping("test3")
public void testLock3(@RequestBody TestRequest request) throws InterruptedException {
    System.out.println("开始!");
    for (int j = 0; j < 10; j++) {
        int num = Integer.parseInt(stringRedisTemplate.opsForValue().get("test")) + 1;
        System.out.println(Thread.currentThread().getId() + "--" + num);
        stringRedisTemplate.opsForValue().set("test", String.valueOf(num));
    }
    System.out.println(Thread.currentThread().getId() + "结束!");
}

发现的问题

然后就会发现,因为这个锁有有效时间,然后 test2 还没有干完活,它的锁就失效了,然后 test3 就立马拿到锁然后把活干完了再解锁。但此时 test2 活还没干完,但是它已经加过锁了,然后当他干完活去解锁的时候,就会出现解锁失败的报错了。

问题原因

因为 DistributedLockAspect 中使用的加锁方式是带有 等待时间、上锁时间 参数的,那么这个锁就不会自动续期,超过等待时间就上锁失败,超过上锁时间就自动解锁。而普通的 lock.lock() 是可以让看门狗自动续期 30s 的。

lock.lock(); 是阻塞式等待的,默认加锁 30s;不用担心业务时间长,锁自动过期被删掉;加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在 30s内自动过期,不会产生死锁问题;

自己指定解锁时间 lock.lock(10,TimeUnit.SECONDS),10秒钟自动解锁,自己指定解锁时间 redisson 不会自动续期。

  • 30
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值