Redis(四)——Redis 处理接口幂等性(代码实现、两种方式拦截令牌)、Redis 分布式锁(两种引入 lua 脚本、Redission实现分布式锁)、Redis做限流工具(简单限流,限流插件)

Redis(四)——Redis 处理接口幂等性(代码实现、两种方式拦截令牌)、Redis 分布式锁(两种引入 lua 脚本、通过 Redission 实现分布式锁)、Redis做限流工具(简单限流,限流插件——Redis-Cell模块(使用了漏斗算法)、java 代码操作(使用 Lettuce 扩展))

一、Redis 处理接口幂等性

幂等性:同一个接口,相同的参数,执行多次的结果应当是一样的。

一般来说 get 请求是不用管幂等性的。因为 get 请求是查询,查一次跟查 10 次都不影响数据库的数据。
但如果是 post 这种请求,用户如果手快,点了两下,不就添加两条记录进来了吗。

虽然可以靠前端解决这个问题,但是跟权限一样,前端解决是为了提高用户体验,后端解决才是真的保证数据的完整性。
但是这不冲突,前后端都要一起做。

那怎么做呢?
1、先定义一个注解,作为一个标记,将来,凡是加了注解的接口,都进行幂等性的检查,没加该注解的接口,就不用检查。
2、给需要进行幂等性处理的接口生成一个令牌。
3、每次请求的时候,都校验该令牌(看 redis 中是否有该令牌):
(1)有:如果方法携带了令牌,且 reids 中也有的时候,说明这是一个合法请求,请求通过,同时删除 redis 中的令牌。
(2)无:如果方法携带了令牌,且 redis 中没有的时候,说明这个请求可能已经执行过了,请求中止。

1、前期准备

在这里插入图片描述

Redis(四)——Redis 处理接口幂等性(代码实现、两种方式拦截令牌)、Redis 分布式锁(两种引入 lua 脚本、Redission实现分布式锁)、Redis做限流工具(简单限流,限流插件)_第1张图片

Redis(四)——Redis 处理接口幂等性(代码实现、两种方式拦截令牌)、Redis 分布式锁(两种引入 lua 脚本、Redission实现分布式锁)、Redis做限流工具(简单限流,限流插件)_第2张图片

2、代码实现

目录结构:
Redis(四)——Redis 处理接口幂等性(代码实现、两种方式拦截令牌)、Redis 分布式锁(两种引入 lua 脚本、Redission实现分布式锁)、Redis做限流工具(简单限流,限流插件)_第3张图片

a、AutoIdempotent

Redis(四)——Redis 处理接口幂等性(代码实现、两种方式拦截令牌)、Redis 分布式锁(两种引入 lua 脚本、Redission实现分布式锁)、Redis做限流工具(简单限流,限流插件)_第4张图片

b、RedisService

这个类干三件事:
Redis(四)——Redis 处理接口幂等性(代码实现、两种方式拦截令牌)、Redis 分布式锁(两种引入 lua 脚本、Redission实现分布式锁)、Redis做限流工具(简单限流,限流插件)_第5张图片

c、IdempontentException

定义一个异常:
Redis(四)——Redis 处理接口幂等性(代码实现、两种方式拦截令牌)、Redis 分布式锁(两种引入 lua 脚本、Redission实现分布式锁)、Redis做限流工具(简单限流,限流插件)_第6张图片

d、TokenService

Redis(四)——Redis 处理接口幂等性(代码实现、两种方式拦截令牌)、Redis 分布式锁(两种引入 lua 脚本、Redission实现分布式锁)、Redis做限流工具(简单限流,限流插件)_第7张图片

e、接口

Redis(四)——Redis 处理接口幂等性(代码实现、两种方式拦截令牌)、Redis 分布式锁(两种引入 lua 脚本、Redission实现分布式锁)、Redis做限流工具(简单限流,限流插件)_第8张图片

f、两种方式拦截

需要有个地方取拦截令牌并检查,所以需要有拦截器;这里有两种方式创建这个拦截器:

(1)、第一种——IdempotentInterceptor

如果使用第一种方式,则还需要在 config 注册拦截器,看 g 小节。
Redis(四)——Redis 处理接口幂等性(代码实现、两种方式拦截令牌)、Redis 分布式锁(两种引入 lua 脚本、Redission实现分布式锁)、Redis做限流工具(简单限流,限流插件)_第9张图片
截图后面还有两个方法,不用动,默认就行。

(1)、第二种——AOP

首先需要加入 AOP 的依赖:
Redis(四)——Redis 处理接口幂等性(代码实现、两种方式拦截令牌)、Redis 分布式锁(两种引入 lua 脚本、Redission实现分布式锁)、Redis做限流工具(简单限流,限流插件)_第10张图片

然后代码:
Redis(四)——Redis 处理接口幂等性(代码实现、两种方式拦截令牌)、Redis 分布式锁(两种引入 lua 脚本、Redission实现分布式锁)、Redis做限流工具(简单限流,限流插件)_第11张图片

g、config 注册拦截器

如果使用的是第一种拦截器,这里就需要注册(指的是 @Configuration 这个注解); 如果用的是第二种(aop),则这里就不需要开启(只需要注释掉 @Configuration 这个注解即可)。
Redis(四)——Redis 处理接口幂等性(代码实现、两种方式拦截令牌)、Redis 分布式锁(两种引入 lua 脚本、Redission实现分布式锁)、Redis做限流工具(简单限流,限流插件)_第12张图片

3、使用方法

后端接口是写好了,但是前端怎么使用呢?

一般会用在比较显式的地方,比如载入网页,比如刷新网页的时候就已经获得了这个 token,然后在使用 post 请求等等之类的地方的时候就使用掉这个 token。

二、Redis 分布式锁(下面的代码都有用到 博客 Redis(一)中的工具类!)

下面的代码都有用到 博客 Redis(一)中的工具类!

下面会提出个问题,然后一步步深入;看下面的文字和截图一步步看下去即可。

1、问题场景

Redis(四)——Redis 处理接口幂等性(代码实现、两种方式拦截令牌)、Redis 分布式锁(两种引入 lua 脚本、Redission实现分布式锁)、Redis做限流工具(简单限流,限流插件)_第13张图片

2、不完整的代码

这里一点一点展示不完整的代码,主要用作从 0 开始学习使用:

基本用法:
Redis(四)——Redis 处理接口幂等性(代码实现、两种方式拦截令牌)、Redis 分布式锁(两种引入 lua 脚本、Redission实现分布式锁)、Redis做限流工具(简单限流,限流插件)_第14张图片
Redis(四)——Redis 处理接口幂等性(代码实现、两种方式拦截令牌)、Redis 分布式锁(两种引入 lua 脚本、Redission实现分布式锁)、Redis做限流工具(简单限流,限流插件)_第15张图片Redis(四)——Redis 处理接口幂等性(代码实现、两种方式拦截令牌)、Redis 分布式锁(两种引入 lua 脚本、Redission实现分布式锁)、Redis做限流工具(简单限流,限流插件)_第16张图片
Redis(四)——Redis 处理接口幂等性(代码实现、两种方式拦截令牌)、Redis 分布式锁(两种引入 lua 脚本、Redission实现分布式锁)、Redis做限流工具(简单限流,限流插件)_第17张图片

但是上面的代码看似没有问题,其实还是有问题。

看下面的代码:
Redis(四)——Redis 处理接口幂等性(代码实现、两种方式拦截令牌)、Redis 分布式锁(两种引入 lua 脚本、Redission实现分布式锁)、Redis做限流工具(简单限流,限流插件)_第18张图片
但是这样还是有问题,就是超时问题。

3、解决超时问题

Redis(四)——Redis 处理接口幂等性(代码实现、两种方式拦截令牌)、Redis 分布式锁(两种引入 lua 脚本、Redission实现分布式锁)、Redis做限流工具(简单限流,限流插件)_第19张图片
上图中第一个角度:最核心的锁不要锁大片的代码,就跟 mysql 一样不要表锁,要行锁。这样并发性能才能上来。类似于不要锁方法,应该锁最关键的代码就 ok 了。

上图中的第二个角度,代码类似于这样(这里仅是举例子):
Redis(四)——Redis 处理接口幂等性(代码实现、两种方式拦截令牌)、Redis 分布式锁(两种引入 lua 脚本、Redission实现分布式锁)、Redis做限流工具(简单限流,限流插件)_第20张图片
但是上面的代码还是有问题,因为要先获取,然后又要比较,比较完再去释放,很明显不是原子操作,在这过程中依然是有可能因为各种各样的事故导致锁没有被释放。所以是希望这部分操作具有原子性;或者同时成功同时失败。这时就需要引入 lua 脚本。

4、引入 Lua 脚本

a、方式一 ——redis中写好 lua 脚本,java调用(推荐)

Redis(四)——Redis 处理接口幂等性(代码实现、两种方式拦截令牌)、Redis 分布式锁(两种引入 lua 脚本、Redission实现分布式锁)、Redis做限流工具(简单限流,限流插件)_第21张图片

if redis.call("get",KEYS[1])==ARGV[1] then
	return redis.call("del",KEYS[1])
else
	return 0
end

接下来,可以给 Lua 脚本求一个 SHA1 和,命令如下:

redis-cli -a (这里填密码) -x script load < lua/releasewherevalueequal.lua

结果展示:
在这里插入图片描述
然后就在 java 代码里面通过这个字符串去调用 lua 脚本。

在这里插入图片描述

通过 SHA1 校验文件,一样的文件校验的结果是相同的,只要里面有一丁点不一样,那么结果都会不同,所以根据这个性质,可以用来校验文件。

b、方式二——java中写好 lua 脚本,让 redis 调用(不推荐)

也可以通过如下方式生成校验和(不推荐):

SCRIPT LOAD 'if redis.call("get",KEYS[1])==ARGV[1] then return
redis.call("del",KEYS[1]) else return 0 end'
"79b954ef3863314d4ff2324b4a520053b6dcc018"

接下来,在 Java 端调用这个脚本。

注意,如果要调用这里的代码,记得要把 Redis 博客一的工具类也带上。

public class JedisDemo6 {
    public static void main(String[] args) {
        new Redis().execute(jedis -> {
            String value = UUID.randomUUID().toString();
            String k1 = "k1";
            String set = jedis.set(k1, value, new SetParams().nx().ex(5L));
            if (set != null && "ok".equals(set)){
//                成功拿到锁
//                do something
//                如果 value 等于 k1 的值,说明现在的锁是我自己当时上的锁
//                if (value.equals(jedis.get("k1"))){
//                    jedis.del("k1");
//                }
//                通过原子操作释放锁
                jedis.evalsha("b8059ba43af6ffe8bed3db65bac35d452f8115d8",Arrays.asList(k1), Arrays.asList(value));
            }else {
//                没拿到锁。暂停操作。
            }
        });
    }
}

5、Redisson——实现分布式锁的工具

上面的方式还是有点麻烦,不过如果是自己写代码就是上面的方案。

为什么会这么麻烦呢?因为 jedis 不支持直接去调用分布式锁,里面没有相关的 api。

虽然 jedis 不支持,但是有个东西是支持的,那就是 Redisson。

相对于 Jedis 这种原生态的应用,Redisson 对 Redis 请求做了较多的封装,对于锁,也提供了对应的方法可以直接使用:

首先依赖导入:

<dependency>
            <groupId>org.redissongroupId>
            <artifactId>redissonartifactId>
            <version>3.16.8version>
dependency>

接着 java 代码:

Config config = new Config();

        //配置 Redis 基本连接信息
        config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("123");

        //获取一个 RedissonClient 对象
        RedissonClient redisson = Redisson.create(config);

        //获取一个锁对象实例
        RLock lock = redisson.getLock("lock");
        try {
            //获取锁
            boolean b = lock.tryLock(500, 1000, TimeUnit.MILLISECONDS);
            if (b) {
                //获取到锁了,开始写业务
                RBucket<Object> bucket = redisson.getBucket("javaboy");
                bucket.set("www.javaboy.org");
                Object o = bucket.get();
                System.out.println(o);
            } else {
                System.out.println("没拿到锁");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放锁
            lock.unlock();
        }

在这段代码中,核心的就是 lock.tryLock(500, 1000, TimeUnit.MILLISECONDS); ,第一个参数是尝试加锁的等待时间为 500 毫秒,第二个参数表示锁的超时时间为 1000 毫秒,也就是这个锁在 1000毫秒后会自动失效。

我们可以发现,这和我们在方案一里边配置的参数是一样的,其实思路是不变的,Redisson 只不过是将我们写的和锁相关的方法封装起来了而已。

三、Redis 做限流工具

如果是传统的企业级后台,一般流量是不会很大的。如果是互联网应用,那基本会涉及到高并发,大数据这种;那么这种时候,就需要限流了。

比如做秒杀,搞活动,那么那天的流量就会非常大,如果所有流量一起涌过来,那么肯定一下处理不了这么多。

比如秒杀,100件商品,涌进来 1000 个请求,最简单的想法就是挨个进来取走商品,等到商品没有了再返回。其实没必要用这种方式。我们可以就让 100 个请求通过就行,剩下的 900 个就直接告诉它们秒杀失败。

因为是很常见的现象,所以限流有很多工具可用,包括后面会讲到的 Spring Cloud 里面就有限流工具。

redis 做限流可以自己写,也可以使用插件来限流。

1、预备知识

Pipeline(管道)本质上是由客户端提供的一种操作。Pipeline 通过调整指令列表的读写顺序,可以大幅度的节省 IO 时间,提高效率。

2、简单限流(参考理解)

为了方便理解下面的代码,这里贴出一幅图,这幅图的含义就是下面代码要执行的思想:
Redis(四)——Redis 处理接口幂等性(代码实现、两种方式拦截令牌)、Redis 分布式锁(两种引入 lua 脚本、Redission实现分布式锁)、Redis做限流工具(简单限流,限流插件)_第22张图片

public class RateLimiter {
	private Jedis jedis;

    public RateLimiter(Jedis jedis) {
        this.jedis = jedis;
    }

    /**
     * @param user     调用者的名称(单纯只是为了多人调用限流方法时不冲突)
     * @param action   具体的操作
     * @param period   这是一个周期,即一个时间窗,单位为秒
     * @param maxCount 在一个时间窗内,可以处理的方法/请求数量
     * @return 		   当前请求/方法是否被允许执行
     */
    public boolean isAllowed(String user, String action, Integer period, Integer maxCount) {
         //根据 user 和 action 生成 key
        String key = user + "-" + action;
         //Redis 中的管道操作
        Pipeline pipelined = jedis.pipelined();
        pipelined.multi();
         //获取当前时间戳
        long nowTime = System.currentTimeMillis();
        System.out.println("nowTime = " + nowTime);
         //移除当前时间窗之外的 key
        pipelined.zremrangeByScore(key, 0, nowTime - period * 1000);
        System.out.println("nowTime-period*1000 = " + (nowTime - period * 1000));
         //统计当前时间窗内有多少 key
        Response<Long> response = pipelined.zcard(key);
         //提交管道中的命令
        pipelined.exec();
         //关闭管道
        pipelined.close();
         //要求时间窗内的请求数量少于要求的数量
        boolean b = response.get() < maxCount;
        // 如果小于,那么说明还能放些请求进来;否则就是不能再放请求进来。
        if (b) {
             //先将当前操作添加进来
            jedis.zadd(key, nowTime, String.valueOf(nowTime));
        }
        return b;
    }
}
public static void main(String[] args) {
        new Redis().execute(jedis -> {
            RateLimiter rateLimiter = new RateLimiter(jedis);
            for (int i = 0; i < 10; i++) {
                System.out.println("rateLimiter.isAllowed(\"zhangsan\",
                        \"publish\", 5, 3) = " + rateLimiter.isAllowed("zhangsan", "publish", 5, 1));
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }

3、限流插件——Redis-Cell模块(使用了漏斗算法)

a、安装插件

Redis4.0 开始提供了一个 Redis-Cell 模块,这个模块使用漏斗算法,提供了一个非常好用的限流指令。漏斗算法就像名字一样,是一个漏斗,请求从漏斗的大口进,然后从小口出进入到系统中,这样,无论是多大的访问量,最终进入到系统中的请求,都是固定的。

使用漏斗算法,需要我们首先安装 Redis-Cell 模块(但是一般不建议下载最新的):
http://https//github.com/brandur/redis-cell

安装步骤(可以参考用这里的。如果安装有问题,去下载下面这个版本的压缩包即可。):

wget https://github.com/brandur/redis-cell/releases/download/v0.2.4/redis-cellv0.2.4-x86_64-unknown-linux-gnu.tar.gz

tar -zxvf redis-cell-v0.2.4-x86_64-unknown-linux-gnu.tar.gz

mkdir redis-cell

mv libredis_cell.d ./redis-cell

mv libredis_cell.so ./redis-cell

接下来修改 redis.conf 文件,加载额外的模块:
Redis(四)——Redis 处理接口幂等性(代码实现、两种方式拦截令牌)、Redis 分布式锁(两种引入 lua 脚本、Redission实现分布式锁)、Redis做限流工具(简单限流,限流插件)_第23张图片

loadmodule /root/redis-5.0.7/redis-cell/libredis_cell.so

然后,启动 Redis:
在这里插入图片描述
redis 启动成功后,如果存在 CL.THROTTLE 命令(控制台输入这个指令,如果后面有提示,说明安装成功),说明 redis-cell 已经安装成功了。

b、CL.THROTTLE 命令的五个参数和五个返回值的讲解

漏斗算法有一个限流的桶。

CL.THROTTLE 命令一共有五个参数:

  1. 第一个参数是 key
  2. 第二个参数是令牌桶容量
  3. 令牌产生个数
  4. 令牌产生时间
  5. 本次取走的令牌数

比如产生 50 个,需要 60 秒,设置好后会均匀固定的 60 秒内放入 50 个令牌。如果请求进来能拿到令牌,说明这个令牌是被允许的,如果没有令牌了,说明请求已经超过了限流的要求了。

执行完成后,返回值也有五个:

  1. 第一个 0 表示允许,1表示拒绝
  2. 第二个参数是令牌桶的容量
  3. 第三个参数是当前桶内剩余的令牌数
  4. 失败时表还需要等待多少秒可以有足够的令牌
  5. 表预计多少秒后令牌桶会满

使用展示:
Redis(四)——Redis 处理接口幂等性(代码实现、两种方式拦截令牌)、Redis 分布式锁(两种引入 lua 脚本、Redission实现分布式锁)、Redis做限流工具(简单限流,限流插件)_第24张图片

第四个参数是 -1,意思是没有失败的意思。

4、java 代码操作(使用 Lettuce 扩展)

Redis-Cell 没有提供依赖,所以导致 jedis 调用不了 Redis-Cell。redis 也是装了插件才能用得了。

但是可以自己自定义操作。Lettuce 里面提供了方式,可以快速自定义命令。

首先定义一个命令接口:
Redis(四)——Redis 处理接口幂等性(代码实现、两种方式拦截令牌)、Redis 分布式锁(两种引入 lua 脚本、Redission实现分布式锁)、Redis做限流工具(简单限流,限流插件)_第25张图片
定义完成后,接下来,直接调用即可:
Redis(四)——Redis 处理接口幂等性(代码实现、两种方式拦截令牌)、Redis 分布式锁(两种引入 lua 脚本、Redission实现分布式锁)、Redis做限流工具(简单限流,限流插件)_第26张图片
打印效果:
在这里插入图片描述

如果以后遇到没有学过的 redis 的插件,java代码这里又没有提供对应的 jar 包或者依赖之类的,就通过 Lettuce 快速自定义命令。

你可能感兴趣的:(Java,redis,java,linux,redis,缓存)