redis · 2021-09-04 0

redis搭建及基本使用

一、创建redis容器

1.拉取镜像

docker pull redis:6.0.10

2.创建容器

方式一:不指定配置文件

docker run -itd -p 6379:6379 --name redis-6379 redis:6.0.10

方式二:指定配置文件

docker run -itd -p 6379:6379 --name redis-6379 redis:6.0.10 redis-server /data/redis.conf

redis.conf 如下:

# bind 127.0.0.1
port 6379
pidfile /var/run/redis_6379.pid
logfile ""
dbfilename dump.rdb
dir ./
protected-mode no

3.进入容器

docker exec -it redis-6379

二、命令操作redis

redis有五种基本结构:

  1. String: 字符串
  2. Hash: 散列
  3. List: 列表
  4. Set: 集合
  5. Sorted Set: 有序集合

连接redis

redis-cli -h 127.0.0.1 -p 6379

1.String 字符串

语法:

  • set key value [EX seconds] [PX milliseconds] [NX|XX]
    EX seconds,将键的过期时间设置为 seconds 秒。PX milliseconds,将键的过期时间设置为 milliseconds 毫秒。 NX,只在键不存在时, 才对键进行设置操作。XX,只在键已经存在时, 才对键进行设置操作。
  • get key

例子:

127.0.0.1:6379> set name 'zhang san'
OK
127.0.0.1:6379> get name
"zhang san

2. Hash 散列

语法:

  • hset key field value
  • hkeys key
  • hvals key
  • hget key field

例子:

127.0.0.1:6379> hset student name 'zhang san' age 18
(integer) 2
127.0.0.1:6379> hkeys student
1) "name"
2) "age"
127.0.0.1:6379> hvals student
1) "zhang san"
2) "18"
127.0.0.1:6379> hget student name
"zhang san"

3.List 列表

语法:

  • lpush key value [value ...]
  • lpop key
  • blpop key [key ...] timeout

例子:

127.0.0.1:6379> lpush skill java
(integer) 1
127.0.0.1:6379> lpush skill mysql
(integer) 2
127.0.0.1:6379> lpush skill html js css
(integer) 5
127.0.0.1:6379> lpop skill
"css"
127.0.0.1:6379> lrange skill 0 10
1) "js"
2) "html"
3) "mysql"
4) "java"
127.0.0.1:6379> blpop skill 5
1) "skill"
2) "js"
127.0.0.1:6379> lrange skill 0 10
1) "html"
2) "mysql"
3) "java"

4.Set 集合

语法:

  • sadd key member [member ...]
  • smembers key

例子:

127.0.0.1:6379> sadd season spring
(integer) 1
127.0.0.1:6379> sadd season summer autumn winter
(integer) 3
127.0.0.1:6379> sadd season summer
(integer) 0
127.0.0.1:6379> smembers season
1) "spring"
2) "winter"
3) "summer"
4) "autumn"

5.Sorted Set 有序集合

语法:

  • zadd <key> <score> <member>
  • zscore <key> <member>
    返回指定 key-member 的序列
    zrange <key> <start> <stop> [withcores]
    查询指定key从 start 到 stop 的 member,从小到大排序
  • zrangebysocre <key> <min> <max> [withscores] [limit offset count]
  • zrank <key> <member>
    从小到大排序,返回指定 key-member 的下标位置
  • zrevrank <key> <member>
    从大到小排序,返回指定 key-member 的下标位置
  • zcard <key>
    返回key内元素个数

例子:

127.0.0.1:6379> zadd direction 4 right
(integer) 1
127.0.0.1:6379> zadd direction 1 up
(integer) 1
127.0.0.1:6379> zadd direction 2 down
(integer) 1
127.0.0.1:6379> zadd direction 3 left
(integer) 1
127.0.0.1:6379> zscore direction up
"1"
127.0.0.1:6379> zrange direction 0 10
1) "up"
2) "down"
3) "left"
4) "right"
127.0.0.1:6379> zrangebyscore direction -inf +inf withscores
1) "up"
2) "1"
3) "down"
4) "2"
5) "left"
6) "3"
7) "right"
8) "4"
127.0.0.1:6379> zrangebyscore direction -inf 3 withscores
1) "up"
2) "1"
3) "down"
4) "2"
5) "left"
6) "3"

6.stream

语法:

  • xadd key ID field value [field value ...]
    向队列添加消息,如果指定的队列不存在,则创建一个队列。key,队列名称,如果不存在就创建。ID,消息 id,我们使用 * 表示由 redis 生成,可以自定义,但是要自己保证递增性。field value,记录。
  • xdel key ID [ID ...]
    删除消息
  • xlen key
    获取流包含的元素数量,即消息长度
  • xrange key start_id end_id [COUNT count]
    获取消息列表。start,开始值, - 表示最小值。end,结束值, + 表示最大值。count,数量。
  • xrevrange key end_id start_id [COUNT count]
  • xread [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] start_id [start_id ...]
    以阻塞或非阻塞方式获取消息列表。milliseconds ,可选,阻塞毫秒数,没有设置就是非阻塞模式,可以是大于等于0的任何值,0表一直阻塞直到出现了可返回的元素为止。若一直没有元素,超时则返回空列表。
  • xgroup create <key> <consume_group_name> <id-or-$>
    只能为已经存在的流创建消费者组,如果用户给定的流不存在,那么将返回一个错误。0-0 表示从头开始消费。$ 表示只接受新消息。
  • xgroup setid <key> <consume_group_name> <id-or-$>
    为消费者组设置新的最后递送消息ID
  • xgroup delconsumer <key> <consume_group_name> <consume_name>
    删除消费者
  • xgroup destroy <key> <consume_group_name>
    删除消费者组,成功执行返回1,组不存在返回0,流不存在则报错
  • xack <key> <consume_group_name> <id>
    确认消息的消费
  • xinfo groups <key>
    查看消费者组
  • xinfo consumers <key> <consume_group_name>
    查看消费者
  • xpending <key> <consume_group_name> [<consume_name>]
    查看等待列表

xrange 的 id 是一个范围,范围是闭区间,而 xread 的 id 是一个值,且是开区间

例子:

127.0.0.1:6379> xadd stream_key * name zhang1 age 1
"1690128603631-0"
127.0.0.1:6379> xadd stream_key * name zhang2 age 2
"1690128609550-0"
127.0.0.1:6379> xadd stream_key * name zhang3 age 3
"1690128612823-0"
127.0.0.1:6379> xadd stream_key * name zhang4 age 4
"1690128617415-0"
127.0.0.1:6379> xadd stream_key * name zhang5 age 5
"1690128621359-0"
127.0.0.1:6379> xadd stream_key * name zhang6 age 6
"1690128626410-0"
127.0.0.1:6379> xdel stream_key 1690128609550-0
(integer) 1
127.0.0.1:6379> xlen stream_key
(integer) 5
127.0.0.1:6379> xrange stream_key - +
1) 1) "1690128603631-0"
   2) 1) "name"
      2) "zhang1"
      3) "age"
      4) "1"
2) 1) "1690128612823-0"
   2) 1) "name"
      2) "zhang3"
      3) "age"
      4) "3"
3) 1) "1690128617415-0"
   2) 1) "name"
      2) "zhang4"
      3) "age"
      4) "4"
4) 1) "1690128621359-0"
   2) 1) "name"
      2) "zhang5"
      3) "age"
      4) "5"
5) 1) "1690128626410-0"
   2) 1) "name"
      2) "zhang6"
      3) "age"
      4) "6"
127.0.0.1:6379> xrange stream_key 1690128612823-0 1690128617415-0
1) 1) "1690128612823-0"
   2) 1) "name"
      2) "zhang3"
      3) "age"
      4) "3"
2) 1) "1690128617415-0"
   2) 1) "name"
      2) "zhang4"
      3) "age"
      4) "4"
127.0.0.1:6379> xrange stream_key - + COUNT 1
1) 1) "1690128603631-0"
   2) 1) "name"
      2) "zhang1"
      3) "age"
      4) "1"

从 stream 头部读取两条消息

127.0.0.1:6379> xread count 2 streams stream_key 0-0
1) 1) "stream_key"
   2) 1) 1) "1690128603631-0"
         2) 1) "name"
            2) "zhang1"
            3) "age"
            4) "1"
      2) 1) "1690128612823-0"
         2) 1) "name"
            2) "zhang3"
            3) "age"
            4) "3"
127.0.0.1:6379> xread count 2 streams stream_key 1690128612823-0
1) 1) "stream_key"
   2) 1) 1) "1690128617415-0"
         2) 1) "name"
            2) "zhang4"
            3) "age"
            4) "4"
      2) 1) "1690128621359-0"
         2) 1) "name"
            2) "zhang5"
            3) "age"
            4) "5"
127.0.0.1:6379> xread count 2 streams mystream stream_key 0-0 0-0
1) 1) "stream_key"
   2) 1) 1) "1690128603631-0"
         2) 1) "name"
            2) "zhang1"
            3) "age"
            4) "1"
      2) 1) "1690128612823-0"
         2) 1) "name"
            2) "zhang3"
            3) "age"
            4) "3"

消费者组

127.0.0.1:6379>  xgroup create stream_key consumer-group-name-1 0-0
OK
127.0.0.1:6379> xreadgroup group consumer-group-name-1 consumer-name-1 count 1 streams stream_key >
1) 1) "stream_key"
   2) 1) 1) "1690128603631-0"
         2) 1) "name"
            2) "zhang1"
            3) "age"
            4) "1"
127.0.0.1:6379> xreadgroup group consumer-group-name-1 consumer-name-2 count 1 streams stream_key >
1) 1) "stream_key"
   2) 1) 1) "1690128612823-0"
         2) 1) "name"
            2) "zhang3"
            3) "age"
            4) "3"
127.0.0.1:6379> xreadgroup group consumer-group-name-1 consumer-name-2 count 1 streams stream_key >
1) 1) "stream_key"
   2) 1) 1) "1690128617415-0"
         2) 1) "name"
            2) "zhang4"
            3) "age"
            4) "4"
127.0.0.1:6379> xinfo groups stream_key
1) 1) "name"
   2) "consumer-group-name-1"
   3) "consumers"
   4) (integer) 2
   5) "pending"
   6) (integer) 3
   7) "last-delivered-id"
   8) "1690128617415-0"
127.0.0.1:6379> xinfo consumers stream_key consumer-group-name-1
1) 1) "name"
   2) "consumer-name-1"
   3) "pending"
   4) (integer) 1
   5) "idle"
   6) (integer) 139919
2) 1) "name"
   2) "consumer-name-2"
   3) "pending"
   4) (integer) 2
   5) "idle"
   6) (integer) 34591
127.0.0.1:6379> xpending stream_key consumer-group-name-1
1) (integer) 3
2) "1690128603631-0"
3) "1690128617415-0"
4) 1) 1) "consumer-name-1"
      2) "1"
   2) 1) "consumer-name-2"
      2) "2"
127.0.0.1:6379> xack stream_key consumer-group-name-1 1690128617415-0
(integer) 1
127.0.0.1:6379> xpending stream_key consumer-group-name-1
1) (integer) 2
2) "1690128603631-0"
3) "1690128612823-0"
4) 1) 1) "consumer-name-1"
      2) "1"
   2) 1) "consumer-name-2"
      2) "1"
127.0.0.1:6379> xpending stream_key consumer-group-name-1 - + 10 consumer-name-2
1) 1) "1690128612823-0"
   2) "consumer-name-2"
   3) (integer) 432958
   4) (integer) 1

为了解决组内消息读取单处理期间消费者崩溃带来的消息丢失问题,STREAM设计了Pending列表,用于记录读取但并未处理完毕的消息。命令 XPENDING 用来获取消费组或消费内消费者的未处理完毕的消息。

使用命令 XACK 完成告知消息处理完成。

每个Pending的消息有4个属性:

  • 消息ID
  • 所属消费者
  • IDLE,已读取时长
  • delivery counter,消息被读取次数

如果某个消息,不能被消费者处理,也就是不能被XACK,这是要长时间处于Pending列表中,即使被反复的转移给各个消费者也是如此。此时该消息的delivery counter就会累加,当累加到某个我们预设的临界值时,就认为时坏消息(也叫死信,DeadLetter,无法投递的消息),由于有了判定条件,我们将坏消息删除掉即可。

7.发布订阅

发布订阅:

publish <channel_name> <message>
subscribe <channel_name>

例子:

订阅:

127.0.0.1:6379> subscribe channel_name 
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel_name"
3) (integer) 1
1) "message"
2) "channel_name"
3) "hello"
1) "message"
2) "channel_name"
3) "world"

发布:

127.0.0.1:6379> publish channel_name hello
(integer) 1
127.0.0.1:6379> 
127.0.0.1:6379> publish channel_name world
(integer) 1

8.事务

单个 Redis 命令的执行是原子性的,但 Redis 事务的执行并不是原子性的。

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

Redis 事务不支持回滚,多数事务失败是由语法错误或者数据结构类型错误导致的,语法错误说明在命令入队前就进行检测的,而类型错误是在执行时检测的,Redis为提升性能而采用这种简单的事务

  • MULTI:开启事务,redis会将后续的命令逐个放入队列中
  • EXEC:执行事务中的所有操作命令
  • DISCARD:取消事务,放弃执行事务块中的所有命令

例子:

127.0.0.1:6379> set name 'zhang san'
QUEUED
127.0.0.1:6379> get name
QUEUED
127.0.0.1:6379> set age 18
QUEUED
127.0.0.1:6379> exec
1) OK
2) "zhang san"
3) OK
127.0.0.1:6379> keys *
1) "age"
2) "name"

9.key 键

keys *
exists name
expire name 20
del name student skill season direction
  • flushall 清空数据库并执行持久化操作,rdb 文件会发生发生变化
  • flushdb 清空数据库,但是不执行持久化操作,fdb 文件不会发生变化,而 redis 数据是从 rdb 快照文件中读取加载到内存的,所以在 flushdb 之后,如果想恢复数据库,则可以直接 kill 掉 redis-server 进程,然后重启服务,这样 redis 重新读取 rdb 文件。要直接 kill 掉 redis-server 服务,shutdown 操作会触发持久化

10.auth

auth 验证用户名和密码,默认的用户名为 default

auth  <password>
auth <username> <password>

查看当前用户密码

config get requirepass

设置当前用户密码

config set requirepass <password>

11.acl

  • acl users 列出所有注册的用户名
  • acl list 以配置文件格式显示用户详细信息
  • acl setuser <username> 创建或修改一个用户
  • acl setuser <username> on > <password> 添加密码,可添加多个密码,例:acl setuser zhangsan on >123456
  • acl setuser <username> on < <password> 移出密码,例:acl setuser zhangsan on <123456
  • acl getuser <username> 得到一个用户的详细信息
  • acl deluser <username> 删除列表中的用户
  • acl whoami 返回当前的连接用户
  • acl setuser <username> on nopass 清理密码,密码是任意值
  • acl setuser <username> resetpass 清理密码,只能设置密码或则设置nopass才能登陆
  • acl setuser <username> reset 重置用户和密码

例:

127.0.0.1:6379> acl list
1) "user default on #8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92 ~* +@all"
2) "user zhangsan1 off -@all"
3) "user zhangsan2 on #8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92 -@all"
4) "user zhangsan3 on nopass -@all"
5) "user zhangsan4 on -@all"
6) "user zhangsan5 off -@all"

参 数说明:

  • user 用户
  • default 表示默认用户名,或则自己定义的用户名
  • on 表示是否启用该用户,默认为off(禁用)
  • #... 表示用户密码,nopass表示不需要密码
  • ~* 表示可以访问的Key(正则匹配)
  • -@all表示没有任何权限;+@all表示有所有权限;

12.client

  • client list 返回所有连接到服务器的客户端信息和统计数据,客户端较多时频繁执行存在阻塞redis的可能
  • client kill 杀死指定客户端连接线程
127.0.0.1:6379> client list
id=4 addr=127.0.0.1:52060 fd=8 name= age=13 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 argv-mem=10 obl=0 oll=0 omem=0 tot-mem=61466 events=r cmd=client user=default
127.0.0.1:6379> client kill 127.0.0.1:52060
OK
127.0.0.1:6379> client list
id=5 addr=127.0.0.1:45766 fd=8 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 argv-mem=10 obl=0 oll=0 omem=0 tot-mem=61466 events=r cmd=client user=default

13.config

  • config set timeout 5 设置 redis 超时时间是 5 秒,timeout 参数来限制客户端连接的空闲时间。空闲时间到 5 秒,redis主动清理长时间空闲的客户端连接,回收资源。timeout 参数值的单位为秒(s),取值范围为0~100000。默认值为0,表示无限制。建议配置timeout参数以使Redis具有主动回收资源的能力。否则,如果客户端出现异常,连接池资源得不到及时回收,可能因空闲连接占满连接池导致服务崩溃。核心应用出现这样的问题可能引发整个业务的混乱,后果严重。
127.0.0.1:6379> config get timeout
1) "timeout"
2) "0"
127.0.0.1:6379> config set timeout 5
OK
127.0.0.1:6379> config get timeout
1) "timeout"
2) "5"
127.0.0.1:6379> client list
id=5 addr=127.0.0.1:47426 fd=8 name= age=4 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 argv-mem=10 obl=0 oll=0 omem=0 tot-mem=61466 events=r cmd=client user=default
127.0.0.1:6379> client list
id=6 addr=127.0.0.1:50992 fd=8 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 argv-mem=10 obl=0 oll=0 omem=0 tot-mem=61466 events=r cmd=client user=default

三、java操作redis

maven引入

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <!--<version>2.9.0</version>-->
    <version>3.3.0</version>
</dependency>

1.String 字符串

@Test
public void testString() {
    Jedis jedis = new Jedis("localhost");
    jedis.set("name", "zhang san");
    String str = jedis.get("name");
    System.out.println(str);
}

2. Hash 散列

@Test
public void testHash() {
    Map<String, String> student = new HashMap<>();
    student.put("name", "zhang san");
    student.put("age", "10");

    Jedis jedis = new Jedis("localhost");
    jedis.hmset("student", student);

    String name = jedis.hget("student", "name");
    String age = jedis.hget("student", "age");
    System.out.println(String.format("name: %s age: %s", name, age));
}

3.List 列表

@Test
public void testList() {
    Jedis jedis = new Jedis("localhost");
    jedis.lpush("skill", "java");
    jedis.lpush("skill", "js");
    jedis.lpush("skill", "mysql");

    List<String> arr = jedis.lrange("skill", 0, 10);
    arr.stream().forEach(System.out::println);
}

4.Set 集合

@Test
public void testSet() {
    Jedis jedis = new Jedis("localhost");
    jedis.sadd("season", "spring");
    jedis.sadd("season", "summer");
    jedis.sadd("season", "autumn");
    jedis.sadd("season", "winter");

    Set<String> season = jedis.smembers("season");
    season.stream().forEach(System.out::println);
}

5.Sorted Set 有序集合

@Test
public void testZSet() {
    Jedis jedis = new Jedis("localhost");
    jedis.zadd("direction", 4,"right");
    jedis.zadd("direction", 1, "up");
    jedis.zadd("direction", 2, "down");
    jedis.zadd("direction", 3, "left");

    Set<String> direction = jedis.zrange("direction", 0, 10);
    direction.stream().forEach(System.out::println);

    Double score = jedis.zscore("direction", "right");
    System.out.println(score);
}

6.stream

@Test
public void testStreamEntry() {
    StreamEntryID newEntry = StreamEntryID.NEW_ENTRY;
    StreamEntryID lastEntry = StreamEntryID.LAST_ENTRY;
    StreamEntryID unreceivedEntry = StreamEntryID.UNRECEIVED_ENTRY;
    System.out.println(newEntry);
    System.out.println(lastEntry);
    System.out.println(unreceivedEntry);
}

@Test
public void testXAdd() {
    Jedis jedis = new Jedis("localhost");

    Map<String, String> hash1 = new HashMap<>();
    hash1.put("name", "zhang1");
    hash1.put("age", "1");

    StreamEntryID id1 = jedis.xadd("stream_key", StreamEntryID.NEW_ENTRY, hash1);
    System.out.println(id1);

    Map<String, String> hash2 = new HashMap<>();
    hash2.put("name", "zhang2");
    hash2.put("age", "2");

    StreamEntryID id2 = jedis.xadd("stream_key", StreamEntryID.NEW_ENTRY, hash2);
    System.out.println(id2);
}

@Test
public void testXRead() {
    Jedis jedis = new Jedis("localhost");

    Long len = jedis.xlen("stream_key");
    System.out.println(len);

    List<Map.Entry<String, List<StreamEntry>>> list = jedis.xread(2, 1000,
            new AbstractMap.SimpleEntry<>("stream_key", new StreamEntryID()));
    System.out.println(list);
}

@Test
public void testXRange() {
    Jedis jedis = new Jedis("localhost");

    List<StreamEntry> list = jedis.xrange("stream_key", null, null, 100);
    System.out.println(list);
}
@Test
public void testXInfoGroup() {
    Jedis jedis = new Jedis("localhost");

    List<StreamGroupInfo> xinfoGroups = jedis.xinfoGroup("stream_key");
    xinfoGroups.stream().map(StreamGroupInfo::getGroupInfo).forEach(System.out::println);

    List<StreamConsumersInfo> xinfoConsumers = jedis.xinfoConsumers("stream_key", "group_name_1");
    xinfoConsumers.stream().map(StreamConsumersInfo::getConsumerInfo).forEach(System.out::println);
}

@Test
public void testXGroupCreate() {
    Jedis jedis = new Jedis("localhost");

    String result = jedis.xgroupCreate("stream_key", "group_name_1",
            new StreamEntryID(), true);
    System.out.println(result);
}

@Test
public void testXReadgroup() {
    Jedis jedis = new Jedis("localhost");

    List<Map.Entry<String, List<StreamEntry>>> list = jedis.xreadGroup("group_name_1",
            "consumer_1", 10, 100, false,
            new AbstractMap.SimpleEntry<>("stream_key", StreamEntryID.UNRECEIVED_ENTRY));
    System.out.println(list);
}

7.发布订阅

发布:

@Test
public void testPublish() throws InterruptedException {
    Jedis jedis = new Jedis("localhost");

    for (int i = 0; i < 10; i++) {
        TimeUnit.SECONDS.sleep(5);
        jedis.publish("myChannel", "publish data " + i);
    }
}

订阅:

public class Subscriber extends JedisPubSub {

    // 收到消息会调用
    @Override
    public void onMessage(String channel, String message) {
        System.out.println(String.format("receive redis published message, channel %s, message %s", channel, message));
    }

    // 订阅了频道会调用
    @Override
    public void onSubscribe(String channel, int subscribedChannels) {
        System.out.println(String.format("subscribe redis channel success, channel %s, subscribedChannels %d",
                channel, subscribedChannels));
    }

    // 取消订阅 会调用
    @Override
    public void onUnsubscribe(String channel, int subscribedChannels) {
        System.out.println(String.format("unsubscribe redis channel, channel %s, subscribedChannels %d",
                channel, subscribedChannels));
    }
}
@Test
public void testSubscribe() {
    Jedis jedis = new Jedis("localhost");
    jedis.subscribe(new Subscriber(), "myChannel");
}

四、缓存雪崩、击穿、穿透

1.缓存雪崩

缓存雪崩:是指大面积的缓存失效,直接请求DB。

解决办法:把每个 key 的失效时间的都加个随机值

2.缓存穿透

缓存穿透:指缓存和数据库中都没有数据,而用户不断发起请求,我们数据库的 id 都是 1 开始自增上去的,如发起为 id 值为 -1 的数据

解决办法:在接口层增加校验;数据库不存在的数据,在 redis 层缓存为 Null

3.缓存击穿

缓存击穿:指一个 key 非常热点,在不停的扛着大并发,大并发几种对一个点进行访问,当这个 key 在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像一个完好无损的桶上凿开一个洞

解决办法:设置热点数据永远不过期;我们可以在第一个查询数据的请求上使用一个 互斥锁来锁住它,其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存,后面的线程进来发现已经有缓存了,就直接走缓存。

五、RDB 与 AOF

1.RDB 配置

RDB 是 Redis 默认的持久化方案。在指定的时间间隔内,执行指定次数的写操作,则会将内存中的数据写入到磁盘中。即在指定目录下生成一个dump.rdb文件。Redis 重启会通过加载dump.rdb文件恢复数据。

1 RDB核心规则配置

# save <seconds> <changes>
# save ""
save 900 1
save 300 10
save 60 10000

save <指定时间间隔> <执行指定次数更新操作>,满足条件就将内存中的数据同步到硬盘中。官方出厂配置默认是 900秒内有1个更改,300秒内有10个更改以及60秒内有10000个更改,则将内存中的数据快照写入磁盘。
若不想用RDB方案,可以把 save "" 的注释打开,下面三个注释。

2 指定本地数据库文件名,一般采用默认的 dump.rdb

dbfilename dump.rdb

3 指定本地数据库存放目录,一般也用默认配置

dir ./

4 默认开启数据压缩

rdbcompression yes

配置存储至本地数据库时是否压缩数据,默认为yes。Redis采用LZF压缩方式,但占用了一点CPU的时间。若关闭该选项,但会导致数据库文件变的巨大。建议开启。

2.触发RDB快照

  1. 在指定的时间间隔内,执行指定次数的写操作
  2. 执行save(阻塞, 只管保存快照,其他的等待) 或者是bgsave (异步)命令
  3. 执行flushall 命令,清空数据库所有数据
  4. 执行shutdown 命令,保证服务器正常关闭且不丢失任何数据

3.AOF 配置

Redis 默认不开启。它的出现是为了弥补RDB的不足(数据的不一致性),所以它采用日志的形式来记录每个写操作,并追加到文件中。Redis 重启的会根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作

1 redis 默认关闭,开启需要手动把no改为yes

appendonly yes

2 指定本地数据库文件名,默认值为 appendonly.aof

appendfilename "appendonly.aof"

3 指定更新日志条件

# appendfsync always
appendfsync everysec
# appendfsync no

always:同步持久化,每次发生数据变化会立刻写入到磁盘中。性能较差当数据完整性比较好(慢,安全)
everysec:出厂默认推荐,每秒异步记录一次(默认值)
no:不同步

4 配置重写触发机制

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发。一般都设置为3G,64M太小了。

4.触发AOF快照

根据配置文件触发,可以是每次执行触发,可以是每秒触发,可以不同步。

5.AOF 的重写机制

AOF的工作原理是将写操作追加到文件中,文件的冗余内容会越来越多。所以 Redis 新增了重写机制。当AOF文件的大小超过所设定的阈值时,Redis就会对AOF文件的内容压缩。

重写的原理:Redis 会fork出一条新进程,读取内存中的数据,并重新写到一个临时文件中。并没有读取旧文件(旧文件已经很大了)。最后替换旧的aof文件。

触发机制:当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发。这里的“一倍”和“64M” 可以通过配置文件修改。

六、内存淘汰策略和过期键删除策略

1.内存淘汰策略

内存不足的淘汰策略,当内存不足时,淘汰数据

# 默认为不移除key策略
# maxmemory-policy noeviction

8种淘汰策略

LRU (Least Recently Used) 最近最少使用;LFU (Least Frequently Used) 最不经常使用。

  • volatile-lru:在设置过期时间的数据中淘汰最少使用的数据。
  • allkeys-lru:在所有的数据中淘汰最少使用的数据。
  • volatile-lfu:在设置过期时间的数据中淘汰使用频率最低的数据。
  • allkeys-lfu:在所有的数据中淘汰使用使用频率最低的数据。
  • volatile-random:在设置过期时间的数据中淘汰任意随机数据。
  • allkeys-random:在所有的数据中随机淘汰数据。
  • volatile-ttl:在设置过期时间的数据中淘汰最早过期的数据。
  • noeviction:默认策略,不淘汰数据,新增或者修改数据会抛异常,但是读操作正常进行,不受影响

2.过期键删除策略

Redis实际使用的是惰性删除+定期删除的策略

  • 定时过期,每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。

  • 惰性过期,只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。

  • 定期过期,每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。

七、AOF、RDB 和复制功能对过期键的处理

1.生成 RDB 文件

在执行 SAVE 命令或 BGSAVE 命令创建一个新的 RDB 文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新的 RDB 文件中。

如:redis中包含 r1、r2、r3 三个键,并且 r1 已经过期,那么程序只会讲 r2 和 r3 保存到 RDB 文件中。
因此,过期键不会对新的 RDB 文件造成影响。

2.载入 RDB 文件

在启动 redis 服务器时,如果服务器开启了 RDB 功能,那么服务器将对 RDB 文件进行载入;

如果服务器以主服务器模式运行,那么在载入 RDB 文件时,过期的键会被过滤掉,不会被载入到redis数据库中。

如果以从服务器模式运行,那么无论键是否过期都会被载入到数据库中。但,因为主从服务器在进行数据同步时,从服务器就会被清空,所以,一般来说,过期键对从服务器也不会造成影响。

3.AOF 文件写入

当服务器开启 AOF 的运行模式时,如果某个键过期了,但没有被惰性或定期删除,那么 AOF 不会理会。如果被惰性或定期删除了, AOF 会在文件末尾追加一条 DEL 命令,来显示地记录该键已被删除。

4.AOF 重写

当 AOF 重写时,过期的键不会被载入到 redis 数据库中。

5.复制

当服务器在 复制 模式下时,从服务器的过期键删除动作都是由主服务器来进行的。

主服务器在删除一个过期键之后,会显示地向所有从服务器发送一个 DEL 命令,告知从服务器删除这个过期键。
从服务器在执行客户端发送的读命令时,即使碰到过期的键也不会删除,而是继续的正常操作。
从服务器只有在接到主服务器发来的 DEL 命令之后,才会删除过期键。

八、redis 与 mysql 双写一致性

方案一| 先更新数据库,再更新缓存

问题:

高并发时,如果线程A更新完数据库,线程B再次更新数据库,线程B比线程A先一步更新缓存,那么缓存还是线程A的数据,数据库是线程B的数据

方案二 | 先删除缓存,再更新数据库

问题:

线程A更新数据,先删除缓存,此时,线程B读取数据,因为此时线程A还未上锁,所以线程B可以从数据库读取数据,并且再次存入缓存中

改进| 延时双删

即根据业务需要,在延迟多少时间后再把缓存删除

问题:

  • 延迟删除,会影响接口吞吐量,影响接口性能
  • 第二次删除可能存在问题

方案三| 先更新数据库,再删除缓存

问题:

  • 线程A读取数据,恰好此时没有缓存时,从数据库读到旧值,线程B更新新值到数据库后删除缓存,删除缓存,线程A更新缓存
  • 概率较低,数据库操作中极少出现查询比更新慢的情况

基于消息的队列| 异步更新缓存

基于订阅binlog的同步机制

  • 模仿mysql的主从复制交互协议,将自己伪装成mysql slave 向mysql master 发送dump协议
  • mysql master收到dump请求,推送bin log给的slave
  • 解析bin log对象
  • 利用消息队列进行分发消费