Redis学习

Redis 是一个可基于内存亦可持久化的日志型的高性能的key-value数据库,一般用于分布式缓存服务或者分布式锁。

一、概述

Redis 是一个可基于内存亦可持久化的日志型的高性能的key-value数据库,一般用于分布式缓存服务或者分布式锁。

二、集群安装

假设我们由三台主机:node-01、node-02、node-03,我们考虑搭建一个三主三从的Redis集群。

  1. 下载redis6.2.6稳定版。
    在这里插入图片描述

  2. 下载后解压到指定的目录,进入到解压目录下使用make进行安装,可参考官网。
    在这里插入图片描述

  3. 三台主机上都操作2步骤。这样在三台主机上都安装了Redis,并且在三台主机的Redis安装目录下创建一个redis-cluster文件夹,用来存放Redis集群信息。

  4. 在三台主机上的redis-cluster文件夹下边创建两个文件夹,分别命令为redis-6380、redis-6381,并将Redis安装目录下边的redis.conf配置文件分别拷贝到redis-6380、redis-6381文件夹下,分别找到对应的配置项并做以下修改(这里以redis-6380下的redis.conf配置文件为例):

# 配置端口号
port 6380
# 开启集群模式
cluster-enabled yes
# 配置节点信息文件,在集群运行期间会根据这里的配置生成
cluster-config-file nodes-6380.conf
# 设置集群中节点被认为故障状态的超时时间。如果在这个时间范围内,集群没有收到该节点的回应,那么就认为该节点已经故障,
cluster-node-timeout 5000
# 开启AOF持久化策略。AOF和RDB持久化可以同时启用,如果同时启用了AOF和RDB,Redis将以AOF为基准,即具有更好持久性保证的文件
appendonly yes
  1. 按照步骤4创建的配置文件启动Redis服务。在三台主机上分别执行以下命令:
./redis-server $REDIS_HOME/redis-cluster/redis-6380/redis.conf &
./redis-server $REDIS_HOME/redis-cluster/redis-6381/redis.conf &
  1. 在任意一台主机上执行以下命令创建一个三主三从的Redis集群(注意:如果是Redis以下的版本,可能不是这个命令创建集群的):
# 创建一个Redis集群,Redis会自动帮我们规划主从服务。--cluster-replicas 1代表每一个主机下边会分配一个从机。
redis-cli --cluster create 172.19.141.8:6380 172.19.141.8:6381 172.19.141.9:6380 172.19.141.9:6381 172.19.141.10:6380 172.19.141.10:6381 --cluster-replicas 1

在这里插入图片描述

  1. 连接Redis集群客户端:
# -c:表示在集群中的各个节点之间跳转,如果没有-c参数,那么在node-01上查不到存放在node-02上的数据。-h:表示要连接的主机地址,-p:表示为连接的端口号。
redis-cli -c -h node-01 -p 6381
  1. 集群卸载:分别在三台主机上关闭Redis服务,并删除三台主机上的AOF和RDB备份文件和nodes-*.conf文件,即可删除Redis集群。

三、Redis基本数据类型以及命令使用

Redis的常用数据类型以及常用命令见:http://redis.cn/commands.html#。

字符串(Strings)

字符串是Redis中的一种最基本的数据类型,一个字符串的key的值最多能够存储512M的内容。Redis中的字符串是二进制安全的,也就是说,一个字符串的key可以存储任意类型的数据,比如:一张二进制格式的图片,或者一个序列化的对象。

数据结构:

动态字符串。

添加键值对:

# 一次设置一个key-vlaue:set key value
// 设置key为ip的键值对不存在的时候,设置key为ip的键,值为127.0.0.1的键值对,并且过期时间为永不过期。
172.19.141.10:6380> set ip 127.0.0.1 XX KEEPTTL GET
-> Redirected to slot [3877] located at 172.19.141.8:6380
"127.0.0.1"
172.19.141.8:6380>   
// [EX seconds|PX milliseconds|EXAT timestamp|PXAT milliseconds-timestamp|KEEPTTL] [NX|XX] [GET]
EX|PX|EXAT|PXAT:设置key的过期时间  
KEEPTTL:表示key永远不过期
NX|XX:当key不存在的时候创建|当key存在的时候创建
GET:设置key-value并获取值 
    
# 一次设置多个key-vlaue:mset key1 value1 key2 value2 key3 value3 ……
// 一次设置多个key-vlaue,存在时候直接覆盖,不存在时候新增,要保证这些key能得到同一个hash,否则报错,最简单的办法就是让这些不同的key属于同一个组。mset不是原子操作,如果多个key中由重复的,那么重复的key会覆盖之前的key,其他的key会插入插入成功。  
172.19.141.8:6380> mset {user}age 20 {user}id 1001 {user}class 1002
-> Redirected to slot [5474] located at 172.19.141.9:6380
OK
172.19.141.9:6380> 
// 一次设置多个key-vlaue,不存在时候才能设置成功,要保证这些key能得到同一个hash,否则报错,最简单的办法就是让这些不同的key属于同一个组。msetnx是原子操作,如果多个key中有重复的,那么所有的key都不会插入成功。 
172.19.141.9:6380> msetnx {user}age1 20 {user}id1 1001 {user}class1 1003
(integer) 1
172.19.141.9:6380> 
172.19.141.9:6380> msetnx {user}age1 20 {user}id3 1001 {user}class3 1003
(integer) 0  // 所有的key没有插入成功
172.19.141.9:6380> 

获取键的值:

# 一次获取一个key的值
// 获取key为ip的值
172.19.141.8:6380> get ip
"127.0.0.1"
172.19.141.8:6380> 
    
# 一次获取多个key的值
172.19.141.9:6380> mget {user}age {user}age1 {user}class
1) "20"
2) "20"
3) "1003"
172.19.141.9:6380>

# 获取字符串中的某个范围内的字符,类似截取字符串,并且是包含起始位置
172.19.141.9:6380> getrange {user}class 0 1
"10"
172.19.141.9:6380>

提示:可以使用set key strvalue nx ex time创建简单的锁,如果返回OK那么就说明创建锁成功。

列表(Lists)

列表就是一个简单的字符串列表,按照插入顺序进行排序。实际上类似于java中的双向链表,既可以从头部进行插入,也可以从尾部进行插入。

数据结构:

快速链表。

添加键值对:

// 向userlist中添加数据,从头部插入数据
172.19.141.9:6380> lpush userlist lucy nana juck mary 
-> Redirected to slot [4430] located at 172.19.141.8:6380
(integer) 4
172.19.141.8:6380>
// 向userlist中添加数据,从尾部插入数据
172.19.141.8:6380> rpush userlist lucy1 nana1 juck1 mary1 
(integer) 8
172.19.141.8:6380>    

弹出键的值:

弹出值的时候,一并会从list中的移除该值,当所有的元素被弹出的时候,那么这个key也就被删除了。

// 获取userlist,从头部弹出数据
172.19.141.8:6380> lpop userlist 8
1) "mary"
2) "juck"
3) "nana"
4) "lucy"
5) "lucy1"
6) "nana1"
7) "juck1"
8) "mary1"
172.19.141.8:6380> 
// 获取userlist,从尾部弹出数据
172.19.141.8:6380> rpop userlist 8
1) "mary1"
2) "juck1"
3) "nana1"
4) "lucy1"
5) "lucy"
6) "nana"
7) "juck"
8) "mary"
172.19.141.8:6380>

获取键的值:

// 获取指定范围内的所有的元素,-1代表获取所有的数据
172.19.141.8:6380> lrange userlist 0 -1
1) "mary"
2) "juck"
3) "nana"
4) "lucy"
5) "lucy1"
6) "nana1"
7) "juck1"
8) "mary1"
172.19.141.8:6380>

集合(Sets)

sets集合是一个无序的字符串合集,提供的功能和lists相似,但是sets集合可以自动去重,并且是无序的,你可以以O(1) 的时间复杂度(无论集合中有多少元素时间复杂度都为常量)完成 添加,删除以及测试元素是否存在的操作。

数据结构:

字典,通过hash表实现。

添加键值对:

172.19.141.8:6380> sadd userset lucy lucy mery
-> Redirected to slot [10424] located at 172.19.141.9:6380
(integer) 2
172.19.141.9:6380>

弹出键的值:

弹出值的时候,一并会从set中移除该值,当所有的元素被弹出的时候,那么这个key也就被删除了。

172.19.141.9:6380> spop userset
"mery"
172.19.141.9:6380>  

哈希(Hashes)

hashs是字段和字段值的映射,很适合存储对象结构。类似于java中的Map<String, Object>结构。比如,有一个user实体类,实体类中有name字段,age字段,sex字段等。

数据结构:

ziplist和hashtable。

添加键值对:

172.19.141.9:6380> hmset user name juck age 28 address china sex meal 
OK
172.19.141.9:6380>

获取键的值:

// 获取某个字段的值
172.19.141.9:6380> hmget user name
1) "juck"
172.19.141.9:6380> 
// 获取所有的字段和值
172.19.141.9:6380> hgetall user
1) "name"
2) "juck"
3) "age"
4) "28"
5) "address"
6) "china"
7) "sex"
8) "meal"
172.19.141.9:6380>

有序集合(Sorted sets)

sorted sets和sets非常相似,只是sorted sets中的成员都会关联一个评分(score),sorted sets内部会根据这个score队成员按最低分到最高分排列。

数据结构:

hashs和跳跃表。

添加键值对:

172.25.184.9:6380> zadd leaderbord 100 java 89 c 98 c++ 90 php
(integer) 4
172.25.184.9:6380>

获取键的值:

// 获取某个字段的值
172.25.184.9:6380> zadd leaderbord 100 java 89 c 98 c++ 90 php
(integer) 4
172.25.184.9:6380>
// 获取所有的字段和值,默认按照分数从低到高排序
172.25.184.9:6380> zrange leaderbord 0 -1 
1) "c"
2) "php"
3) "c++"
4) "java"
172.25.184.9:6380>

HyperLogLog

HyperLogLog 是用来做基数统计的算法,比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。

添加键值对:

172.25.184.9:6380> pfadd num 1, 3, 5, 7, 5, 7, 8
-> Redirected to slot [2765] located at 172.25.184.8:6380
(integer) 1
172.25.184.8:6380>

获取键的值:

172.25.184.8:6380> pfcount num
(integer) 5
172.25.184.8:6380>

GEO

GEO 主要用于存储地理位置信息,并对存储的信息进行操作,比如根据两个经纬度计算两个地点之间的距离。

添加键值对:

172.25.184.9:6380> geoadd city 23.666 46.333 beijing 23.4444 67.4444 shanghai
-> Redirected to slot [11479] located at 172.25.184.10:6380
(integer) 2
172.25.184.10:6380>

获取键的值:

// 获取shanghai的地理位置经纬度
172.25.184.10:6380> geopos city shanghai
1) 1) "23.44439774751663208"
   2) "67.44439923593716912"
172.25.184.10:6380> 
    
// 计算shanghai和beijing的直线距离,单位km
172.25.184.10:6380> geodist city beijing shanghai km
"2348.1778"
172.25.184.10:6380> 

Stream

Redis Stream 主要用于消息队列,提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。

添加键值对:

127.0.0.1:6381> xadd mystream * message1 "hello redis!" message2 "hello word"
"1647439358497-0"
127.0.0.1:6381>

获取键的值:

// 从消息队列中从最小位置开始,取一条数据。
127.0.0.1:6380> xrange mystream - + count 1
1) 1) "1647439358497-0"
   2) 1) "message1"
      2) "hello redis!"
      3) "message2"
      4) "hello word"
127.0.0.1:6380> 

四、发布订阅

Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。Redis 客户端可以订阅任意数量的频道。实际上就是一种数据传渠道。数据信息可以通过 发布订阅在两个不同的客户端之间进行传输。

分别在node-01和node-03上开启两个客户端,node-01上订阅频道kk,node-03上向kk频道发布消息,观察:

// node-01上订阅频道kk
172.25.184.9:6380> subscribe kk 
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "kk"
3) (integer) 1
1) "message"
2) "kk"
3) "hello node-01"

// node-03上向kk频道发布消息
node-01:6381> publish kk "hello node-01"
(integer) 0
node-01:6381>

五、事务

Redis事务是一个隔离性的操作,事务中的所有的操作都会被序列化,按照顺序执行。事务在执行的过程中,不会被其他的来自客户端的命令打断。实际上,Redis的事务就是将多个命令进行串联,将命令放到队列中,防止其他命令插队,然后从队列中一次拿出命令执行。

Redis的事务一般分为三个步骤:开启事务(multi) ==》命令组队 ==》执行命令。

  • Redis的事务中,如果在组队阶段,有一个命令错误,那么在执行的时候,所有的命令都会执行失败。

  • Redis的事务中,如果执行阶段,有一个命令执行出错,那么只会有这一条命令执行失败,其他的命令都会执行成功。

// 事务
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)> exec
1) OK
2) OK
127.0.0.1:6379>

// 开启事务后,组队过程中一个命令错误,执行时直接出错
127.0.0.1:6379> multi 
OK
127.0.0.1:6379(TX)> set k3 1
QUEUED
127.0.0.1:6379(TX)> set k2 
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379(TX)> set k4 1
QUEUED
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379>
    
// 开启事务后,执行过程中一个命令错误(incr不能对字符串增加1),其他命令执行成功
127.0.0.1:6379> multi 
OK
127.0.0.1:6379(TX)> set a1 a1
QUEUED
127.0.0.1:6379(TX)> incr a1
QUEUED
127.0.0.1:6379(TX)> set b1 b1
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
127.0.0.1:6379>    
 
  1. Redis中的乐观锁:

乐观锁类似于在数据上增加一个版本号的校验,以下使用watch机制进行模拟:

// 第一个客户端中,对nums进行监视,并且开启事务,在事务中准备对nums增加100
127.0.0.1:6379> get nums
"100"
127.0.0.1:6379> watch nums
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incrby nums 100
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 200
127.0.0.1:6379>  

// 第二个客户端中,对nums进行监视,并且开启事务,在事务中准备对nums增加1,但是在第一个客户端执行exec之后,在第二个客户端执行exec,事务直接报错,这就是由于乐观锁的原因。
127.0.0.1:6379> watch nums
OK
127.0.0.1:6379> multi 
OK
127.0.0.1:6379(TX)> incr nums
QUEUED
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379>

总结:Redis中的三个特性:

  • 单独的隔离操作:

Redis事务中的所有命令,都会被放在一个队列中,并且按照顺序执行,不会被其他的命令打断。

  • 没有隔离级别的概念:

Redis事务中,如果没有真正执行exec命令,那么所有的命令实际上都不会被执行。

  • 不保证原子性:

Redis事务中,不保证原子性

​ 如果在组队过程中,命令出现错误,那么整个事务都会执行失败。

​ 如果在组队过程中,命令没有出现错误,但是在执行的过程中出现错误,那么只会是出错的命令执行失败,其他的命令执行成功,没有回滚操作。

六、持久化

Redis中为我们提供了两种持久化方式。

RDB:

RDB持久化方式能够在指定的时间间隔能对你的数据进行快照存储。RDB默认是开启的,Redis服务启动后,会在软件安装该目录下生成一个叫做dump.rdb文件,该文件保存的是某一个时间内的数据快照。RDB数据持久化更适合于对数据的完成行要求不是很高的场景下。

// redis.conf配置文件中
# 开启rdb持久化策略,默认开启。
dbfilename dump.rdb 
    
# RDB持久化策略工作默认的几个配置。
# save 3600 1 # 每个小时内,如果至少有一个key发生改变就持久化一次
# save 300 100 # 每五分钟内,如果至少有100个key发生改变就持久化一次
# save 60 10000 # 每分钟内,如果至少有10000个key发生改变就持久化一次

# RDB持久化后生成的文件名,默认dump.rdb。
dbfilename dump.rdb

# RDB持久化后生成的文件dump.rdb保存的文件路径,默认在软件安装目录下。
dir ./

工作方式:

当 Redis 需要保存 dump.rdb 文件时, 服务器执行以下操作:

  • Redis 调用forks. 同时拥有父进程和子进程。
  • 子进程将数据集写入到一个临时 RDB 文件中。
  • 当子进程完成对新 RDB 文件的写入时,Redis 用新 RDB 文件替换原来的 RDB 文件,并删除旧的 RDB 文件。

这种工作方式称为写时复制机制。

优点:

  1. rdb文件是一个非常紧凑的文件,非常节省空间,它保存的是某一个时间点的快照信息。可以根据需求恢复数据到指定的时间点。
  2. rdb文件恢复时,父进程只需要开启一个子进程(fork)去完成数据恢复操作,为父进程可以去正常处理其他任务,充分发挥了Redis的性能。
  3. 相比较AOF,RDB文件要小的很多,方便在网络之间传输,恢复的速度也比较块。

缺点:

  1. Redis最后一个时间间隔内处理的数据可能会被丢失。比如,在最后一分钟内,key的改变量没有达到10000,但是服务却奔溃了,那么数据就会丢失。
  2. 在RDB持久化的时候,需要开辟一个子进程将数据集持久化到磁盘,如果数据量比较大的时候,fork的工作量将会非常繁重,此时可能会影响Redis的读写响应速度。

AOF:

AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾。Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。

// redis.conf配置文件中
# 开启rdb持久化策略,默认关闭。
appendonly yes
    
# AOF持久化后生成的文件名,默认appendonly.aof,默认存储位置和RDB的文件路径一致。    
appendfilename "appendonly.aof"
    
# AOF持久化到磁盘的频率:no-不开启,持久化交给系统,everysec-每秒钟持久化一次,always-总是执行,每执行一个写命令就持久化一次。    
appendfsync everysec

工作方式:

AOF 重写和 RDB 创建快照一样,都巧妙地利用了写时复制机制:

  • Redis 执行 fork() ,同时拥有父进程和子进程。
  • 子进程开始将新 AOF 文件的内容写入到临时文件。
  • 对于所有新执行的写入命令,父进程一边将它们累积到一个内存缓存中,一边将这些改动追加到现有 AOF 文件的末尾,这样即使在重写的中途发生停机,现有的 AOF 文件也还是安全的。
  • 当子进程完成重写工作时,它给父进程发送一个信号,父进程在接收到信号之后,将内存缓存中的所有数据追加到新 AOF 文件的末尾。
  • 现在 Redis 原子地用新文件替换旧文件,之后所有命令都会直接追加到新 AOF 文件的末尾。

优点:

  1. AOF更加安全,写操作都被记录到文件中,那么在启动的时候,只需要执行这些写操作文件即可恢复数据,并且根据 AOF持久化到磁盘的频率,可以做到很少数据的丢失。
  2. AOF文件是一个只进行追加的日志文件,如果由于某些原因(磁盘空间已满,写的过程中宕机等等)未执行完整的写入命令,可使用redis-check-aof工具修复这些问题。
  3. Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。
  4. AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析也很轻松。 导出 AOF 文件也非常简单: 举个例子, 如果你不小心执行了 FLUSHALL 命令, 但只要 AOF 文件未被重写, 那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态

缺点:

  1. 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。
  2. 根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB 。 在一般情况下, 每秒 fsync 的性能依然非常高, 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 不过在处理巨大的写操作时,RDB 速度更快。

七、主从复制

Redis的主从复制模式是Redis的一种高可用的实现机制,在主从复制模式,至少要有主服务器 (master) 和从服务器 (slave),一个主服务器可以有多个从服务器,一主多从模式也是实际场景上最多使用的模式。主服务器主要用来执行写操作,从服务器主要用来执行读操作,真正意义上做到读写分离,并且在主从模式中,只能由主服务器向从服务器进行复制。

主从复制特点:

  • 数据冗余(数据热备份):主服务器将数据热备份到多台从服务器上,这样即使主服务器奔溃,那么数据也会在从服务器上保存,也可从从服务器上进行恢复。
  • 故障转移:当主服务器奔溃之后,无法对外提供服务器,可以立马进行故障转移,从服务器中选举一台作为新的主服务器对外提供服务。
  • 负载均衡:读写分离,做到读请求和写请求分别在不同的机器上执行。
  • 高可用:主从复制加上哨兵模式即可自动做到故障转移,保证服务在任意时刻都可以对外提供服务。
  • 容在恢复:即使主服务器奔溃,也可从从服务器上进行恢复。

Redis主从复制搭建(一主一从):

  1. 复制两个redis配置文件:cp …/redis.conf ./redis-6380.conf cp …/redis.conf ./redis-6381.conf。

  2. 修改配置文件(以redis-6380.conf为例):

    // 修改redis-6380.conf文件
    # 配置端口号
    port 6380
    # 修改PID文件
    pidfile /var/run/redis_6381.pid
    
  3. 分别在两个窗口启动6380,6381两个服务:

    ./redis-server /orkasgb/software/redis-6.2.6/master-slave/redis-6380.conf
    ./redis-server /orkasgb/software/redis-6.2.6/master-slave/redis-6381.conf
    
  4. 分别在两个窗口启动6380,6381两个客户端,分别查看两个服务的状态,默认两个服务都是主服务器(master角色):

    [root@node-04 src]# ./redis-cli -p 6380
    127.0.0.1:6380>  info replication
    # Replication
    role:master
    connected_slaves:0
    master_failover_state:no-failover
    master_replid:5ecb2c6e3f4192a115968038458ac77267d774cd
    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
    127.0.0.1:6380>
        
    [root@node-04 src]# ./redis-cli -p 6381
    127.0.0.1:6381> info replication
    # Replication
    role:master
    connected_slaves:0
    master_failover_state:no-failover
    master_replid:441486b83b680189c93333d8a9110231cefb8b96
    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
    127.0.0.1:6381> 
    
  5. 我们将6381设置为6380的从服务器,并查看日志。

    // 在6381的客户端执行新版本命令为replicaof 127.0.0.1 6380、旧版本命令为:slaveof 127.0.0.1 6380
    127.0.0.1:6381> replicaof 127.0.0.1 6380
    OK
    127.0.0.1:6381> 
    
    // 6380的角色依旧是master,但是多了一个slave:6381,状态:在线
    127.0.0.1:6380>  info replication
    # Replication
    role:master
    connected_slaves:1
    slave0:ip=127.0.0.1,port=6381,state=online,offset=5432,lag=1
    master_failover_state:no-failover
    master_replid:2a21331c3267804415503a8930f57ba0eefe9aed
    master_replid2:0000000000000000000000000000000000000000
    master_repl_offset:5432
    second_repl_offset:-1
    repl_backlog_active:1
    repl_backlog_size:1048576
    repl_backlog_first_byte_offset:1
    repl_backlog_histlen:5432
    127.0.0.1:6380> 
     
    // 6381的角色变成了slave,没有从机
    127.0.0.1:6381> info replication
    # Replication
    role:slave
    master_host:127.0.0.1
    master_port:6380
    master_link_status:up
    master_last_io_seconds_ago:3
    master_sync_in_progress:0
    slave_read_repl_offset:5474
    slave_repl_offset:5474
    slave_priority:100
    slave_read_only:1
    replica_announced:1
    connected_slaves:0
    master_failover_state:no-failover
    master_replid:2a21331c3267804415503a8930f57ba0eefe9aed
    master_replid2:0000000000000000000000000000000000000000
    master_repl_offset:5474
    second_repl_offset:-1
    repl_backlog_active:1
    repl_backlog_size:1048576
    repl_backlog_first_byte_offset:1
    repl_backlog_histlen:5474
    127.0.0.1:6381>  
        
    // 6381的后台日志: 
    35847:M 15 Mar 2022 06:06:01.680 * Ready to accept connections
    // 在降级成别人的slave之前,他会把自己的曾经作为主服务器的参数进行缓存。
    35847:S 15 Mar 2022 07:49:39.810 * Before turning into a replica, using my own master parameters to synthesize a cached master: I may be able to synchronize with the new master with just a partial transfer.
    // 开始链接6380。
    35847:S 15 Mar 2022 07:49:39.810 * Connecting to MASTER 127.0.0.1:6380
    // 主从复制请求创建。    
    35847:S 15 Mar 2022 07:49:39.811 * MASTER <-> REPLICA sync started
    35847:S 15 Mar 2022 07:49:39.811 * REPLICAOF 127.0.0.1:6380 enabled (user request from 'id=3 addr=127.0.0.1:59268 laddr=127.0.0.1:6381 fd=8 name= age=6048 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=44 qbuf-free=40910 argv-mem=22 obl=0 oll=0 omem=0 tot-mem=61486 events=r cmd=replicaof user=default redir=-1')
    // 数据同步是一个非阻塞事件,主服务器在同步数据的时候,还能继续接受写操作,从服务器也可以继续提供读操作。    
    35847:S 15 Mar 2022 07:49:39.811 * Non blocking connect for SYNC fired the event.
    // 主服务器应答了从服务器的同步数据的请求,可以继续。     
    35847:S 15 Mar 2022 07:49:39.811 * Master replied to PING, replication can continue...
    35847:S 15 Mar 2022 07:49:39.811 * Trying a partial resynchronization (request 441486b83b680189c93333d8a9110231cefb8b96:1).
    // 拿到主服务器分配的同步信息:runid为2a21331c3267804415503a8930f57ba0eefe9aed,同步数据偏移量为0。        
    35847:S 15 Mar 2022 07:49:39.812 * Full resync from master: 2a21331c3267804415503a8930f57ba0eefe9aed:0
    // 从服务器丢弃自己的之前的缓存的作为主服务器的信息。        
    35847:S 15 Mar 2022 07:49:39.812 * Discarding previously cached master state.
    // 从主服务器上接收到了一些数据,并把它保存在了磁盘上。       
    35847:S 15 Mar 2022 07:49:39.852 * MASTER <-> REPLICA sync: receiving 185 bytes from master to disk
    // 从服务器清空了自身的数据。     
    35847:S 15 Mar 2022 07:49:39.853 * MASTER <-> REPLICA sync: Flushing old data
    // 从服务器把保存在磁盘上数据载入内存。         
    35847:S 15 Mar 2022 07:49:39.853 * MASTER <-> REPLICA sync: Loading DB in memory
    35847:S 15 Mar 2022 07:49:39.945 * Loading RDB produced by version 6.2.6
    35847:S 15 Mar 2022 07:49:39.945 * RDB age 0 seconds
    35847:S 15 Mar 2022 07:49:39.945 * RDB memory usage when created 1.85 Mb
    35847:S 15 Mar 2022 07:49:39.945 # Done loading RDB, keys loaded: 1, keys expired: 0.
    // 从服务器最终完成数据同步。   
    35847:S 15 Mar 2022 07:49:39.945 * MASTER <-> REPLICA sync: Finished with success    
    
    // 6380的后台日志:
    34469:M 15 Mar 2022 06:05:39.525 * RDB memory usage when created 0.77 Mb
    34469:M 15 Mar 2022 06:05:39.525 # Done loading RDB, keys loaded: 1, keys expired: 0.
    34469:M 15 Mar 2022 06:05:39.525 * DB loaded from disk: 0.000 seconds
    34469:M 15 Mar 2022 06:05:39.525 * Ready to accept connections
    // 准备应答6381的数据同步的请求
    34469:M 15 Mar 2022 07:49:39.811 * Replica 127.0.0.1:6381 asks for synchronization
    // 判断从服务器发送过来的信息,因为主服务器要确认从服务器是否之前和自己同步过数据,如果是,那么就进行增量同步,否则就进行全量同步。    
    34469:M 15 Mar 2022 07:49:39.811 * Partial resynchronization not accepted: Replication ID mismatch (Replica asked for '441486b83b680189c93333d8a9110231cefb8b96', my replication IDs are '5ecb2c6e3f4192a115968038458ac77267d774cd' and '0000000000000000000000000000000000000000')
    // 为它的从服务器创建了新的runid,并且设定同步偏移量为0。
    34469:M 15 Mar 2022 07:49:39.811 * Replication backlog created, my new replication IDs are '2a21331c3267804415503a8930f57ba0eefe9aed' and '0000000000000000000000000000000000000000'
    // 创建了BGSAVE任务,并且rdb文件保存的目的是磁盘。
    34469:M 15 Mar 2022 07:49:39.811 * Starting BGSAVE for SYNC with target: disk
    // 创建了BGSAVE任务进程号为754882
    34469:M 15 Mar 2022 07:49:39.812 * Background saving started by pid 754882
    // 文件准备保存在磁盘上   
    754882:C 15 Mar 2022 07:49:39.850 * DB saved on disk
    // 写时复制机制占用了的内存大小
    754882:C 15 Mar 2022 07:49:39.851 * RDB: 0 MB of memory used by copy-on-write
    // 文件保存在磁盘完成  
    34469:M 15 Mar 2022 07:49:39.852 * Background saving terminated with success
    // 6381的数据同步的请求完成
    34469:M 15 Mar 2022 07:49:39.852 * Synchronization with replica 127.0.0.1:6381 succeeded
    

    同步原理:

    Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。具体步骤如下:

    • 从服务器配置主服务器的连接信息(执行 replicaof 127.0.0.1 6380)。

    • 从服务器连接上主服务器,并向主服务器发送SYNC命令

    • 主服务器判断是否为全量复制(runid和数据偏移量):如果是全量复制,则进入下一步;否则执行增量复制的子流程。

      • 全量同步:

        1. 主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令。
        2. 主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令。
        3. 从服务器收到快照文件后丢弃自己机器上所有旧数据,载入收到的快照。
        4. 主服务器快照发送完毕后开始向从自己缓冲区中的写命令继续发送到从服务器上。
        5. 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令
      • 增量同步:

        主服务器将自身的写操作命令发送到从服务器上,从服务器开始执行相应的命令增量同步发生在主从服务器完全正常工作的情况下。

  6. 从机只能执行读操作,主机可以执行写操作:

    // 从机读操作正常,写操作错误
    127.0.0.1:6381> keys *
    1) "a"
    127.0.0.1:6381> get a
    "1"
    127.0.0.1:6381> set b 2
    (error) READONLY You can't write against a read only replica.
    127.0.0.1:6381> get c
    "3"
    127.0.0.1:6381>
    
    // 主机读操作正常,写操作正常
    127.0.0.1:6380> keys *
    1) "a"
    127.0.0.1:6380> get a
    "1"
    127.0.0.1:6380> set c 3
    OK
    127.0.0.1:6380> get c
    "3"
    127.0.0.1:6380>    
    

薪火相传:

上述操作中,6380主机器,6381是从机器,隶属于6380。可以再继续创建6382为从机,隶属于6381,其机制和6380与6381的一致。

反客为主:

当主服务器奔溃之后,将无法对外继续提供服务,那么需要有从服务器顶替主服务器。

// 主服务器上执行shutdown,模拟主服务器奔溃
127.0.0.1:6380> shutdown
not connected>
  
// 从服务器上观察信息,它所属的master服务器状态为down
127.0.0.1:6381> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6380
master_link_status:down
master_last_io_seconds_ago:-1
master_sync_in_progress:0
slave_read_repl_offset:6686
slave_repl_offset:6686
master_link_down_since_seconds:10
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:2a21331c3267804415503a8930f57ba0eefe9aed
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:6686
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:6686
127.0.0.1:6381> 

// 在从服务器上执行新版本命令:replicaof no one,旧版本命令:slaveof no one,并观察从服务器角色已经变成master。
127.0.0.1:6381> replicaof no one
OK
127.0.0.1:6381> info replication
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:1851f3ce6216dfed7a78198bf49da46f475024d8
master_replid2:2a21331c3267804415503a8930f57ba0eefe9aed
master_repl_offset:6686
second_repl_offset:6687
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:6686
127.0.0.1:6381>
    
// 再次启动6380,发现6380变成了master,没有从机,并没有加入到6381的主服务器中,如果想要6380隶属于6381的从机,那么需要重复上述操作
127.0.0.1:6380> info replication
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:d451a5f9b59ea76245689c6e46ed79e59b6211a7
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
127.0.0.1:6380>

哨兵模式:

哨兵模式是反客为主的自动化版本,当主服务器奔溃时,应当由程序自动发现并执行反客为主的流程,立刻进行故障转移。Redis中提供了一种哨兵模式帮我们实现。

// 将Redis安装目录下的sentinel.conf拷贝到master-slave目录下,作如下修改:端口号修改为6380,代表当前主从中,6380是主服务器,并且监控它。1代表至少一个哨兵节点判定主服务器故障时就可以进行故障转移
sentinel monitor mymaster 127.0.0.1 6380 1
    
// 启动哨兵
./redis-sentinel /orkasgb/software/redis-6.2.6/master-slave/sentinel.conf

// 启动日志
332671:X 16 Mar 2022 02:22:57.988 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
332671:X 16 Mar 2022 02:22:58.004 # Sentinel ID is 95c2efce33a520fc38ebad5c311838d1b1a84cb8
332671:X 16 Mar 2022 02:22:58.004 # +monitor master mymaster 127.0.0.1 6380 quorum 1
332671:X 16 Mar 2022 02:22:58.008 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6380
    
// 手动停掉6380,观察结果
332671:X 16 Mar 2022 02:22:58.004 # +monitor master mymaster 127.0.0.1 6380 quorum 1
332671:X 16 Mar 2022 02:22:58.008 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6380
// 哨兵节点已经发现6380故障了    
332671:X 16 Mar 2022 02:26:05.826 # +sdown master mymaster 127.0.0.1 6380
332671:X 16 Mar 2022 02:26:05.826 # +odown master mymaster 127.0.0.1 6380 #quorum 1/1
332671:X 16 Mar 2022 02:26:05.826 # +new-epoch 1
332671:X 16 Mar 2022 02:26:05.826 # +try-failover master mymaster 127.0.0.1 6380
// 这里就开始投票选举   
332671:X 16 Mar 2022 02:26:05.932 # +vote-for-leader 95c2efce33a520fc38ebad5c311838d1b1a84cb8 1
332671:X 16 Mar 2022 02:26:05.932 # +elected-leader master mymaster 127.0.0.1 6380
332671:X 16 Mar 2022 02:26:05.932 # +failover-state-select-slave master mymaster 127.0.0.1 6380
332671:X 16 Mar 2022 02:26:06.032 # +selected-slave slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6380
// 在从机服务器上执行slaveof no one    
332671:X 16 Mar 2022 02:26:06.032 * +failover-state-send-slaveof-noone slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6380
// 等待从机服务器响应
332671:X 16 Mar 2022 02:26:06.084 * +failover-state-wait-promotion slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6380
// 将6381晋升为mymaster    
332671:X 16 Mar 2022 02:26:07.090 # +promoted-slave slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6380
// 6380进行故障转移
332671:X 16 Mar 2022 02:26:07.090 # +failover-state-reconf-slaves master mymaster 127.0.0.1 6380
// 6380进行故障转移结束    
332671:X 16 Mar 2022 02:26:07.183 # +failover-end master mymaster 127.0.0.1 6380
// 切换mymaster为6381  
332671:X 16 Mar 2022 02:26:07.183 # +switch-master mymaster 127.0.0.1 6380 127.0.0.1 6381
332671:X 16 Mar 2022 02:26:07.184 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6381
332671:X 16 Mar 2022 02:26:37.213 # +sdown slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6381
 
// 此时6381已经变成master角色,没有从机。并且可以执行读写操作。
127.0.0.1:6381> info replication
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:f7f0c651546ad8933d6939c0346a4dc0696c7e8e
master_replid2:ac5ecd4a80d21511b4fd2bbd5c8a267d2412df92
master_repl_offset:56221
second_repl_offset:13373
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:56221
127.0.0.1:6381> 
127.0.0.1:6381> set d 234
OK
127.0.0.1:6381> keys *
1) "c"
2) "a"
3) "d"
127.0.0.1:6381> get d
"234"
127.0.0.1:6381>
    
 // 再次启动6380,发现已经称为6381的从服务器。
127.0.0.1:6381> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6380,state=online,offset=65611,lag=1
master_failover_state:no-failover
master_replid:f7f0c651546ad8933d6939c0346a4dc0696c7e8e
master_replid2:ac5ecd4a80d21511b4fd2bbd5c8a267d2412df92
master_repl_offset:65611
second_repl_offset:13373
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:65611
127.0.0.1:6381> 

// 观察6380上,有刚才停机时在6381上增加的key d,说明数据已经和6381同步,并且可读不可写。
127.0.0.1:6380> keys *
1) "d"
2) "a"
3) "c"
127.0.0.1:6380> set e 342
(error) READONLY You can't write against a read only replica.
127.0.0.1:6380>

主从选举策略:

1、优先选择优先级最小(replica-priority,默认100)的从机作为新的主机。

2、选择数据偏移量最小(从机的数据和主机的数据最接近的)的从机作为新的主机。

3、选择runnid最小的从机作为新的主机。

八、Redis应用问题:

缓存穿透

应用服务器需要查询的某一个数据在Redis缓存中无法查询到,所以需要查询数据库,导致大量的请求都会直接取查询数据,如果数据库中也没有,那么请求会不断的查询数据库,最终会导致数据库奔溃。黑客会利用这个漏洞进行恶意攻击。

现象:

- 应用服务器压力变大。
- Redis的命中率降低。
- 一直查询数据库。

原因:

- 网站收到恶意攻击,出现很多非正常url访问。
- Redis中查询不到所需要的数据。

解决方案:

如果对于一个没有缓存且数据库中也无法查询到的数据,由于请求会在缓存查询不到的时候查询数据库,并且一般都是数据库中查询到了数据才去缓存数据,查不到不做缓存,那么这种情况下,将会导致请求不断的直接去查询数据库,而Redis也没有起到缓存的效果。

  • 对空值也进行缓存:上述现象中,当数据库中也无法查询到数据时,对查询的key也进行空值缓存,并设置该key的过期较短可以一定程度上避免上述问题。
  • 设置白名单:对请求而的地址进行白名单设置,可以使用bitmap进行设置。
  • 采用布隆过滤器:查询某个key的时候,将这个key利用布隆过滤器进行过滤。
  • 对服务器进行实时监测

缓存击穿

应用服务器查询的某一个key在Redis中存在,但是此时,Redis中这个key过期了,大量查询请求同样会直接查询数据库,然后将数据数据缓存到Redis中,同样导致数据库压力增大,甚至奔溃,即是所谓的热点key问题。

现象:

  • 数据库访问压力突然变大。
  • Redis正常运行。

解决方案:

热点key存在某个时刻出现大量并发访问的现象,这种情况下造成数据库访问压力过大。

  • 设置热门key永不过期:上述现象中,可以适当加大热门key的过期时间。
  • 预先设置热门key:将一些热门key提前预置到Redis中。
  • 加锁:
    • 当发现缓存中获取的key为空的时候,不立即取查询数据库,而是让一个线程加锁并使用setnx构建一个缓存,当返回结果为成功的时候,再去查询数据库,重新构建缓存,并且删除之前设置的缓存。

缓存雪崩

应用服务器查询大量的key在Redis中存在,但是此时,Redis中这些key集中在同一时间点过期了,大量查询请求同样会直接查询数据库,然后将数据数据缓存到Redis中,同样导致数据库压力增大,甚至奔溃。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

现象:

  • 数据库访问压力变大,服务相应速度变慢。

解决方案:

大量不同的key在某个时刻集中出现过期现象,这种情况下造成数据库访问压力过大。

  • 设置多级缓存:Redis缓存 + 其他缓存(memcache)。
  • 随机设置key的过期时间:随机设置key的过期时间,不让大量key的过期时间在同一个时刻。
  • 加锁或者队列:
    • 当key过期的时间,直接加锁,不让大量请求在同一时刻落在数据库上。
  • 设置过期标志:当key快要过期的时候,开启一个新的线程,让线程去查询数据库更新缓存。

九、分布式锁(单机版)

Redis可以使用Lua脚本实现分布式锁。在Lua脚本执行期间,任何其他客户端的命令都不能被执行,必须等待Lua脚本执行完成后才能执行,所以Lua脚本可以实现真正意义上的分布式锁。即Lua能保证原子性。

Lua脚本(包含SETNX + EXPIRE两条指令)

if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then
   redis.call('expire',KEYS[1],ARGV[2])
else
   return 0
end;

分布式伪代码:

String lua_scripts = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" +
            " redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end";   
Object result = jedis.eval(lua_scripts, Collections.singletonList(key_resource_id), Collections.singletonList(values));
//判断是否成功
return result.equals(1L);

案例:

模拟秒杀场景:设置产品数量product_num为200个,controller层调用以下两个方法,使用Jmeter进行并发访问。从结果来看,在并发量很小的情况下,基本上可以解决超卖问题,但是在并发量很大的情况下,问题也是很明显的,具体见注释说明。

@Service
public class RedisDistributedLock {

	@Resource
	Redisson redisson;

	/**
	 * Redis实现简单的分布式锁(高并发场景下依旧存在问题)
	 * <p>
	 * ===========================问题分析1============================
	 * <p>
	 * 线程1总共需要15s执行完成,处理业务花费10s,此时redis自动删除线程1的锁。线程2刚好进来,
	 * 发现所已经被删除,获取锁,总共需要8s执行完成,比如执行到6s的时候,线程1执行结束了,开始删除锁,
	 * 但是此时的锁是线程2的锁,也就是说,线程1在线程2还未执行完成后,删除了线程2的锁,那么导致,线程3又
	 * 可以获得锁执行,一次类推,同样会导致超卖问题,即锁永久失效,相当于没有加锁。解决思路:1.给每个线程设置一个唯一的值,
	 * 类似于线程id,删除锁之前判断,这个值,相当于只有自己的才能删除自己设置的锁。2.开辟一个新的线程,不断的
	 * 延长当前线程设置的锁的超时时间。
	 * <p>
	 * ===========================问题分析2============================
	 * <p>
	 * 思索问题,因为设置锁和设置锁过期时间是分两步操作,那么不保证原子性,如果jedis.del("product_lock")不放在
	 * finally中,那么就会发生死锁。解决思路:使用lua脚本实现原子性。
	 */
	public String distributedLock() {
		Jedis jedis = null;
		try {
			jedis = new Jedis(HostAndPort.from("172.25.184.11:6381"));
			long lock = jedis.setnx("product_lock", "100001");
			jedis.expire("product_lock", 10);
			if (lock == 0L) {
				System.out.println("系统繁忙,请稍后再试!");
				return "系统繁忙,请稍后再试!";
			}

			String productNum = jedis.get("product_num");
			if (0 == Integer.parseInt(productNum)) {
				System.out.println("秒杀活动结束,敬请期待!");
				return "秒杀活动结束,敬请期待!";
			}
			jedis.decr("product_num");
			System.out.println("恭喜你,抢到了,目前还剩 " + productNum);
		} catch (Exception e) {
			System.out.println("系统繁忙,请稍后再试!");
			return "系统繁忙,请稍后再试!";
		} finally {
			if (jedis != null) {
				jedis.del("product_lock");
			}
		}
		System.out.println("秒杀成功了!");
		return "秒杀成功了!";
	}

	/**
	 * Redisson实现分布式锁
	 *<p>
	 * ===========================问题分析1============================
	 * <p>
	 * 主从架构失效问题:假如使用主从架构,如果线程1将写锁命令发送在master上,master写成功后将写成功的
	 * 响应反馈给客户端,突然master就奔溃了,而此时,写锁命令还没有来得及同步到slave,由于redis架构的
	 * 高可用特性,选择了一个slave作为了新的master,此时线程2获取锁,发现新的master上并没有锁,那么加锁
	 * 成功,同样会出现超卖问题。解决思路:选择zookeeper、redis集群或者容忍这种问题。因为在分布式架构中,
	 * CAP只能只能满足其中两种,CP或者AP。
	 */
	public String redissonLock() {
		RLock productLock = redisson.getLock("product_lock");
		try {
			productLock.lock();
			Jedis jedis = new Jedis(HostAndPort.from("172.25.184.11:6381"));
			String productNum = jedis.get("product_num");
			if (0 == Integer.parseInt(productNum)) {
				System.out.println("秒杀活动结束,敬请期待!");
				return "秒杀活动结束,敬请期待!";
			}
			jedis.decr("product_num");
			System.out.println("恭喜你,抢到了,目前还剩 " + jedis.get("product_num"));
		} catch (Exception e) {
			System.out.println("系统繁忙,请稍后再试!");
			return "系统繁忙,请稍后再试!";
		} finally {
			productLock.unlock();
		}
		System.out.println("秒杀成功了!");
		return "秒杀成功了!";
	}
}

十、缓存更新策略

缓存的数据会存在一定时间内和数据库数据不一致的情况,需要根据某种策略同步更新缓存数据。

  • LRU/LFU/FIFO算法:剔除算法一般基于缓存中存活时间以及访问量来判断是否需要删除缓存。Redis中使用maxmemory-policy这个参数控制缓存更新策略,默认值:noeviction,不使用淘汰策略。
    • LRU:最近最少使用。判断最近最少使用,最远的数据优先被淘汰。
    • LFU:最不经常使用。判断一段时间内最少使用的数据优先被淘汰。
    • FIFO:先进先出队列。判断被存储的时间最长,最远的数据优先被淘汰。
  • 超时剔除:设置key在过期时间后自动删除。Redis中提供了expire命令设置key的过期时间,当key被删除后,再从数据库中获取真实数据重新进行缓存。
  • 主动更新:可以利用监控机制,当数据库数据更新后,立即更新缓存。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夜间沐水人

文章编写不易,一分钱也是爱。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值