1.1 介绍:基于内存的可持久化的非关系型数据库。

1.1.1.优势:

数据在内存中, 读取速度快(高性能),操作缓存承受的请求远大于直接访问数据库(高并发)
支持丰富的数据类型, 支持string, list, set,sorted set(zset), hash
操作是原子性。
丰富的特性, 用于缓存, 消息(发布/订阅), 按key设置过期时间。

1.1.2.应用场景:

— 缓存
当前端要多次对数据库查询, 并且查询的结果是变化很少, 可以使用缓存。(一些热点信息)
存放一些登录信息。
— 数据存储:
redis提供硬件持久化机制, 所以保证了数据的完整性和安全性, 可以CRUD

1.1.3.Map也可以做缓存, 两者区别:

使用map做缓存, 是本地缓存, 简单快速, 但是如果项目是分布式的话, 需要每一个工程都缓存一份, 缓存一致性无法解决。并且缓存数量并不大, 而且没有过期策略。
使用redis做缓存, 是分布式缓存, 具有一致性。但是需要redis服务的可靠性, 缓存量大, 可以持久化,有过期策略, 提供了丰富的api。
PS: 实际业务中都是采用多级缓存,本地缓存+分布式缓存一起使用。

1.1.4 Memcache和redis区别:

Memcache: key(250字节),value(1M)有长度限制, 并且只有简单的数据类型, 不支持数据持久化, 不支持主从, 不支持分片。
redis: 有丰富的数据类型, 支持数据持久化, 支持主从, 支持分片。

1.2 数据类型

redis支持五种数据类型, String(字符串), hash(哈希), list(列表), set(集合), zset(sorted set)有序集合

1.2.1 String

string是redis最基本的数据类型, 一个key对应一个value. string类型可以包含任何数据, 即便是图片或者是序列化对象。string类型最大可以存储512M。
实际应用场景:
可以做缓存,可以做计数器(incr命令),共享用户Session.

  1. set key value 设定指定的key值。
  2. get key value 获取指定的key值。
  3. setnx(set if not exists) 在指定key不存在时候, 设定指定值。
  4. msetnx key1 value1 key2 value2... key不存在, 设置多个指定值
  5. setrange 用指定的字符串覆盖给定key所存储的字符串。 ex: setrange strTest 5 "Redis"
  6. getrange key start end 获取指定字符串的子字符串。 ex: getrange foo 0 5
  7. getset key value 设定指定key值, 并返回key的值。
  8. 如果key不存在, 那么返回的是null
  9. 如果key值存在, 返回的是旧值。 ex: set foo "test1" -> getset foo "test2" 返回结果是test1
  10. mset key1 value1 key2 value2... 同时设定多个键值对。
  11. mget key1 key2 返回给定key的值。
  12. strlen key 返回key存储的字符串值的长度。 ex: strlen strTest
  13. append key value 为指定的key追加值。
  14. key存在: value追加在key的末尾。
  15. key不存在: set命令一致。
  16. // 设置值并设置过期时间
  17. setex key timeout value 为指定的key设定值及其过期时间。 ex: setex foo 60 "foo"
  18. 等价于: set foo "foo" && expire foo 60
  19. psetex key millisecond value 命令与setex相似, 但是是以毫秒为单位设置key的生存时间。
  20. // 数字增加相关
  21. incr key key中存储的数值加一。
  22. decr key key中存储的数值减一。
  23. incrby key num. key存储的值加上给定的增量值。 ex: incrby strTest 10
  24. decrby key num. key存储的值减去给定的增量值。 ex: decrby strTest 10
  25. incrbyfloat key floatnum. key存储的值加上给定的浮点增量值。 ex: incrbyfloat strTest 1.1.

1.2.2 Hash

hash是一个String类型的键值对集合, 特别适合用于对象存储。

hset: 设置hash filed为指定值。如果key不存在, 则先新建。
ex: 设置hash表, 名字为use, 然后添加字段, 对应的值为halo.
        hset user name halo    
hmset 同时设置hash的多个字段(但是其实hset也完全可以实现功能)
ex: hmset user name "halo" age 10...
hsetnx 设置指定值,如果字段field不存在, 那么新建. 如果存在, 返回0.
ex: hsetnx user class xxx (成立)
---------------------------------
hget 获取存储在hash表中指定字段的值。    ex: hget user name
hmget 获取全部指定的字段。    ex: hmget user name age 
hgetall 获取某个hash表中全部field以及对应的value        ex: hgetall user
hvals 返回hash表中所有的value.     ex: hvals user 
hkeys 返回hash表中所有的key。      ex: hkeys user
hexists 测试指定字段是否存在.     ex: hexists user name
hlen     返回指定hash的filed数量    ex: hlen user
--------------------------------
hdel 删除指定hash的filed值        ex: hdel user name
hincrby     给定filed加上给定的值        ex: hincrby user age 10

1.2.3 List

是每一个元素都是string类型的双向链表,我们可以通过push,pop操作从链表的头部和尾部添加或删除元素,所以list既是栈,也是队列。
实用场景:
异步消息队列(rpush, lpop,blpop指令)

lpush     在key对应的list头部添加字符串元素    ex: lpush listTest redis good
rpush     在key对应的list尾部添加元素                ex: rpush listTest mac client
linsert 在key对应的list的特定位置前后添加字符串。 
    ex: linsert listTest before redis mc 在redis之前添加元素mc
          linsert listTest after  redis mc2
lset         设置list中指定下标的元素值.     ex: lset listTest2 3 four4
lrem         从key对应list中删除n个和value相同的元素(n > 0 从头开始; n = 0 全部删除; n < 0 从尾开始删除)
    ex: lrem listTest3 3 one     从头部开始算, 删除三个one。
          lrem listTest3 -1 one    从尾部开始算, 删除一个one。
            lrem listTest3 0 one     删除全部的one
ltrim     保留指定范围内的数据。
    ex: ltrim listTest3 2 7        保留下标是2到7的元素
          ltrim listTest3 2 -1     保留下标2到最后一个的元素
lpop         从list的头部删除元素,并返回删除元素    ex: lpop listTest3 
blpop        从list头部返回数据, 如果没有陷入阻塞。    ex: blpop listTest3 10
rpop        从list的尾部删除元素,并返回元素
lindex     返回index位置的元素    ex: lindex listTest3 4
llen         返回list的长度

1.2.4 Set

set是string类型的无序集合, 通过哈希表实现, 添加, 删除, 查找复杂度都是O(1).

sadd     向指定的set中添加元素, 不允许同一个元素重复存在,已存在的元素添加不成功。    ex: sadd setTest1 redis
srem    删除指定元素的值                ex: srem setTest1 redis
spop    随机返回并删除set中一个元素    ex: spop setTest1
sdiff 返回两个集合差集(以写在前面的key为标准)    
    setTest1 -> one two three five/ setTest2 one two four 
  sdiff setTest1 setTest2        返回结果: three five
  sdiff setTest2 setTest1        返回结果: four 
sdiffstore 返回所有给定的key与第一个key的差集, 并将结果存到另外一个key中。
    setTest1 -> one two three five/ setTest2 one two four 
    sdiff result setTest1 setTest2    -> result中是three
sinter    返回给定key的交集
    sinter setTest1 setTest2     --> 结果是 one two
sinterstore 返回给定key的交集,并保存到另外一个key中
    sinterstore setResult2 setTest1 setTest2
sunion     返回给定key的并集
    sunion setTest1 setTest2
  sunionstore setTest1 setTest2
smove     从第一个set中移动数据到第二个set
    smove setResult setResult2 three
// --------------------------------
scard     返回set中元素个数        ex: scard setResult2
smembers 查看set中全部元素        ex:  smembers setTest1
sismember 测试数据是不是set的元素
    sismember setResult2 three
srandmember    随机返回一个元素    ex: srandmember setResult2

1.2.5 Zset

zset是string类型的有序集合,并且不允许有重复的值.

zadd zset集合 score(排序值) value 指定集合zadd添加元素,如果该元素存在, 更新位置
        zadd zsetTest1 1 one
zrem    删除zset中指定元素
        zrem zsetTest1 one
zincrby zset集合 incr value        
// -----------------------------
zset集合中存在元素, 则该元素score增加对应的值, 否则该集合中添加该元素, score值就是指定的increament值
    zincrby zsetTest1 2 one
zrank     返回集合指定元素的索引值。这个索引值不是score值, 是集合排序后实际的下标。
    zrank zsetTest one 
zrevrank 返回值与zrank相反。
zrange(zcard) 查看全部元素
        zrange zsetTest1 0 -1 withscores    --> 查看顺序就要带上withscores
zrevrange 与zrange相反。
....

1.2.6 主题订阅模式(pub/sub)

是一种消息通信模式, 发送者发送消息, 订阅者接受消息,redis客户端可以订阅任意数量的频道。可以做即时通信, 群聊等功能。群聊时候,所有成员都订阅(subscribe)一个主题, 然后成员发送群消息时候实际就是向这个主题发布(publish)一条消息。
subscribe 订阅给定的一个或多个频道的消息。
image.png

publish 将消息发到指定频道
image.png

pubsub 查看订阅与发布状态
image.png

java实现消息订阅


/**
 * redis发布订阅消息监听器
 */
@Slf4j
public class RedisMsgPubSubListener extends JedisPubSub {

    public static Jedis jedis = null;

    static {
        jedis = new Jedis("10.16.116.23", 6320);
        jedis.auth("rpass6320");
    }

    @Override
    public void unsubscribe() {
        super.unsubscribe();
    }

    @Override
    public void unsubscribe(String... channels) {
        super.unsubscribe(channels);
    }

    @Override
    public void subscribe(String... channels) {
        super.subscribe(channels);
    }

    @Override
    public void psubscribe(String... patterns) {
        super.psubscribe(patterns);
    }

    @Override
    public void punsubscribe() {
        super.punsubscribe();
    }

    @Override
    public void punsubscribe(String... patterns) {
        super.punsubscribe(patterns);
    }

    @Override
    public void onMessage(String channel, String message) {
        log.info("onMessage: channel[{}], message[{}]", channel, message);
    }

    @Override
    public void onPMessage(String pattern, String channel, String message) {
        log.info("onPMessage: pattern[{}], channel[{}], message[{}]", pattern, channel, message);
    }

    @Override
    public void onSubscribe(String channel, int subscribedChannels) {
        log.info("onSubscribe: channel[{}], subscribedChannels[{}]", channel, subscribedChannels);
    }

    @Override
    public void onPUnsubscribe(String pattern, int subscribedChannels) {
        log.info("onPUnsubscribe: pattern[{}], subscribedChannels[{}]", pattern, subscribedChannels);
    }

    @Override
    public void onPSubscribe(String pattern, int subscribedChannels) {
        log.info("onPSubscribe: pattern[{}], subscribedChannels[{}]", pattern, subscribedChannels);
    }

    @Override
    public void onUnsubscribe(String channel, int subscribedChannels) {
        log.info("channel:{} is been subscribed:{}", channel, subscribedChannels);
    }
}

class Server {
    public static void pub() {
        System.out.println("发布者");
        while (true) {
            Scanner scanner = new Scanner(System.in);
            String next = scanner.next();
            if (ThreadLocalRandom.current().nextInt(100) % 2 == 0) {
                RedisMsgPubSubListener.jedis.publish("sagittarius.topic.odd.num", "sagittarius.topic.odd.num -->" + next);
            } else {
                RedisMsgPubSubListener.jedis.publish("sagittarius.topic.even.num", "sagittarius.topic.even.num -->" + next);
            }
        }
    }
}

class Client {
    public static void sub(String topic) {
        System.out.println("订阅者");
        RedisMsgPubSubListener sp = new RedisMsgPubSubListener();
        RedisMsgPubSubListener.jedis.subscribe(sp, topic);
    }
}


class ServerTest1 {
    public static void main(String[] args) {
        Server.pub();
    }
}

class ClientTest1 {
    public static void main(String[] args) {
        Client.sub("sagittarius.topic.odd.num");
    }
}

class ClientTest2 {
    public static void main(String[] args) {
        Client.sub("sagittarius.topic.even.num");
    }
}

生产者:
image.png
消费者:
image.png
image.png

1.3 redis命令

1.3.1 keys命令

del key 用于删除key    ex: del foo
exists key 检查给定的key是否存在 ex: exists foo
keys pattern 查找符合给定模式的key。
move key db(数据库索引下标)    将当前数据库中的key移动到给定的数据库。 ex: move foo 1

<< -----  过期时间相关
expire key 设置key的过期时间,单位是秒 ex: expire foo 10
pexpire key 设置过期时间,单位是毫秒     ex: pexpire foo 10000 
ttl key     以秒为单位返回key的剩余过期时间.     ex: ttl foo
pttl key     以毫秒为单位返回key的剩余过期时间。
expireat key 也是设置过期时间, 但是expireat接受的参数是时间戳。
pexpireat key 设置过期时间, 接受的是时间戳, 毫秒级别。
persist key     用于移除给定key的过期时间, 使得key永远不过期。ex: persist foo 
----------->>

randomkey 从当前数据库随机返回一个key。
flushkey/flushdb     清空当前的库。
rename oldkey newkey 修改key的名称。
    当newkey不存在, 修改名字。
  当neyKey存在, 覆盖老得key。
renamenx oldkey newkey 只有key不存在的时候,才会修改key的名称。
type key 返回key存储的值类型。
dbsize     返回当前数据库key数量。

1.4 redis持久化

1.4.1 rdb持久化

RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘。也是默认的持久化方式,这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为dump.rdb.

方式1: save触发方式:

该命令会阻塞redis服务器. redis不会执行其他命令, 直到rdb过程完成。
image.png

方式2: bgsave触发方式

fork出一个子进程创建db文件,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。基本不阻塞服务器进程。
image.png
PS: bgsave原理:
执行bgsave方法的时候其实系统会调用fork方法创建进程。这种方式叫做copy-on-write(写时复制)
如果有多个调用者同时请求相同的资源, 会获取相同的指针, 指向相同的资源。当某一个调用者去修改资源内容的时候, 系统会复制副本给到调用者, 其他调用者还是使用原来的资源。 如果执行了bgsave方法的时候, 系统备份一份副本给备份线程去备份。主线程还继续使用, 等备份完成以后, 备份线程再指向原来的资源。

方式3: 自动触发: 自动触发需要配置一系列的配置

save:这里是用来配置触发 Redis的 RDB 持久化条件,也就是什么时候将内存中的数据保存到硬盘。
save 900 1 900 秒内如果至少有 1 个 key 的值变化,则保存
save 300 10 300 秒内如果至少有 10 个 key 的值变化,则保存
save 60 10000 60 秒内如果至少有 10000 个 key 的值变化,则保存
stop-writes-on-bgsave-error:设为yes。当启用了RDB且最后一次后台保存数据失败,Redis是否停止接收数据。
rdbcompression: 默认是yes。对于存储到磁盘中的快照,可以设置是否进行压缩存储。

RDB的优势和劣势:

优势: 全量备份,非常适合误操作后恢复。生成rdb文件时候不会影响主线程,此外恢复速度比AOF要快。
劣势: 进行快照持久化时,会开启一个子进程专门负责快照持久化,子进程会拥有父进程的内存数据,父进程修改内存子进程不会反应出来,所以在快照持久化期间修改的数据不会被保存,可能丢失数据。

1.4.2 AOF持久化

redis将收到的写命令都通过write函数以append的形式追加到文件中, 也可以说是日志记录。
image.png

配置文件的修改:

appendonly yes 开启AOF持久化
appendfilename “appendonly.aof” AOF文件名称
// 写入AOF文件的三种方式:
appendfsync always 每次有新命令, 都将缓存区数据写入并同步到AOF文件
appendfsync everysec 每秒将缓存区数据写入并同步到AOF文件
appendfsync no 将缓存区数据写入到AOF文件, 但同步由操作系统来处理

AOF重写:

由于AOF一直是不断的将命令追加到文件的末尾来记录数据库状态, 慢慢AOF文件体积会越来越大, 一些命令其实可以合并的.为了处理这种情况, AOF提供了重写机制.
方式1: 调用命令 bgrewriteaof
image.png
方式2: 配置文件配置:
auto-aof-rewrite-percentage 100 #当前AOF文件大小和上一次重写时AOF文件大小的比值
auto-aof-rewrite-min-size 64mb #文件的最小体积

AOF 重写步骤:

     1. 创建子进程进行AOF重写
     1. 将客户端的写命令追加到AOF重写缓冲区(可以理解为是一个临时文件)
     1. 子进程完成AOF重写工作后,会向父进程发送一个信号
     1. 父进程接收到信号后,将AOF重写缓冲区的所有内容写入到新AOF文件中
     1. 对新的AOF文件进行改名,原子的覆盖现有的AOF文件

image.png

AOF的优势和劣势:

优点: AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据。AOF文件过大, 会出现后台重写, 不影响客户端使用。AOF文件适合误操作的紧急恢复。如果有人误操作了flushall。我们可以拿到AOF文件将flushall命令删除, 通过恢复机制可以恢复所有数据。
缺点: 对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大。根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB。

1.4.3 Redis数据的恢复机制

  - 如果只配置 AOF ,重启时加载 AOF 文件恢复数据;
  - 如果同时配置了 RDB 和 AOF ,启动是只加载 AOF 文件恢复数据;
  - 如果只配置 RDB,启动是将加载 dump 文件恢复数据。

RDB-AOF混合持久化方式: gbsave做全量持久化, AOF做增量持久化。

1.5 redis的内存淘汰策略

1.5.1 过期策略

redis采用”定期删除 + 惰性删除“的过期策略。
定期删除: redis每隔100ms就随机抽取一些设置了过期时间的key, 检查这些key是否过期, 过期了就删除。(如果检查全部的话, 会浪费大量时间在检测上, 会导致redis挂掉)。这种做法导致的问题就是有大量过期的没有被删除。
惰性删除: 在你要获取key的时候, redis先去检测下这个key是否过期, 如果过期,就删除key, 不返回结果.

1.5.2 内存淘汰机制

在采用了”定期删除 + 惰性删除”的模式之后, 还会有大量的过期的未被访问的key在内存中。 为解决这种问题, 内存淘汰机制产生了。内存淘汰机制是redis内存占用过大时候, 去进行内存淘汰, 也就是删除一些key, 保证内存占用率不会太高。
六种方式:
noeviction:当内存不足以容纳新写入数据时,新写入操作会报错,无法写入新数据,一般不采用。 allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key,这个是最常用的。
allkeys-random:当内存不足以容纳新写入的数据时,在键空间中,随机移除key,一般也不使用。 volatile-lru:volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key(这个一般不太合适) 。
volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

1.6 redis的线程模型

redis一个核心的东西叫文件事件处理器。它由多个套接字(与客户端socket连接), io多路服用程序, 文件事件分派器, 事件处理器(命令请求处理器, 命令回复处理器, 连接应答处理器)这几部分组成。文件事件处理器是单线程处理的, 所以redis是单线程处理的
image.png

1.6.1 消息处理流程:

io多路复用程序监听多个socket,并根据套接字当前执行的任务来关联不同的事件处理器。当被监听的套接字有操作的时候(连接应答,读取,写入,关闭), 与操作相应的文件事件会产生, 最终关联不同的事件器。(这里虽说文件事件会同时出现,但是io多路复用程序会将所有产生事件的套接字推到队列去, 然后一个一个推到文件事件分派器,只有上一个套接字事件完成,才会推新的套接字事件)。
PS: 不同的操作socket会产生不同的事件:
客户端对redis执行write/close/connect操作, socket产生AE_READABLE事件。
客户端对reids执行read操作, socket产生AE_WRITEABLE事件。
不同的操作选择的事件处理器不同:
为了对连接服务器的各个客户端进行应答, 服务器要为监听套接字关联连接应答处理器。
为了接收客户端传来的命令请求, 服务器要为客户端套接字关联命令请求处理器。
为了向客户端返回命令的执行结果, 服务器要为客户端套接字关联命令回复处理器。

1.6.2 客户端与redis通信的一次流程:

1.在redis启动初始化的时候,redis会将连接应答处理器跟AE_READABLE事件关联起来,
2.客户端跟redis发起连接,此时会产生一个AE_READABLE事件,然后由连接应答处理器来处理跟客户端建立连接, 创建客户端对应的socket,同时将这个socket的AE_READABLE事件跟命令请求处理器关联起来。
   3.当客户端向redis发起请求的时候,首先就会在socket产生一个AE_READABLE事件,此时会连接到命令请 求处理器。命令请求处理器就会从socket中读取请求相关数据,然后进行执行和处理。当redis这边准备好 了给客户端的响应数据之后,就会将socket的AE_WRITABLE事件跟命令回复处理器关联起来。
4.当客户端准备好读取响应数据时,就会在socket上产生一个AE_WRITABLE事件,此时由命令回复处理
器处理,就是将准备好的响应数据写入socket,供客户端来读取。命令回复处理器写完之后,就会删除这个 socket的AE_WRITABLE事件和命令回复处理器的关联关系。

1.6.3 redis的单线程模型为何效率高

纯内存操作
核心是基于非阻塞的IO多路复用机制(https://www.yuque.com/wangyanyang/qz7fwa/yv1pyz)
单线程反而避免了多线程的频繁上下文切换问题

1.7 通信协议

RESP(Redis Serialization Protocol) 是redis序列化的协议, 该协议将传输的数据结构分为五种最小的单元类型, 单元结束时统一加上回车换行符号\r\n。
单行字符串 以 + 符号开头。 +hello world\r\n
多行字符串 以 $ 符号开头,后跟字符串长度。 $11\r\nhello world\r\n
特殊情况: (null -> $-1\r\n, “” -> $0\r\n\r\n)
整数值 以 : 符号开头,后跟整数的字符串形式。 :1024\r\n
错误消息 以 - 符号开头。 -WRONGTYPE xxx
数组 以 号开头,后跟数组的长度。3\r\n:1\r\n:2\r\n:3\r\n

1.7.1 客户端向服务端发请求

客户端向服务端发送的指令全部用多行字符串数组表示。 例子: set author codehole会被序列化成下面的字符串:
*3\r\n$3\r\nset\r\n$6\r\nauthor\r\n$8\r\ncodehole\r\n

1.7.1 客户端向服务端返响应

服务器向客户端回复的响应要支持多种数据结构, 不过再复杂的响应消息也是以上 5 种基本类型的组合。

1.8 redis 事务(不严格)

1.8.1 使用:

multi 指示事务的开始, exec 指示事务的执行, discard 指示事务的丢弃。
image.png
如上图所示, 在开启事务之后, 所有的命令在exec之前不会执行, 而是缓存在一个事务队列中。只有收到exec指令后, 全部顺序执行。
image.png
注意的点: redis并不满足一致性, 其中一条命令失败, 其他命令还会继续执行。它只满足事务的隔离性(当前执行中的事务不会被其他事务打断)

1.8.2 watch机制(解决并发)

是一种乐观锁的机制, watch在事务开始之前会监控一个关键变量A, 当事务要执行的时候, redis会检查变量A自watch之后, 是否被修改过. 如果有修改过, exec返回null告知执行失败。
image.png
如果我们在watch之后, 不进行变量修改, 事务执行成功
image.png


附加: 一些问题:

附加一: 从海量数据里查询某一固定前缀的key

常规情况会使用key指令查找符合给定模式的key。缺点是: redis是单线程的,会一次性返回所有符合的key.key的数量过大会使得服务卡顿。解决方案:可以使用scan指令, 以非阻塞的方式实现key的查找。 scan的语法: SCAN cursor [MATCH pattern] [COUNT count]. scan命令是基于游标的迭代器, scan命令每次被执行的时候, 都会想用户返回一个新的游标, 用户在下次迭代的时候需要使用这个新游标作为scan命令的游标参数, 当scan命令的游标参数被设置为0的时候, 服务起开始一次新的迭代, 而服务器向用户返回0的游标的时候, 表示迭代结束。
ex: scan 0 —> 使用0作为游标, 开始新一次的迭代。
scan命令回复的是包含两个元素的数组, 第一个元素是下一次迭代的游标, 第二个数组包含了所有迭代的元素。
**image.png

scan 0 match ‘k13*’ 加上给定模式去匹配元素。
image.png

scan 0 match ‘k1‘ count 5. 使用count选项让客户告知迭代命令, 每次迭代从数据机中取几个数据。
image.png
*

附加二: redis的缓存与数据一致性问题:

在项目中引入redis以后肯定会存在redis和mysql数据库数据不一致的问题, 常规思路肯定是先失效缓存然后更新数据库或者是先更新数据库再失效缓存。这两种方式都无法真正保证一致性问题。举个例子:
先失效索引再更新数据库, 线程A进来以后删除了缓存, 还没等到更新数据库。线程B进来了,从数据库捞到了数据,并放了缓存。此时缓存中数据是不对的。
先更新数据库在失效索引,线程A先修改了库, 在删除缓存的时候, 此时服务宕机了, 那缓存也是没有更新的。

PS:为什么不是更新缓存而是删除缓存: 如果要缓存的这些信息如果是写的操作比读的操作多, 那么修改一次就要更新一次缓存。然后这个缓存信息还不会被频繁的访问, 其实这样子是不划算的。这里也有一个懒加载的思想, 如果删除缓存信息后, 只有访问到了再往缓存里面放数据.

方式1: 前后延迟双删:

延迟双删:在写库的时候进行del操作.第二次删除是通过延迟的方式。

public void update(String key,Object data){
// 删除缓存
redis.delKey(key);
// 修改库
db.updateData(data);
// 休眠一会 -> 就是为了解决数据库还未更新另外的线程设置了旧的缓存数据。 
// 延迟一段时间, 就算缓存中设置了旧的数据, 可以删除它。 
Thread.sleep(500);
// 再删除缓存(其实这也是懒加载模式)
redis.delKey(key);
}
补充PS: 最好给缓存设置过期时间,就算双删失败了, 等缓存失效后, 自然会请求数据库并重新读取值写入缓存。

方式2: 异步更新缓存(基于订阅binlog的同步机制)

启动一个订阅程序去订阅数据库的binlog, 获取需要操作的数据, 并发送到消息中间件, 然后消费消息, 进行删除。
image.png

附加三: 缓存雪崩, 击穿, 穿透

缓存雪崩:

某一个时刻所有的key都失效, 所有的请求都落到数据库中, 数据库扛不住,挂掉。那么依赖这个库的所有接口全部报错。
预防方案: 在设置缓存的时候, 把缓存的失效时间加一个随机值, 就会避免了 数据同一时间全部失效。如果redis是集群部署, 将这些数据分布在不同的库中也可以避免。要不就是设置缓存数据不过期, 等数据更新的时候相应的更新缓存(一致性问题)。当然了也可以做一下熔断。

缓存穿透:

缓存中和数据库中都没有的数据, 用户不断的发起请求, 数据库导致压力过大, 击垮数据库。
预防方案: 接口要做参数校验, 对于提供出去的接口, 不能相信调用方, 你永远不知道调用方会传什么值。需要做详细的校验。如果取不到值, 可以在缓存中设置null或者其他值。当然需要设置一个过期时间。

缓存击穿:

一个key是个热点数据, 在扛着大并发, 当这个key失效以后, 所有的请求都直接请求到数据库中.
预防方案: 最简单的方式,将该key设置为永不过期。或者加一个互斥锁, 类似代码如下:

// 这种方式只适用于单机模式。
public static String getCacheData(String key) {
    // 缓存中拿数据
    String result = getDataByKV(key);
    if(result == null) {
        if(上锁) {
            // 从db中拿
            result = getDataByDB(key);
            if(result != null) {
                // 进缓存
                setDataToKV(key,result);
            }
            释放锁
        } else {
            // 睡一会再拿
            Thread.sleep(1000);
            getCacheData(key);
        }
    }
    return result;
}

附加四: redis实现分布式锁:

将setnx(不存在时候设置值)与expire(设置过期时间)结合起来使用。
image.png

// 设置值
long status = redisService.setnx(key,"Hello");
if(status == 1) {
    // 设置过期时间
    redisService.expire(key,10);
}

PS:上述情况存在一个问题, 如果在设置缓存值之后, 还没有设置设置过期时间, 此时该线程挂掉了,那么这个锁永远无法释放。redis的set指令有一个复杂的命令, 可以杜绝这种情况。
ex: set key value ex(按秒)/px(按毫秒) time nx(不存在设值)/xx(存在设值)
image.png

附加五: 使用redis做异步队列

使用list结果作为队列, rpush生产数据, lpop消费数据。但是会存在等待队列里面没有数据, 那么可以在应用层引入sleep机制去调用lpop重试机制。redis提供了blpop指令, 如果list没有内容的时候会陷入阻塞, 直到有内容或者阻塞时间到了。
image.png

上述实现的异步队列是无法做到一次生产多次消费的, 可以使用redis的的主题订阅模式(详见1.2.6)。这种发布订阅的方式做异步队列也不是很好, 消息的发布无法保证可达。