redis-redis记录

重新捡起丢了六年的东西.


前篇


docker 安装


设置外网访问

  1. 注释 bind, 并设置 protected-mode no
  2. bind 指定 ip (0.0.0.0 表示所有 ip)

protected-mode 的启用有两个条件, 1. 没有设置 bind, 2. 没有设置密码访问


安全访问 - 设置密码

可以通过两种方式来达到安全的目的, 如果两者同时启用, 那么是最安全的

  1. 设置密码: requirepass 123456
  2. 绑定 ip: bind 127.0.0.1

方式一: 服务启动后 设置密码

方式二: 服务启动时 指定配置

配置文件启动 - 密码设置

docker 启动 redis镜像里是没有配置文件的, 去官方下载修改, 需要修改的几个参数

  • 一般来说我们需要修改的内容仅有下面几个

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程
    deamonize yes
    # 你可以绑定单一接口,如果没有绑定,所有接口都会监听到来的连接, 0.0.0.0 表示任何ip都可以连
    # bind 127.0.0.1
    # 因为redis本身同步数据文件是按上面save条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为no
    appendonly no
    # 设置Redis连接密码,如果配置了连接密码,客户端在连接Redis时需要通过
    # auth <password>命令提供密码,默认关闭,当前密码为123456
    requirepass 123456
    pidfile /var/run/redis_6379.pid # 如果有多个 Redis 实例, 需要改成对应的端口
  • 启动指定配置文件

    1
    2
    3
    4
    5
    docker run -d -p 6379:6379 --name redis3 
    -v /root/redis/redis.conf:/etc/redis/redis.conf
    -v root/redis/data:/data
    hub.c.163.com/library/redis:latest
    redis-server /etc/redis/redis.conf --appendonly yes
    • 将本地配置文件映射到镜像中,这样更改本地文件就可以直接修改镜像中的配置了,更改之后使用docker restart+容器ID 重启服务。

      • /docker/redis/redis.conf 是宿主机子上的文件, /etc/redis/redis.conf 是容器里面的文件

      启动时最后也可以加配置参数“–appendonly yes”


配置文件

ocker 启动 redis镜像里是没有配置文件的, 去官方下载修改, 需要修改的几个参数

一般来说我们需要修改的内容仅有下面几个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程
deamonize yes
# 你可以绑定单一接口,如果没有绑定,所有接口都会监听到来的连接, 0.0.0.0 表示任何ip都可以连
# bind 127.0.0.1
# 因为redis本身同步数据文件是按上面save条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为no
appendonly no
# 设置Redis连接密码,如果配置了连接密码,客户端在连接Redis时需要通过
# auth <password>命令提供密码,默认关闭,当前密码为123456
requirepass 123456
pidfile /var/run/redis_6379.pid # 如果有多个 Redis 实例, 需要改成对应的端口

# replicaof <masterip> <masterport> # 本机做为从机, 从 master 上复制
# masterauth <master-password> # master 节点密码

dir ./ # rdb 的 dbfilename 文件存放目录, aof 文件也保存到该目录下, 如果有多个 Redis 实例, 也要区分一下这个文件
dbfilename dump.rdb # rdb 的文件名

save 900 1 # 表示 900 秒内有 1 个更改, 就触发一次 rdb 持久化, 如果注释掉表示不启用这个触发策略
auto-aof-rewrite-min-size 64mb # aof 持久化, 大小取模达到 64mb 就重写一次. 在生产环境中, 这个值一般都修改为 5gb 以上, 减少重写次数, 提高性能

主从配置

配置文件方式

命令方式


Redis 集群演变过程

  1. 单机版

  2. 主从复制

    主: 读写, 从: 读.

    缺点: 写 无法负载均衡, 故障恢复无法自动化

  3. 哨兵

    在复制的基础上, 实现了自动化故障恢复

    缺点: 写 无法负载均衡, 故障转移 (选取主节点) 期间 服务不可用

  4. 集群

    解决了 写 无法负载均衡 等以上缺点, 高可用.


Redis 是二进制存储

验证: 设置一个 abc, 其二进制是 1100001 1100010 1100011

1
2
3
4
5
6
7
8
9
10
redis:6379> set wilker abc
OK
redis:6379> get wilker
"abc"
redis:6379> setbit wilker 6 1
(integer) 0
redis:6379> setbit wilker 7 0
(integer) 1
redis:6379> get wilker
"bbc"

位图

位数组的顺序 和 字符的位顺序 是相反的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
127.0.0.1:6379> setbit s 1 1
(integer) 0
127.0.0.1:6379> setbit s 2 1
(integer) 0
127.0.0.1:6379> setbit s 4 1
(integer) 0
127.0.0.1:6379> setbit s 9 1
(integer) 0
127.0.0.1:6379> setbit s 10 1
(integer) 0
127.0.0.1:6379> setbit s 13 1
(integer) 0
127.0.0.1:6379> setbit s 15 1
(integer) 0
127.0.0.1:6379> get s
"he"

单机版


集群

可以理解为 多组哨兵, 这里 是 3 组, 也就是 3 个 分片, Redis 1000 组以内性能不会有很大影响

每一组 都是 一主多从, 数据都是一样的, 但是组与组之间是无关系的, 数据不是一致的

对 hello 计算 hash 值, 取模分配到其中一个 组 中.


缓存几大问题

  1. 缓存穿透

    是指数据控当中没有的数据, 缓存也没有的数据, 导致一直会查询数据库, 没有起到 减少数据库查询 的目的, 数据库压力过大

    解决方案:

    1. 将空值缓存起来. 简单粗暴的方法, 如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,设置比较短的过期时间, 如 几十秒
    2. 使用 布隆过滤器, 可以确定某个数据 肯定不存在, 这种情况就不需要去查数据库, 但已足够过滤大部分不合法攻击; 但无法确定 肯定存在, 这种情况还需要去查 数据库.
  2. 缓存击穿

    是指缓存中没有但数据库中有的数据 (一般是缓存时间到期), 这是由于并发用户特别多, 同时读缓存没有读到数据, 有同时去数据库取数据, 引起数据库压力瞬间增大, 造成过大压力.

    解决方案:

    1. 互斥锁
  3. 缓存雪崩

    是指缓存服务器重启或者大量缓存集中在某一个时间段内失效

    解决方案:

    1. 主要就是要搭建高可用集群, 保证机器的高可用
    2. 对不同的数据使用不同的失效时间, 甚至对相同的数据, 不同的请求使用不同的失效时间.
  4. 缓存与数据库一致性

    解决方案:

    1. 先删除缓存, 再修改数据库. 如果数据库修改失败了, 那么数据库中依旧是 旧数据, 缓存中中空的, 那么数据不会不一致. 因为读的时候缓存没有, 则读数据库中的旧数据, 然后更新到缓存中.

      如果更新数据库的过程中又有其他线程查询更新缓存, 那么还是有数据不一致问题.

      解决办法:

      1. 延时双删. 也就是更新完数据库后再删一次缓存, 此时删除缓存要延时一下.

        1. 串行化. 也就是使用队列


使用缓存的思路

先查询 缓存, 有则返回; 没有则查询 数据库, 查询到的数据加入到 缓存 中


布隆过滤器

布隆过滤器一般用来判断一个数据是否在一个很大的数据集合里面。当然可以用数组,集合,树等数据结构和各种查找法都可以做同样的事情,但是布隆过滤器有更好的时间效率和空间效率。比特币实现SPV节点时使用了布隆过滤器来查询交易。布隆过滤器可以判断一个数在不在集合里,但存在一定的误判率。

布隆过滤器的核心是一个超大的位数组和几个哈希函数。假设位数组的长度为m,哈希函数的个数为k。

以上图为例,在这里维数组长度为18,哈希函数个数为3个。首先将维数组所有位全部置0。集合中有的3个数据x,y,z,通过3个哈希函数对每一个数据进行计算,得到该数据的哈希值,这个哈希值对应维数组上面的一个点,然后将对应位数组的位置1。这样3个数据会生成9个点。对于另外一个数据w,查询它 在不在集合中的方法是对w通过3个哈希函数映射到位数组上,判断3个映射位置是否为1。只要有一个位置为0,就能说明w一定不在集合中。反之如果3个点都为1,则说明这个元素可能在集合中。此处不能判断元素一定在集合中,因为存在一定的误判率。比如对于上图中的4,5,6这3个位置都为1,但是它是不同的数据映射到的点。如果有一个数据刚好映射到这3个位置,虽然它不在集合中,但是我们也会误判它。

添加元素

  • 将要添加的元素给k个哈希函数进行计算
  • 得到位于位数组上面的k个位置
  • 将位数组上对应位置1

查询元素

  • 将要查询的元素给k个哈希函数
  • 得到对应于位数组上的k个位置
  • 如果k个位置有一个为0,则肯定不在集合中
  • 如果k个位置全部为1,则可能在集合中

比如判断某个 用户名 是否存在, 可以在启动服务的时候, 把数据库中的 用户名 全 load 出来, 通过 布隆过滤 算法丢到 Redis 的某个 key 中 (bit), 查询的时候通过 布隆过滤器 查询是否存在这个用户.

特点: bloom filter 只能添加, 不能删除. 因为 算出来的 哈希值取模 后的数组下标可能会 碰撞, 然后将值置为 1, 如果删除的话所有 碰撞 在一起的都删除了, 所以只能加, 不能减.


Redis 中集成 redisbloom


持久化

Redis 提供了 两种持久化方案, 默认开启 RDB

RDB

Redis 会 fork 一个与当前进程一模一样的子进程来进行持久化, 这个子进程的所有数据 都与 原进程一模一样, 会先将数据写入到一个临时文件中, 待持久化结束了, 再用这个临时文件替换上一次持久化好的文件, 整个过程中, 原进程不进行任何的 io 操作, 这就确保了极高的性能.

持久化文件在那? 触发时机

shutdown 时, 如果没有开启 aof, 会触发

配置文件中默认的快照配置

执行命令 save 或者 bgsave, save 是直观保存, 其他不管, 全部阻塞, bgsave 是 Redis 会 fork 子进程 后台异步进行快照操作, 同时可以响应客户端的请求

适合大规模数据恢复, 对完整性和一致性不高, 在一定间隔时间做一次备份, 会丢失备份前突然宕机的数据.

AOF

将 Redis 的操作日志以追加的方式写入到文件, 读操作是不记录的

持久化文件在那? 触发时机

no : 表示等操作系统惊醒数据缓存同步到磁盘, (快, 持久化没保证)

always : 同步持久化, 每次发生数据变更时, 立刻记录到磁盘 (慢, 安全)

everysec : 表示每秒同步一次 (默认值, 很快, 但可能丢失 1s 以内的数据)

官方建议 两种持久化同时使用, 如果两个同时启用, 优先使用 aof 持久化.

性能建议:

rdb 制作后备用途, save 900 1 就够了, 也就是 15分钟备份一次

aof 方式 auto-aof-rewrite-min-size 64mb, 在生产环境中, 这个值一般都修改为 5gb 以上, 减少重写次数, 提高性能


命令

SET

SET key value [EX seconds][PX milliseconds][NX|XX]

  • EX seconds:设置指定的过期时间,单位秒。
  • PX milliseconds:设置指定的过期时间,单位毫秒。
  • NX:仅当key不存在时设置值。
  • XX:仅当key存在时设置值。

可以看出来,SET 命令的天然原子性完全可以取代 SETNXEXPIRE 命令。


获取有 key

使用模糊查找所有 key

1
2
3
redis:6379> KEYS *
1) "mykey"
2) "jsonkey"

go-redis使用


分布式锁

上锁: 可以用 SET 命令的 NX 参数, 还在使用 2.6.12 版本之前的同学只能使用另一法宝:Lua脚本来保证原子性了。

解锁: 使用lua


事务

单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。

事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。


原子性

设计到多个命令要想保持原子性必须得使用 Lua 脚本。

比如 分布式锁

  • 加锁

    • Redis 在 2.6.12 版本开始,为 SET 命令增加了一系列选项, 可以确保原子性:

      1
      SET key value [EX seconds][PX milliseconds][NX|XX]
    • 还在使用 2.6.12 版本之前的同学只能使用另一法宝:Lua脚本来保证原子性了。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      public static boolean tryLock(String key, String uniqueId, int seconds) {
      String luaScript = "if redis.call('setnx', KEYS[1], KEYS[2]) == 1 then " +
      "redis.call('expire', KEYS[1], KEYS[3]) return 1 else return 0 end";
      List<String> keys = new ArrayList<>();
      keys.add(key);
      keys.add(uniqueId);
      keys.add(String.valueOf(seconds));
      Object result = jedis.eval(luaScript, keys, new ArrayList<String>());
      return result.equals(1L);
      }
  • 解锁

    1
    2
    3
    4
    5
    6
    // 释放分布式锁
    public static boolean releaseLock(String key, String uniqueId) {
    String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
    "return redis.call('del', KEYS[1]) else return 0 end";
    return jedis.eval(luaScript, Collections.singletonList(key), Collections.singletonList(uniqueId)).equals(1L);
    }