Redis详解

目录:Redis


1 Redis简介

应用场景:缓存、分布式锁、限流

特点

  • 数据持久化
  • 多种不同数据结构类型之间的映射
  • 支持主从模式的数据备份
  • 自带了发布订阅系统
  • 定时器、计数器

1.部分常用命令

# 删除所有key
flushdb

# windows打开Redis
redis-server redis.windows.conf

# windows操作redis
redis-cli

# 清空Redis
FLUSHALL

2 Redis安装

1.直接编译安装(推荐使用)

提前准备好GC环境

yum install gcc-c++

下载并安装Redis

wget http://download.redis.io/releases/redis-5.0.7.tar.gz
tar -zxvf redis-5.0.7.tar.gz
cd redis-5.0.7/
make
make install

安装完成后,启动 Redis

redis-server redis.conf

启动成功页面如下:
在这里插入图片描述

2.使用Docker

Docker安装好之后,启动Docker

docker run --name redis_name -d -p 6379:6379 redis --requirepass 123

Docker 上的 Redis 启动成功之后,可以从宿主机上连接(前提是宿主机上存在 redis-cli):

redis-cli -a 123

如果宿主机上没有安装 Redis,那么也可以进入到 Docker 容器种去操作 Redis:

docker exec -it javaboy-redis redis-cli -a 123

3.直接安装

CentOS:

yum install redis

Ubuntu:

apt-get install redis

Mac:

brew install redis

4.通过在此案体验,可以直接使用Redis功能

在线体验

5.后台启动

首先,修改 redis.conf 配置文件
在这里插入图片描述

配置完成后,保存退出,再次通过 redis-server redis.conf 命令启动 Redis,此时,就是在后台启
动了。
在这里插入图片描述

3 命令

1.对所有基本数据类型key的操作

1.DEL删除key

DEL key [key ...]

2.序列化给定的key

DUMP key

在这里插入图片描述

3.EXISTS判断key是否存在

EXISTS key [key ...]

4.TTL/PTTL查看key的过期时间秒/毫秒

-1永不过期 -2已过期

TTL key
PTTL key

5.EXPIRE设置key的过期时间

如果key在过期时间被重新set了,那么过期时间会失效。

EXPIRE key seconds

6.PERSIST移除一个key的过期时间

PERSIST key

7.KEYS查看所有key

KEYS pattern

在这里插入图片描述

2.对基本数据类型value的操作

  • 四种数据类型(list/set/zset/hash),在第一次使用的时候,如果容器不存在,就自动创建一个。
  • 四种数据类型(list/set/zset/hash),如果里面没有元素了,那么立即删除容器,释放内存。

1.String

String是Redis中最简单的数据接口,在Redis中,所有的key都是字符串。但是不同的key对应的value则具备不同的数据结构,我们所说的五种不同的数据类型,主要是value的数据类型不同。

Redis中的字符串是动态字符串,内部是可以修改的,像Java中的StringBuffer,它采用分配冗余空间的方式来减少内存的频繁分配。在Redis内部结构中,一般实际分配的内存会大于需要的内存,当字符串小于1M的时候,扩容都是在现有的空间基础上加倍,扩容每次扩1M空间,最大可存512M。

1.String基本操作
1.set/get key的赋值/获取
# 给key赋值
set key value [EX seconds] [PX milliseconds] [NX|XX]
# 获取key对应的value
get key

在这里插入图片描述

2.MGET/MSET 批量获取/存储
MGET key [key ...]
MSET key value [key value ...]

在这里插入图片描述

3.SETNX 设置不存在key的value
# 默认情况下,set命令会覆盖已经存在的key,SETNX则不会(要在key不存在的时候设置)
SETNX key value

在这里插入图片描述

4.MSETNX 批量设置不存在key的value
# 默认情况下,set命令会覆盖已经存在的key,SETNX则不会(要在key不存在的时候设置)
MSETNX key value [key value ...]
5.GETRANGE 获取子串
# 得到key对应的value子串,类似Java里面的substring。start end是起始和终止位置,end=-1表示最后一个字符串,-2表示倒数第二个字符串。
GETRANGE key start end

在这里插入图片描述

6.SETRANGE 按照范围覆盖一个已经存在key的value
# 按照范围覆盖一个已经存在key的value,不足offset的话,用0补齐
SETRANGE key offset value

在这里插入图片描述

7.GETSET 获取并更新一个key
# 获取并更新一个key
GETSET key value

在这里插入图片描述

8.APPEND 追加
# 使用append命令时,如果key已经存在,则直接在对应的value后追加值,否则就创建新的键值对。
APPEND key value

在这里插入图片描述

9.DECR/INCR value加/减n
# 实现value的-1操作,前提是value是一个数字。
# value不是数字就会报错
# value不存在,就会给一个默认值为0,在默认值的基础上-1
DECR key
INCR key

在这里插入图片描述

10.DECRBY/INCRBY value加/减n
# 和DECR类似,但是可以自己设置步长(decrement)
DECRBY key decrement
INCRBY key increment
# 可以浮点数
INCRBYFLOAT key increment

在这里插入图片描述
在这里插入图片描述

11.TTL查看key的有效期
# 查看key的有效期,-1永远不过期,-2没有这个key
TTL key

在这里插入图片描述

12.SETEX/PSETEX 给key设置value和过期时间秒/毫秒
# 给key设置value的同时还设置过期时间,单位秒
SETEX key seconds value
# 给key设置value的同时还设置过期时间,单位毫秒
PSETEX key milliseconds value

在这里插入图片描述

13.STRLEN 查看字符串长度
# 查看字符串长度
STRLEN key
2.String(BIT)二进制

在Redis中,String都是以二进制的方式来存储的。例如set k1 a,a对应的ASCII码是97,97转为二进制是01100001,BIT相关命令就是对二进制操作的。

1.GETBIT获取key对应的value在offset处的value值
# 获取key对应的value在offset处的value值
# 01100001  GETBIT key 0指的是第一位是0
GETBIT key offset
2.SETBIT获取key对应的value在offset处的value值
# 设置key对应的value在offset处的value值
# 01100001  SETBIT key 0指的是第一位是0
SETBIT key offset
3.BITCOUNT统计二进制数中1的个数
# 统计二进制数中1的个数,[start end]指的是数本身不是二进制的索引位置
BITCOUNT key [start end]

2.List

1.LPUSH /RPUSH从左至右/右至左依次插入表头位置
LPUSH key value [value ...]

在这里插入图片描述

2.LRANGE返回列表指定区间的元素
LRANGE key start stop

在这里插入图片描述

3.RPOP/LPOP移除并返回列表的尾/头元素
RPOP key

在这里插入图片描述

4.LINDEX返回列表中,下标为index的元素
LINDEX key index

在这里插入图片描述

5.LTRIM对一个列表进行修剪
LTRIM key start stop

在这里插入图片描述

6.blpop阻塞式弹出,相当于lpop阻塞版

3.Set

Set中的元素不能重复

1.sadd添加元素到一个key中
sadd key member [member ...]

在这里插入图片描述

2.SMEMBERS获取一个key中的所有元素
SMEMBERS key

在这里插入图片描述

3.SREM移除指定的元素
SREM key member [member ...]

在这里插入图片描述

4.某一个成员是否在集合中
SISMEMBER key member

在这里插入图片描述

5.返回集合的数量
SCARD key

在这里插入图片描述

6.SRANDMEMBER随机返回元素不会出栈
SRANDMEMBER key [count]

在这里插入图片描述

7.SPOP随机返回并且出栈一个元素
SPOP key [count]

在这里插入图片描述

8.把一个元素从一个集合移到另一个集合
SMOVE source destination member

在这里插入图片描述

9.SDIFF返回两个集合的差集
SDIFF key [key ...]

在这里插入图片描述

10.SDIFFSTORE返回两个集合的差集保存至新集合
SDIFFSTORE destination key [key ...]

在这里插入图片描述

11.SINTER返回两个集合的交集
SINTER key [key ...]

在这里插入图片描述

12.SINTERSTORE返回两个集合的交集保存至新集合
SINTERSTORE destination key [key ...]
13.SUNION返回两个集合的并集
SUNION key [key ...]
14.SINTERSTORE返回两个集合的并集保存至新集合
SUNIONSTORE destination key [key ...]

4.Hash

在Hash结构中,key是一个字符串,value是一个key/value键值对。

1.HSET添加值
HSET key field value

在这里插入图片描述

2.HGET获取key
HGET key field

在这里插入图片描述

3.HMSET批量添加值
HMSET key value [key value ...]

在这里插入图片描述

4.HMGET批量获取key
HMGET key [key ...]

在这里插入图片描述

5.删除一个指定的field
HDEL key field [field ...]

在这里插入图片描述

6.HSETNX如果key和value相同,不会覆盖已有的value
HSETNX key field value

在这里插入图片描述

7.HVALS获取所有的value
HVALS key

在这里插入图片描述

8.HKEYS获取所有的key
HKEYS key

在这里插入图片描述

9.HGETALL同时获取kuy和value
HGETALL key

在这里插入图片描述

10.HEXISTS返回field是否存在
HEXISTS key field

在这里插入图片描述

11.HINCRBY给指定的value自增n
HINCRBY key field increment
12.HINCRBYFLOAT给指定的value自增浮点数
HINCRBYFLOAT key field increment
13.HLEN返回某一个key中value的数量
HLEN key
14.返回某一个key中的某一个field的字符串长度
HSTRLEN key field

5.ZSet

1.ZADD将指定元素添加到有序集合中
ZADD key [NX|XX] [CH] [INCR] score member [score member ...]

在这里插入图片描述

2.返回member的score值
ZSCORE key member

在这里插入图片描述

3.ZRANGE返回集合中的一组元素
ZRANGE key start stop [WITHSCORES]

在这里插入图片描述

4.ZRANGE倒序返回集合中的一组元素
ZREVRANGE key start stop [WITHSCORES]

在这里插入图片描述

5.返回元素个数
ZCARD key

在这里插入图片描述

6.ZCOUNT按照score的范围返回元素的个数
ZCOUNT key min max

在这里插入图片描述

7.ZRANGEBYSCORE按照score的范围返回元素
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]

在这里插入图片描述

8.ZRANK从小到大返回元素的排名
ZRANK key member

在这里插入图片描述

9.ZREVRANK从大到小返回元素的排名
ZREVRANK key member

在这里插入图片描述

10.ZINCRBY score自增
ZINCRBY key increment member
11.ZINTERSTORE两个集合的交集放入新集合
ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX]

在这里插入图片描述

12.弹出一个元素
ZREM key member [member ...]

在这里插入图片描述

13.ZLEXCOUNT返回有序集合中的成员数量
ZLEXCOUNT key min max

在这里插入图片描述

14.ZRANGEBYLEX返回有序集合中的成员
ZRANGEBYLEX key min max [LIMIT offset count]

在这里插入图片描述

4 Java 客户端

1.开启远程连接

Redis默认不支持远程连接,需要手动开启。

  • 注释掉bind 127.0.0.1
  • 开启密码校验,# requirepass foobared改为requirepass 123456

2.Jedis 连接 Redis

1、创建普通的maven工程
2、添加Jedis依赖

        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.2.0</version>
            <type>jar</type>
            <scope>compile</scope>
        </dependency>

3、测试是否连接成功

    public static void main(String[] args) {
        // 1、构造一个Jedis对象
        Jedis jedis = new Jedis("192.168.41.133", 6379);
        // 2.密码认证
//        jedis.auth("123456");
        // 3.测试是否连接成功
        String ping = jedis.ping();
        System.out.println(ping);
        jedis.set("k1","1");
        jedis.incr("k1");
        String k1 = jedis.get("k1");
        System.out.println(k1);
    }

3.Jedis 连接池优化

在实际应用中,Jedis实例我们一般都是通过连接池来获取,由于Jedis对象不是线程安全的,所以,当我们使用Jedis对象时,从连接池获取Jedis,使用完成之后,再还给连接池。

    public static void main(String[] args) {
        JedisPool jedisPool=null;
            // 1、创建一个Jedis连接池
            jedisPool = new JedisPool("192.168.41.133", 6379);
            // 2、从连接池中获取一个Jedis连接
            Jedis jedis = jedisPool.getResource();
        try {
            // 3、Jedis操作
            String ping = jedis.ping();
            System.out.println(ping);
            jedis.set("k1","1");
            jedis.incr("k1");
            String k1 = jedis.get("k1");
            System.out.println(k1);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            if(jedisPool!=null){
                // 4.归还连接
                jedisPool.close();
            }
        }
    }

利用JDK7中的try-with-resource特性,可以对上面的代码进行改造:

    public static void main(String[] args) {
        // 1、创建一个Jedis连接池
        JedisPool jedisPool = new JedisPool("192.168.41.133", 6379);
        // 2、从连接池中获取一个Jedis连接
        try(Jedis jedis = jedisPool.getResource()) {
            // 3、Jedis操作
            String ping = jedis.ping();
            System.out.println(ping);
            jedis.set("k1","1");
            jedis.incr("k1");
            String k1 = jedis.get("k1");
            System.out.println(k1);
        }
    }

但是上述代码无法实现强约束,我们可以写一个接口。

public interface CallwithJedis {
    void call(Jedis jedis);
}
public class RedisUtil {
    private JedisPool jedisPool;

    public RedisUtil(){
        // "对象池",即通过一定的规则来维护对象集合的容器
        GenericObjectPoolConfig config = new GenericObjectPoolConfig();
        // 连接池最大空闲书
        config.setMaxIdle(300);
        // 连接池最大连接数
        config.setMaxTotal(1000);
        // 连接时最大等待时间,如果是-1,表示没有限制
        config.setMaxWaitMillis(3000);
        // 在空闲时间检查有效性
        config.setTestOnBorrow(true);
        /**
         * 1、Redis地址
         * 2、Redis端口
         * 3、连接超时时间
         * 4、密码
         */
        jedisPool=new JedisPool(config,"192.168.41.133",30000);
    }
    public void execute(CallwithJedis callWithJedis){
        try(Jedis jedis = jedisPool.getResource()) {
            callWithJedis.call(jedis);
        }
    }
}
    public static void main(String[] args) {
        RedisUtil redis = new RedisUtil();
        redis.execute(jedis->{
            jedis.ping();
            jedis.set("k1","3");
            String k1 = jedis.get("k1");
            System.out.println(k1);
        });
    }

4.Lettuce

Lettuce和Jedis的比较:

  • Jedis在实现过程中是直接连接Redis的,在多个线程之间共享一个Jedis实例,这是线程不安全的,如果想在多线程场景下使用Jedis,就得使用连接池,这样每个线程都有自己的Jedis实例。
  • Lettuce基于Netty NIO框架来构建,所以克服了Jedis中线程不安全的问题,Lettuce支持同步、异步以及响应式调用,多个线程可以共享一个连接实例。

使用Lettuce:
1、创建一个普通的maven工程
2、添加依赖

        <dependency>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
            <version>6.1.2.RELEASE</version>
        </dependency>

3、测试

    public static void main(String[] args) {
        RedisClient redisClient = RedisClient.create("reids://'123'@192.168.41.133");
        StatefulRedisConnection<String, String> connect = redisClient.connect();
        RedisCommands<String, String> sync = connect.sync();
        sync.set("k1","3");
        String k1 = sync.get("k1");
        System.out.println(k1);
    }

5 使用场景

1.分布式锁

1.场景

一个简单的用户操作,一个线程去修改用户的状态,首先从数据库中读出用户的状态,然后在内存中进行修改,修改完成后,再存回去,在单线程中,这个操作没有问题,但是在多线程中,由于读取、修改、存,这是三个操作,不是原子操作,所以在多线程中,这样会出问题。
对于这种问题可以使用分布式锁限制程序的并发执行。

2.实现思路

进来一个线程,先占位,当别的线程来操作时,发现已经有人占位了,就会放弃或者稍后再试。

1.方案1:setnx指令

在Redis中,占位一般使用setnx指令,先进来的线程先占位,线程的操作执行完后,再调用del指令释放位置。

    public static void main(String[] args) {
        RedisUtil redis = new RedisUtil();
        redis.execute(jedis->{
            Long setnx = jedis.setnx("k1", "v1");
            if(setnx==1){
                // 没人占位
                System.out.println("可以使用...");
                jedis.set("name", "张三");
                System.out.println(jedis.get("name"));
                // 释放资源
                jedis.del("k1");
            }else {
                // 有人占位,停止/暂缓操作
                System.out.println("正在使用中...");
            }
        });
    }
2.方案2:给锁添加过期时间

上面代码的问题:如果代码业务执行的过程中抛异常或者挂了,这样导致del指令没有被调用,这样k1无法释放,后来的请求全部堵塞,锁也得不到释放。
要解决这个问题,我们可以给锁添加一个过期时间,确保锁在一定的时间之后,能够得到释放。

    public static void main(String[] args) {
        RedisUtil redis = new RedisUtil();
        redis.execute(jedis->{
            Long setnx = jedis.setnx("k1", "v1");
            if(setnx==1){
                // 给锁添加一个过期时间,如果代码业务执行的过程中抛异常或者挂了,这样导致del指令没有被调用,释放k1
                jedis.expire("k1",5);
                // 没人占位
                System.out.println("可以使用...");
                jedis.set("name", "张三");
                System.out.println(jedis.get("name"));
                // 释放资源
                jedis.del("k1");
            }else {
                // 有人占位,停止/暂缓操作
                System.out.println("正在使用中...");
            }
        });
    }
3.方案3:同时执行setnx和expire

这样改造后,还有一个问题,在获取锁和设置过期之间,如果服务器突然挂掉了,这时候锁被占用无法得到释放,也会造成死锁,因为获取锁和设置过期时间是两个操作,不具备原子性。
为了解决这个问题,从Redis2.8开始,setnx和expire可以通过一个命令一起来执行。

    public static void main(String[] args) {
        RedisUtil redis = new RedisUtil();
        redis.execute(jedis->{
            String set = jedis.set("k1", "v1", new SetParams().nx().ex(5));
            if(set!=null&&set.equals("OK")){
                // 给锁添加一个过期时间,如果代码业务执行的过程中抛异常或者挂了,这样导致del指令没有被调用,释放k1
                jedis.expire("k1",5);
                // 没人占位
                System.out.println("可以使用...");
                jedis.set("name", "张三");
                System.out.println(jedis.get("name"));
                // 释放资源
                jedis.del("k1");
            }else {
                // 有人占位,停止/暂缓操作
                System.out.println("正在使用中...");
            }
        });
    }
4.方案4:Lua脚本

为了防止业务代码在执行的时候抛出异常,我们给每一个锁添加了一个超时时间,超时之后,锁会被自动释放,但是这也带来了一个新的问题,如果执行业务非常好使,可能会出现紊乱。
例如:第一个线程首先获取到锁,然后开始执行业务代码,但是业务代码比较耗时,执行了8s,这样会在第一个线程的任务还未执行成功锁就会被释放了,此时第二个线程会获取到锁,在第二个线程执行了3s,第一个线程也执行完了,此时释放的是第二个线程的锁,释放之后,第三个线程进来。

两个角度入手解决这个问题:

  • 尽量避免在获取锁之后执行耗时操作。
  • 可以将锁的value设置为一个随机字符串,每次释放锁的时候,都去比较随机数是否一致,如果一致,再去释放,否则,不释放。

由于释放锁的时候,一是要去查看锁的value,第二个比较value的值是否正确,第三步释放锁,这三个步骤不具备原子性,为了解决这个问题,引入Lua脚本。

Lua脚本的优势

  • 使用方便,Redis中内置Lua脚本的支持
  • Lua脚本可以在Redis服务端原子的执行多个Redis命令
  • 由于网络在很大程度上会影响Redis性能,而使用Lua脚本可以让多个命令一次执行,可以有效解决网络给Redis带来的性能问题。

在Redis中使用Lua脚本的两种思路:

  • 提前在Redis服务端写好脚本,然后在Java客户端去调用脚本(推荐)
  • 可以直接在Java端写Lua脚本,写好后需要执行时,每次将脚本发送到Redis上去执行
1.方案1Lua脚本在Redis:evalsha

1、编写Lua脚本

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

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

cat release1.1.lua | redis-cli -a ll script load --pipe

script load会在Redis中缓存Lua脚本,并返回脚本内容的SHA1校验和,然后在Java端调用时,时传入SHA1校验和作为参数,这样Redis服务端就知道执行哪个脚本了。
在这里插入图片描述3.测试的代码

    public static void main(String[] args) {
        RedisUtil redis = new RedisUtil();
        redis.execute(jedis -> {
            // 1、随机获取一个字符串
            String value = UUID.randomUUID().toString();
            // 2、获取锁
            String k1 = jedis.set("k1", value, new SetParams().nx().ex(5));
            // 3、判断是否成功拿到锁
            if(k1!=null&&k1.equals("OK")){
                // 4、具体业务操作
                jedis.set("k2","hhhh");
                System.out.println(jedis.get("k2"));
                // 5、释放锁
                jedis.evalsha("b8059ba43af6ffe8bed3db65bac35d452f8115d8", Arrays.asList("k1"),Arrays.asList(value));
            }else {
                System.out.println("没拿到锁");
            }
        });
    }
2.方案1Lua脚本在Java:eval
                // 5、释放锁
                String var1="if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
                        "    return redis.call(\"del\",KEYS[1])\n" +
                        "else\n" +
                        "    return 0\n" +
                        "end";
                List<String> var2 = Arrays.asList("k1");
                List<String> var3 = Arrays.asList(value);
                jedis.eval(var1,var2,var3);

2.消息队列

1.场景

我们不需要使用专业的消息中间件的时候,例如只有一个消息队列,只有一个消费者,就没有必要用专业的消息中间件(RabbitMQ、RocketMQ、ActiveMQ、Kalfka),这种情况我们可以使用Redis做消息队列。
Redis的消息队列不是特别专业,没有很多高级特性,适用简单场景,如果对于消息可靠性有极高的追求,不适合使用Redis做消息队列。

2.普通消息队列实现思路

Redis做消息队列,List数据结构就可以实现,我们可以使用lpush/rpush操作来实现入队,然后使用lpop/rpop来实现出队。
在客户端,我们会维护一个死循环来不停的从队列中读取到消息,并处理,如果队列中有消息,则直接获取到,如果没有消息,就会陷入死循环,直到下一次有消息进入,这种死循环会造成大量的资源浪费,这时候。可以使用blpop/brpop设置一个阻塞等待的超时时间,超时之后就报一个错,如果有新的消息进来,就会唤醒。

3.延迟消息队列

可以通过zset来实现,因为zset里面有一个score,我们可以把时间作为score,将value存到redis中,然后通过轮询的方式,去不断读取消息出来。
如果消息是字符串,直接发送即可,如果是一个对象,则需要对对象进行序列化,这里我们使用JSON来实现序列化和反序列化。

1.添加JSON依赖
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.11.4</version>
        </dependency>
2.构造消息对象
public class Message {
    private String id;
    private Object data;
}
3.构造延迟消息队列
public class DelayMessageQueue {
    private Jedis jedis;
    private String queue;

    public DelayMessageQueue(Jedis jedis, String queue) {
        this.jedis = jedis;
        this.queue = queue;
    }
    // 消息入队,data要发送的消息
    public void queue(Object data){
        Message message = new Message();
        message.setId(UUID.randomUUID().toString());
        message.setData(data);
        // 序列化
        String s = null;
        try {
            s = new ObjectMapper().writeValueAsString(message);
            System.out.println(new Date()+"  message 准备发送");
            // 消息发送,延迟5s
            jedis.zadd(queue, System.currentTimeMillis() + 5000, s);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
    }

    // 消息消费
    public void loop(){
        // 只要当前线程未打断
        while (!Thread.interrupted()){
            // 读取score在0到当前时间戳的消息
            Set<String> zrange = jedis.zrangeByScore(queue, 0, System.currentTimeMillis(), 0, 1);
            // 没有读到消息
            if(zrange.isEmpty()){
                try {
                    // 休息500ms再继续
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    break;
                }
                continue;
            }
            // 如果读到了消息,则直接加载
            String next = zrange.iterator().next();
            if(jedis.zrem(queue,next)>0){
                // 抢到了
                try {
                    Message message = new ObjectMapper().readValue(next, Message.class);
                    System.out.println(new Date()+"  收到了信息  "+message);
                } catch (JsonProcessingException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
4.测试方法
public class MessageTest {
    public static void main(String[] args) {
        // 读取消息
        RedisUtil redis = new RedisUtil();
        redis.execute(jedis -> {
            // 创建一个消息队列
            DelayMessageQueue messageQueue = new DelayMessageQueue(jedis, "ll-queue");
            // 消息的生产者
            Thread producer = new Thread(){
                @Override
                public void run() {
                    for (int i = 0; i < 5; i++) {
                        messageQueue.queue("消息队列  "+i);
                    }
                }
            };
            // 消息的消费者
            Thread consumer = new Thread(){
                @Override
                public void run() {
                    messageQueue.loop();
                }
            };

            // 启动
            producer.start();
            consumer.start();
            // 休息7s,停止程序
            try {
                Thread.sleep(7000);
                consumer.interrupt();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }
}
5.测试结果

在这里插入图片描述

3.操作位图

1.场景

用户一年的签到记录(包括签到时间),如果用String类型来存储,那就需要365个key/value,操作起来麻烦。通过位图可以有效地简化这个操作。

2.实现思路

它的统计很简单,用0和1代表是否签到。每天的记录占一个位,365天就是365个位,大概46个字节,这样可以有效的节省存储空间,如果有一天要统计童虎一共签到了多少天,统计1的个数即可。
对于位图的操作,可以直接操作对应的字符串(get/set),可以直接操作位(getbit/setbit)。

3.基本操作

1.整存零取(存位,获取字符串)

存储一个Java字符串:

字符ASCII二进制
J7401001010
a9701100001
v11801110110
SETBIT name 1 1
SETBIT name 4 1
SETBIT name 6 1
GET name
SETBIT name 9 1
SETBIT name 10 1
SETBIT name 15 1
GET name
SETBIT name 17 1
SETBIT name 18 1
SETBIT name 19 1
SETBIT name 21 1
SETBIT name 22 1
GET name
SETBIT name 24 0
SETBIT name 25 1
SETBIT name 26 1
SETBIT name 31 1
GET name
2.零存整取(存字符串,获取位)
set k1 Java

4.统计

1.统计签到次数
# start end统计的起始位置,指的是字符的起始位置,而不是位的起始位置
# 例如BITCOUNT Java 0 0,是统计J里面有多少个1
BITCOUNT key [start end]
2.统计用户从哪一天开始签到的
# 在指定范围内出现的第一个1或0的位置
BITPOS key bit [start] [end]

5.Bit批处理

在Redis3.2之后,新加了一个功能bitfield,可以对bit进行批量操作。

BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SA
1.get
# BITFIELD name get u4 0
# 获取name中的位,从0开始获取4个位,返回一个无符号的数字
# u:无符号  i:有符号,第一个符号表示符号位,1表示负数

在这里插入图片描述

2.set
set

BITFIELD name set u8 8 98
# 用无符号的98转为8位二进制代替从第8位开始接下来的8位数字

在这里插入图片描述

3.INCRBY

对指定范围进行自增,可能出现向上溢出也可能向下溢出。Redis的处理方案是折返,8位无符号数855,+1溢出变为0;8位有符号数,+1变为-128。
也可以修改默认的溢出策略,改为fail,表示执行失败。
在这里插入图片描述

sat留在最大/最小值:
在这里插入图片描述

4.HyperLogLog

1.场景

一般我们评估一个网站的访问量,有几个主要的参数:

  • pv:Page View,网页的浏览量
  • uv:User View,访问的用户,有多少用户访问

pv或uv的统计,可以自己来做,也可以借助一些第三方的工具,比如cnzz、友盟等。

2.实现思路

pv:可以通过Redis的计数器来实现
uv:uv会涉及到去重,一个用户一天进了十次,那也只算一个用户,需要去重。

我们首先需要在前端给每一个用户生成一个唯一的id,无论是登录用户还是未登录用户,都要有一个唯一的id,这个id伴随着请求一起到达后端,在后端我们通过set集合的sadd命令来存储这个id,最后通过scard统计集合的大小,进而得出uv数据。

如果是千万级别的uv,需要的存储空间就非常惊人,像uv统计,一般也不需要特别精确,800w的uv和803w的uv,其实差别不大。

HyperLogLog提供了一套不怎么精确但是够用的去重方案,会有误差,官方给出的误差数据是0.81,这个精确度,统计uv够用了。

HyperLogLog提供了两个命令:pfadd和pfcount。

# 添加过程中自动去重
PFADD key element [element ...]
PFCOUNT key [key ...]

3.实现

数据量小会精确
在这里插入图片描述

数据量大会误差,理论值是1001,实际输出的值是993,有误差但是在可以接受的范围内

    public static void main(String[] args) {
        RedisUtil redis = new RedisUtil();
        redis.execute(jedis -> {
            for (int i = 0; i < 1000; i++) {
                jedis.pfadd("uv","u"+i,"u"+(i+1));
            }
            // 理论值是1001,实际输出的值是993
            System.out.println(jedis.pfcount("uv"));
        });
    }

合并多个统计结果,在合并过程中,会自动去重多个集合中重复的元素。

PFMERGE destkey sourcekey [sourcekey ...]

在这里插入图片描述

5.布隆过滤器(Bloom Filter)

1.场景

HyperLogLog用来估数,有偏差但也够用。HyperLogLog没有判断是否包含的方法,但是我们有这样的业务需求。

例如刷今日头条,推送的内容有相似的,但是没有重复的,这就涉及到如何在推送的时候去重。

解决方法很多,例如将用户的浏览历史记录下来,然后每次推送都去比较该条消息是否已经给用户推送了,但是这种方式效率极低,不推荐。

解决这个问题,就需要靠布隆过滤器。布隆过滤器不一定只能在Redis上使用。

典型应用场景:新闻推送、解决Redis穿透(缓存击穿)问题

2.实现思路

使用布隆过滤器不会像使用缓存那么浪费空间,但是它也有一个问题:不太精确。越精确,越占空间。

布隆过滤器相当于是一个不太精确地set集合,我们可以利用它里面的contains方法去判断一个对象是否存在,但是这种判断并不是很精确的。一般来说,通过contains方法判断某个值不存在,那就一定不存在,但是判断某个值存在,则那它可能不存在。

原理:
每一个布隆过滤器在Redis中都对应了一个大型的位数组以及不同的hash函数,所谓add操作:首先根据几个不同的hash函数给元素进行hash算一个整数索引值,拿到这个索引值之后,对位数组的长度进行取模运算,得到一个位置,每一个hash函数都会得到一个位置,将位数组中对应的位置设为1,这样就完成了添加操作。

结论:如果位数组很长的话,那么起冲突的可能性就越小,但是也很占空间; 如果位数组很小,那么误判的概率就很高。

在这里插入图片描述

3.布隆过滤器的安装

1.docker安装RedisBloom

1、运行一个redisbloom容器
需要先关闭之前的redis容器

docker run -p 6379:6379 --name redisbloom redislabs/rebloom:latest

在这里插入图片描述

2、进入命令行测试

docker exec -it redis-redisbloom bash
redis-cli

3、添加一个过滤器与记录

BF.ADD newFilter foo

4、判断记录是否存在

BF.EXISTS newFilter foo

在这里插入图片描述

2.自己编译安装RedisBloom
cd redis-5.0.7
git clone https://github.com/RedisBloom/RedisBloom.git
cd RedisBloom/
make
cd ..
redis-server redis.conf --loadmodule ./RedisBloom/redisbloom.so

安装完成后,执行 bf.add 命令,测试安装是否成功。
每次启动时都输入 redis-server redis.conf --loadmodule ./RedisBloom/redisbloom.so 比较
麻烦,我们可以将要加载的模块在 redis.conf 中提前配置好。

################################## MODULES #####################################
# Load modules at startup. If the server is not able to load modules
# it will abort. It is possible to use multiple loadmodule directives.
#
# loadmodule /path/to/my_module.so
# loadmodule /path/to/other_module.so
loadmodule /root/redis-5.0.7/RedisBloom/redisbloom.so

最下面这一句,配置完成后,以后只需要 redis-server redis.conf 来启动 Redis 即可。

4.布隆过滤器的使用

布隆过滤器原则上是不能删除的。主要命令是添加和判断是否存在。

1.命令操作布隆过滤器
# 添加和批量添加
BF.ADD key ...options...
bf.madd

# 判断是否存在和批量判断
BF.EXISTS key ...options...
bf.mexists

在这里插入图片描述

在这里插入图片描述

2.Jedis操作布隆过滤器

1、添加依赖
Jedis的版本要3以上

        <dependency>
            <groupId>com.redislabs</groupId>
            <artifactId>jrebloom</artifactId>
            <version>1.2.0</version>
        </dependency>

2、测试

package com.ll.bloom;

import com.ll.util.RedisUtil;
import io.rebloom.client.Client;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import redis.clients.jedis.JedisPool;

public class BloomTest {
    public static void main(String[] args) {
        GenericObjectPoolConfig config = new GenericObjectPoolConfig();
        // 连接池最大空闲书
        config.setMaxIdle(300);
        // 连接池最大连接数
        config.setMaxTotal(1000);
        // 连接时最大等待时间,如果是-1,表示没有限制
        config.setMaxWaitMillis(3000);
        // 在空闲时间检查有效性
        config.setTestOnBorrow(true);
        JedisPool pool = new JedisPool(config,"192.168.41.133", 6379);
        Client client = new Client(pool);
        for (int i = 0; i < 100000; i++) {
            client.add("name","value"+i);
            System.out.println(client.exists("name","value"+i));
        }
        // 检查数据是否存在
        System.out.println(client.exists("name","value15963"));
        // value999999不存在可能被判定为存在
        System.out.println(client.exists("name","value999999"));
    }
}

5.布隆过滤器的参数配置

默认情况下,我们使用的布隆过滤器它的错误率是0.01,默认的元素大小是100,如果元素超过100,错误率会上升。这两个参数是可以配置的。

BF.RESERVE key ...options...

# 10000条数据,错误率0.0001
BF.RESERVE k1 0.0001 10000

6.Redis穿透(缓存击穿)

假设有1亿条用户数据,现在查询用户要去数据库中查,效率低而且压力大,所以我们会把请求首先在Redis中处理(活跃用户存在Redis中),Redis中没有的用户,再去数据库中查询。

现在可能会存在一个恶意请求,携带了很多不存在的用户,这时候Redis无法拦截下来的请求,所以请求会跑到数据库中去,这时候这些恶意请求会击穿我们的缓存,甚至数据库,进而引起“雪崩效应”。

这是我们可以使用布隆过滤器。将1亿条用户数据存在Redis中不显示,但是可以存在布隆过滤器里面,请求来了,先去判断数据是否存在,如果存在,再去数据库中查询,否则就不去数据库中查询。

7.限流

1.Pipeline

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

2.简单限流

package com.ll.limit;

import com.ll.util.RedisUtil;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.Response;

public class RateLimit {
    private Jedis jedis;

    public Jedis getJedis() {
        return jedis;
    }

    public void setJedis(Jedis jedis) {
        this.jedis = jedis;
    }

    /**
     * 限流方法
     * @param user 操作的用户,相当于是限流的对象
     * @param action 具体的操作
     * @param period 时间窗,限流的周期
     * @param maxCount 限流的次数
     * @return
     */
    public boolean isAllowed(String user,String action,int period,int maxCount){
        // 1、数据用zset保存,首先生成一个key
        String key = user + "-" + action;
        // 2、获取当前时间戳
        long nowTime = System.currentTimeMillis();
        // 3、建立管道
        Pipeline pipelined = jedis.pipelined();
        // 开启
        pipelined.multi();
        // 4、将当前操作存下来
        pipelined.zadd(key,nowTime,String.valueOf(nowTime));
        // 5、移除时间窗之外的数据
        pipelined.zremrangeByScore(key,0,nowTime-period*1000);
        // 6、统计剩下的key
        Response<Long> response = pipelined.zcard(key);
        // 7、将当前key设置一个过期时间,过期时间就是时间窗
        pipelined.expire(key,period+1);
        // 5、关闭管道
        pipelined.exec();
        pipelined.close();
        // 8、比较时间窗内的操作熟
        return response.get()<=maxCount;
    }

    public static void main(String[] args) {
        RedisUtil redis = new RedisUtil();
        redis.execute(jedis -> {
            RateLimit rateLimit = new RateLimit();
            rateLimit.setJedis(jedis);
            for (int i = 0; i < 20; i++) {
                // 五秒内有3次操作
                System.out.println(rateLimit.isAllowed("k1","publis",5,3));
            }
        });
    }
}

3.深入限流Redis-Cell

Redis4.0开始提供了一个Redis-cell模块,这个模块使用了漏斗算法,提供了一个非常好用的限流指令。

漏斗算法:请求从漏斗的大口进,然后从小口出进入到系统中,无论多大的访问量,最终进入到系统中的请求都是固定的。

1.docker安装Redis-Cell模块

使用漏斗算法,需要先安装Redis-Cell模块。

docker pull hsz1273327/redis-cell //拉取镜像
docker run -d -p 6379:6379 hsz1273327/redis-cell  //映射至6380端口
2.CL.THROTTLE命令操作
CL.THROTTLE key ...options...

CL.THROTTLE一个有5个参数
1、key
2、漏斗的容量
3、漏水的速率,时间窗内可以操作的次数
4、时间窗
5、每次漏出数量(可选)
执行完成后,返回值也有5个
1、0表示允许,1表示拒绝
2、漏斗的容量
3、漏斗的剩余空间
4、如果拒绝了多长时间后可以再试
5、多长时间后,漏斗会完全空出来
在这里插入图片描述

2.Lettuce扩展

1、首先定义一个命令接口

package com.ll.lettuce;

import io.lettuce.core.dynamic.Commands;
import io.lettuce.core.dynamic.annotation.Command;

import java.util.List;

public interface RedisCommandInterface extends Commands {
    @Command("CL.THROTTLE ?0 ?1 ?2 ?3 ?4")
    List<Object> throttle(String key,Long init,Long count,Long period,Long quota);
}

2、直接调用即可

public class ThrottleTest {
    public static void main(String[] args) {
        RedisClient redisClient = RedisClient.create("reids://''@192.168.41.133");
        StatefulRedisConnection<String, String> connect = redisClient.connect();
        RedisCommandFactory factory = new RedisCommandFactory(connect);
        RedisCommandInterface commands = factory.getCommands(RedisCommandInterface.class);
        List<Object> list = commands.throttle("k2", 10L, 10L, 60L, 1L);
        System.out.println(list);
    }
}

布隆过滤器

延时队列

Geo

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值