Redis实现的分布式锁和分布式限流

作者:黄青石

https://zhuanlan.zhihu.com/p/56135195

 

随着现在分布式越来越普遍,分布式锁也十分常用,这篇文章解释了使用zookeeper实现分布式锁,本次咱们说一下如何用Redis实现分布式锁和分布限流。

cnblogs.com/huangqingshi/p/9650837.html

Redis有个事务锁,就是如下的命令,这个命令的含义是将一个value设置到一个key中,如果不存在将会赋值并且设置超时时间为30秒,如何这个key已经存在了,则不进行设置。

 
  1. SET key value NX PX 30000

这个事务锁很好的解决了两个单独的命令,一个设置set key value nx,即该key不存在的话将对其进行设置,另一个是expire key seconds,设置该key的超时时间。我们可以想一下,如果这两个命令用程序单独使用会存在什么问题:

  • 如果一个set key的命令设置了key,然后程序异常了,expire时间没有设置,那么这个key会一直锁住。

  • 如果一个set key时出现了异常,但是直接执行了expire,过了一会儿之后另一个进行set key,还没怎么执行代码,结果key过期了,别的线程也进入了锁。

还有很多出问题的可能点,这里我们就不讨论了,下面咱们来看看如何实现吧。

本文使用的Spring Boot 2.x + Spring data redis + Swagger +lombok + AOP + lua脚本。在实现的过程中遇到了很多问题,都一一解决实现了。

依赖的POM文件如下:

 
  1. <?xml version="1.0" encoding="UTF-8"?>

  2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  3.         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  4.    <modelVersion>4.0.0</modelVersion>

  5.    <parent>

  6.        <groupId>org.springframework.boot</groupId>

  7.        <artifactId>spring-boot-starter-parent</artifactId>

  8.        <version>2.1.2.RELEASE</version>

  9.        <relativePath/> <!-- lookup parent from repository -->

  10.    </parent>

  11.    <groupId>com.hqs</groupId>

  12.    <artifactId>distributedlock</artifactId>

  13.    <version>0.0.1-SNAPSHOT</version>

  14.    <name>distributedlock</name>

  15.    <description>Demo project for Spring Boot</description>

  16.  

  17.    <properties>

  18.        <java.version>1.8</java.version>

  19.    </properties>

  20.  

  21.    <dependencies>

  22.        <dependency>

  23.            <groupId>org.springframework.boot</groupId>

  24.            <artifactId>spring-boot-starter-aop</artifactId>

  25.        </dependency>

  26.        <dependency>

  27.            <groupId>org.springframework.boot</groupId>

  28.            <artifactId>spring-boot-starter-web</artifactId>

  29.        </dependency>

  30.        <dependency>

  31.            <groupId>org.springframework.boot</groupId>

  32.            <artifactId>spring-boot-starter-data-redis</artifactId>

  33.        </dependency>

  34.        <dependency>

  35.            <groupId>org.springframework.boot</groupId>

  36.            <artifactId>spring-boot-devtools</artifactId>

  37.            <scope>runtime</scope>

  38.        </dependency>

  39.        <dependency>

  40.            <groupId>org.projectlombok</groupId>

  41.            <artifactId>lombok</artifactId>

  42.            <optional>true</optional>

  43.        </dependency>

  44.        <dependency>

  45.            <groupId>org.springframework.boot</groupId>

  46.            <artifactId>spring-boot-starter-test</artifactId>

  47.            <scope>test</scope>

  48.        </dependency>

  49.        <dependency>

  50.            <groupId>io.springfox</groupId>

  51.            <artifactId>springfox-swagger-ui</artifactId>

  52.            <version>2.9.2</version>

  53.        </dependency>

  54.        <dependency>

  55.            <groupId>io.springfox</groupId>

  56.            <artifactId>springfox-swagger2</artifactId>

  57.            <version>2.9.2</version>

  58.            <scope>compile</scope>

  59.        </dependency>

  60.        <dependency>

  61.            <groupId>redis.clients</groupId>

  62.            <artifactId>jedis</artifactId>

  63.            <version>2.9.0</version>

  64.        </dependency>

  65.    </dependencies>

  66.  

  67.    <build>

  68.        <plugins>

  69.            <plugin>

  70.                <groupId>org.springframework.boot</groupId>

  71.                <artifactId>spring-boot-maven-plugin</artifactId>

  72.            </plugin>

  73.        </plugins>

  74.    </build>

  75.  

  76. </project>

使用了两个lua脚本,一个用于执行lock,另一个执行unlock。

咱们简单看一下,lock脚本就是采用Redis事务执行的set nx px命令,其实还有set nx ex命令,这个ex命令是采用秒的方式进行设置过期时间,这个px是采用毫秒的方式设置过期时间。

value需要使用一个唯一的值,这个值在解锁的时候需要判断是否一致,如果一致的话就进行解锁。这个也是官方推荐的方法。另外在lock的地方我设置了一个result,用于输出测试时的结果,这样就可以结合程序去进行debug了。

 
  1. local expire = tonumber(ARGV[2])

  2. local ret = redis.call('set', KEYS[1], ARGV[1], 'NX', 'PX', expire)

  3. local strret = tostring(ret)

  4. //用于查看结果,我本机获取锁成功后程序返回随机结果"table: 0x7fb4b3700fe0",否则返回"false"

  5. redis.call('set', 'result', strret)

  6. if strret == 'false' then

  7.    return false

  8. else

  9.    return true

  10. end

 

 
  1. redis.call('del', 'result')

  2. if redis.call('get', KEYS[1]) == ARGV[1] then

  3.    return redis.call('del', KEYS[1])

  4. else

  5.    return 0

  6. end

来看下代码,主要写了两个方法,一个是用与锁另外一个是用于结解锁。这块需要注意的是使用RedisTemplate,这块意味着key和value一定都是String的,我在使用的过程中就出现了一些错误。首先初始化两个脚本到程序中,然后调用执行脚本。

 
  1. package com.hqs.distributedlock.lock;

  2.  

  3.  

  4. import lombok.extern.slf4j.Slf4j;

  5. import org.springframework.beans.factory.annotation.Autowired;

  6. import org.springframework.data.redis.core.RedisTemplate;

  7. import org.springframework.data.redis.core.script.RedisScript;

  8. import org.springframework.stereotype.Component;

  9.  

  10. import java.util.Collections;

  11.  

  12. @Slf4j

  13. @Component

  14. public class DistributedLock {

  15.  

  16.    //注意RedisTemplate用的String,String,后续所有用到的key和value都是String的

  17.    @Autowired

  18.    private RedisTemplate<String, String> redisTemplate;

  19.  

  20.    @Autowired

  21.    RedisScript<Boolean> lockScript;

  22.  

  23.    @Autowired

  24.    RedisScript<Long> unlockScript;

  25.  

  26.    public Boolean distributedLock(String key, String uuid, String secondsToLock) {

  27.        Boolean locked = false;

  28.        try {

  29.            String millSeconds = String.valueOf(Integer.parseInt(secondsToLock) * 1000);

  30.            locked =redisTemplate.execute(lockScript, Collections.singletonList(key), uuid, millSeconds);

  31.            log.info("distributedLock.key{}: - uuid:{}: - timeToLock:{} - locked:{} - millSeconds:{}",

  32.                    key, uuid, secondsToLock, locked, millSeconds);

  33.        } catch (Exception e) {

  34.            log.error("error", e);

  35.        }

  36.        return locked;

  37.    }

  38.  

  39.    public void distributedUnlock(String key, String uuid) {

  40.        Long unlocked = redisTemplate.execute(unlockScript, Collections.singletonList(key),

  41.                uuid);

  42.        log.info("distributedLock.key{}: - uuid:{}: - unlocked:{}", key, uuid, unlocked);

  43.  

  44.    }

  45.  

  46. }

还有一个就是脚本定义的地方需要注意,返回的结果集一定是Long, Boolean,List, 一个反序列化的值。这块要注意。

 
  1. package com.hqs.distributedlock.config;

  2.  

  3.  

  4. import com.sun.org.apache.xpath.internal.operations.Bool;

  5. import lombok.extern.slf4j.Slf4j;

  6. import org.springframework.beans.factory.annotation.Qualifier;

  7. import org.springframework.context.annotation.Bean;

  8. import org.springframework.context.annotation.Configuration;

  9. import org.springframework.core.io.ClassPathResource;

  10. import org.springframework.data.redis.core.script.DefaultRedisScript;

  11. import org.springframework.data.redis.core.script.RedisScript;

  12. import org.springframework.scripting.ScriptSource;

  13. import org.springframework.scripting.support.ResourceScriptSource;

  14.  

  15.  

  16. @Configuration

  17. @Slf4j

  18. public class BeanConfiguration {

  19.  

  20.    /**

  21.     * The script resultType should be one of

  22.     * Long, Boolean, List, or a deserialized value type. It can also be null if the script returns

  23.     * a throw-away status (specifically, OK).

  24.     * @return

  25.     */

  26.    @Bean

  27.    public RedisScript<Long> limitScript() {

  28.        RedisScript redisScript = null;

  29.        try {

  30.            ScriptSource scriptSource = new ResourceScriptSource(new ClassPathResource("/scripts/limit.lua"));

  31. //            log.info("script:{}", scriptSource.getScriptAsString());

  32.            redisScript = RedisScript.of(scriptSource.getScriptAsString(), Long.class);

  33.        } catch (Exception e) {

  34.            log.error("error", e);

  35.        }

  36.        return redisScript;

  37.  

  38.    }

  39.  

  40.    @Bean

  41.    public RedisScript<Boolean> lockScript() {

  42.        RedisScript<Boolean> redisScript = null;

  43.        try {

  44.            ScriptSource scriptSource = new ResourceScriptSource(new ClassPathResource("/scripts/lock.lua"));

  45.            redisScript = RedisScript.of(scriptSource.getScriptAsString(), Boolean.class);

  46.        } catch (Exception e) {

  47.            log.error("error" , e);

  48.        }

  49.        return redisScript;

  50.    }

  51.  

  52.    @Bean

  53.    public RedisScript<Long> unlockScript() {

  54.        RedisScript<Long> redisScript = null;

  55.        try {

  56.            ScriptSource scriptSource = new ResourceScriptSource(new ClassPathResource("/scripts/unlock.lua"));

  57.            redisScript = RedisScript.of(scriptSource.getScriptAsString(), Long.class);

  58.        } catch (Exception e) {

  59.            log.error("error" , e);

  60.        }

  61.        return redisScript;

  62.    }

  63.  

  64.  

  65.    @Bean

  66.    public RedisScript<Long> limitAnother() {

  67.        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();

  68.        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("/scripts/limit.lua")));

  69.        redisScript.setResultType(Long.class);

  70.        return redisScript;

  71.    }

  72.  

  73. }

好了,这块就写好了,然后写好controller类准备测试。

 
  1. @PostMapping("/distributedLock")

  2.    @ResponseBody

  3.    public String distributedLock(String key, String uuid, String secondsToLock, String userId) throws Exception{

  4. //        String uuid = UUID.randomUUID().toString();

  5.        Boolean locked = false;

  6.        try {

  7.            locked = lock.distributedLock(key, uuid, secondsToLock);

  8.            if(locked) {

  9.                log.info("userId:{} is locked - uuid:{}", userId, uuid);

  10.                log.info("do business logic");

  11.                TimeUnit.MICROSECONDS.sleep(3000);

  12.            } else {

  13.                log.info("userId:{} is not locked - uuid:{}", userId, uuid);

  14.            }

  15.        } catch (Exception e) {

  16.            log.error("error", e);

  17.        } finally {

  18.            if(locked) {

  19.                lock.distributedUnlock(key, uuid);

  20.            }

  21.        }

  22.  

  23.        return "ok";

  24.    }

我也写了一个测试类,用于测试和输出结果, 使用100个线程,然后锁的时间设置10秒,controller里边需要休眠3秒模拟业务执行。

 
  1. @Test

  2.    public void distrubtedLock() {

  3.        String url = "http://localhost:8080/distributedLock";

  4.        String uuid = "abcdefg";

  5. //        log.info("uuid:{}", uuid);

  6.        String key = "redisLock";

  7.        String secondsToLive = "10";

  8.  

  9.        for(int i = 0; i < 100; i++) {

  10.            final int userId = i;

  11.            new Thread(() -> {

  12.                MultiValueMap<String, String> params = new LinkedMultiValueMap<>();

  13.                params.add("uuid", uuid);

  14.                params.add("key", key);

  15.                params.add("secondsToLock", secondsToLive);

  16.                params.add("userId", String.valueOf(userId));

  17.                String result = testRestTemplate.postForObject(url, params, String.class);

  18.                System.out.println("-------------" + result);

  19.            }

  20.            ).start();

  21.        }

  22.  

  23.    }

获取锁的地方就会执行do business logic, 然后会有部分线程获取到锁并执行业务,执行完业务的就会释放锁。

分布式锁就实现好了,接下来实现分布式限流。先看一下limit的lua脚本,需要给脚本传两个值,一个值是限流的key,一个值是限流的数量。

获取当前key,然后判断其值是否为nil,如果为nil的话需要赋值为0,然后进行加1并且和limit进行比对,如果大于limt即返回0,说明限流了,如果小于limit则需要使用Redis的INCRBY key 1,就是将key进行加1命令。并且设置超时时间,超时时间是秒,并且如果有需要的话这个秒也是可以用参数进行设置。

 
  1. //lua 下标从 1 开始

  2. // 限流 key

  3. local key = KEYS[1]

  4. //限流大小

  5. local limit = tonumber(ARGV[1])

  6.  

  7. // 获取当前流量大小

  8. local curentLimit = tonumber(redis.call('get', key) or "0")

  9.  

  10. if curentLimit + 1 > limit then

  11.    // 达到限流大小 返回

  12.    return 0;

  13. else

  14.    // 没有达到阈值 value + 1

  15.    redis.call("INCRBY", key, 1)

  16.    // EXPIRE后边的单位是秒

  17.    redis.call("EXPIRE", key, 10)

  18.    return curentLimit + 1

  19. end

  20.  

执行limit的脚本和执行lock的脚本类似。

 
  1. package com.hqs.distributedlock.limit;

  2.  

  3. import lombok.extern.slf4j.Slf4j;

  4. import org.springframework.beans.factory.annotation.Autowired;

  5. import org.springframework.data.redis.core.RedisTemplate;

  6. import org.springframework.data.redis.core.script.RedisScript;

  7. import org.springframework.stereotype.Component;

  8.  

  9. import java.util.Collections;

  10.  

  11. /**

  12. * @author huangqingshi

  13. * @Date 2019-01-17

  14. */

  15. @Slf4j

  16. @Component

  17. public class DistributedLimit {

  18.  

  19.    //注意RedisTemplate用的String,String,后续所有用到的key和value都是String的

  20.    @Autowired

  21.    private RedisTemplate<String, String> redisTemplate;

  22.  

  23.  

  24.    @Autowired

  25.    RedisScript<Long> limitScript;

  26.  

  27.    public Boolean distributedLimit(String key, String limit) {

  28.        Long id = 0L;

  29.  

  30.        try {

  31.            id = redisTemplate.execute(limitScript, Collections.singletonList(key),

  32.                    limit);

  33.            log.info("id:{}", id);

  34.        } catch (Exception e) {

  35.            log.error("error", e);

  36.        }

  37.  

  38.        if(id == 0L) {

  39.            return false;

  40.        } else {

  41.            return true;

  42.        }

  43.    }

  44.  

  45. }

接下来咱们写一个限流注解,并且设置注解的key和限流的大小:

 
  1. package com.hqs.distributedlock.annotation;

  2.  

  3. import java.lang.annotation.ElementType;

  4. import java.lang.annotation.Retention;

  5. import java.lang.annotation.RetentionPolicy;

  6. import java.lang.annotation.Target;

  7.  

  8. /**

  9. * 自定义limit注解

  10. * @author huangqingshi

  11. * @Date 2019-01-17

  12. */

  13. @Target(ElementType.METHOD)

  14. @Retention(RetentionPolicy.RUNTIME)

  15. public @interface DistriLimitAnno {

  16.    public String limitKey() default "limit";

  17.    public int limit() default 1;

  18. }

然后对注解进行切面,在切面中判断是否超过limit,如果超过limit的时候就需要抛出异常exceeded limit,否则正常执行。

 
  1. package com.hqs.distributedlock.aspect;

  2.  

  3. import com.hqs.distributedlock.annotation.DistriLimitAnno;

  4. import com.hqs.distributedlock.limit.DistributedLimit;

  5. import lombok.extern.slf4j.Slf4j;

  6. import org.aspectj.lang.JoinPoint;

  7. import org.aspectj.lang.annotation.Aspect;

  8. import org.aspectj.lang.annotation.Before;

  9. import org.aspectj.lang.annotation.Pointcut;

  10. import org.aspectj.lang.reflect.MethodSignature;

  11. import org.springframework.beans.factory.annotation.Autowired;

  12. import org.springframework.context.annotation.EnableAspectJAutoProxy;

  13. import org.springframework.stereotype.Component;

  14.  

  15. import java.lang.reflect.Method;

  16.  

  17. /**

  18. * @author huangqingshi

  19. * @Date 2019-01-17

  20. */

  21. @Slf4j

  22. @Aspect

  23. @Component

  24. @EnableAspectJAutoProxy(proxyTargetClass = true)

  25. public class LimitAspect {

  26.  

  27.    @Autowired

  28.    DistributedLimit distributedLimit;

  29.  

  30.    @Pointcut("@annotation(com.hqs.distributedlock.annotation.DistriLimitAnno)")

  31.    public void limit() {};

  32.  

  33.    @Before("limit()")

  34.    public void beforeLimit(JoinPoint joinPoint) throws Exception {

  35.        MethodSignature signature = (MethodSignature) joinPoint.getSignature();

  36.        Method method = signature.getMethod();

  37.        DistriLimitAnno distriLimitAnno = method.getAnnotation(DistriLimitAnno.class);

  38.        String key = distriLimitAnno.limitKey();

  39.        int limit = distriLimitAnno.limit();

  40.        Boolean exceededLimit = distributedLimit.distributedLimit(key, String.valueOf(limit));

  41.        if(!exceededLimit) {

  42.            throw new RuntimeException("exceeded limit");

  43.        }

  44.    }

  45.  

  46. }

因为有抛出异常,这里我弄了一个统一的controller错误处理,如果controller出现Exception的时候都需要走这块异常。如果是正常的RunTimeException的时候获取一下,否则将异常获取一下并且输出。

 
  1. package com.hqs.distributedlock.util;

  2.  

  3. import lombok.extern.slf4j.Slf4j;

  4. import org.springframework.http.HttpStatus;

  5. import org.springframework.web.bind.annotation.ControllerAdvice;

  6. import org.springframework.web.bind.annotation.ExceptionHandler;

  7. import org.springframework.web.bind.annotation.ResponseBody;

  8. import org.springframework.web.bind.annotation.ResponseStatus;

  9. import org.springframework.web.context.request.NativeWebRequest;

  10.  

  11. import javax.servlet.http.HttpServletRequest;

  12. import java.util.HashMap;

  13. import java.util.Map;

  14.  

  15. /**

  16. * @author huangqingshi

  17. * @Date 2019-01-17

  18. * 统一的controller错误处理

  19. */

  20. @Slf4j

  21. @ControllerAdvice

  22. public class UnifiedErrorHandler {

  23.    private static Map<String, String> res = new HashMap<>(2);

  24.  

  25.    @ExceptionHandler(value = Exception.class)

  26.    @ResponseStatus(HttpStatus.OK)

  27.    @ResponseBody

  28.    public Object processException(HttpServletRequest req, Exception e) {

  29.        res.put("url", req.getRequestURL().toString());

  30.  

  31.        if(e instanceof RuntimeException) {

  32.            res.put("mess", e.getMessage());

  33.        } else {

  34.            res.put("mess", "sorry error happens");

  35.        }

  36.        return res;

  37.    }

  38.  

  39. }

好了,接下来将注解写到自定义的controller上,limit的大小为10,也就是10秒钟内限制10次访问。

 
  1. @PostMapping("/distributedLimit")

  2.    @ResponseBody

  3.    @DistriLimitAnno(limitKey="limit", limit = 10)

  4.    public String distributedLimit(String userId) {

  5.        log.info(userId);

  6.        return "ok";

  7.    }

也是来一段Test方法来跑,老方式100个线程开始跑,只有10次,其他的都是limit。没有问题。

总结一下,这次实现采用了使用lua脚本和Redis实现了锁和限流,但是真实使用的时候还需要多测试,另外如果此次Redis也是采用的单机实现方法,使用集群的时候可能需要改造一下。

关于锁这块其实Reids自己也实现了RedLock, java实现的版本Redission。也有很多公司使用了,功能非常强大。各种场景下都用到了。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Redis分布式是一种常见的并发控制机制,用于在分布式系统中解决多个请求并发访问资源时的互斥问题。当多个客户端同时尝试获取同一时,只有一个客户端能够成功获取,其他客户端则需要等待,直到被释放。这通常通过在Redis中设置一个过期时间的键来实现,例如使用`SETNX`命令设置一个唯一的标识,如果该键不存在则设置并返回true,表示获得。 数据库事务则是数据库操作的一种执行方式,它保证了对数据库的一组操作要么全部成功,要么全部回滚,从而保持数据的一致性。在一个事务中,所有相关的操作被视为一个原子操作,这有助于避免并发修改导致的数据不一致问题。 当Redis分布式和数据库事务同时使用时,通常会在以下几个场景中: 1. **分布式事务管理**:如果系统需要支持跨数据库的操作,可以先尝试获取Redis,成功后开始一个数据库事务。只有在事务内完成所有操作并提交时,才释放。如果事务失败(如部分操作出错),也会被自动释放,防止数据不一致。 2. **限流与熔断**:在高并发场景下,可能需要使用分布式实现限流或熔断机制。先获取,如果获取成功,再检查是否超过阈值,如果在事务范围内完成这些操作后,释放。 3. **临时存储**:在需要临时存储数据,等待其他服务处理完成后进行持久化的情况,可以先在Redis中获取,然后在事务中进行数据写入,事务完成后,如果一切正常,释放
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值