Redis

Redis入门

Redis可以用于数据库、缓存和消息队列。

连接Redis

frank@frank-virtual-machine:/usr/local/bin$ redis-server kconfig/redis.conf 
frank@frank-virtual-machine:/usr/local/bin$ redis-cli -p 6379
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> 

Redis默认有16个数据库

# Set the number of databases. The default database is DB 0, you can select
# a different one on a per-connection basis using SELECT <dbid> where
# dbid is a number between 0 and 'databases'-1
databases 16

基础命令

127.0.0.1:6379> set name zhaobin
OK
127.0.0.1:6379> keys *
1) "name"
127.0.0.1:6379> get name
"zhaobin"
127.0.0.1:6379> exists name
(integer) 1
127.0.0.1:6379> exists name11
(integer) 0
127.0.0.1:6379[1]> move name 0
(integer) 1
127.0.0.1:6379> keys *
1) "age"
2) "name"
127.0.0.1:6379> EXPIRE name 10
(integer) 1
127.0.0.1:6379> ttl name
(integer) -2
127.0.0.1:6379> type age
string

选择数据库查看大小

127.0.0.1:6379> select 3
OK
127.0.0.1:6379[3]> dbsize
(integer) 0
127.0.0.1:6379[3]> set name zhaobin
OK
127.0.0.1:6379[3]> dbsize
(integer) 1
127.0.0.1:6379[3]> get name
"zhaobin"

清空数据库

flushdb        #清空当前数据库
flushall    #清空所有数据库

Redis是单线程的

Redis是基于内存操作的,CPU不是Redis性能限制的瓶颈,限制的瓶颈是机器的内存和网络带宽。

Redis是将数据全部放在内存中的。

五大数据类型

Redis-Key

Srting

List

命令都是以l开头的,list可以用作栈、队列和阻塞队列

127.0.0.1:6379> LPUSH list one    #将值插入到列表的头部
(integer) 1
127.0.0.1:6379> LPUSH list two
(integer) 2
127.0.0.1:6379> LPUSH list three
(integer) 3
127.0.0.1:6379> LRANGE list 0 2    #倒着取数据
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> Rpush list right    #将值插入到list的尾部
(integer) 4
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "two"
3) "one"
4) "right"
####################################################
LPOP    #移除list的头部(左)
RPOP    #移除list的尾部(右)
####################################################
Lindex    #下标
####################################################
Llen    #取长度
####################################################
Lrem    #移除指定的值

消息队列(Lpush Rpop

栈(Lpush Lpop

Set

命令都是以s开头的

无序不重复

sadd

smembers

sismember

srem

Hash

Map集合,key-<key-value>

hset key key value

hget

Zset

有序集合

在set的基础上,增加了一个值。

127.0.0.1:6379> zadd myset 1 one
(integer) 1
127.0.0.1:6379> zadd myset 2 two
(integer) 1
127.0.0.1:6379> zadd myset 3 three
(integer) 1
127.0.0.1:6379> ZRANGE myset 0 -1
1) "one"
2) "two"
3) "three"

三种特殊数据类型

BitMap

Hyperloglog

Geospatial

事务

Redis事务的本质:一组命令的集合!一个事务所有的命令都会被序列化,在事务的执行过程中,会按照顺序执行。

一次性、顺序性、排他性

Redis事务没有隔离的级别。

Redis单条命令保证原子性,但是Redis事务不保证原子性。

Redis事务

  • 开启事务(multi)
  • 命令入队()
  • 执行事务(exec)

正常执行事务

127.0.0.1:6379> MULTI        #开启事务
OK
#命令入队
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> exec    #执行事务
1) OK
2) OK
3) "v2"
4) OK

放弃事务

127.0.0.1:6379> MULTI        #开启事务
OK
#命令入队
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> discard    #放弃事务
OK
127.0.0.1:6379> get k4        #事务队列中的命令都不会被执行
(nil)

编译型异常

事务中所有的命令都不会被执行。

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> GETSET k3    #错误的命令
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> exec        #执行事务报错            所有的命令都不会被执行
(error) EXECABORT Transaction discarded because of previous errors.

运行时异常

执行命令的时候其他命令是可以正常执行的。

127.0.0.1:6379> set k1 "v1"
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> incr k1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> get k3
QUEUED
127.0.0.1:6379(TX)> exec
1) (error) ERR value is not an integer or out of range
2) OK
3) OK
4) "v3"

<font = red>

监控 Watch

悲观锁

无论做什么都会加锁。

乐观锁

  • 获取version
  • 更新的时候比较version

Redis监视测试

正常执行成功

127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> watch money        #监视money对象
OK
127.0.0.1:6379> MULTI            #事务正常结束,数据期间没有发生变动,正常执行成功
OK
127.0.0.1:6379(TX)> DECRBY money 20
QUEUED
127.0.0.1:6379(TX)> INCRBY out 20
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 80
2) (integer) 20

测试多线程修改值,使用watch可以当作redis的乐观操作。

127.0.0.1:6379> watch money        #监视money
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> DECRBY money 10
QUEUED
127.0.0.1:6379(TX)> INCRBY out 10
QUEUED
127.0.0.1:6379(TX)> exec        #执行前另外一个线程修改了money,修改失败
(nil)

127.0.0.1:6379> unwatch            #1.发现事务执行失败,先解锁
OK
127.0.0.1:6379> WATCH money        #2.获取最新的值,再次监视。select version
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> DECRBY money 1
QUEUED
127.0.0.1:6379(TX)> INCRBY out 1
QUEUED
127.0.0.1:6379(TX)> exec        #3.比较监视的对象是否发生变化,没有则执行成功。没有自旋
1) (integer) 99
2) (integer) 21
127.0.0.1:6379> 

Jedis

Redis官方推荐的java连接开发工具。

测试

1.导入对应的依赖

    <dependencies>
        <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.2.0</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.62</version>
        </dependency>
    </dependencies>

2.编码测试

  • 连接数据库
  • 操作命令
  • 断开连接
import redis.clients.jedis.Jedis;

public class TestPing {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1",6379);
        System.out.println(jedis.ping());
    }
}

常用API

String

List

Set

Hash

Zset

事务

import com.alibaba.fastjson.JSONObject;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

public class TestPing {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1",6379);

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("hello", "world");
        jsonObject.put("name", "zhaobin");
        String result = jsonObject.toJSONString();

        Transaction multi = jedis.multi();

        try {
            multi.set("user1", result);
            multi.set("user2", result);

            multi.exec();        //执行事务
        } catch (Exception e) {
            multi.discard();        //放弃事务
           e.printStackTrace();
        } finally {
            System.out.println(jedis.get("user1"));
            System.out.println(jedis.get("user2"));
            jedis.close();
        }
    }
}

输出

{"name":"zhaobin","hello":"world"}
{"name":"zhaobin","hello":"world"}

Process finished with exit code 0

SpringBoot整合

Jedis:采用的直连,多个线程操作的话,是不安全的。

避免不安全,采用jedis pool连接池。 BIO

lettuce:采用netty,实例可以在多个线程*享,不存在线程不安全的情况。可以减少线程数量,更像NIO模式。

Redis.conf

网络

bind 127.0.0.1        #绑定的ip
protected-mode yes    #保护模式
port 6379            #端口设置

通用

daemonize yes        #以守护进程的方式运行,默认是no
pidfile /var/run/redis_6379.pid    #如果一后台的方式运行,需要指定一个pid文件。
#日志
# Specify the server verbosity level.
# This can be one of:
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably)    生产环境
# warning (only very important / critical messages are logged)
loglevel notice

# Specify the log file name. Also the empty string can be used to force
# Redis to log on the standard output. Note that if you use standard
# output for logging but daemonize, logs will be sent to /dev/null
logfile ""        #日志生成文件名

databases 16    #数据库的数量

快照

持久化:在规定的时间内,执行了多少次操作,则会持久化到文件.rdb或者.aof

# Save the DB to disk.
#
# save <seconds> <changes>
#
# Redis will save the DB if both the given number of seconds and the given
# number of write operations against the DB occurred.
#
# Snapshotting can be completely disabled with a single empty string argument
# as in following example:
#
# save ""
#
# Unless specified otherwise, by default Redis will save the DB:
#   * After 3600 seconds (an hour) if at least 1 key changed
#   * After 300 seconds (5 minutes) if at least 100 keys changed
#   * After 60 seconds if at least 10000 keys changed
#
# You can set these explicitly by uncommenting the three following lines.
#
save 3600 1                    360s内至少有一个key进行了修改则会进行持久化操作
save 300 100
save 60 10000


stop-writes-on-bgsave-error yes        #持久化出错是否需要继续工作
rdbcompression yes                    #是否压缩rdb文件,需要消耗一些CPU资源
rdbchecksum yes                        #保存rdb文件的时候,进行错误的检查校验

# The working directory.
#
# The DB will be written inside this directory, with the filename specified
# above using the 'dbfilename' configuration directive.
#
# The Append Only File will also be created inside this directory.
#
# Note that you must specify a directory here, not a file name.
dir ./                                #rdb保存的位置

maxmemory-policy

maxmemory-policy noeviction        #默认为noeviction

volatile-lru -> Evict using approximated LRU, only keys with an expire set.    #只对设置了过期时间的key进行LRU
allkeys-lru -> Evict any key using approximated LRU.        #删除LRU算法的key
volatile-lfu -> Evict using approximated LFU, only keys with an expire set.
allkeys-lfu -> Evict any key using approximated LFU.
volatile-random -> Remove a random key having an expire set.    #随机删除即将过期的key
allkeys-random -> Remove a random key, any key.                    #随机删除
volatile-ttl -> Remove the key with the nearest expire time (minor TTL)#删除即将过期的
noeviction -> Dont evict anything, just return an error on write operations. #永不过期,返回错误

Append ONLY模式(AOF)

appendonly no                #默认不开启AOF,默认使用RDB持久化

Redis持久化

Redis是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失。所以Redis提供了持久化功能。

RDB(Redis Database)

触发机制

  • save规则满足的情况下
  • 执行flushall
  • 退出redis

恢复RDB文件

将rdb文件放在redis的启动目录

config get dir

优点:

  • 适合大规模的数据恢复
  • 对数据的完整性要求不高

缺点:

  • 需要一定的时间间隔进行操作,如果redis意外宕机了,最后一次的修改数据就没了。
  • fork进程的时候,会占用一定的内存空间。

AOF(Append Only File)

将所有命令都记录下来,恢复将所有命令都执行一遍。

redis-check-aof --fix             #修复aof文件   数据可能会丢失
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

如果aof文件大于64M,则会fork一个新的进程将文件进行重写。(合并命令)

优点:

1.每一次修改都同步,文件完整性

2.默认是每秒同步一次,可能会丢失一秒的数据。

缺点:

1.相对于数据文件来说,aof远远大于rdb,修复的速度也比rdb慢。

2.aof运行效率比rdb慢。

Redis发布订阅

Redis发布订阅(pub/sub)是一种消息通信模式,发送者(pub)发送消息,订阅者(sub)接受消息。

Redis客户端可以订阅任意数量的频道。

订阅端:

frank@frank-virtual-machine:/usr/local/bin$ redis-cli -p 6379
127.0.0.1:6379> SUBSCRIBE frank
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "frank"
3) (integer) 1
1) "message"
2) "frank"
3) "hello world"
1) "message"
2) "frank"
3) "welcome"

发布端

frank@frank-virtual-machine:/usr/local/bin$ redis-cli -p 6379
127.0.0.1:6379> PUBLISH frank "hello world"
(integer) 1
127.0.0.1:6379> PUBLISH frank "welcome"
(integer) 1

原理:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jZLb0pcI-1624946464873)(/home/frank/.config/Typora/typora-user-images/image-20210510154408080.png)]

Redis主从复制

主从复制的概念
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master/leader),后者称为从节点(slave/follower) ; 数据的复制是单向的,只能由主节点到从节点。Master以写为主,Slave以读为主。

默认情况下,每台Redis服务器都是主节点 ;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。

主从复制的作用

  • 读写分离:主节点写,从节点读,提高服务器的读写负载能力
  • 数据冗余︰主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
  • 故障恢复︰当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复 ; 实际上是一种服务的冗余。
  • 负载均衡︰在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载 ; 尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
  • 高可用(集群)基石︰除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

环境配置

只配置从库,不配置主库。

127.0.0.1:6379> INFO replication            #查看当前库的信息
# Replication
role:master                #角色
connected_slaves:0        #从机的数量
master_failover_state:no-failover
master_replid:4e0c8aa500279505d09120520406c85c132612b5
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

修改的redis.config文件

  1. 端口
  2. pid名字
  3. log文件名字
  4. dump.rdb名字

启动

root       50099    1042  0 16:44 ?        00:00:00 redis-server 127.0.0.1:6379
root       50134    1042  0 16:45 ?        00:00:00 redis-server 127.0.0.1:6380
root       50142    1042  0 16:45 ?        00:00:00 redis-server 127.0.0.1:6381
frank      50151   49915  0 16:46 pts/0    00:00:00 grep --color=auto redis

一主二从

127.0.0.1:6380> SLAVEOF 127.0.0.1 6379            #配置从机
OK
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:4
master_sync_in_progress:0
slave_repl_offset:28
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:e16592878a71fd546a6e3bd91b3fd8e89314c2f6
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:28
127.0.0.1:6379> info replication        $主机
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6380,state=online,offset=168,lag=0
slave1:ip=127.0.0.1,port=6381,state=online,offset=168,lag=0
master_failover_state:no-failover
master_replid:e16592878a71fd546a6e3bd91b3fd8e89314c2f6
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:168
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:168

从config文件中配置从机(永久)

# replicaof <masterip> <masterport>            主机信息
# masterauth <master-password>                主机有密码

主机可以写也可以读,但是从机只能读。

复制原理

  • 全量复制
  • 增量复制

哨兵模式

当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵(Sentinel)模式

哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。

Redis

这里的哨兵有两个作用

  • 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
  • 当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。

然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式

Redis

故障切换(failover)过程:

假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。这样对于客户端而言,一切都是透明的。

1.配置哨兵配置文件

sentinel monitor myredis 127.0.0.1 6379 1            #sentinel monitor 被监控的名称 host port 1

2.启动哨兵

frank@frank-virtual-machine:/usr/local/bin$ sudo redis-sentinel kconfig/sentinel.conf 
51265:X 10 May 2021 21:08:19.847 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
51265:X 10 May 2021 21:08:19.847 # Redis version=6.2.3, bits=64, commit=00000000, modified=0, pid=51265, just started
51265:X 10 May 2021 21:08:19.847 # Configuration loaded
51265:X 10 May 2021 21:08:19.848 * Increased maximum number of open files to 10032 (it was originally set to 1024).
51265:X 10 May 2021 21:08:19.848 * monotonic clock: POSIX clock_gettime
                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 6.2.3 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._                                  
 (    '      ,       .-`  | `,    )     Running in sentinel mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 26379
 |    `-._   `._    /     _.-'    |     PID: 51265
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           https://redis.io       
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'                                               

51265:X 10 May 2021 21:08:19.852 # Sentinel ID is 4aa5668a1e0ea7e9b42228b7c65c37c8588813a4
51265:X 10 May 2021 21:08:19.852 # +monitor master myredis 127.0.0.1 6379 quorum 1

优点:

1.哨兵集群,基于主从复制模式

2.主从可以切换,故障可以转移

缺点:

配置麻烦

Redis缓存穿透和雪崩

缓存穿透

缓存穿透的概念很简单,用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。

解决方案:

布隆过滤器:布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,当用户想要查询的时候,使用布隆过滤器发现不在集合中,就直接丢弃,不再对持久层查询。

缓存空对象:当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源;

但是这种方法会存在两个问题:

  1. 如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
  2. 即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。

缓存击穿

key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

微博热搜

解决方案:

设置热点永不过期

加互斥锁

业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。

SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。

雪崩击穿

在某一时间段,缓存集中过期失效。Redis宕机。

解决方案:

1)redis高可用

这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。

(2)限流降级

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

(3)数据预热

数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。


上一篇:【STM32F429】第19章 ThreadX信号量


下一篇:docker服务发现——etcd集群