redis学习笔记

文章目录

一. redis 安装

准备gcc环境
yum install gcc-c++
下载解压安装编译
wget http://download.redis.io/releases/redis-6.0.7.tar.gz
tar -zxvf redis-6.0.7.tar.gz
cd /redis-6.0.7
make
make install
启动redis
redis-server redis.conf

启动成功时的图片

以后台方式启动

修改redis.conf
表示以守护进程启动redis
启动redis

redis-server redis.conf
查看redis相关状态参数
192.168.120.128:7002> info
# Server
redis_version:5.0.7
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:cbf6682a2e2282ba
redis_mode:cluster
os:Linux 2.6.32-573.el6.i686 i686
arch_bits:32
multiplexing_api:epoll
atomicvar_api:sync-builtin
gcc_version:4.4.7
process_id:26018
run_id:53c3f6713cc75d0addbd74484a6f943fe5dda933
tcp_port:7002
uptime_in_seconds:11586
uptime_in_days:0
hz:10
configured_hz:10
lru_clock:6441313
executable:/usr/local/redis-5.0.7/redis-server
config_file:/usr/local/redis-cluster/7002/redis7002.conf

# Clients
connected_clients:1
client_recent_max_input_buffer:2
client_recent_max_output_buffer:0
blocked_clients:0

# Memory
used_memory:2216832
used_memory_human:2.11M
used_memory_rss:3043328
used_memory_rss_human:2.90M
used_memory_peak:2277240
used_memory_peak_human:2.17M
used_memory_peak_perc:97.35%
used_memory_overhead:2124072
used_memory_startup:1008760
used_memory_dataset:92760
used_memory_dataset_perc:7.68%
allocator_allocated:2286408
allocator_active:2560000
allocator_resident:2789376
total_system_memory:3048718336
total_system_memory_human:2.84G
used_memory_lua:25600
used_memory_lua_human:25.00K
used_memory_scripts:0
used_memory_scripts_human:0B
number_of_cached_scripts:0
maxmemory:3221225472
maxmemory_human:3.00G
maxmemory_policy:noeviction
allocator_frag_ratio:1.12
allocator_frag_bytes:273592
allocator_rss_ratio:1.09
allocator_rss_bytes:229376
rss_overhead_ratio:1.09
rss_overhead_bytes:253952
mem_fragmentation_ratio:1.37
mem_fragmentation_bytes:827800
mem_not_counted_for_evict:364
mem_replication_backlog:1048576
mem_clients_slaves:16778
mem_clients_normal:49550
mem_aof_buffer:364
mem_allocator:jemalloc-5.1.0
active_defrag_running:0
lazyfree_pending_objects:0

# Persistence
loading:0
rdb_changes_since_last_save:0
rdb_bgsave_in_progress:0
rdb_last_save_time:1600276095
rdb_last_bgsave_status:ok
rdb_last_bgsave_time_sec:0
rdb_current_bgsave_time_sec:-1
rdb_last_cow_size:360448
aof_enabled:1
aof_rewrite_in_progress:0
aof_rewrite_scheduled:0
aof_last_rewrite_time_sec:-1
aof_current_rewrite_time_sec:-1
aof_last_bgrewrite_status:ok
aof_last_write_status:ok
aof_last_cow_size:0
aof_current_size:1508
aof_base_size:0
aof_pending_rewrite:0
aof_buffer_length:0
aof_rewrite_buffer_length:0
aof_pending_bio_fsync:0
aof_delayed_fsync:0

# Stats
total_connections_received:24
total_commands_processed:13185
instantaneous_ops_per_sec:1
total_net_input_bytes:667512
total_net_output_bytes:91514
instantaneous_input_kbps:0.05
instantaneous_output_kbps:0.00
rejected_connections:0
sync_full:1
sync_partial_ok:0
sync_partial_err:1
expired_keys:0
expired_stale_perc:0.00
expired_time_cap_reached_count:0
evicted_keys:0
keyspace_hits:49
keyspace_misses:0
pubsub_channels:0
pubsub_patterns:0
latest_fork_usec:296
migrate_cached_sockets:0
slave_expires_tracked_keys:0
active_defrag_hits:0
active_defrag_misses:0
active_defrag_key_hits:0
active_defrag_key_misses:0

# Replication
role:master
connected_slaves:1
slave0:ip=192.168.120.128,port=7004,state=online,offset=16922,lag=0
master_replid:2f3921b931af86ea59e520ef221ccd1d26c41b14
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:16922
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:16922

# CPU
used_cpu_sys:15.205688
used_cpu_user:11.936185
used_cpu_sys_children:0.021996
used_cpu_user_children:0.005999

# Cluster
cluster_enabled:1

# Keyspace
db0:keys=1,expires=0,avg_ttl=0


二. 五种数据类型

String

String 是 Redis 里边最最简单的一种数据结构。在 Redis 中,所有的 key 都是字符串,但是,不同的key 对应的 value 则具备不同的数据结构,我们所说的五种不同的数据类型,主要是指 value 的数据类型不同。
Redis 中的字符串是动态字符串,内部是可以修改的,像 Java 中的 StringBuffer,它采用分配冗余空间
的方式来减少内存的频繁分配。在 Redis 内部结构中,一般实际分配的内存会大于需要的内存,当字符串小于 1M 的时候,扩容都是在现有的空间基础上加倍,扩容每次扩1M 空间,最大 512M。

相关常用命令

  • set (set 就是给一个 key 赋值的。)

    set a 123
    
  • get (get 用来获取一个 key 的 value。)

    get a 
    
  • append (使用 append 命令时,如果 key 已经存在,则直接在对应的 value 后追加值,否则就创建新的键值对。)

    append a 4
    
  • decr

    可以实现对 value 的减 1 操作(前提是 value 是一个数字),如果 value 不是数字,会报错,如果
    value 不存在,则会给一个默认的值为 0,在默认值的基础上减一,并返回减后的值。

    decr b
    
  • decrby

    和 decr 类似,但是可以自己设置步长,该命令第二个参数就是步长,并返回减后的值。

    decrby b 2
    
  • getrange

    getrange 可以用来返回 key 对应的 value 的子串,这有点类似于 Java 里边的 substring。这个命令第
    二个和第三个参数就是截取的起始和终止位置,其中,-1 表示最后一个字符串,-2 表示倒数第二个字符
    串,以此类推…

    getrange a 0 -1
    
  • getset

    获取并更新某一个 key,返回更新前的值。

    getset b 1
    
  • incr

    给某一个 key 的 value 自增,并返回新值。

    incr b
    
  • incrby

    给某一个 key 的 value 自增,同时还可以设置步长。

    incrby b 2
    
  • incrbyfloat

    和 incrby 类似,但是自增的步长可以设置为浮点数。

    incrbyfloat b 0.5
    
  • mget 和 mset

    批量获取和批量存储

    mget a b
    mset a 1 b 2
    
  • ttl

    查看 key 的有效期(如果此key没有设置则返回-1,已过期的话返回-2)

    ttl a
    
  • setex

    在给 key 设置 value 的同时,还设置过期时间。

    setex a 10 3
    
  • psetex

    和 setex 类似,只不过这里的时间单位是毫秒。

    psetex a 10000 3
    
  • setnx

    默认情况下, set 命令会覆盖已经存在的 key,setnx 则不会。

    setnx b 1
    
  • msetnx

    批量设置(其中一个失败,全部失败)

    msetnx a 123 b 1
    
  • setrange

    覆盖一个已经存在的 key 的value。

    setrange b 2 12
    
  • strlen

    查看字符串长度

    strlen b
    

bit命令

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

  • getbit

    key 对应的 value 在 offset 处的 bit 值。

    getbit b 0
    
  • setbit

    修改 key 对应的 value 在 offset 处的 bit 值

    setbit b 1 0
    
  • bitcount

    统计当前key的value中二进制数据中 1 的个数。

    bitcount b
    
List
  • lpush

    将所有指定的值插入到存于 key 的列表的头部。如果 key 不存在,那么在进行 push 操作前会创建一个空列表。 如果 key 对应的值不是一个 list 的话,那么会返回一个错误 (相当于栈,先进后出)

    lpush d java python c#
    
  • lrange

    返回列表指定区间内的元素

    lrange d 0 -1
    
  • rpush

    向存于 key 的列表的尾部插入所有指定的值

    rpush d php js
    
  • rpop

    移除并返回列表的尾元素。

    rpop d
    
  • lpop

    移除并返回列表的头元素。

    lpop d
    
  • lindex

    返回列表中,下标为 index 的元素。

    lindex d 0
    
  • ltrim

    ltrim 可以对一个列表进行修剪。(相当于subList())

    ltrim d 0 0
    
  • blpop

    阻塞式的弹出,相当于 lpop 的阻塞版。

    blpop d 10
    
Set
  • sadd

    添加元素到一个 key 中

    sadd d 1
    
  • smembers

    获取一个 key 下的所有元素

    smembers d
    
  • srem

    移除指定的元素

    srem d 1
    
  • sismemeber

    返回某一个成员是否在集合中

    sismemeber d 1
    
  • scard

    返回集合的数量

    scard d 
    
  • srandmember

    随机返回指定个元素

    srandmember d  2
    
  • spop

    随机返回并且出栈指定元素

    spop d  2
    
  • smove

    把一个元素从一个集合移到另一个集合中去

    SMOVE d e 1
    
  • sdiff

    返回两个集合的差集

    SDIFF e d
    
  • sinter

    返回两个集合的交集

    sinter e d
    
  • sdiffstore

    这个类似于 sdiff ,不同的是,计算出来的结果会保存在一个新的集合中

    sdiffstore f d e
    
  • sinterstore

    类似于 sinter,只是将计算出来的交集保存到一个新的集合中。

    sinterstore f d e
    
  • sunion

    求并集

    sunion d e
    
  • sunionstore

    求并集并且将结果保存到新的集合中

    sunion f d e 
    
Hash

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

  • hset

    添加值

    hset a name zs
    
  • hget

    获取值

    hget a name
    
  • hmset

    批量设置

    hmset b name lisi age 20
    
  • hmget

    批量获取

    hmget b name age
    
  • hdel

    删除指定的 field (可批量)

    hdel b name age
    
  • hsetnx

    默认情况下,如果 key 和 field 相同,会覆盖掉已有的 value,hsetnx 则不会。

    hsetnx a name lisi
    
  • hvals

    获取所有的 value

     hvals a
    
  • hkeys

    获取所有的 key

     hkeys a
    
  • hgetall

    同时获取所有的 key 和 value

     hgetall a
    
  • hexists

    返回 field 是否存在

     hexists a name
    
  • hincrby

    给指定的key的 value 自增指定的步长

     hincrby a age 4
    
  • hincrbyfloat

    可以自增一个浮点数

     hincrbyfloat a age 0.5
    
  • hlen

    返回 某一个 key 中 value 的数量

     hlen a
    
  • hstrlen

    返回某一个 key 中的某一个 field 的字符串长度

     hstrlen a name
    
Zset
  • zadd

    将指定的元素添加到有序集合中

     zadd b 10 zs 20 lisi
    
  • zscore

    返回 member 的 score 值

     zscore b zs
    
  • zrange

    返回集合中的指定区间内的一组元素。

     zrange b 0 -1
    
  • zrevrange

    倒叙返回集合中的指定区间内的一组元素。

     zrevrange b 0 -1 withscores
    
  • zcard

    返回元素个数

     zcard b
    
  • zcount

    返回 score 在某一个区间内的元素。

     ZCOUNT b 10 20 //闭区间
     ZCOUNT b (10 (20 //开区间
     ZCOUNT b (10 20  //左开右闭区间
    
  • zrangebyscore

    按照 score 的范围返回元素。

     zrangebyscore b 10 20
    
  • zrank

    根据score返回元素在此set集合中的排名(从小到大)

     zrank b lisi
    
  • zrevrank

    根据score返回元素在此set集合中的排名(从大到小)

     zrevrank b lisi
    
  • zincrby

    将指定元素的score 自增指定步长 返回自增后的score值

     zincrby b 2 zs
    
  • zinterstore

    给两个集合求交集并添加到一个新的集合中(相同mamber的score相加)

    zinterstore d 2 b c
    
  • zrem

    弹出指定个元素

    zrem d zs lisi
    
  • zlexcount

    计算有序集合中成员数量

    zlexcount b - +
    
  • zrangebylex

    返回指定区间内的成员

    zrangebylex b - +
    
Key的操作
  • del

    删除指定key

    del a b 
    
  • dump

    序列化给定的 key

    dump a 
    
  • exists

    判断一个 key 是否存在

    exists a 
    
  • ttl

    查看一个 key 的有效期 (如果此key没有设置有效期则返回-1,已过期或者key不存在的话返回-2)

    ttl a 
    
  • expire

    给一个 key 设置有效期,如果 key 在过期之前被重新 set 了,则过期时间会失效。

    expire a 10
    
  • persist

    移除一个 key 的过期时间

    persist a 
    
  • pttl

    和 ttl 一样,只不过这里返回的是毫秒

    pttl a 10000
    
  • keys *

    返回所有key

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

三. 开启远程校验连接

redis默认是只能在宿主机上本地连接的,所以这里要做一个相应的配置
配置redis.conf

注释掉bind
在这里插入图片描述
开启密码校验
在这里插入图片描述
保存 重启即可

注意:如果远程连接不上的话 可能是你的linux没有关闭防火墙,关闭即可.(或者开放相应的6379端口)

四. Java 连接redis

4.1 jedis连接

1.引入依赖

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

2.测试

		Jedis jedis=new Jedis("192.168.32.128",6379);
        jedis.auth("123"); //密码
        String s = jedis.ping("成功");
        System.out.println("s = " + s);
		//使用连接池
		GenericObjectPoolConfig config = new GenericObjectPoolConfig();
        //连接池最大连接数
        config.setMaxTotal(1000);
        //最大空闲数
        config.setMaxIdle(300);
        //最大等待时间
        config.setMaxWaitMillis(30000);
        //在空闲时检查有效性
        config.setTestOnBorrow(true);
        JedisPool pool = new JedisPool(config, "192.168.32.128", 6379, 10000, "123");
        
        //Jdk1.7之后提供的语法糖 try...with..resources 不用在catch finally里手动归还(关闭)连接了
        try (Jedis jedis = pool.getResource()) {
            System.out.println("jedis.ping() = " + jedis.ping());
        }
4.2 Lettuc 连接

Lettuce 和 Jedis 的一个比较:

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

1.引入依赖

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

2.测试

		 RedisClient client = RedisClient.create("redis://123@192.168.32.128");
        StatefulRedisConnection<String, String> connect = client.connect();
        RedisCommands<String, String> commands = connect.sync();

        commands.set("name","123");
        System.out.println("commands.get(\"name\") = " + commands.get("name"));

五. redis做分布式锁

问题场景

例如一个简单的用户操作,一个线程去修改用户的状态,首先从数据库中读出用户的状态,然后在内存中进行修改,修改完成后,再存回去。在单线程中,这个操作没有问题,但是在多线程中,由于读取、修改、存 这是三个操作,不是原子操作,所以在多线程中,这样会出问题.

5.1 基本用法

分布式锁实现的思路很简单,就是进来一个线程先占位,当别的线程进来操作时,发现已经有人占位了,就会放弃或者稍后再试。
在 Redis 中,占位一般使用 setnx 指令,先进来的线城先占位,线程的操作执行完成后,再调用 del 指令释放锁资源。

		GenericObjectPoolConfig config = new GenericObjectPoolConfig();
        //连接池最大连接数
        config.setMaxTotal(1000);
        //最大空闲数
        config.setMaxIdle(300);
        //最大等待时间
        config.setMaxWaitMillis(30000);
        //在空闲时检查有效性
        config.setTestOnBorrow(true);
        JedisPool pool = new JedisPool(config, "192.168.32.128", 6379, 10000, "123");

        //Jdk1.7之后提供的语法糖 try...with..resources 不用在catch finally里手动归还(关闭)连接了
        try (Jedis jedis = pool.getResource()) {
           
                
                String set = jedis.set("name", "zs", new SetParams().nx().ex(10)); //给锁添加一个过期时间,防止应用在运行过程中抛出异常导致锁无法及时得到释放
                if (set != null && "OK".equals(set)) {

                    //拿到锁后执行相关业务逻辑
                    String name = jedis.get("name");
                    System.out.println("name = " + name);

                    jedis.del("name");//释放锁资源
                } else {

                    System.out.println("没有拿到锁"); //做出相应的暂缓操作
                }
            
        }
5.2 超时问题

为了防止业务代码在执行的时候抛出异常,我们给每一个锁添加了一个超时时间,超时之后,锁会被自动释放,但是这也带来了一个新的问题:如果要执行的业务非常耗时,可能会出现紊乱。

举个例子:

第一个线程首先获取到锁,然后开始执行业务代码,但是业务代码比较耗时,执行了 8 秒(假设这里超时时间设置了5s),这样,会在第一个线程的任务还未执行成功锁就会被释放了,此时第二个线程会获取到锁开始执行,在第二个线程刚执行了 3 秒,第一个线程也执行完了,此时第一个线程会释放锁,但是注意,它释放的第二个线程的锁,释放之后,第三个线程进来。

解决思路:

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

对于第二种方案,由于释放锁的时候,要去查看锁的 value,第二步比较 value 的值是否正确,第三步释放锁,有三个步骤,很明显三个步骤不具备原子性,为了解决这个问题,我们得引入 Lua 脚本。

Lua 脚本的优势:

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

在 Redis 中,使用 Lua 脚本,大致上两种思路:

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

首先在 Redis 服务端创建 Lua 脚本,内容如下:

//第一个参数keys[1]为被设置锁的键,第二个参数为要比较的值
if redis.call("get",KEYS[1])==ARGV[1] then
 return redis.call("del",KEYS[1])
else
 return 0
end

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

cat equals.lua | redis-cli -a 123 script load --pipe

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

		GenericObjectPoolConfig config = new GenericObjectPoolConfig();
        //连接池最大连接数
        config.setMaxTotal(1000);
        //最大空闲数
        config.setMaxIdle(300);
        //最大等待时间
        config.setMaxWaitMillis(30000);
        //在空闲时检查有效性
        config.setTestOnBorrow(true);
        JedisPool pool = new JedisPool(config, "192.168.32.128", 6379, 10000, "123");

        //Jdk1.7之后提供的语法糖 try...with..resources 不用在catch finally里手动归还(关闭)连接了
        try (Jedis jedis = pool.getResource()) {
            for (int i = 0; i < 10; i++) {
                String s = UUID.randomUUID().toString();
                String set = jedis.set("name", "zs", new SetParams().nx().ex(10)); //给锁添加一个过期时间,防止应用在运行过程中抛出异常导致锁无法及时得到释放
                if (set != null && "OK".equals(set)) {

                    //拿到锁后执行相关业务逻辑
                    String name = jedis.get("name");
                    System.out.println("name = " + name);

                    //调用服务端提前写好的lua脚本 释放锁资源
                    jedis.evalsha("b8059ba43af6ffe8bed3db65bac35d452f8115d8", Arrays.asList("name"), Arrays.asList(s));

                } else {
                    System.out.println("没有拿到锁"); //做出相应的暂缓操作
                }
            }
        }

六 .redis做消息队列

我们平时说到消息队列,一般都是指 RabbitMQ、RocketMQ、ActiveMQ 以及大数据里边的 Kafka,这些是我们比较常见的消息中间件,也是非常专业的消息中间件,作为专业的中间件,它里边提供了许多功能。
但是,当我们需要使用消息中间件的时候,并非每次都需要非常专业的消息中间件,假如我们只有一个消息队列,只有一个消费者,那就没有必要去使用上面这些专业的消息中间件,这种情况我们可以直接使用 Redis 来做消息队列。
Redis 的消息队列不是特别专业,他没有很多高级特性,适用简单的场景,如果对于消息可靠性有着极高的追求,那么不适合使用 Redis 做消息队列。

6.1 消息队列

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

6.2 延迟消息队列

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

添加依赖

 		<dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.11.2</version>
        </dependency>

构造消息对象

public class Message {
    private String id;
    private Object data;

    public Message() {
    }

    public Message(String id, Object data) {
        this.id = id;
        this.data = data;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    @Override
    public String toString() {
        return "Message{" +
            "id='" + id + '\'' +
            ", data=" + data +
            '}';
    }
}

封装延迟消息队列

public class Queue {

    private Jedis jedis;

    private String queue;

    public Queue(Jedis jedis, String queue) {
        this.jedis = jedis;
        this.queue = queue;
    }

    /**
     * 入队
     *
     * @param data 需要发送的消息
     */
    public void push(Object data) {
        try {

            String id = UUID.randomUUID().toString();
            Message message = new Message(id, data);
            String msg = new ObjectMapper().writeValueAsString(message); //序列化消息对象
            jedis.zadd(queue, System.currentTimeMillis() + 5000, msg); //发送消息 score 延迟 5 秒
            System.out.println(new Date() + ">>>发送消息>>>" + msg);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
    }

    /**
     * 出队
     */
    public void lope() {

        while (!Thread.interrupted()) {
            //读取 score 在 0 到当前时间戳之间的消息
            Set<String> set = jedis.zrangeByScore(queue, 0, System.currentTimeMillis(), 0, 1);
            if (set.isEmpty()) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
                continue;
            }
            Iterator<String> iterator = set.iterator();
                String next = iterator.next();
                if (jedis.zrem(queue, next) > 0) {
                    try {
                        Message message = new ObjectMapper().readValue(next, Message.class);
                        System.out.println(new Date() + ">>>message >>> " + message);

                    } catch (JsonProcessingException e) {
                        e.printStackTrace();
                    }
                }

        }

    }
}

测试

public class QueueTest {
    public static void main(String[] args) {
        GenericObjectPoolConfig config = new GenericObjectPoolConfig();
        //连接池最大连接数
        config.setMaxTotal(1000);
        //最大空闲数
        config.setMaxIdle(300);
        //最大等待时间
        config.setMaxWaitMillis(30000);
        //在空闲时检查有效性
        config.setTestOnBorrow(true);
        JedisPool pool = new JedisPool(config, "192.168.228.128", 6379, 10000, "123");

        //Jdk1.7之后提供的语法糖 try...with..resources 不用在catch finally里手动归还(关闭)连接了
        try (Jedis jedis = pool.getResource()) {
            Queue queue = new Queue(jedis, "queue1");
            Thread produce = new Thread() {
                @Override
                public void run() {
                    for (int i = 0; i < 3; i++) {
                        queue.push("123>>>"+i);
                    }
                }
            };


            Thread consumer = new Thread() {
                @Override
                public void run() {
                    for (int i = 0; i < 3; i++) {
                        queue.lope();
                    }
                }
            };

            produce.start();
            consumer.start();



           // Thread.sleep(7000);
           // Thread.interrupted();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

七 .Bit操作

7.1 场景示例

用户一年的签到记录,如果你用 string 类型来存储,那可能需要 365 个 key/value,操作起来麻烦,而且还产生不必要的内存消耗,通过位图可以有效的简化这个操作。

它的统计很简单:01111000111

每天的记录占一个位,365 天就是 365 个位,大概 46 个字节,这样可以有效的节省存储空间,如果有一天想要统计用户一共签到了多少天,统计 1 的个数即可。

对于位图的操作,可以直接操作对应的字符串(get/set),可以直接操作位(getbit/setbit).

7.2 基本操作
7.2.1 零存整取
字符ASCII码二进制
J7401001010
A6501000001
V8601010110
A6501000001

在这里插入图片描述

7.2.2 整存零取

存一个字符串进去,但是通过位操作获取字符串。
在这里插入图片描述

7.3 统计

例如签到记录:01111000111

1 表示签到的天,0 表示没签到,统计总的签到天数:

可以使用 bitcount。

在这里插入图片描述

bitcount 中,可以统计的起始位置,但是注意,这个起始位置是指字符的起始位置而不是 bit 的起始位置。

除了 bitcount 之外,还有一个 bitpos。bitpos 可以用来统计在指定范围内出现的第一个 1 或者 0 的位置,这个命令中的起始和结束位置都是字符索引,不是 bit 索引,一定要注意。
在这里插入图片描述

7.4 Bit 批处理

在 Redis 3.2 之后,新加了一个功能叫做 bitfiled ,可以对 bit 进行批量操作。

例如:

BITFIELD name get u4 0

表示获取 name 中的位,从 0 开始获取,获取 4 个位,返回一个无符号数字。

u 表示无符号数字
i 表示有符号数字,有符号的话,第一个符号就表示符号位,1 表示是一个负数。
bitfiled 也可以一次执行多个操作。

get

BITFIELD name get u4 0 get u4 4 get i4 8

在这里插入图片描述
set

BITFIELD name set u8 8 66

在这里插入图片描述

表示用无符号的98转换成的8位二进制数 替换从第8位开始接下来的8位数字

incrby

对指定范围进行自增操作,自增操作可能会出现溢出,既可能是向上溢出,也可能是向下溢出。Redis中对于溢出的处理方案是折返。8 位无符号数 255 加 1 溢出变为 0;8 位有符号数 127,加 1 变为 -128.也可以修改默认的溢出策略,可以改为 fail ,表示执行失败。

BITFIELD name incrby u2 6 1

表示从第6位开始的接下来的2位无符号数字+1操作

也可以修改默认的溢出策略,可以改为 fail ,表示执行失败。

BITFIELD name overflow fail incrby u2 6 1

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

BITFIELD name overflow sat incrby u2 6 1

在这里插入图片描述

八 .HyperLogLog

8.1 场景示例

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

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

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

如果自己实现,pv 比较简单,可以直接通过 Redis 计数器就能实现。但是 uv 就不一样,uv 涉及到另外一个问题,去重。

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

如果是千万级别的 UV,需要的存储空间就非常惊人。而且,像 UV 统计这种,一般也不需要特别精确,800w 的 uv 和 803w 的 uv,其实差别不大。所以,今天的主角—HyperLogLog

Redis 中提供的 HyperLogLog 就是专门用来解决这个问题的,HyperLogLog 提供了一套不怎么精确但是够用的去重方案,会有误差,官方给出的误差数据是 0.81%,这个精确度,统计 UV 够用了。

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

pfadd 用来添加记录,类似于 sadd ,添加过程中,重复的记录会自动去重。

pfcount 则用来统计数据。

数据量少的时候看不出来误差。

PFADD uv 1 2 3 4 5 6 5
PFCOUNT uv

在这里插入图片描述
pfmerge ,合并多个统计结果,在合并的过程中,会自动
去重多个集合中重复的元素。

PFMERGE uv uv1

在这里插入图片描述

九. 布隆过滤器(Bloom Filter)

9.1 什么是布隆过滤器

本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。

相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。

9.2 数据结构及原理

布隆过滤器其中重要的实现就是位图的实现,也就是位数组,并且在这个数组中每一个位置只占有1个bit,而每个bit只有0和1两种状态。如图bitarray所示!bitarray也叫bitmap,大小也就是布隆过滤器的大小。
在这里插入图片描述

如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的 bit 位置变为 1,例如针对值 “baidu” 和三个不同的哈希函数分别生成了哈希值 1、4、7,则
在这里插入图片描述
Ok,我们现在再存一个值 “tencent”,如果哈希函数返回 3、4、8 的话,图继续变为:
在这里插入图片描述
值得注意的是,4 这个 bit 位由于两个值的哈希函数都返回了这个 bit 位,因此它被覆盖了。现在我们如果想查询 “dianping” 这个值是否存在,哈希函数返回了 1、5、8三个值,结果我们发现 5 这个 bit 位上的值为 0,说明没有任何一个值映射到这个 bit 位上,因此我们可以很确定地说 “dianping” 这个值不存在。而当我们需要查询 “baidu” 这个值是否存在的话,那么哈希函数必然会返回 1、4、7,然后我们检查发现这三个 bit 位上的值均为 1,那么我们可以说 “baidu” 存在了么?答案是不可以,只能是 “baidu” 这个值可能存在。

这是为什么呢?答案跟简单,因为随着增加的值越来越多,被置为 1 的 bit 位也会越来越多,这样某个值 “taobao” 即使没有被存储过,但是万一哈希函数返回的三个 bit 位都被其他值置位了 1 ,那么程序还是会判断 “taobao” 这个值存在。

总结 :

Bloom Filter 中,误判的概率和位数组的大小有很大关系,位数组越大,误判概率越小,当然占用的存储空间越大;位数组越小,误判概率越大,当然占用的存储空间就小。

9.3 场景示例

布隆过滤器广泛应用于网页黑名单系统、垃圾邮件过滤系统、爬虫网址判重系统等,有人会想,我直接将网页URL存入数据库进行查找不就好了,或者建立一个哈希表进行查找不就OK了。

当数据量小的时候,这么思考是对的,但如果整个网页黑名单系统包含100亿个网页URL,在数据库查找是很费时的,并且如果每个URL空间为64B,那么需要内存为640GB,一般的服务器很难达到这个需求。

那么,在这种内存不够且检索速度慢的情况下,不妨考虑下布隆过滤器,但业务上要可以忍受判断失误率。
在这里插入图片描述

9.4 Bloom Filter 安装

Redis 因其支持 setbit 和 getbit 操作,且纯内存性能高等特点,因此天然就可以作为布隆过滤器来使用。但是布隆过滤器的不当使用极易产生大 Value,增加 Redis 阻塞风险,因此生产环境中建议对体积庞大的布隆过滤器进行拆分。

拆分的形式方法多种多样,但是本质是不要将 Hash(Key) 之后的请求分散在多个节点的多个小 bitmap 上,而是应该拆分成多个小 bitmap 之后,对一个 Key 的所有哈希函数都落在这一个小 bitmap 上。

9.4.1 编译安装

官网

进入到redis目录下

yum update nss
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 中提前配置好。

在这里插入图片描述
配置完成后,以后只需要 redis-server redis.conf 来启动 Redis 即可。

9.5 基本用法

主要是两类命令,添加和判断是否存在。

  • bf.add\bf.madd 添加和批量添加
  • bf.exists\bf.mexists 判断是否存在和批量判断

在这里插入图片描述

使用Java代码操作

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

测试

		GenericObjectPoolConfig config = new GenericObjectPoolConfig();
        config.setMaxTotal(1000);
        config.setMaxIdle(300);
        config.setMaxWaitMillis(30000);
        config.setTestOnBorrow(true);
        JedisPool pool = new JedisPool(config, "192.168.228.128", 6379, 10000, "123");

        Client client=new Client(pool);
        /*for (int i = 0; i < 100000; i++) {

            client.add("name","test>>"+i);
        }*/
        boolean exists = client.exists("name", "test>>999999999");
        System.out.println("exists = " + exists); //true 误判 实则不存在

默认情况下,我们使用的布隆过滤器它的错误率是 0.01 ,默认的元素大小是 100。但是这两个参数也是可以配置的。
我们可以调用 bf.reserve 方法进行配置。

BF.RESERVE key 0.00001 100000

第一个参数是 key,第二个参数是错误率,错误率越低,占用的空间越大,第三个参数预计存储的数量,当实际数量超出预计数量时,错误率会上升。

9.6 典型场景

解决 Redis 穿透或者又叫缓存击穿问题。

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

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

为了解决这个问题,我们就可以使用布隆过滤器。将 1亿条用户数据存在 Redis 中不现实,但是可以存在布隆过滤器中,请求来了,首先去判断数据是否存在,如果存在,再去数据库中查询,否则就不去数据库中查询。

十. Redis 限流

10.1 预备知识

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

10.2 简单限流
public class RateLimiter {
    private Jedis jedis;

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

    /**
     * 限流方法
     *
     * @param user     限流用户
     * @param action   具体操作
     * @param period   限流的时间窗口
     * @param maxCount 限流次数
     * @return
     */
    public boolean isAllowd(String user, String action, int period, int maxCount) {
        //数据用zset保存,按照一定规则生成一个key
        String key = user + "-" + action;

        //当前时间戳
        long nowTime = System.currentTimeMillis();

        //建立管道
        Pipeline pipelined = jedis.pipelined();
        pipelined.multi();

        //将当前操作保存下来
        pipelined.zadd(key, nowTime, String.valueOf(nowTime));

        //移除时间窗之外的数据
        pipelined.zrangeByScore(key, 0, nowTime - period * 1000);
        //给当前key设置过期时间
        pipelined.expire(key, period + 1);

        //统计剩下的key
        Response<Long> response = pipelined.zcard(key);

        //关闭管道
        pipelined.exec();
        pipelined.close();

        return response.get() <= maxCount;
    }

    public static void main(String[] args) {
        GenericObjectPoolConfig config = new GenericObjectPoolConfig();
        config.setMaxTotal(1000);
        config.setMaxIdle(300);
        config.setMaxWaitMillis(30000);
        config.setTestOnBorrow(true);
        JedisPool pool = new JedisPool(config, "192.168.228.128", 6379, 10000, "123");
        Jedis jedis = pool.getResource();
        RateLimiter limiter=new RateLimiter(jedis);
        for (int i = 0; i < 1000; i++) {

            boolean allowd = limiter.isAllowd("张三", "play game", 5, 3);

            System.out.println("allowd = " + allowd);
        }
    }
}
10.3 深入限流操作

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

使用漏斗算法,需要我们首先安装 Redis-Cell 模块:

官网地址

安装步骤:

wget https://github.com/brandur/redis-cell/releases/download/v0.2.4/redis-cell-v0.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 配置文件

loadmodule /usr/local/redis-4.0.10/redis-cell/libredis_cell.so

在这里插入图片描述
redis 启动成功后,如果存在 CL.THROTTLE 命令,说明 redis-cell 已经安装成功了。

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

  1. 第一个参数是 key
  2. 第二个参数是漏斗的容量
  3. 时间窗内可以操作的次数
  4. 时间窗
  5. 每次漏出数量

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

  1. 第一个 0 表示允许,1表示拒绝
  2. 第二个参数是漏斗的容量
  3. 第三个参数是漏斗的剩余空间
  4. 如果拒绝了,多长时间后,可以再试
  5. 多长时间后,漏斗会完全空出来
10.4 Lettuce扩展

定义命令接口

public interface RedisCommandInterface  extends Commands {

	/**
     *
     * @param key key
     * @param init 桶的初始化容量
     * @param count 时间窗内可以操作的次数
     * @param period 时间窗
     * @param num 每次漏出的数量
     * @return
     */
    @Command("CL.THROTTLE ?0 ?1 ?2 ?3 ?4")
    List<Object> throttle(String key,Long init,Long count,Long period,Long num);

}

用lettuce调用即可

 public static void main(String[] args) {
        RedisClient client = RedisClient.create("redis://123@192.168.13.128");
        StatefulRedisConnection<String, String> connect = client.connect();
        RedisCommandFactory factory=new RedisCommandFactory(connect);
        RedisCommandInterface commands = factory.getCommands(RedisCommandInterface.class);
        List<Object> test = commands.throttle("test", 10L, 3L, 10L, 1L);
        System.out.println("test = " + test);
    }

十一. Redis集成Geo

Redis3.2 开始提供了 GEO 模块。该模块使用了 GeoHash 算法。

11.1 GeoHash 算法
11.1.1 基本原理

GeoHash 是一种地址编码方法,使用这种方式,能够将二维的空间经纬度数据编码成一个一维字符串。

地球上经纬度的划分:

以经过伦敦格林尼治天文台旧址的经线为 0 度经线,向东就是东经,向西就是西经。如果我们将西经定义负,经度的范围就是 [-180,180]。

纬度北纬 90 度到南纬 90 度,如果我们将南纬定义负,则纬度的范围就是 [-90,90]。

接下来,以本初子午线和赤道为界,我们可以将地球上的点分配到一个二维坐标中:

在这里插入图片描述
如果在小块范围内递归对半划分呢?

在这里插入图片描述
可以看到,划分的区域更多了,也更精确了。geohash算法就是基于这种思想,划分的次数更多,区域更多,区域面积更小了。通过将经纬度编码,给地理位置分区

11.1.2 具体实现
  1. 首先将经纬度变成二进制。
    比如这样一个点(39.923201, 116.390705)
    纬度的范围是(-90,90),其中间值为0。对于纬度39.923201,在区间(0,90)中,因此得到一个1;(0,90)区间的中间值为45度,纬度39.923201小于45,因此得到一个0,依次计算下去,即可得到纬度的二进制表示,如下表:
    在这里插入图片描述
    最后得到纬度的二进制表示为: 10111000110001111001
    同理可以得到经度116.390705的二进制表示为: 11010010110001000100

  2. 经纬度合并
    经度占偶数位,纬度占奇数位,注意,0也是偶数位。
    为: 11100 11101 00100 01111 00000 01101 01011 00001

  3. 按照base32编码
    Base32编码表的其中一种如下,是用0-9、b-z(去掉a, i, l, o)这32个字母进行编码。具体操作是先将上一步得到的合并后二进制转换为10进制数据,然后对应生成Base32码。需要注意的是,将5个二进制位转换成一个base32码。上例最终得到的值为 wx4g0ec1

Geohash比直接用经纬度的高效很多,而且使用者可以发布地址编码,既能表明自己位于北海公园附近,又不至于暴露自己的精确坐标,有助于隐私保护。

优点

  • 用一个字符串表示经纬度
  • GeoHash 表示的是一个区域,而不是一个点
  • GeoHash编码的前缀可以表示更大的区域。例如wx4g0ec1,它的前缀wx4g0e表示包含编码wx4g0ec1在内的更大范围。 这个特性可以用于附近地点搜索。

编码越长,表示的范围越小,位置也越精确。因此我们就可以通过比较GeoHash匹配的位数来判断两个点之间的大概距离。

存在问题

geohash算法有两个问题。首先是边缘问题。

在这里插入图片描述

如图,如果车在红点位置,区域内还有一个黄点。相邻区域内的绿点明显离红点更近。但因为黄点的编码和红点一样,最终找到的将是黄点。这就有问题了。

要解决这个问题,只要再查找周边8个区域内的点,看哪个离自己更近即可。

另外就是曲线突变问题。
在这里插入图片描述

如图,其中0111和1000两个编码非常相近,但它们的实际距离确很远。所以编码相近的两个单位,并不一定真实距离很近,这需要实际计算两个点的距离才行。

11.2 redis中应用

添加地址:

GEOADD city 116.390705 39.923201 beijing
GEOADD city 114.0592002900 22.5536230800 shenzhen

查看城市间的距离:

GEODIST city beijing shenzhen km

查看城市坐标

GEOPOS city beijing shenzhen

查看城市位置的hash

GEOHASH city beijing shenzhen

通过 hash 值可以查看定位。http://geohash.org/ws1073z2kh0
在这里插入图片描述

查看附近的地方:

GEORADIUSBYMEMBER city beijing 2000 km count 4 asc

以北京为中心,方圆 2000km 以内的城市找出来 3 个,按照远近顺序排列,这个命令不会排除 北京。

根据经纬度查询

GEORADIUS city 114.0592002900 22.5536230800 2000 km withdist

十二. Scan

12.1 简介

scan 实际上是 keys 的一个升级版。

可以用 keys 来查询 key,在查询的过程中,可以使用通配符。keys 虽然用着还算方便,但是没有分页功能。同时因为 Redis 是单线程,所以 key 的执行会比较消耗时间,特别是当数据量大的时候,影响整个程序的运行。
为了解决 keys 存在的问题,从 Redis2.8 中开始,引入了 scan。

scan 具备 keys 的功能,但是不会阻塞线程,而且可以控制每次返回的结果数。

12.2 基本用法

准备1w条测试数据

	Jedis jedis=new Jedis("192.168.13.128",6379);
        jedis.auth("123");
        for (int i = 0; i < 10000; i++) {
            jedis.set("k"+i,"v"+i);
        }

scan 命令一共提供了三个参数,第一个 cursor,第二个参数是 key,第三个参数是 limit。

cursor 实际上是指一维数组的位置索引,limit 则是遍历的一维数组个数(所以每次返回的数据大小可能不确定)。

scan 928 match k1* count 100

在这里插入图片描述
下次遍历时就从返回的游标处开始遍(cursor的值) ,知道返回的游标值为0时,遍历结束.

12.3 原理

SCAN 的遍历顺序。

假设目有三条数据:

127.0.0.1:6379> keys *
1) "key1"
2) "db_number"
3) "myKey"
127.0.0.1:6379> scan 0 match * count 1
1) "2"
2) 1) "key1"
127.0.0.1:6379> scan 2 match * count 1
1) "1"
2) 1) "myKey"
127.0.0.1:6379> scan 1 match * count 1
1) "3"
2) 1) "db_number"
127.0.0.1:6379> scan 3 match * count 1
1) "0"
2) (empty list or set)

在遍历的过程中,大家发现游标的顺序是 0 2 1 3,从十进制来看好像没有规律,但是从转为二进制,则是有规律的:

00->10->01->11

这种规律就是高位进1,传统的二进制加法,是从右往左加,这里是从左往右加。

实际上,在 Redis 中,它的具体计算流程给是这样:

  1. 将要计算的数字反转
  2. 给反转后的数字加 1
  3. 再反转

那么为什么不是按照 0、1、2、3、4…这样的顺序遍历呢?因为主要考虑到两个问题:

  1. 字典扩容

  2. 字典缩容

    在这里插入图片描述

假如我们将要访问 110 时,发生了扩容,此时 scan 就会从 0110 开始遍历,之前已经被遍历过的元素就不会被重复遍历了。从而提高了检索效率

假如我们将要访问 110 时,发生缩容,此时 scan 就会从 10 开始遍历,这个时候,也会遍历到 010,但是 010 之前的不会再被遍历了。所以,在发生缩容的时候,可能返回重复的元素。

12.4 其他命令

scan 是一系列的指令,除了遍历所有的 key 之外,也可以遍历某一个类型的 key,对应的命令有:

zscan–>zset
hscan–>hash
sscan–>set

十三. redis单线程处理高并发

13.1 阻塞IO与非阻塞IO

Java 在 JDK1.4 中引入 NIO,但是也有很多人在使用阻塞 IO,这两种 IO 有什么区别?

在阻塞模式下,如果你从数据流中读取不到指定大小的数据量,IO 就会阻塞。比如已知会有 10 个字节发送过来,但是我目前只收到 4 个,还剩六个,此时就会发生阻塞。如果是非阻塞模式,虽然此时只收到 4 个字节,但是读到 4 个字节就会立即返回,不会傻傻等着,等另外 6 个字节来的时候,再去继续读取。

所以阻塞 IO 性能低于 非阻塞 IO。

如果有一个 Web 服务器,使用阻塞 IO 来处理请求,那么每一个请求都需要开启一个新的线程;但是如果使用了非阻塞 IO,基本上一个小小线程池就够用了,因为不会发生阻塞,每一个线程都能够高效利用。

13.2 redis线程模型

首先一点,Redis 是单线程。单线程如何解决高并发问题的?
实际上,能够处理高并发的单线程应用不仅仅是 Redis,除了 Redis 之外,还有 NodeJS、Nginx 等等也是单线程。
Redis 虽然是单线程,但是运行很快,主要有如下几方面原因:

  1. Redis 中的所有数据都是基于内存的,所有的计算也都是内存级别的计算,所以快。
  2. Redis 是单线程的,所以有一些时间复杂度高的指令,可能会导致 Redis 卡顿,例如 keys。
  3. Redis 在处理并发的客户端连接时,使用了非阻塞 IO。

在使用非阻塞 IO 时,有一个问题,就是线程如何知道剩下的数据来了?

这里就涉及到一个新的概念叫做多路复用,本质上就是一个事件轮询 API。

  1. Redis 会给每一个客户端指令通过队列来排队进行顺序处理。
  2. Redis 做出响应时,也会有一个响应的队列。

十四. 通信协议

Redis 通信使用了文本协议,文本协议比较费流量,但是 Redis 作者认为数据库的瓶颈不在于网络流量,而在于内部逻辑,所以采用了这样一个费流量的文本协议。
这个文本协议叫做 Redis Serialization Protocol,简称 RESP

Redis 协议将传输的数据结构分为 5 种最小单元,单元结束时,加上回车换行符 \r\n。

  1. 单行字符串以 + 开始,例如 +javaboy.org\r\n
  2. 多行字符串以 $ 开始,后面加上字符串长度,例如$11\r\njavaboy.org\r\n
  3. 整数值以: 开始,例如 :1024\r\n
  4. 错误消息以 - 开始
  5. 数组以 * 开始,后面加上数组长度。

需要注意的是,如果是客户端连接服务端,只能使用第 5 种。

14.1准备工作

做两件事情:

为了方便客户端连接 Redis,我们关闭 Redis 的保护模式和密码校验(在 redis.conf 文件中)

protected no
# requirepass xxxx

配置完成后,重启 Redis。

通过 Socket+RESP 来定义两个最最常见的命令 set 和 get。

14.2 demo
public class RESPTest {

    private Socket socket;

    public RESPTest() {
        try {
            socket = new Socket("192.168.13.128", 6379);
        } catch (IOException e) {
            System.out.println("redis连接异常");
            e.printStackTrace();
        }
    }

    /**
     *  执行 Redis 中的 set 命令 [set,key,value]
     * @param type
     * @param key
     * @param value
     * @return
     * @throws IOException
     */
    public String set(String type, String key, String value) throws IOException {
        StringBuilder sb = new StringBuilder();

        sb.append("*3")
            .append("\r\n")
            .append("$")
            .append(type.length())
            .append("\r\n")
            .append(type)
            .append("\r\n")
            .append("$")
            .append(key.getBytes().length)
            .append("\r\n")
            .append(key)
            .append("\r\n")
            .append("$")
            .append(value.getBytes().length)
            .append("\r\n")
            .append(value)
            .append("\r\n");
        byte[] bytes = new byte[1024];
        socket.getOutputStream().write(sb.toString().getBytes());
        socket.getInputStream().read(bytes);
        System.out.println("sb.toString() = " + sb.toString());
        return new String(bytes);
    }

    /**
     * Redis 中的 get 命令 [get,key]
     * @param key
     * @return
     * @throws IOException
     */
    public String get(String key) throws IOException {
        StringBuilder sb = new StringBuilder();

        sb.append("*2")
            .append("\r\n")
            .append("$")
            .append("get".length())
            .append("\r\n")
            .append("get")
            .append("\r\n")
            .append("$")
            .append(key.getBytes().length)
            .append("\r\n")
            .append(key)
            .append("\r\n");
        socket.getOutputStream().write(sb.toString().getBytes());
        byte[] bytes = new byte[1024];
        socket.getInputStream().read(bytes);
        System.out.println("sb.toString() = " + sb.toString());
        return new String(bytes);
    }

    public static void main(String[] args) throws IOException {
        RESPTest jedisTest = new RESPTest();
        String set = jedisTest.set("set", "测试", "v1");
        String get = jedisTest.get("测试");
        System.out.println("set = " + set);
        System.out.println("get = " + get);
    }
}

十五. 持久化

15.1 快照方式(RDB)
15.1.1 原理

Redis 使用操作系统的多进程机制来实现快照持久化:Redis 在持久化时,会调用 glibc 函数 fork 一个子进程,然后将快照持久化操作完全交给子进程去处理,而父进程则继续处理客户端请求。在这个过程中,子进程能够看到的内存中的数据在子进程产生的一瞬间就固定下来了,再也不会改变,也就是为什么 Redis 持久化叫做 快照。

是redis默认的持久化方式,这种方式就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为dump.rdb。

15.1.2 配置

在 Redis 中,默认情况下,快照持久化的方式就是开启的。
如果想要关闭快照方式

   	save ""
   	#save 900 1
	#save 300 10
	#save 60 10000

默认情况下会产生一个 dump.rdb 文件,这个文件就是备份下来的文件。当 Redis 启动时,会自动的去加载这个 rdb 文件,从该文件中恢复数据。

具体配置:

save 900 1  #每900 秒内如果有一个键被修改,则进行快照
save 300 10  #每300秒内如果有10个键被修改,则进行快照
save 60 10000  #每60秒内如果有10000个键被修改,则进行快照
stop-writes-on-bgsave-error yes  # 快照执行出错后,是否继续处理客户端的写命令
rdbcompression yes  #是否对快照文件进行压缩
dbfilename dump.rdb  # 表示生成的快照文件名
dif ./  # 表示生成的快照文件位置
15.1.3 备份流程
  1. 在 Redis 运行过程中,我们可以主动向 Redis 发送一条 save 命令来创建一个快照。但是需要注意,save 是一个阻塞命令,Redis 在收到 save 命令开始处理备份操作之后,在处理完成之前,将不再处理其他的请求。其他命令会被挂起,所以 save 使用的并不多。
  2. 我们一般可以使用 bgsave,bgsave 会 fork 一个子进程去处理备份的事情,不影响父进程处理客户端请求。
  3. 我们定义的备份规则,如果有规则满足,也会自动触发 bgsave。
  4. 另外,当我们执行 shutdown 命令时,也会触发 save 命令,备份工作完成后,Redis 才会关闭。
  5. 用 Redis 搭建主从复制时,在 从机连上主机之后,会自动发送一条 sync 同步命令,主机收到命令之后,首先执行 bgsave 对数据进行快照,然后才会给从机发送快照数据进行同步。
15.1.4 优缺点

优点:

  • 一旦采用该方式,那么你的整个Redis数据库将只包含一个文件,这样非常方便进行备份
  • RDB 可以最大化 Redis 的性能:父进程在保存RDB文件时唯一要做的就是 fork 出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无须执行任何磁盘 I/O 操作。
  • client 也可以使用save或者bgsave命令通知redis做一次快照持久化。save操作是在主线程中保存快照的,由于redis是用一个主线程来处理所有client的请求,这种方式会阻塞所有client请求。所以不推荐使用。推荐使用bgsave

缺点:

  • 如果你需要尽量避免在服务器故障时丢失数据,做不到 实时持久化/秒级持久化,那么 RDB 不适合你。 虽然 Redis 允许你设置不同的保存点(save point)来控制保存 RDB 文件的频率, 但是, 因为RDB 文件需要保存整个数据集的状态, 所以它并不是一个轻松的操作。 因此你可能会至少 5 分钟才保存一次 RDB 文件。 在这种情况下, 一旦发生故障停机, 你就可能会丢失好几分钟的数据。
  • 每次保存 RDB 的时候,Redis 都要 fork() 出一个子进程,并由子进程来进行实际的持久化工作。 在数据集比较庞大时, fork() 可能会非常耗时,造成服务器在某某毫秒内停止处理客户端; 如果数据集非常巨大,并且 CPU 时间非常紧张的话,那么这种停止时间甚至可能会长达整整一秒。(CPU的阻塞)
15.2 AOF

与快照持久化不同,AOF 持久化是将被执行的命令追加到 aof 文件末尾,在恢复时,只需要把记录下来的命令从头到尾执行一遍即可。

默认情况下,AOF 是没有开启的。我们需要手动开启:

appendonly yes  #开始aof持久化方式
appendfilename "appendonly.aof"  #aof文件名称
appendfsync everysec  #备份的时机,每秒备份一次
no-appendfsync-on-rewrite no  # 表示 aof 文件在压缩时,是否还继续进行同步操作
auto-aof-rewrite-percentage 100 # 表示当目前 aof 文件大小超过上一次重写时的 aof 文件大小的百分之多少的时候,再次进行重写
auto-aof-rewrite-min-size 64mb  # 如果之前没有重写过,则以启动时的 aof 大小为依据,同时要求 aof 文件至少要大于 64M
15.3 两种持久化方式的区别
  1. aof文件比rdb更新频率高,优先使用aof还原数据。

  2. aof比rdb更安全也更大

  3. rdb性能比aof好

  4. 如果两个都配了优先加载AOF

15.4 相关问题

如果在持久化期间突然宕机会怎样?

取决于aof日志sync属性的配置,如果不要求性能,在每条写指令时都sync一下磁盘,就不会丢失数据。但是在高性能的要求下每次都sync是不现实的,一般都使用定时sync,比如1s1次,这个时候最多就会丢失1s的数据。

bgsave的原理是什么?

fork和cow。fork是指redis通过创建子进程来进行bgsave操作,cow指的是copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。

十六.Redis事物

正常来说,一个可以商用的数据库往往都有比较完善的事务支持,Redis 当然也不例外。相对于关系型数据库中的事务模型,Redis 中的事务要简单很多。因为简单,所以 Redis 中的事务模型不太严格,所以我们不能像使用关系型数据库中的事务那样来使用 Redis。

在关系型数据库中,和redis相互对应的为:

begin—>multi
commit—>exec
rollback—>discard

关于原子性:

注意,Redis 中的事务并不能算作原子性。它仅仅具备隔离性,也就是说当前的事务可以不被其他事务打断。

由于每一次事务操作涉及到的指令还是比较多的,为了提高执行效率,我们在使用客户端的时候,可以通过pipeline 来优化指令的执行。

Redis 中还有一个 watch 指令,watch 可以用来监控一个 key,通过这种监控,我们可以确保在 exec之前,watch 的键的没有被修改过,如果在exec之前该监控的key被修改了,那么该事物是不会提交成功的.

	public static void main(String[] args) {
        System.out.println(saveMoney("zs", 100));
    }

    public static Integer saveMoney(String userId, Integer money) {
        Jedis jedis = new Jedis("192.168.146.128", 6379);
        String s = jedis.get(userId);
        if (s == null) {
            jedis.set(userId, String.valueOf(0));
        }
        String s1 = jedis.get(userId);
        //开启监控
        jedis.watch(userId);
        Transaction multi = jedis.multi();
        int v = Integer.parseInt(s1) + money;
        multi.set(userId, String.valueOf((v)));

        List<Object> exec = multi.exec();
        if (exec != null) {

            return v;
        }
       
        return null;
    }

十七 redis主从复制

17.1 CAP

在分布式环境下,CAP 原理是一个非常基础的东西,所有的分布式存储系统,都只能在 CAP 中选择两项实现。

分布式技术原理

C:consistent 一致性
A:availability 可用性
P:partition tolerance 分布式容忍性

在一个分布式系统中,这三个只能满足两个:在一个分布式系统中,P 肯定是要实现的,c 和 a 只能选择其中一个。大部分情况下,大多数网站架构选择了 ap。

在 Redis 中,实际上就是保证最终一致。

Redis 中,当搭建了主从服务之后,如果主从之间的连接断开了,Redis 依然是可以操作的,相当于它满足可用性,但是此时主从两个节点中的数据会有差异,相当于牺牲了一致性。但是 Redis 保证最终一致,就是说当网络恢复的时候,从机会追赶主机,尽量保持数据一致。

17.2 主从复制

主从复制可以在一定程度上扩展 redis 性能,redis 的主从复制和关系型数据库的主从复制类似,从机能够精确的复制主机上的内容。实现了主从复制之后,一方面能够实现数据的读写分离,降低master的压力,另一方面也能实现数据的备份。

17.2.1 配置

准备3个redis实例

192.168.72.128:6379  主
192.168.72.128:6380  从
192.168.72.128:6381  从
  1. 将 redis.conf 文件更名为 redis6379.conf,方便我们区分,然后把 redis6379.conf 再复制两份,分别为 redis6380.conf 和 redis6381.conf。如下:

    在这里插入图片描述

  2. 打开 redis6379.conf,将如下配置均加上 6379,(默认是6379的不用修改),如下

    port 6379
    pidfile /var/run/redis_6379.pid
    logfile "6379.log"
    dbfilename dump6379.rdb
    appendfilename "appendonly6379.aof"
    
  3. 同理,分别打开 redis6380.conf 和 redis6381.conf 两个配置文件,将第二步涉及到 6379 的分别改为 6380 和 6381。

  4. 配置主从关系,另外因为我们的redis是开启保护模式和密码校验的因此要在6380和6381的配置里加上密码校验

      slaveof 127.0.0.1 6379  #配置主服务器ip 端口
      masterauth 123     #配置主服务器密码
    
  5. 启动3个redis实例

     redis-server redis6379.conf
     redis-server redis6380.conf
     redis-server redis6381.conf
    
  6. 进入控制台

    reids-cli -p 6379 -a 123
    reids-cli -p 6380 -a 123
    reids-cli -p 6381 -a 123
    

此时redis主从复制就算是搭建成功了

可以通过以下命令查看当前实例的状态

127.0.0.1:6379> INFO replication  
# Replication
role:master  #角色为主节点
connected_slaves:2
slave0:ip=127.0.0.1,port=6380,state=online,offset=392,lag=1
slave1:ip=127.0.0.1,port=6381,state=online,offset=392,lag=0
master_replid:a7a352299d4857c363d62d8d02a4fbee53a2e171
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:392
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:392


127.0.0.1:6380> info replication
# Replication
role:slave  #角色为从节点
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:6
master_sync_in_progress:0
slave_repl_offset:1358
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:a7a352299d4857c363d62d8d02a4fbee53a2e171
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1358
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:1358

此时,我们在主机中存储一条数据,在从机中就可以 get 到这条数据了。

17.2.2 注意事项
  1. 如果主机已经运行了一段时间了,并且了已经存储了一些数据了,此时从机连上来,那么从机 会将主机上所有的数据进行备份,而不是从连接的那个时间点开始备份。
  2. 配置了主从复制之后,主机上可读可写,但是从机只能读取不能写入(可以通过修改redis.conf中 slave-read-only 的值让从机也可以执行写操作)。
  3. 在整个主从结构运行过程中,如果主机不幸挂掉,重启之后,他依然是主机,主从复制操作也能够继续进行。
17.2.3 复制原理

每一个 master 都有一个 replication ID,这是一个较大的伪随机字符串,标记了一个给定的数据集。每个 master 也持有一个偏移量,master 将自己产生的复制流发送给 slave 时,发送多少个字节的数据,自身的偏移量就会增加多少,目的是当有新的操作修改自己的数据集时,它可以以此更新 slave 的状态。复制偏移量即使在没有一个 slave 连接到 master 时,也会自增,所以基本上每一对给定的Replication ID, offset 都会标识一个 master 数据集的确切版本。当 slave 连接到 master 时,它们使用PSYNC 命令来发送它们记录的旧的 master replication ID 和它们至今为止处理的偏移量。通过这种方式,master 能够仅发送 slave 所需的增量部分。但是如果 master 的缓冲区中没有足够的命令积压缓冲记录,或者如果 slave 引用了不再知道的历史记录(replication ID),则会转而进行一个全量重同步:在这种情况下,slave 会得到一个完整的数据集副本,从头开始(参考redis官网)。

简单来说,就是以下几个步骤:

  1. slave 启动成功连接到 master 后会发送一个 sync 命令。
  2. Master 接到命令启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令。
  3. 在后台进程执行完毕之后,master 将传送整个数据文件到 slave,以完成一次完全同步。
  4. 全量复制:而 slave 服务在接收到数据库文件数据后,将其存盘并加载到内存中。
  5. 增量复制:Master 继续将新的所有收集到的修改命令依次传给 slave,完成同步。
  6. 但是只要是重新连接 master,一次完全同步(全量复制)将被自动执行。
17.2.4 接力搭建

上面的搭建模式是这样的一主二从
在这里插入图片描述
也可以这样搭建
在这里插入图片描述
将6381的主节点配置改为6380即可(修改6380.conf)

  slaveof 127.0.0.1 6380  #配置主服务器ip 端口
  masterauth 123     #配置主服务器密码
17.3 哨兵模式

主从复制存在问题:

当主机宕机时,就会发生群龙无首的情况,如果在主机宕机时,能够从从机中选出一个来充当主机,那么就不用我们每次去手动重启主机了,这就涉及到一个新的话题,那就是哨兵模式。

所谓的哨兵模式,其实并不复杂,我们还是在我们前面的基础上来搭建哨兵模式。假设现在我的master 是 6379,两个从机分别是 6380 和 6381,两个从机都是从 6379 上复制数据。先按照上文的步骤,我们配置好一主二仆,然后在 redis 目录下打开 sentinel.conf 文件,做如下配置

sentinel monitor mymaster 127.0.0.1 6379 1 #哨兵所监控的主节点ip和port

其中 mymaster 是给要监控的主机取的名字,随意取,后面是主机地址,最后面的 2 表示有多少个sentinel 认为主机挂掉了,就进行切换(我这里只有一个,因此设置为1)。好了,配置完成后,输入如下命令启动哨兵:

redis-sentinel sentinel.conf

然后启动我们的一主二仆架构,启动成功后,关闭 master,观察哨兵窗口输出的日志,如下:
在这里插入图片描述
可以看到, 6379 挂掉之后,redis 内部重新举行了选举,6381 重新上位。此时,如果 6379重启,也会变成slave

17.4 Jedis 操作哨兵模式

准备工作

  1. 所有的实例均配置 masterauth (在 redis.conf 配置文件中)
  2. 所有实例均需要配置绑定地址:bind 192.168.72.128

另外,哨兵配置的时候,监控的 master 也不要直接写 127.0.0.1,按如下方式写

sentinel monitor mymaster 192.168.72.128 6381 1
		JedisPoolConfig config = new JedisPoolConfig();
        //连接池最大连接数
        config.setMaxTotal(1000);
        //最大空闲数
        config.setMaxIdle(300);
        //最大等待时间
        config.setMaxWaitMillis(30000);
        //在空闲时检查有效性
        config.setTestOnBorrow(true);
        Set<String> set = new HashSet<>();
        set.add("192.168.120.128:26379");
        String master = "mymaster";
        JedisSentinelPool sentinelPool = new JedisSentinelPool(master, set, config, "123");
        Jedis jedis=null;
        while (true) {
            try {
                jedis = sentinelPool.getResource();
                String name = jedis.get("name");
                System.out.println("name = " + name);

                Thread.sleep(5000);

            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (jedis != null) {
                    jedis.close();
                }
            }
        }
17.5 spring boot操作哨兵

引入依赖

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

配置

spring:
 redis:
  password: 123
  timeout: 5000
  sentinel:
   master: mymaster
   nodes: 192.168.120.128:26379

测试代码

@SpringBootTest
class SentinelApplicationTests {
  @Autowired
  StringRedisTemplate redisTemplate;
  @Test
  void contextLoads() {
    while (true) {
      try {
        String k1 = redisTemplate.opsForValue().get("name");
        System.out.println(k1);
     } catch (Exception e) {
     } finally {
        try {
          Thread.sleep(5000);
       } catch (InterruptedException e) {
          e.printStackTrace();
       }
     }
   }
 }
}

十八 Redis集群

18.1 集群原理

Redis 集群运行原理如下:

  1. 所有的 Redis 节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽
  2. 节点的 fail 是通过集群中超过半数的节点检测失效时才生效
  3. 客户端与 Redis 节点直连,不需要中间 proxy 层,客户端不需要连接集群所有节点,连接集群中任
    何一个可用节点即可
  4. Redis-cluster 把所有的物理节点映射到 [0-16383]slot 上,cluster (簇)负责维护 node<->slot<->value 。Redis 集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个key-value 时,Redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,Redis 会根据节点数量大致均等的将哈希槽映射到不同的节点
18.2 投票

投票过程是集群中所有 master 参与,如果半数以上 master 节点与 master 节点通信超过 cluster-node-timeout 设置的时间,认为当前 master 节点挂掉。

18.3 怎么样判定节点不可用
  1. 如果集群任意 master 挂掉,且当前 master 没有 slave.集群进入 fail 状态,也可以理解成集群的 slot映射 [0-16383] 不完整时进入 fail 状态。
  2. 如果集群超过半数以上 master 挂掉,无论是否有 slave,集群进入 fail 状态,当集群不可用时,所有对集群的操作做都不可用,收((error) CLUSTERDOWN The cluster is down)错误。
18.4 集群搭建

注意:在redis5.x版本之前搭建集群需要ruby环境,5.x后可直接搭建 这里用的是5.0.7版本,因此不需要ruby环境

#安装ruby环境
yum install ruby
yum install rubygems

首先我们对集群做一个简单规划,假设我的集群中一共有三个节点,每个节点一个主机一个从机,这样我一共需要 6 个 Redis 实例。首先创建 redis-cluster 文件夹,在该文件夹下分别创建 7001、7002、7003、7004、7005、7006 文件夹,用来存放我的 Redis 配置文件,如下:

在这里插入图片描述

将redis.conf复制6份分别放在每个相应的文件夹下

修改7001的配置

daemonize yes #实例在后台运行
cluster-enabled yes #开启集群
cluster-config-file nodes-7001.conf
cluster-node-timeout 15000 #超时时间
port 7001 #端口
appendonly yes #开启aof
bind 192.168.120.128
requirepass  123  #开启登录密码校验
masterauth   123  #主从密码校验

其他文件夹下的配置改为想应的数字即可,然后启动全部实例
在这里插入图片描述
创建集群

redis-cli --cluster create 192.168.120.128:7001 192.168.120.128:7002 192.168.120.128:7003 192.168.120.128:7004 192.168.120.128:7005 192.168.120.128:7006 -a 123 --cluster-replicas 1  #表示开启一个副本(3主3从)

在这里插入图片描述
这样集群就搭建好了 可以登录任一实例查看集群信息

 redis-cli -p 7001 -h 192.168.120.128 -c   #  -c表示以及群方式连接
 CLUSTER INFO  #查看单个集群信息
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:6
cluster_current_epoch:6
cluster_my_epoch:1
cluster_stats_messages_ping_sent:5760
cluster_stats_messages_pong_sent:5730
cluster_stats_messages_sent:11490
cluster_stats_messages_ping_received:5725
cluster_stats_messages_pong_received:5760
cluster_stats_messages_meet_received:5
cluster_stats_messages_received:11490
#查看整个集群信息
CLUSTER NODES 

77b78f15bf70966e6d94e63320d06c31dc0dd1de 192.168.120.128:7004@17004 slave 210361b83deac43ac1b7d1390eddf4dc09245d3b 0 1600265830000 4 connected
ab579c03cc6d811fcb682300da13179a92d4b3bd 192.168.120.128:7001@17001 myself,master - 0 1600265829000 1 connected 0-5460
210361b83deac43ac1b7d1390eddf4dc09245d3b 192.168.120.128:7002@17002 master - 0 1600265830989 2 connected 5461-10922
1056911c7fd77601ec9283fd0f41eb3f9b88fb5c 192.168.120.128:7005@17005 slave 5ff75c7a8c971097daf990485b165751cd19203f 0 1600265831000 5 connected
d85314780426fbd42b38392e46f3419f966cacca 192.168.120.128:7006@17006 slave ab579c03cc6d811fcb682300da13179a92d4b3bd 0 1600265829000 6 connected
5ff75c7a8c971097daf990485b165751cd19203f 192.168.120.128:7003@17003 master - 0 1600265831996 3 connected 10923-16383
18.5 动态添加节点

复制7006文件夹改名7007,将7007.conf里的数字7006全部改为7007

启动7007

redis-server ../redis-cluster/7007/redis7007.conf

将7007添加到集群中

 redis-cli --cluster add-node 192.168.120.128:7007 192.168.120.128:7006 -a 123

在这里插入图片描述
成功加进集群后,我们可以看到 新加进去的7007和其他master不一样的是没有分到hash槽的,这样在以后读取数据时,就不会从7007这个节点去读取,因此我们要重新给7007节点分配hash槽,因为之前的16384个slots已经全部分配完了,因此想要给7007加slots 必须要从之前的任一(或全部)节点分割出指定的slots

redis-cli --cluster reshard 192.168.120.128:7001 --cluster-from ab579c03cc6d811fcb682300da13179a92d4b3bd --cluster-to bb88d4b0f35d5d35126913f73de5f81910b3872b --cluster-slots 1024 -a 123

表示从7001节点分割出1024个slot给7007节点

在这里插入图片描述

18.6 删除节点

在删除节点前,首先要将该节点的hash槽给分配出去 否则时删除不掉的,会导致hash slote不完整

redis-cli --cluster reshard 192.168.120.128:7001 --cluster-from bb88d4b0f35d5d35126913f73de5f81910b3872b --cluster-to ab579c03cc6d811fcb682300da13179a92d4b3bd --cluster-slots 1024 -a 123

删除节点

redis-cli --cluster del-node 192.168.120.128:7007 bb88d4b0f35d5d35126913f73de5f81910b3872b #要删除的节点的
18.7 Jedis操作cluster
 public static void main(String[] args) {
        Set<HostAndPort> node=new HashSet<>();
        node.add(new HostAndPort("192.168.120.128",7001));
        node.add(new HostAndPort("192.168.120.128",7002));
        node.add(new HostAndPort("192.168.120.128",7003));
        node.add(new HostAndPort("192.168.120.128",7004));
        node.add(new HostAndPort("192.168.120.128",7005));
        node.add(new HostAndPort("192.168.120.128",7006));
        GenericObjectPoolConfig config = new GenericObjectPoolConfig();
        //连接池最大连接数
        config.setMaxTotal(1000);
        //最大空闲数
        config.setMaxIdle(300);
        //最大等待时间
        config.setMaxWaitMillis(30000);
        //在空闲时检查有效性
        config.setTestOnBorrow(true);
        JedisCluster jedisCluster=new JedisCluster(node,10000,5,3,"123",config);
        String set = jedisCluster.set("name", "zs");
        System.out.println("set = " + set);
        String name = jedisCluster.get("name");
        System.out.println("name = " + name);
    }

十九 Stream

19.1 基本介绍

Redis5.0带来了Stream类型。从字面上看是流类型,但其实从功能上看,应该是Redis对消息队列(MQ,Message Queue)的完善实现。用过Redis做消息队列的都了解,基于Reids的消息队列实现有很多种,例如:

  • PUB/SUB,订阅/发布模式
  • 基于List的 LPUSH+BRPOP 的实现
  • 基于Sorted-Set的实现

Redis5.0中发布的Stream类型,也用来实现典型的消息队列。该Stream类型的出现,几乎满足了消息队列具备的全部内容,包括但不限于:

  • 消息ID的序列化生成
  • 消息遍历
  • 消息的阻塞和非阻塞读取
  • 消息的分组消费
  • 未完成消息的处理
  • 消息队列监控

在Stream种,有一个消息链表,所有加入链表中的消息都会被串联起来. 每一条消息都有一个唯一的ID,还有对应的消息内容,也就是键值对.

一个Stream可以有多个消费者,每一个消费者都有一个游标,这个游标根据消息的消费情况在链表上移动,多个消费者之间相互独立,互不影响.

19.2 基本命令

xadd 添加消息

xadd user * name zs age 20 # *,表示由Redis生成消息ID

在这里插入图片描述

xdel 删除消息

XDEL emp 1600273546214-0

在这里插入图片描述

xrange 获取消息列表

xrange user - +   # 表示从ID最小到最大
xrange user - 1600273302219-0 

在这里插入图片描述

del 删除stream

del emp

在这里插入图片描述
xlen 获取消息列表的消息个数

xlen user

在这里插入图片描述

xread 消费消息

xread count 1 streams user 0-0 # 从头部开始读取

在这里插入图片描述

xread block 0 count 1 streams user $ # 阻塞式的从尾部开始读取 0表示阻塞无限时长,直到尾部有新消息进来

在这里插入图片描述
XGROUP 创建消费者组

XGROUP create user g1 0-0 #创建一个从头部消费的消费者组 取名g1
XGROUP create user g2 $   #创建一个从尾部消费的消费者组 取名g2

消费g1里的消息 每次消费一条 g为给消费者取得名字

XREADGROUP group g1 g count 1 streams user > 

阻塞式的消费g2里的消息 每次消费一条

XREADGROUP group g2 g count 1 block 0 streams user >

二十.相关问题

20.1 过期策略

Redis中所有的key都可以设置过期时间

所有被设置过期时间的key都会放到一个独立的字典中 ,删除这些key有3种不同的策略

  1. 定时任务定期删除,redis默认每秒进行10次过期扫描,每次从字典中随机拿出20个key,删除这20个key中已经过期的key,如果删除key的比例超过1/4,则再拿出20个key删除,以此类推.
  2. 当客户端访问此key的时候再去检查过期,如果过期则删除.
  3. 主动删除:当内存超过maxmemory限定时,触发主动清理策略,该策略由启动参数的配置决定

如果时主从架构,从机是不会进行过期扫描的,主机的key过期后会自动同步到从机上去

20.2 淘汰策略

因为Redis是基于内存的,所以当Redis所使用的内存超过物理内存限制的时候,内存中的数据就会和磁盘产生频繁的交换,这种交换会使redis的性能急剧下降,所以在实际开发中,是不允许出现这中交换(swmp)行为的.

所以redis提供了几种淘汰策略:

通过修改redis.conf中

 maxmemory-policy noeviction
  1. volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。
  2. volatile-ttl:从已设置过期时间的数据集中挑选过期时间最小的数据淘汰。
  3. volatile-random:从已设置过期时间的数据集中任意选择数据淘汰。
  4. volatile-lfu:从已设置过期时间的数据集挑选使用频率最低的数据淘汰。
  5. allkeys-lru:从所有数据集中挑选最近最少使用的数据淘汰
  6. allkeys-lfu:从所有数据集中挑选使用频率最低的数据淘汰。
  7. allkeys-random:从所有数据集(server.db[i].dict)中任意选择数据淘汰
  8. no-enviction(驱逐):禁止驱逐数据,这也是默认策略。意思是当内存不足以容纳新入数据时,新写入操作就会报错,请求可以继续进行,线上任务也不能持续进行,采用no-enviction策略可以保证数据不被丢失。(写操作停止,读和删除正常)
20.3 lazy free (异步删除)

Redis是一个单线程程序,如果一次要删除一个很大的key的话,可能会造成redis的卡顿.

UNLINK user  #异步删除 (redis4.0后有效)
FLUSHALL async
FLUSHDB async

也可以在redis.conf中配置异步相关的

lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes
replica-lazy-flush yes
20.4 缓存穿透

什么是缓存穿透?如何避免?

一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。一些恶意的请求会故意查询不存在的key,请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透

如何避免?

  1. 对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert了之后清理缓存。

  2. 对一定不存在的key进行过滤。可以把所有的可能存在的key放到一个大的Bitmap中,查询时通过该bitmap过滤。

20.5 缓存雪崩

什么是缓存雪崩?何如避免?

当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,会给后端系统带来很大压力。导致系统崩溃。

如何避免?

  1. 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。

  2. 做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期

  3. 不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。

20.6 缓存击穿

什么是缓存击穿,如何避免?

就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。

如何避免?

  • 解决方式也很简单,可以将热点数据设置为永远不过期;或者基于 redis or zookeeper 实现互斥锁,等待第一个请求构建完缓存之后,再释放锁,进而其它请求才能通过该 key 访问数据。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值