高性能数据处理、NoSQL、分库分表【Redis】

高并发核心技术Redis

一、Redis概述

1 NoSQL介绍

1.1 NoSQL由来

任何技术的出现都是一步一步演进出来的。

  1. 在互联网诞生初期,一个网站一个应用访问量都不大, 使用单机Mysql数据库可以应对。
    在这里插入图片描述
  2. 随着用户的不断增多,单机MySQL可能无法放下,服务器也可能承受不住一个网站,大多数的情况都是查询,为了减少数据库的压力,引入读写分离策略,让主数据库处理事务性增、改、删操作,而从数据库处理SELECT查询操作。
    通过读写分离的方式,解决问题。

在这里插入图片描述
3. Mysql读的问题得到了缓解,写的压力依然存在,开始通过分库分表和MySQL集群来解决写的问题。
在这里插入图片描述
4. 当前,数据量越来越大、数据类型也越来越复杂,Mysql数据库在面对数据量很多、变化很大数据,如大文本字段、图片等,MySQL效率就低了,并且扩展性差,大数据量场景下 IO 压力大,结构更改困难。如果有一种数据库专门来存储此类数据,分担读取数据的压力,MySQL压力就会变得很小。
NoSQL数据库就应运而生,可以很好的解决上述问题。
在这里插入图片描述

1.2 什么是NoSQL

NoSQL = Not Only SQL 不仅仅是SQL
泛指非关系型数据库,对应对比关系型数据库,NoSQL没有sql语句使用,不需要设计表结构,没有表关系。

很多数据类型如:用户的个人信息,社交网络,地理位置,这些数据类型的存储不需要一个固定的模式,类似于 Map<String,Object>使用键值对来控制。

1.3 NoSQL的特点

  1. 方便扩展(数据之间没有关系,很好扩展)

  2. 大数据量高性能(Redis一秒写8万次,读取11万次,NoSQL缓存记录级,是一种细粒度的缓存,性能比较高)

  3. 数据类型多样性(不需要事先设计数据库,如果是数据量特别大的表,设计繁琐)

  4. 传统RDBMS 与 NoSQL

     RDBMS:
     - 结构化数据
     - 结构化查询语句SQL
     - 数据和关系都存储在单独的表中
     - 数据操纵语言,数据定义语言
     - 严格的一致性
     - 基础事务
     NoSQL:
     - 没有固定的查询语言
     - 键值对存储、列存储、文档存储、图关系数据库
     - 最终一致性
     - CAP定理和BASE理论
     - 高性能,高可用,高可扩
    

1.4 NoSQL的四大分类

在这里插入图片描述

2 Redis概述

2.1 Redis是什么

  1. Redis(Remote Dictionary Server 远程字段服务)是一个开源的使用 ANSI C语言编写、支持网络、内存亦可持久化的key-value数据库,并提供多种语言的 API。
  2. Redis是一个key-value存储系统,它支持存储的value类型相对更多,包括string、list、set、zset(sorted set --有序集合)和hash。这些数据结构都支持push/popadd/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,Redis支持各种不同方式的排序。为了保证效率,数据都是缓存在内存中,Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
  3. Redis提供了java、C/C++、PHP、JavaScript、Perl、Object-C、Python、Ruby、Erlang等客户端,使用很方便。

2.2 Redis能干嘛

  1. 读写效率高,用于高速缓存
  2. 发布,订阅消息(消息通知)
  3. 地图信息分析
  4. 活动排行榜或计数
  5. …………

2.3 Redis特点

  1. 多样的数据类型:Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
  2. Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
  3. Redis的所有操作都是原子性的。
  4. 支持主从复制及集群。

3 Redis安装

3.1 下载地址

Redis官方网址:https://redis.io/

3.2 下载Redis

  1. 下载6.2.4 for Linux
    在这里插入图片描述
  2. redis-6.2.4.tar.gz上传至CentOS并解压,解压后得到redis-6.2.4目录
    解压命令:
tar -zxvf redis-6.2.4.tar.gz

3.3 安装GCC

安装C语言编译环境

dnf group install "Development Tools"

通过使用 gcc --version命令打印 GCC 版本,来验证 GCC 编译器是否被成功安装:

gcc --version

3.4 安装Redis

1. 编译Redis
redis-6.2.4目录下执行:

make

出现下图代表编译成功
在这里插入图片描述
2. 安装Redis
redis-6.2.4目录下执行:

make install

出现下图代表安装成功
在这里插入图片描述
3. 安装目录:/usr/local/bin
在这里插入图片描述
redis-benchmark:Redis自带的基准性能测试工具

redis-check-aof:对有问题的 AOF 文件进行修复,AOF 和 RDB 文件后面会说明

redis-check-rdb:对有问题的 RDB 文件进行修复

redis-sentinel:Redis集群使用

redis-cli:客户端
redis-server:服务器启动

4. 服务启动
前台启动:/usr/local/bin下执行

./redis-server

在这里插入图片描述
后台启动:
拷贝redis-6.2.4目录中的redis.conf文件到其他目录

mkdir /usr/local/myredis

cp redis.conf /usr/local/myredis/redis.conf

设置/usr/local/myredis/redis.conf文件中的daemonize属性,由no改为yes
/usr/local/bin下执行

./redis-server /usr/local/myredis/redis.conf

通过ps aux | grep redis-server查看服务是否启动
在这里插入图片描述
5. 客户端启动
/usr/local/bin下执行

./redis-cli

在这里插入图片描述
ping命令可以检测服务器是否正常(服务器返回PONG)

ping

在这里插入图片描述

4 Redis基本知识

1. 端口6379的由来
6379 = Merz
在这里插入图片描述
Merz全名Alessia Merz,是意大利的一位广告女郎。
在这里插入图片描述
2. 默认有16个数据库,且初始状态默认选择0号数据库(即第一个数据库)。
3. 可以使用select 进行数据库切换。

select 8 切换到8号数据库

在这里插入图片描述
4. 统一密码管理,所有库密码都一致。
5. dbsize-查看当前数据库的key数量。
6. flushdb-清空当前库。
7. flushall-清空所有库。
8. 为什么Redis是单线程且效率极高:
1).绝大部分请求是纯粹的内存操作。
2).避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑。各种锁的问题,不存在加锁释放锁操作。
3).使用IO多路复用技术,可以处理并发的连接。
在这里插入图片描述

二、Redis数据类型

1 key键

1.keys * 查看当前库中所有的key 。
2.exists key 判断某个key是否存在。
可以设置多个key,只返回存在的个数,但不返回哪一个存在/不存在。

exists k1 查看k1是否存在,如果存在返回1
exists k1 k2 k3 查看k1 k2 k3是否存在,如果k1 k2存在,k3不存在,则返回2
  1. move key db 将当前数据库的 key 移动到给定的数据库 db 当中。
move k1 8 将k1从当前数据库移动到8号库
  1. type key查看当前key 所储存的值的类型。
    返回当前key所储存的值的类型,如string 、list等。
  2. del key 删除已存在的key,不存在的 key 会被忽略。
    可以设置多个key,返回删除成功的个数。
del k1 删除k1,如果成功返回1,失败返回0
del k1 k2 k3 删除k1 k2 k3,如果k1 k2存在,k3不存在,则返回2
  1. expire key time 给key设置 time 秒的过期时间。
    设置成功返回 1 。 当 key 不存在返回 0。
expire k1 10 给k1设置10秒后过期

7.ttl key以秒为单位返回 key 的剩余过期时间。
当 key 不存在时,返回 -2 。 当 key 存在但没有设置剩余生存时间时,返回 -1 。 否则,以秒为单位,返回 key 的剩余生存时间。
8.persist key移除给定 key 的过期时间,使得 key 永不过期。
当过期时间移除成功时,返回 1 。 如果 key 不存在或 key 没有设置过期时间,返回 0 。

2 五大数据类型-String(字符串)

2.1 简介

String是Redis最基本的类型,一个key对应一个value。
String是二进制安全的,意味着String可以包含任何数据,比如序列化对象或者一张图片。
String最多可以放512M的数据。

2.2 常用命令

  1. set key value 用于设置给定 key 的值。如果 key 已经存储其他值, set 就重写旧值,且无视类型。
set k1 v1 向Redis中设置一个k1的键值对
set k1 100 将k1的值由v1重置为100
  1. get key 用于获取指定 key 的值。如果 key 不存在,返回 nil 。
  2. append key value将给定的value追加到key原值末尾。
    如果 key 已经存在并且是一个字符串, append 命令将 value 追加到 key 原来的值的末尾。
    如果 key 不存在, append 就简单地将给定 key 设为 value ,就像执行 set key value 一样。
append k1 v1 在Redis中不存在k1,所以直接设置k1的值为v1
append k1 v2 向k1的值末尾添加一个v2,最终结果为v1v2
  1. strlen key 获取指定 key 所储存的字符串值的长度。当 key 储存的不是字符串值时,返回一个错误。
  2. setex key time value给指定的 key 设置值及time 秒的过期时间。如果 key 已经存在, setex命令将会替换旧的值,并设置过期时间。
setex k1 10 v1 向Redis中设置一个k1的键值对并且10秒后过期
  1. setnx key value当key不存在时,设置给定 key 的值。如果key存在,则没有任何影响。
setnx k1 v1 向Redis中设置一个k1的键值对
setnx k1 v2 Redis中存在k1,则没有影响,k1的值仍然为v1
  1. incr key将 key 中储存的数字值增一。
    如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 incr 操作。
    如字符串类型的值不能表示为数字、或者是其他类型,那么返回一个错误。
incr k1 因为Redis中不存在k1,所以先初始化为0,再递增,值为1
incr k1 存在k1,递增后k1的值为2
set k2 v2
incr k2 因为k2不为数值,Redis返回一个错误
  1. decr key将 key 中储存的数字值减一。
    如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 decr 操作。
    如字符串类型的值不能表示为数字、或者是其他类型,那么返回一个错误。
decr k1 因为Redis中不存在k1,所以先初始化为0,再递增,值为-1
decr k1 存在k1,递增后k1的值为-2
set k2 v2
decr k2 因为k2不为数值,Redis返回一个错误
  1. incrby/decrby key step将key存储的数字值按照step进行增减。
    如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行incrby/decrby 命令。
    如字符串类型的值不能表示为数字、或者是其他类型,那么返回一个错误。
  2. mset key1 value1 key2 value2 ……同时设置一个或多个 key-value
mset k1 v1 k2 v2 k3 v3 同时向Redis中设置了k1 k2 k3
  1. mget key1 key2 ……返回所有(一个或多个)给定 key 的值。
    如果给定的 key 里面,有某个 key 不存在,那么这个 key 返回特殊值 nil 。
mget k1 k2 同时获取k1 k2
  1. msetnx key1 value1 key2 value2 ……用于所有给定 key 都不存在时,同时设置一个或多个key-value
    msetnx具有原子性特性,有一个失败,则都失败。
msetnx k1 v1 k2 v2 向Redis中设置k1 k2两个键值对
msetnx k1 v2 k3 v2 Redis中存在k1,k1设置失败,由于原子性特性,k3也设置失败
  1. getrange key start end用于获取存储在指定 key 中字符串的子字符串。字符串的截取范围由start 和 end 两个偏移量决定(包括 start 和 end 在内)。
set java helloworld 设置一个key为java,value为helloworld的值
getrange java 0 3 获取索引0-3的值,结果为hell
  1. setrange key offset value用指定的字符串重写给定 key 所储存的字符串值,重写的位置从偏移量 offset 开始。
set java helloworld 设置一个key为java,value为helloworld的值
setrange java 5 baizhan 从偏移位置5(w)开始,用baizhan重写key

2.3 String底层数据结构

String底层数据结构是简单动态字符串(simple dynamic string,SDS)。
类似于JAVA中的 ArrayList,采用预分配方式来减少内存的频繁分配。
在这里插入图片描述
如图,内存实际大小一般都要高于字符串实际大小。当字符串长度小于1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时每次只会多扩1M的空间。字符串最大长度为 512M。

3 五大数据类型-List(列表)

3.1 简介

List是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。

底层是一个双向链表,对两段操作性能极高,通过索引操作中间的节点性能较差。
在这里插入图片描述
一个List最多可以包含 2^32个元素 ( 每个列表超过40亿个元素)。

3.2 常用命令

  1. lpush/ rpush key1 value1 value2 value3……从左边(头部)/右边(尾部)插入一个或多个值。
lpush k1 v1 v2 v3 从左边放入v1 v2 v3

在这里插入图片描述

rpush k1 v4 v5 v6 从右边放入v4 v5 v6

在这里插入图片描述
2. lrange key start end返回 key 列表中的 start 和 end 之间的元素(包含start和end)。 其中0表示列表的第一个元素,-1表示最后一个元素。

lrange k1 0 2 取出列表里前3个值,结果为v3 v2 v1
lrange k1 0 -1 取出列表里全部值,结果为v3 v2 v1 v4 v5 v6

3.lpop/rpop key移除并返回第一个值/最后一个值。
值在键在,值光键亡。

lpop k1 从列表中删除v3,并返回,当前列表全部值v2 v1 v4 v5 v6
rpop k1 从列表中删除v6,并返回,当前列表全部值v2 v1 v4 v5
  1. lindex key index获取列表index位置的值(从左开始)。
  2. llen key获取列表长度。
  3. lrem key count value从左边开始删除与value相同的count个元素。
lrem k1 2 v1 从左边开始删除k1列表中2个v1元素

7.linsert key before/after value newvalue在列表中 value 值的前边/后边插入一个 newvalue 值(从左开始)。

linsert k1 before v1 v5 在v1前面插入一个v5

8.lset key index value将索引为 index 的值设置为 value

4 五大数据类型-Set(集合)

4.1 简介

与List类似是一个列表功能,但Set是自动排重的,当需要存储一个列表数据,又不希望出现重复数据时,Set是一个很好的选择。

Set是String类型的无序集合,它底层其实是一个value为null 的 hash表,所以添加、删除、查找的时间复杂度都是O(1)。

一般来说,一个算法如果是O(1),随着数据增加,查找数据的时间不变。
集合中最大的成员数为 2^32 - 1 ( 每个集合超过40亿个元素)。

4.2 常用命令

  1. sadd key value1 value2……将一个或多个元素添加到集合key中,已经存在的元素将被忽略。
sadd k1 v1 v2 v2 v3 v4 v5 v6向集合中添加值,最终只有v1 v2 v3 v4 v5 v6
  1. smembers key取出该集合的所有元素。
smembers k1
  1. sismember key value判断集合key中是否含有value元素,如有返回1,否则返回0。
sismember k1 v1
  1. scard key返回该集合的元素个数。
scard k1
  1. srem key value1 value2……删除集合中的一个或多个成员元素,不存在的成员元素会被忽略。
srem k1 v1 v2 删除v1 v2
  1. spop key随机删除集合中一个元素并返回该元素。
spop k1 随机删除一个元素,并返回
  1. srandmember key count随机取出集合中 count个元素,但不会删除。
srandmember k1 2 随机取出集合中的2个元素
  1. smove sourcekey destinationkey value将value元素从sourcekey集合移动到destinationkey集合中。
    如果 sourcekey集合不存在或不包含指定的 value元素,则 smove 命令不执行任何操作,仅返回 0。
smove k1 k2 v5 将元素v5从集合k1中移动到集合k2
  1. sinter key1 key2返回两个集合的交集元素。
  2. sunion key1 key2返回两个集合的并集元素。
  3. sdiff key1 key2返回两个集合的差集元素(key1中的,不包含key2)
    在这里插入图片描述
sinter k1 k2 返回v3
sunion k1 k2 返回v1 v2 v3 v4 v5
sdiff k1 k2 返回v1 v2
sdiff k2 k1 返回v4 v5

5 五大数据类型-Hash(哈希)

5.1 简介

Hash是一个键值对的集合。
Hash 是一个 String 类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象。
每个 Hash 可以存储 2^32 - 1 键值对(40多亿)。
在这里插入图片描述

  • Hash存储结构优化
    如果field数量较少,存储结构优化为类数组结构
    如果field数量较多,存储结构使用HashMap结构

5.2 常用命令

  1. hset key field value给key集合中的 field 赋值value。
    如果哈希表不存在,一个新的哈希表被创建并进行 HSET 操作。
    如果字段已经存在于哈希表中,旧值将被重写。
hset user name baizhan 创建一个key为user的哈希,并创建name字段,给name字段赋值为
baizhan
hset user age 3 在key为user的哈希中,创建age字段赋值为3
hset user name shangxuetang 将key为user的哈希name字段修改为shangxuetang
  1. hget key field从 key 哈希中,取出 field 字段的值。
hget user name 从key为user的哈希中取出name字段的值,结果为shangxuetang
  1. hmset key field1 value1 field2 value2……批量设置哈希的字段及值。
hmset user1 name bjsxt age 15 创建一个key为user1的哈希,有两个字段name和age
  1. hexists key field判断指定key中是否存在field
    如果哈希表含有给定字段,返回 1 。 如果哈希表不含有给定字段,或 key 不存在,返回 0 。
hexists user name 返回1
  1. hkeys key获取该哈希中所有的 field。
  2. hvals key获取该哈希中所有的 value。
  3. hincrby key field increment为哈希表 key 中的 field 字段的值加上增量 increment。
    增量也可以为负数,相当于对指定字段进行减法操作。
    如果哈希表的 key 不存在,一个新的哈希表被创建并执行 hincrby 命令。
    如果指定的字段不存在,那么在执行命令前,字段的值被初始化为 0 。
    对一个储存字符串值的字段执行 hincrby 命令将造成一个错误。
hincrby user age 10 对user中的age字段做运算,增加10
  1. hdel key field1 field2……删除哈希表 key 中的一个或多个指定字段,不存在的字段将被忽略。
    返回被成功删除字段的数量,不包括被忽略的字段。
hdel user name age 删除user中的name和age字段
  1. hsetnx key field value给 key 哈希表中不存在的的字段赋值 。
    如果哈希表不存在,一个新的哈希表被创建并进行 hsetnx 操作。
    如果字段已经存在于哈希表中,操作无效。
    如果 key 不存在,一个新哈希表被创建并执行 hsetnx 命令。
hsetnx user name

6 五大数据类型-Zset(有序集合)

6.1 简介

Zset 与 Set 非常相似,是一个没有重复元素的String集合。
不同之处是Zset的每个元素都关联了一个分数(score),这个分数被用来按照从低分到高分的方式排序集合中的元素。集合的元素是唯一的,但分数可以重复。

因为元素是有序的,所以可以根据分数(score)或者次序(position)来获取一个范围内的元素。

6.2 常用命令

  1. zadd key score1 value1 score2 value2……将一个或多个元素(value)及分数(score)加入到有序集key中。
    如果某个元素已经是有序集的元素,那么更新这个元素的分数值,并通过重新插入这个元素,来保证该元素在正确的位置上。
    分数值可以是整数值或双精度浮点数。
    如果有序集合 key 不存在,则创建一个空的有序集并执行 zadd 操作。
zadd k1 100 java 200 c++ 300 python 400 php
  1. zrange key start end [withscores]返回key集合中的索引start和索引end之间的元素(包含start和end)。
    其中元素的位置按分数值递增(从小到大)来排序。 其中 0 表示列表的第一个元素,-1表示最后一个元素。
    withscores是可选参数,是否返回分数。
zrange k1 0 -1 返回集合中所有元素
zrange k1 0 -1 withscores 返回集合中所有元素,并携带元素分数
  1. zrangebyscore key minscore maxscore [withscores]返回 key集合中的分数 minscore 和分数 maxscore 之间的元素(包含 minscore 和 maxscore )。其中元素的位置按分数值递增(从小到大)来排序。
zrangebyscore k1 200 400 返回200-400分之间的元素递增排序
  1. zrevrangebyscore key maxscore minscore [withscores]返回key集合中的分数 maxscore 和分数 minxscore 之间的元素(包含maxscore和minxscore )。其中元素的位置按分数值递减(从大到小)来排序。
zrevrangebyscore k1 400 200 返回200-400分之间的元素递减法排序
  1. zincrby key increment value为元素 value 的 score 加上 increment 的值。
zincrby k1 50 java 给java元素加上50
  1. zrem key value删除该集合下 value 的元素。
zrem k1 php 删除php
  1. zcount key minscore maxscore统计该集合在 minscore 到 maxscore 分数区间中元素的个数。
zcount k1 100 300 统计100分到300分中间元素的个数
  1. zrank key value返回 value 在集合中的排名,从0开始。
zrank k1 c++ 返回c++排名

7 新数据类型-Bitmaps

7.1 简介

在计算机中,用二进制(位)作为存储信息的基本单位,1个字节等于8位。
例如 “abc” 字符串是由 3 个字节组成,计算机存储时使用其二进制表示,"abc"分别对应的ASCII码是97、98、99,对应的二进制是01100001、01100010、01100011,在内存中表示如下:
在这里插入图片描述
合理地使用 位 能够有效地提高内存使用率和开发效率。
Redis提供了Bitmaps这个 “数据结构” 可以实现对位的操作:

  1. Bitmaps 本身不是一种数据结构,实际上它就是字符串(key 对应的 value 就是上图中的一串二进制),但是它可以对字符串的位进行操作。
  2. Bitmaps 单独提供了一套命令,所以在 Redis 中使用 Bitmaps 和使用字符串的方法不太相同。可以把 Bitmaps 想象成一个以位为单位的数组,数组的每个单元只能存储 0 和 1,数组的下标在Bitmaps中叫做偏移量。
    在这里插入图片描述

7.2 常用命令

  1. setbit key offset value设置Bitmaps中某个偏移量的值。
    偏移量从0开始,且value值只能为0或1。
setbit sign 0 1 设置sign的第一位值为1
setbit sign 1 1 设置sign的第二位值为1
setbit sign 2 0 设置sign的第三位值为0
setbit sign 3 1 设置sign的第四位值为1

在这里插入图片描述
2. getbit key offset 获取Bitmaps中某个偏移量的值。
获取key的offset 的值。

getbit sign 3 获取偏移量为3的值,结果为1

如果偏移量未设置值,则也返回0。

getbit sign 99 获取偏移量为99的值,结果为0
  1. bitcount key [start end]统计字符串被设置为1的bit数量。一般情况下,给定的整个字符串都会被进行统计,可以选择通过额外的start和end参数,指定字节组范围内进行统计(包括start和end),0表示第一个元素,-1表示最后一个元素。
bitcount sign 获取整个字符串被设置为1的bit数量,结果为3
如:当前存在一个key为k1的bitmaps存储着[00000001,00000001,00000010,00000011],分别对
应[1,1,2,3]。
setbit num 7  1
setbit num 15 1
setbit num 22 1
setbit num 30 1
setbit num 31 1

bitcount num 1 2 统计索引12两个字节组中bit=1的数量,即统计00000001,00000010中
bit=1的数量,结果为2
bitcount num 1 3 统计索引123三个字节组中bit=1的数量,即统计
00000001,00000010,00000011中bit=1的数量,结果为4
bitcount num 0 -1 统计所有的字节组中bit=1的数量,结果为5

setbit设置或获取的是bit(位)的位置,bitcount计算的是byte(字节)位置。

  1. bitop and/or destkey sourcekey1 sourcekey2……将多个bitmaps通过求交集/并集方式合并成一个新的bitmaps。
bitop and k3 k1 k2 通过求交集将k1 k2合并成k3
bitop or k3 k1 k2 通过求并集将k1 k2合并成k3

在这里插入图片描述

8 新数据类型-Geospatia

8.1 简介

GEO,Geographic,地理信息的缩写。
该类型就是元素的二维坐标,在地图上就是经纬度。Redis基于该类型,提供了经纬度设置、查询、范围查询、距离查询、经纬度 Hash 等常见操作。

8.2 常用命令

  1. geoadd key longitude latitude member [longitude latitude member ..……]用于存储指定的地理空间位置,可以将一个或多个经度(longitude)、纬度(latitude)、位置名(member)添加到指定的 key 中。
geoadd chinacity 116.405285 39.904989 beijing 将北京的经纬度和名称添加到chinacity
geoadd chinacity 104.065735 30.659462 chengdu 121.472644 31.231706 shanghai
将成都和上海的经纬度、名称添加到chinacity

有效的经度:-180 ~ +180 有效的纬度:-85.05 ~ +85.05,当设置的经度纬度值超过范围会报错。
两级无法直接添加。
一般会直接下载城市数据,直接通过java程序直接一次性导入。

  1. geopos key member [member ……]**从给定的 key 里返回所有指定名称(member)的位置(经度和纬度),不存在的返回nil
geopos chinacity shanghai beijing 返回chinacity中名称为shanghai和beijing的经纬度
  1. geodist key member1 member2 [m|km|ft|mi]用于返回两个给定位置之间的距离。最后一个距离单位参数说明:

    • m :米,默认单位。
    • km :千米。
    • mi :英里。
    • ft :英尺。
geodist chinacity shanghai beijing 返回shanghai和beijing之间的距离,结果
1067597.9668,单位米
geodist chinacity shanghai chengdu km 返回shanghai和chengdu之间的距离,结果
1660.0198,单位是千米
  1. georadius key longitude latitude radius m|km|ft|mi以给定的经纬度(longitudelatitude)为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离(radius )的所有位置元素。
georadius chinacity 116 40 1200 km 获取经纬度116 40为中心,在chinacity内1200公里
范围内的所有元素。结果shanghai beijing

9 新数据类型-Hyperloglog

9.1 简介

在我们做站点流量统计的时候一般会统计页面 UV (独立访客:unique visitor)和 PV (即页面浏览量:pageview)。

什么是基数?
数据集{1,2,5,7,5,7,9},那么这个数据集的基数集为{1,2,5,7,9},基数(不重复元素)为5,基数估计就是在误差可接受范围内,快速计算基数。

如果是通过Redis来处理,我们可以使用String类型然后自增计数即可达到统计PV,统计UV可以使用Set,每个用户id是唯一的可以放到这个集合里。

以上方案虽然结果准确,但随着数据不断增加,导致占用的内存空间越来越大,对于非常大的数据集是不合适的。
Hyperloglog 是一种基数估算统计,在输入元素的数量特别巨大时,计算基数所需的空间是固定的,并且很小。

在Redis中,每个Hyperloglog 只占用12KB内存,就可以计算接近 2^64 个不同元素的基数。

因为HyperLogLog 只会更具输入元素来计算基数,而不会存储输入元素本身,所以Hyperloglog 不能像集合那样,返回输入的各个元素。

9.2 常用命令

  1. pfadd key element1 element2……将所有元素参数添加到 Hyperloglog 数据结构中。
    如果至少有个元素被添加返回 1, 否则返回 0。
pfadd book1 java c++ 添加两个元素,当前book1数量为2
pfadd book1 java php 添加一个元素,当前book1数量为3
  1. pfcount key1 key2……计算Hyperloglog 近似基数,可以计算多个Hyperloglog ,统计基数总数。
pfcount book1 计算book1的基数,结果为3
pfadd book2 chinese math 添加两个元素到book2中
pfcount book1 book2 统计两个key的基数总数,结果为5
  1. pfmerge destkey sourcekey1 sourcekey2……将一个或多个Hyperloglog(sourcekey1) 合并成一个Hyperloglog (destkey )。
    比如每月活跃用户可用每天活跃用户合并后计算。
pfmerge book book1 book2 将book1和book2合并成book,结果为5

10 Redis配置文件详解

10.1 units单位

配置大小单位,开头定义基本度量单位,只支持 bytes,大小写不敏感。
在这里插入图片描述

10.2 INCLUDES

Redis只有一个配置文件,如果多个人进行开发维护,那么就需要多个这样的配置文件,这时候多个配置文件就可以在此通过include /path/to/local.conf配置进来,而原本的 redis.conf 配置文件就作为一个总闸。
在这里插入图片描述

10.3 NETWORK

  1. bind:绑定redis服务器网卡IP,默认为127.0.0.1,即本地回环地址。访问redis服务只能通过本机的客户端连接,而无法通过远程连接。如果bind选项为空的话,那会接受所有来自于可用网络接口的连接。
    在这里插入图片描述
  2. protected-mode:本机保护模式,值为yes时只能本机访问不能远程访问。
    在这里插入图片描述
  3. port:指定redis运行的端口,默认是6379。
    在这里插入图片描述
  4. timeout:设置客户端连接时的超时时间,单位为秒。当客户端在这段时间内没有发出任何指令,那么关闭该连接。默认值为0,表示不关闭。
    在这里插入图片描述

10.4 GENERAL

  1. daemonize:设置为yes表示指定Redis以守护进程的方式启动(后台启动)。
    在这里插入图片描述
  2. pidfile:配置PID文件路径,当redis作为守护进程运行的时候,它会把 pid 默认写到/var/redis/run/redis_6379.pid文件里面。
    在这里插入图片描述
  3. loglevel:定义日志级别。默认值为notice,有如下4种取值:
    • debug(记录大量日志信息,适用于开发、测试阶段)。
    • verbose(较多日志信息)。
    • notice(适量日志信息,使用于生产环境)。
    • warning(仅有部分重要、关键信息才会被记录)。
      在这里插入图片描述
  4. logfile:配置log文件地址,默认打印在命令行终端的窗口上。
    在这里插入图片描述
  5. databases:设置数据库的数目。
    在这里插入图片描述

10.5 SECURITY

  1. requirepass:设置redis连接密码。
    比如: requirepass 123 表示 redis 的连接密码为123。
    在这里插入图片描述

10.6 其他配置

  1. maxclients:设置客户端最大并发连接数,默认无限制,Redis可以同时打开的客户端连接数为Redis进程可以打开的最大数量。 如果设置 maxclients为0 ,表示不作限制。当客户端连接数到达限制时,Redis会关闭新的连接并向客户端返回错误信息。
    在这里插入图片描述

  2. maxmemory:设置Redis的最大内存,如果设置为0 。表示不作限制。通常是配合maxmemorypolicy参数一起使用。
    在这里插入图片描述

  3. maxmemory-policy:当内存使用达到maxmemory设置的最大值时,redis使用的内存清除策略。

    清除策略包括:

    • volatile-lru:利用LRU算法移除设置过过期时间的key (LRU:最近使用 Least Recently Used )
    • allkeys-lru:利用LRU算法移除任何key
    • volatile-random:移除设置过过期时间的随机key
    • allkeys-random:移除随机key
    • volatile-ttl:移除即将过期的key(minor TTL)
    • noeviction:不移除任何key,只是返回一个写错误 ,默认选项
      在这里插入图片描述

11 发布与订阅

11.1 什么是发布与订阅

Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。
Redis 客户端可以订阅任意数量的频道。

11.2 Redis的发布与订阅

  1. 客户端订阅频道
    在这里插入图片描述
  2. 当给这个频道发送消息后,消息就会发送给订阅的客户端
    在这里插入图片描述
  3. Redis中发布与订阅命令
    订阅:subscribe channel订阅频道channel。
    发布:publish channel msg向频道channel 发送一条msg消息。

11.3 发布与订阅命令行实现

  1. 打开一个客户端订阅channel 1频道。
    在这里插入图片描述
  2. 打开另一个客户端给channel 1频道发送一条hello消息。
    在这里插入图片描述
    返回1代表订阅者数量。
  3. 打开第一个客户端可以看到发送的消息
    在这里插入图片描述
    客户端只能收到订阅以后发送的消息。

三、Redis持久化

由于Redis的数据都存放在内存中,如果没有配置持久化,Redis重启后数据就全丢失了,于是需要开启 Redis的持久化功能,将数据保存到磁盘上,当Redis重启后,可以从磁盘中恢复数据。
Redis提供了两个不同形式的持久化方式:

  • RDB(Redis DataBase)
  • AOF(Append Only File)

1 持久化操作-RDB

1.1 RDB是什么?

在指定的时间间隔内将内存的数据集快照写入磁盘,也就是行话讲的 Snapshot 快照,它恢复时是将快照文件直接读到内存里。

1.2 备份过程

Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何 IO 操作的,这就确保了极高的性能如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。
在这里插入图片描述

1.3 dump.rdb文件

  1. RDB保存的文件,在redis.conf中配置文件名称,默认为dump.rdb。
    在这里插入图片描述
  2. rdb文件的保存位置,也可以修改。默认在Redis启动时命令行所在的目录下。
    在这里插入图片描述
    redis.conf中配置文件路径
    在这里插入图片描述

1.4 如何触发快照?

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

在这里插入图片描述

  1. 快照默认配置
save 3600 1:表示3600秒内(一小时)如果至少有1个key的值变化,则保存
save 300 100:表示300秒内(五分钟)如果至少有100个 key 的值变化,则保存
save 60 10000:表示60秒内如果至少有 10000个key的值变化,则保存

可以自己配置新的保存规则。

  1. 例:给redis.conf添加新的快照策略,30秒内如果有5次key的变化,则触发快照。配置修改后,需要重启Redis服务。
    在这里插入图片描述
    dump.rdb默认大小是92字节,里面会有一些基本信息。
    在这里插入图片描述
    30秒内设置5个以上的值。
set k1 v1
set k2 v2
set k3 v3
set k4 v4
set k5 v5
set k6 v6
set k7 v7

在这里插入图片描述
dump.rdb大小已经改变。
在这里插入图片描述

1.4.2 flushall

执行 flushall 命令,也会触发 rdb 规则。

1.4.3 save 与 bgsave

手动触发Redis进行RDB持久化的命令有两种:

  1. save
    该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,直到RDB过程完成为止,不建议使用。
  2. bgsave
    执行该命令时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。

这两个命令是在Redis客户端中执行,并不是 redis.conf 中修改。

1.4.4 stop-writes-on-bgsave-error

默认值是yes。当Redis无法写入磁盘的话,直接关闭Redis的写操作。
在这里插入图片描述

1.4.5 rdbcompression

默认值是yes。对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis会采用LZF算法进行压缩。如果你不想消耗CPU来进行压缩的话,可以设置为关闭此功能,但是存储在磁盘上的快照会比较大。
在这里插入图片描述

1.4.6 rdbchecksum

默认值是yes。在存储快照后,我们还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。
在这里插入图片描述

1.5 恢复数据

只需要将rdb文件放在Redis的启动目录,Redis启动时会自动加载dump.rdb并恢复数据。

2 持久化操作-AOF

2.1 AOF是什么?

以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只允许加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,Redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。

2.2 AOF持久化流程

  1. 客户端的请求写命令会被append追加到AOF缓冲区内。
  2. AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作同步到磁盘的AOF文件中。
  3. AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量。
  4. Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的。

在这里插入图片描述

2.3 AOF默认不开启

可以在redis.conf中配置文件名称,默认appendonly.aof。
AOF文件的保存路径,同RDB的路径一致。
在这里插入图片描述
如果AOF和RDB同时启动,Redis默认读取AOF的数据

2.4 AOF启动/修复/恢复

  • 正常恢复
  1. 启动:设置Yes:修改默认的appendonly no,改为yes。
  2. 恢复:重启Redis然后重新加载。

例:设置appendonly为yes,配置修改后,需要重启Redis服务。
在这里插入图片描述
服务器启动后,生成appendonly.aof文件,且大小为0。
在这里插入图片描述
设置数据。

set k11 v11
set k12 v12
set k13 v13
set k14 v14
set k15 v15

appendonly.aof大小已经改变。
在这里插入图片描述

  • 异常恢复
  1. 启动:设置Yes:修改默认的appendonly no,改为yes。
  2. 修复:如遇到AOF文件损坏,通过/user/local/bin/redis-check-aof --fix appendonly.aof进行恢复。
  3. 恢复:重启Redis然后重新加载。

例:服务启动和数据设置同上,模拟损坏appendonly.aof文件

vi appendonly.aof

在文件最后追加Hello World文本,破坏appendonly.aof原有格式,使其不可用。
在这里插入图片描述
重启Redis服务,并连接。由于aof文件被破坏,导致服务器启动失败。
在这里插入图片描述
通过/user/local/bin/redis-check-aof --fix工具对 appendonly.aof 进行恢复。

./redis-check-aof --fix appendonly.aof

在这里插入图片描述
修复成功。再次查看aof文件,破坏的地方已经修复。再次启动服务器成功。
在这里插入图片描述

2.5 AOF同步频率设置

在这里插入图片描述

  1. appendfsync always
    始终同步,每次Redis的写入都会立刻记入日志,性能较差但数据完整性比较好。
  2. appendfsync everysec
    每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。
  3. appendfsync no
    redis不主动进行同步,把同步时机交给操作系统。

2.6 Rewrite

  1. AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制,当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。
    例:设置k1为0,然后incr 进行了4次,k1对应的值会是4,其实就相当于set k1 4
  2. 重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定Redis要满足一定条件才会进行重写。
    在这里插入图片描述
    redis.conf 默认配置
    auto-aof-rewrite-min-size:表示重写时,文件大小必须必这个值要大。
    auto-aof-rewrite-percentage:表示目前文件大小比上次重写后的文件大小大这么多才行。

四、Redis事务

1 Redis事务简介

  1. Redis事务是一组命令的集合,一个事务中的所有命令都将被序列化,按照一次性、顺序性、排他性的执行一系列的命令。
    在这里插入图片描述
  2. Redis 单条命令保证原子性,但是事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。
  3. Redis 事务没有隔离级别的概念。批量操作在执行前被放入缓存队列,并不会被实际执行,也就不存在事务内的查询要看到事务里的更新,事务外查询不能看到。
  4. Redis 事务的三个阶段:
    开始事务
    命令入队
    执行事务

2 Redis事务基本操作

2.1 Multi、Exec、discard

事务从输入Multi命令开始,输入的命令都会依次压入命令缓冲队列中,并不会执行,直到输入Exec后,Redis会将之前的命令缓冲队列中的命令依次执行。

组队过程中,可以通过 discard 来放弃组队。
在这里插入图片描述
例1:

multi 开始事务
set k1 v1 进行组队,并不执行
set k2 v2 进行组队,并不执行
exec 执行队列命令,依次设置k1 k2

在这里插入图片描述
例2:

multi 开始事务
set k3 v3 进行组队,并不执行
set k4 v4 进行组队,并不执行
discard 取消组队,都不执行

在这里插入图片描述

2.2 事务的错误处理

  1. 组队阶段某个命令出现了错误,整个队列中的命令都不执行。
    在这里插入图片描述
    例:整个命令缓存队列都不会执行。
    在这里插入图片描述
  2. 执行阶段某个命令出现了错误,只有报错的命令不会执行,其他正常执行。
    在这里插入图片描述
    例:只有报错的命令没有执行。
    在这里插入图片描述

3 悲观锁与乐观锁

3.1 事务应用场景

  • 一个请求想给余额减 8000
  • 一个请求想给余额减 5000
  • 一个请求想给余额减 1000
    在这里插入图片描述

3.2 悲观锁(Pessimistic Lock)

每次去拿数据的时候都认为别人会修改,所以在每次拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞,直到它拿到锁。传统关系型数据库就用到很多悲观锁,比如行锁、表锁等。
在这里插入图片描述

3.3 乐观锁(Optimistic Lock)

每次去拿数据的时候都认为别人不会修改,所以不会上锁,但在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制。乐观锁适用于多读的应用类型,可以提高吞吐量。Redis就是乐观锁机制实现事务的。
在这里插入图片描述

4 Redis中的乐观锁

4.1 watch key [key……]

在执行 multi 之前,先执行 watch 监视一个或多个key,如果在事务执行之前这个(或这些)key被其它命令所改动,那么事务将被打断。
例:开启两个客户端

1号客户端
set money 10000 设置一个money的key
watch money 监视money
multi 开启事务
set money 100 修改money,暂时不执行事务

2号客户端
watch money 监视money
multi 开启事务
set money 110 修改money,暂时不执行事务

1号客户端
exec 执行事务,返回成功

2号客户端
exec 执行事务,更新失败

1号客户端
在这里插入图片描述
2号客户端
在这里插入图片描述

4.2 unwatch

取消 watch 命令对所有 key 的监视
如果在执行watch命令之后,exec 命令或 discard 命令先执行的话,那么就不需要再执行 unwatch。

五、Redis主从复制

1 Redis主从复制简介

主从复制,是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。前者称为主节点(Master),后者称为从节点(Slave);数据的复制是单向的,只能由主节点到从节点。

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

Master以写为主,Slave以读为主。
在这里插入图片描述
主从复制的作用:

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

一般来说,要将Redis运用于工程项目中,只是用一台Redis是万万不能的,原因如下:

  1. 从结构上,单个redis服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大;
  2. 从容量上,单个redis服务器内存容量有限,就算一台Redis服务器内容容量为256G,也不能将所有内存用做Redis存储内存,一般来说,单台Redis最大使用内存不应该超过20G。

2 Redis主从复制-一主多从

搭建一主二从的Redis服务器

环境搭建

  1. 在同一台虚拟机上配置一主二从Redis服务器,由于单台机器,同一个端口只允许一个进程占用,所以需要修改其他两台Redis的端口。
  2. /usr/local/myredis文件夹下,新创建三个redis的配置文件,分别为redis6379.confredis6380.confredis6381.conf,由文件名可知,redis的端口号分别为6379、6380、6381。
    在这里插入图片描述
    创建出3个空文件,redis.conf是原有的配置文件。
  3. 在三个配置文件写入内容
  • 可以通过include /usr/local/myredis/redis.conf将公共基础配置直接引入文件。
    include /usr/local/myredis/redis.conf统一添加到这三个文件中
  • 在各个文件中,添加对应的pidfile、port、dbfilename
    如:
    redis6379.conf中添加
    pidfile /var/run/redis_6379.pid
    port 6379
    dbfilename dump6379.rdb
    在这里插入图片描述
    redis6380.conf中添加
    pidfile /var/run/redis_6380.pid
    port 6380
    dbfilename dump6380.rdb
    在这里插入图片描述
    redis6381.conf中添加
    pidfile /var/run/redis_6381.pid
    port 6381
    dbfilename dump6381.rdb
    在这里插入图片描述
  1. 通过不同的 redis.conf文件,分别启动 3个 redis服务
./redis-server /usr/local/myredis/redis6379.conf
./redis-server /usr/local/myredis/redis6380.conf
./redis-server /usr/local/myredis/redis6381.conf

在这里插入图片描述
通过ps -ef | grep redis命令查看redis服务,3个服务均已启动
在这里插入图片描述

  1. 在redis客户端中,通过info replication命令可以查看Redis服务器当前状态
    可以给redis-cli命令添加-p参数,来指定链接哪个服务器
./redis-cli -p 6379
./redis-cli -p 6380
./redis-cli -p 6381

在这里插入图片描述
所有的服务器目前都是master

  1. 配从(6380、6381)不配主(6379)
    slaveof ip port成为某个实例的从服务器。
在从机63806381上执行:slaveof 127.0.0.1 6379

在主库(6379)中再次执行info replication
在这里插入图片描述
有两个从库,分别为 6380 和 6381。

  1. 在主库(6379)中写数据,可以在从库(6380、6381)中读取到。

注意:

  • 在从库中进行写操作,会报错。
  • 由于该主从复制在同一台虚拟机上搭建,所以需要修改端口号,如果在多台服务器上搭建主从复制,则需要修改相对应的 ip。
  • 如果在多台服务器上搭建主从复制,一定要开放远程链接。

3 Redis主从复制-复制原理

3.1 主从复制的一些问题

  1. 如果 Master 断开(宕机),Slave 依然连接着 Master,可以正常使用读操作,但是没有写操作。如果Master恢复正常,Slave 依旧可以直接获取 Master 写的信息。
  2. 如果 Slave 断开(宕机),当该Slave重启成功,则会变为 Master,需要通过 slaveof 恢复成 Slave,只要变为Slave,立刻可以从 Master 同步所有数据。

3.2 复制原理

  • Slave启动成功连接到 Master 后会主动发送一个同步(sync)命令。
  • Master 接到 Slave的命令,把 Master 数据进行持久化,把 rdb 文件发送给 Slave,Slave 拿到 rdb 进行读取。
  • 每次 Master 进行写操作之后,会和 Slave 进行数据同步。
    在这里插入图片描述
  • 全量复制:一般发生在 Slave 初始化阶段,这时Slave 需要将 Master 上的所有数据都复制一份。
  • 增量复制:指 Slave 初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。

3.3 薪火相传

上一个 Slave(从机)是下一个 Slave(从机)的 Master(主机)。
在这里插入图片描述

优点:Slave 同样可以接收其他 Slave 的连接和同步请求,那么该 Slave 作为了链条中下一个的 Master, 可以有效减轻 Master 的压力,去中心化降低风险。

缺点:一旦某个 Slave 宕机,后面的 Slave 都无法备份。

注意

  • 也是通过slaveof ip port命令修改 Master。
  • 中途变更转向:会清除之前的数据,重新建立拷贝最新的。
  • Slave6380 本质上仍然是从库,只能读、不能写。

3.4 反客为主

当一个 Master 宕机后,后面的 Slave 可以立刻升 Master,其后面的 Slave 不用做任何修改。
在这里插入图片描述
通过slaveof no one将 Slave 变为 Master。

4 Redis主从复制-哨兵模式(Sentinel)

反客为主的自动版,能够后台监控 Master 是否故障,如果故障了,根据投票数自动将 Slave 转换为 Master。

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

4.1 哨兵模式的使用

  1. 将服务器调整为一主多从(6379带6380、6381)。
    在这里插入图片描述

  2. /usr/local/myredis文件夹下创建 sentinel.conf 文件(名字一定不能错)。
    在这里插入图片描述

  3. 配置哨兵,sentinel.conf 添加内容
    sentinel monitor myredis 127.0.0.1 6379 1
    其中:myredis 为监控对象起的服务器名称(随意),1代表至少有1个哨兵投票同意迁移
    如果Master存在密码,需要配置sentinel auth-pass服务器名 密码sentinel auth-pass myredis 123456
    在这里插入图片描述

  4. 启动哨兵,在/usr/local/bin/文件夹下执行./redis-sentinel /usr/local/myredis/sentinel.conf命令。
    在这里插入图片描述

  5. 模拟Master宕机,哨兵会切换Master

    • 通过kill命令关闭Master(6379)
      在这里插入图片描述
    • 等待一段时间,哨兵窗口就会输出信息
      在这里插入图片描述
      根据窗口信息可知,Master由6379转换为6381,进入6381客户端执行info replication
      在这里插入图片描述
  6. 重新启动 6379 服务器后,自动转换为Slave。
    在这里插入图片描述

4.2 复制延迟

由于所有的写操作都是先在 Master 上操作,然后同步更新到 Slave 上,所以从 Master 同步到 Slave 机器有一定的延迟,Slave 机器数量的增加,会使延迟问题会更加严重。

六、Redis集群

1 Redis集群简介

1.1 Redis集群(RedisCluster)

RedisCluster实现了对Redis的水平扩容,即启动N个Redis节点,将整个数据库分布存储在这N个节点当中,每个节点存储总数据的1/N。

RedisCluster通过分区(partition)来提供一定程度的可用性(availability):即集群有一部分节点失效或者无法进行通讯,集群也可以继续处理命令。

Redis-Cluster采用无中心结构,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接,用来交换彼此的信息。
在这里插入图片描述
为了使得集群在一部分节点宕机或者无法与集群的大多数节点进行通讯的情况下, 仍然可以正常运作,Redis 集群对节点使用了主从复制功能。

2 Redis集群搭建

  1. 删除持久化数据,aof、dump。
  2. 创建6个实例,即在/usr/local/myredis文件夹下分别创建6379、6380、6381、6389、6390、6391的 conf。
  3. 在 redis.conf 中关闭 appendonly。
  4. 在不同的 conf 文件中,配置对应的内容
    • 可以通过include /usr/local/myredis/redis.conf将公共基础配置直接引入文件。
      include /usr/local/myredis/redis.conf统一添加到这三个文件中
    • 在各个文件中,添加对应的 pidfile、port、dbfilename、cluster-enabled(是否打开集
      群)、cluster-config-file(设定节点配置文件名)、cluster-node-timeout (设置节点失联时间,超过该时间(毫秒),集群自动进行主从切换)
      如:
      redis6379.conf 中添加
		pidfile /var/run/redis_6379.pid
		port 6379
		dbfilename dump6379.rdb
		cluster-enabled yes
		cluster-config-file nodes-6379.conf
		cluster-node-timeout 15000

在这里插入图片描述
将其他文件按照相同规则创建出来。

  1. 启动该 6个 redis,并确保是否全部生成 nodes-xxxx.conf 文件。(启动之前务必保证redis服务支持远程连接)
    在这里插入图片描述
    在这里插入图片描述
  2. 进入redis安装目录下的src文件夹
    在这里插入图片描述
  3. 在该文件夹下执行命令
redis-cli --cluster create --cluster-replicas 1 192.168.56.31:6379
192.168.56.31:6380 192.168.56.31:6381 192.168.56.31:6389 192.168.56.31:6390
192.168.56.31:6391

此处使用真实 ip 地址,-replicas 1代表采用最简单的方式配置集群,一台主机,一台从机。
replicas 表示每个 master 需要有几个 slave 。
在这里插入图片描述
执行命令后,redis 提供推荐的主从配置建议,执行同意。
在这里插入图片描述
集群创建成功。

  1. 通过./redis-cli -c -p 6379可连接至集群(由于所有节点相通,任意端口号均可)
    在redis客户端中执行cluster nodes查看节点状态。
    在这里插入图片描述

3 Redis集群操作

3.1 Slot

一个 Redis 集群包含 16384 个哈希槽(hash slot),每个键都属于这 16384 个哈希槽的其中一个。

集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key的 CRC16 校验和 。

集群中的每个节点负责处理一部分哈希槽。 举个例子, 一个集群可以有三个节点, 其中:

  • 节点 1 负责处理 0 号至 5500 号哈希槽。
  • 节点 2 负责处理 5501 号至 11000 号哈希槽。
  • 节点 3 负责处理 11001 号至 16384 号哈希槽。
    在这里插入图片描述

3.2 命令执行

set k1 v1

在这里插入图片描述
根据 k1 计算出的槽值进行切换节点,并存入数据。

不在一个 slot 下的键值,是不能使用 mget、mset 等多建操作。
在这里插入图片描述
可以通过{}来定义组的概念,从而是key中{}内相同内容的键值对放到同一个 slot 中

mset k1{test} v1 k2{test} v2 k3{test} v3

在这里插入图片描述

3.3 故障恢复

  1. 关闭6379服务器,进入redis-cli执行 cluster nodes
    在这里插入图片描述
    6379 服务器 fail,6390成为了新的 master
  2. 重新启动 6379 服务器,再次查看,6379 成为了 Slave
    在这里插入图片描述

七、JAVA与Redis

1 Jedis操作Redis

1.1 Cache Aside Pattern(缓存模式)

读:

  1. 先读缓存,再读数据库
  2. 如果缓存命中,则直接返回缓存数据
  3. 如果缓存未命中,则访问数据库,并将数据重置回缓存,然后返回。
    在这里插入图片描述
    写:
    先操作数据库写,再淘汰缓存(这里淘汰缓存是删除,而不是更新)
    在这里插入图片描述
    Jedis是Redis官方推荐的Java连接开发工具。要在Java开发中使用好Redis中间件,必须对Jedis熟悉才能写成漂亮的代码。

1.2 引入Jedis

  1. 启动redis并在IDEA中创建一个maven工程
    在这里插入图片描述
  2. 引入maven依赖
<dependency>
	<groupId>redis.clients</groupId>
	<artifactId>jedis</artifactId>
	<version>3.6.0</version>
</dependency>
  1. 创建相关文件
    在这里插入图片描述

1.3 常用方法

  1. Jedis连接到redis
Jedis jedis = new Jedis("192.168.56.31",6379); //第一个参数是ip地址,第二个参数是
端口
  • 在连接之前,需要开放redis连接服务,即关闭 redis.conf 中的 bind 本机和关闭本机保护模式
    在这里插入图片描述
  • 通过systemctl stop firewalld.service 命令关闭CentOS8防火墙
Jedis jedis = new Jedis("192.168.56.31",6379);
String msg = jedis.ping();//通过ping()方法向redis发送一个ping命令,服务器返回一个
Pong
System.out.println(msg);
jedis.close();//jedis使用完毕需要关闭
  1. String
jedis.set("k1","v1"); //设置一个key
jedis.set("k2","1");//设置一个key
String res = jedis.get("k1");//获取一个key
Long ires = jedis.incr("k2");//对某一个key自增
  1. Keys
Set<String> keys = jedis.keys("*");//返回所有的key
Long time = jedis.ttl("k1");//返回该key剩余过期时间
  1. List
jedis.lpush("list1","v1","v2","v3");//向list中添加数据
List<String> list = jedis.lrange("list1",0,-1 );//返回list全部数据
  1. Set
jedis.sadd("set1" ,"v1","v2","v2","v3");//向set中添加数据
jedis.smembers("set1");//查看该集合中有多少个元素
  1. Hash
jedis.hset("user","age","25");//设置一个hash
jedis.hvals("user");//获取该key的所有value
  1. Zset
jedis.zadd("zset1",100,"java");//向zset中添加一条数据
jedis.zrange("zset1",0,-1);//获取所有的值
  1. Bitmaps
jedis.setbit("b1",0, "1");//将b1偏移量为0的位设置为1
jedis.getbit("b1",0);//获取b1偏移量为0的位
  1. Geospatia
jedis.geoadd("chinacity",130,110,"beijing");//添加一条地理信息数据
  1. Hyperloglog
jedis.pfadd("book","c++","java","php");//将所有元素参数添加到 Hyperloglog 数据结构中。

其实 jedis 中的方法基本同 redis 命令一致。

package com.bzcxy.jedisdemo;

import redis.clients.jedis.Jedis;

import java.util.List;
import java.util.Set;

public class JedisDemo {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.56.31",6379); //第一个参数是ip地址,第二个参数是端口
        String msg = jedis.ping();
        System.out.println(msg);
        //String
        jedis.set("k1","v1");
        jedis.set("k2","1");
        String res = jedis.get("k1");
        Long lres = jedis.incr("k2");
        System.out.println(res);
        System.out.println(lres);
        //key
        Set<String> keys = jedis.keys("*");
        for (String k : keys)
        {
            System.out.print(k + " ");
        }
        System.out.println();
        Long timeout = jedis.ttl("k1");
        System.out.println(timeout);
        //List
        jedis.lpush("list1","v1","v2","v3");
        List<String> list = jedis.lrange("list1",0,-1);
        for (String l : list)
        {
            System.out.print(l + " ");
        }
        System.out.println();
        //set
        jedis.sadd("set1" ,"v1","v2","v2","v3");//向set中添加数据
        Set<String> set = jedis.smembers("set1");
        for (String s : set)
        {
            System.out.print(s + " ");
        }
        System.out.println();
        //hash
        jedis.hset("user","age","25");//设置一个hash
        List<String> hvals = jedis.hvals("user");
        for (String hval : hvals)
        {
            System.out.print(hval + " ");
        }
        System.out.println();
        //zset
        jedis.zadd("zset1",100,"java");//向zset中添加一条数据
        Set<String> zset = jedis.zrange("zset1",0,-1);//获取所有的值
        for(String z : zset)
        {
            System.out.print(z + " ");
        }
        System.out.println();
        //Bitmaps
        jedis.setbit("b1",0,"1");
        Boolean b = jedis.getbit("b1",0);
        System.out.println(b);
        //Geospatia
        jedis.geoadd("chinacity",130,110,"beijing");
        //hyperloglog
        jedis.pfadd("book","c++","java","php");//将所有元素参数添加到 Hyperloglog 数据结构中。

        jedis.close();
    }
}

2 JedisCluster操作Redis集群

  1. 启动redis集群
    在这里插入图片描述
    进入redis安装目录下的src文件夹执行
redis-cli --cluster create --cluster-replicas 1 192.168.56.31:6379
192.168.56.31:6380 192.168.56.31:6381 192.168.56.31:6389 192.168.56.31:6390
192.168.56.31:6391
  1. 在IDEA中新建JedisClusterDemo类
    在这里插入图片描述
  2. JedisCluster连接到redis集群
//HostAndPort具体ip和端口只要是集群中的就可
HostAndPort hp = new HostAndPort("192.168.56.31",6380);
//需要用HostAndPort对象作为参数,连接到集群
JedisCluster jedisCluster = new JedisCluster(hp);
  1. JedisCluster操作集群
jedisCluster.set("b1","v1");//向节点中设置一个值
String val = jedisCluster.get("b1");//从节点中获取一个值
package com.bzcxy.jedisdemo;

import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;

public class JedisClusterDemo {
    public static void main(String[] args) {
        HostAndPort hp = new HostAndPort("192.168.56.31",6380);
        JedisCluster jedisCluster = new JedisCluster(hp);
        jedisCluster.set("b1","v1");
        String res =  jedisCluster.get("b1");
        System.out.println(res);
        jedisCluster.close();
    }
}

3 SpringData整合Redis

  1. 通过IDEA初始化一个Springboot工程,并添加SpringDataRedis组件
    pom.xml中有
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
	<groupId>org.apache.commons</groupId>
	<artifactId>commons-pool2</artifactId>
	<version>2.6.0</version>
</dependency>
<dependency>
	<groupId>com.fasterxml.jackson.core</groupId>
	<artifactId>jackson-core</artifactId>
	<version>2.12.3</version>
</dependency>
<dependency>
	<groupId>com.fasterxml.jackson.core</groupId>
	<artifactId>jackson-databind</artifactId>
	<version>2.12.3</version>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-test</artifactId>
	<scope>test</scope>
</dependency>
  1. 在application.properties中配置
#Redis服务器连接地址
spring.redis.host=192.168.56.31
#Redis服务器连接端口
spring.redis.port=6379
#Redis服务器连接密码(默认为空)
spring.redis.password=
#连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=8
#连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.pool.max-idle=8
#连接池中的最小空闲连接
spring.redis.pool.min-idle=0
#连接超时时间(毫秒)
spring.redis.timeout=30000
  1. 创建配置类
    常见序列化器:
    • StringRedisSerializer: 简单的字符串序列化
    • GenericJackson2JsonRedisSerializer: 可以将任何对象泛化为字符串并序列化(对象必须有无参构造方法)
    • JdkSerializationRedisSerializer: 序列化java对象(需要序列化的类必须实现Serializable接口)
    • Jackson2JsonRedisSerializer:序列化对象(序列化带泛型的数据时,会以map的结构进行存储,反序列化是不能将map解析成对象)
@Configuration
public class RedisConfig{
	@Bean
	public RedisTemplate<String, Serializable>
	redisTemplate(LettuceConnectionFactory connectionFactory)
	{
		RedisTemplate<String, Serializable> redisTemplate = new
		RedisTemplate<>();
		//给key设置StringRedisSerializer序列化器
		redisTemplate.setKeySerializer(new StringRedisSerializer());
		//给value设置GenericJackson2JsonRedisSerializer序列化器
		redisTemplate.setValueSerializer(new
		GenericJackson2JsonRedisSerializer());
		redisTemplate.setConnectionFactory(connectionFactory);
		return redisTemplate;
	}
}
  1. 启动Redis单机版
  2. 编写unit
public class User {
	private Integer id;
	private String name;
	public Integer getId() {
		return id;
	}
	public void setId(Integer id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public User(Integer id, String name) {
		this.id = id;
		this.name = name;
	}
	public User(){}
}
	@Autowired
	private RedisTemplate redisTemplate;
	@Test
	void contextLoads() {
		User u = new User(15,"百战");
		redisTemplate.opsForValue().set("user",u);
		User u1 = (User) redisTemplate.opsForValue().get("user");
		System.out.println(u1.getName());
	}

八、Redis企业级解决方案

1 RedisDesktopManager

一款好用的 Redis 桌面管理工具,支持命令控制台操作,以及常用,查询 key,rename,delete等操作。
RedisDesktopManager 不支持集群操作。

  1. 傻瓜式安装该工具
    在这里插入图片描述
  2. 安装成功后,启动该工具
    在这里插入图片描述
  3. 启动后,创建新的连接
    在这里插入图片描述
  4. 创建连接成功
    在这里插入图片描述
  5. 双击进入数据库,可以查看所有的 key 及 value、过期时间,同时可以修改 key 或删除 key
    在这里插入图片描述
  6. 在数据库上右键可以选择新增 key 或筛选 key
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    注:对key更改后(新增、修改、删除),需要通过右键连接名 ->reload 重新加载
    在这里插入图片描述

2 Redis脑裂

2.1 概念

  1. 假设现在有三台机器,分别安装了redis服务,结构如图
    在这里插入图片描述
  2. 如果此时 master 服务器所在区域网络通信出现异常,导致和两台 slave 机器无法正常通信,但是和客户端的连接是正常的。那么 sentinel 就会从两台 slave 机器中选举其中一个作为新的 master 来处理客户端请求。
    在这里插入图片描述
  3. 这个时候,已经存在两台 master 服务器,client 发送的数据会持续保存在旧的 master 服务器中,而新的 master 和 slave 中没有新的数据。如果一分钟以后,网络恢复正常,服务之间能够正常通信。此时,sentinel 会把旧的master 会变成新的 master 的 slave 节点。
    在这里插入图片描述
  4. 问题出现了,我们都知道,slave 会从 master 中同步数据,保持主从数据一致。这个时候,变成了 slave 节点的旧 master 会丢失掉通信异常期间从客户端接收到的数据。

2.2 解决方案

redis.conf配置参数:

min-replicas-to-write 1
min-replicas-max-lag 10

第一个参数表示最少的 slave 节点为1个
第二个参数表示数据复制和同步的延迟不能超过10秒
配置了这两个参数:如果发生脑裂:原 master 会在客户端写入操作的时候拒绝请求。这样可以避免大量数据丢失。

3 缓存预热

新启动的系统没有任何缓存数据,在缓存重建数据的过程中,系统性能和数据库负载都不太好,所以最好是在系统上线之前就把要缓存的热点数据加载到缓存中,这种缓存预加载手段就是缓存预热。
在这里插入图片描述

4 缓存穿透

4.1 概念

如果某个 key 对应的数据不存在,而又未对该 key 做缓存,所以每次请求都会穿过缓存直接到数据库进行查询,并发量高的情况下进而导致数据库直接宕机,这就是缓存穿透。
在这里插入图片描述

4.2 解决方案

  1. 对空值缓存:如果一个查询返回的数据为空(不管数据是否存在),我们仍然把这个空结果缓存,设置空结果的过期时间会很短,最长不超过5分钟。
  2. 设置白名单:使用 bitmaps 类型定义一个可以访问的名单,用户 id 作为偏移量,每次访问查询是否在白名单中,如果不存在,则拒绝访问。
  3. 布隆过滤器:类似一个 hash set,用来判断某个元素(key)是否在某个集合中。和一般的 hash set 不同的是,这个算法无需存储 key 的值,对于每个 key,只需要 k 个比特位,每个存储一个标志,用来判断 key 是否在集合中。

5 缓存击穿

5.1 概念

某一个热点 key,在缓存过期的一瞬间,同时有大量的请求打进来,由于此时缓存过期了,所以请求最终都会走到数据库,造成瞬时数据库请求量大、压力骤增,甚至可能打垮数据库。
在这里插入图片描述

5.2 解决方案

  1. 加互斥锁:在并发的多个请求中,只有第一个请求线程能拿到锁并执行数据库查询操作,其他的线程拿不到锁就阻塞等着,等到第一个线程将数据写入缓存后,其他线程直接查询缓存。
  2. 热点数据不过期:直接将缓存设置为不过期,然后由定时任务去异步加载数据,更新缓存。

6 缓存雪崩

6.1 概念

缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到 DB,DB瞬时压力过重雪崩。

和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
在这里插入图片描述

6.2 解决方案

  1. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
  2. 如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
  3. 设置热点数据永远不过期。

7 分布式锁

随着业务发展的需要,原单机部署的系统被演化成分布式集群系统,由于分布式系统多线程、多进程且分布在不同的机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的 Java API 并不能提供分布式锁的能力。为了解决这个问题需要一种跨 JVM 的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!

  • 假设订单系统部署两台机器上,不同的用户都要同时买10台 iphone,分别发了一个请求给订单系统。
  • 接着每个订单系统实例都去数据库里查了一下,当前 iphone 库存是12台。
  • 12台库存大于了要买的10台数量啊!
  • 于是每个订单系统实例都发送 SQL 到数据库里下单,然后扣减了10个库存,其中一个将库存从12台扣减为2台,另外一个将库存从2台扣减为 -8 台。
    在这里插入图片描述
    分布式锁主流的实现方案:
  1. 基于数据库实现分布式锁
  2. 基于缓存(Redis等)
  3. 基于Zookeeper
    每一种分布式锁解决方案都有各自的优缺点:
  4. 性能:Redis最高
  5. 可靠性:Zookeeper最高

7.1 设置锁和过期时间

  1. 通过 setnx 上锁
    由于setnx只有不存在该 key 的时候,可以设置成功,并返回1,否则设置失败,并返回0。
setnx lock A //获取锁,并对lock上锁
setnx lock B //其他服务器试图获取锁时,失败
  1. 通过del释放锁
del lock //释放锁,此时其他服务器可以获取锁

在这里插入图片描述
3. 如果锁一直不释放,需要增加过期时间,防止资源浪费。
在这里插入图片描述

expire lock 10 //给锁添加个过期时间
  1. 如果在上锁之后,设置过期时间之前,服务器异常,就无法设置过期时间,可以在上锁的同时设置过期时间。
set lock 1 nx ex 10 //上锁的同时设置过期时间

7.2 防止误删

在这里插入图片描述
避免误删情况出现,可以在加锁过程中添加一个加锁的唯一 id,通过跟该id对比,阻止误删的情况出现。

//连接到自己的redis服务器
Jedis jedis = new Jedis("192.168.56.31",6379);
UUID uuid = UUID.randomUUID();
jedis.setnx("lock",uuid.toString());
/*
执行业务代码
* */
//释放锁的时候,通过uuid对比下是不是自己加的锁
String lockuuid = jedis.get("lock");
if (uuid.toString().equals(lockuuid))
{
	//如果是自己加的锁,则释放
	jedis.del("lock");
}

7.3 保证删除原子性

在这里插入图片描述
Lua脚本
Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

Redis中引入 lua 的优势:

  • 减少网络开销:多个请求通过脚本一次发送,减少网络延迟
  • 原子操作:将脚本作为一个整体执行,中间不会插入其他命令,无需使用事务
  • 复用:客户端发送的脚本永久存在 redis 中,其他客户端可以复用脚本
  • 可嵌入性:可嵌入 JAVA,C# 等多种编程语言,支持不同操作系统跨平台交互

lua 进行比较 uuid,对比成功后删除键值对的代码:

if redis.call('get', KEYS[1]) == ARGV[1]
	then
		return redis.call('del', KEYS[1])
	else
		return 0
end

if 中的比较如果是true , 那么 执行 del 并返回 del 结果;如果 if 结果为false 直接返回 0 。

其中的 KEYS[1] , ARGV[1] 是参数,我们只调用 jedis 执行脚本的时候,传递这两个参数就可以了。
通过 jedis 执行 lua 脚本

//连接到自己的redis服务器
Jedis jedis = new Jedis("192.168.56.31",6379);
UUID uuid = UUID.randomUUID();
jedis.setnx("lock",uuid.toString());
String lockuuid = jedis.get("lock");

String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return
redis.call('del',KEYS[1]) else return 0 end";// lua脚本,用来释放分布式锁
//第一个参数是lua脚本,第二个参数是需要判断的key,第三个参数是key所对应的value
jedis.eval(luaScript,Arrays.asList("lock"),Arrays.asList(lockuuid));

通过 lua 脚本进行比对删除,保证原子性操作,防止出现问题。

8 消息队列

8.1 List消息队列

List 底层的实现就是一个「链表」,在头部和尾部操作元素,时间复杂度都是 O(1),这意味着它非常符合消息队列的模型。
在这里插入图片描述
生产者使用 lpush 发布消息

lpush queue msg1
lpush queue msg2

在这里插入图片描述
消费者这一侧,使用 rpop 拉取消息:

rpop queue
rpop queue

在这里插入图片描述
当队列中已经没有消息了,消费者在执行 RPOP 时,会返回 NULL。

一般编写消费者逻辑时,通过一个“死循环”实现,如果此时队列为空,那消费者依旧会频繁拉取消息,造成资源浪费。

while(true)
{
	String msg = jedis.rpop("queue");
}

Redis 提供「阻塞式」拉取消息的命令:brpop / blpop,这里的 B 指的是阻塞(Block)。
在这里插入图片描述
brpop key timeout:移除并返回最后一个值,同时需要传入一个超时时间(timeout),如果设置为 0,则表示不设置超时,直到有新消息才返回,否则会在指定的超时时间后返回 NULL。

brpop queue 0//获取queue最后一个值,如果没有值,则一直等待

缺点:

  • 不支持重复消费:消费者拉取消息后,这条消息就从 List 中删除了,无法被其它消费者再次消费,即不支持多个消费者消费同一批数据
  • 消息丢失:消费者拉取到消息后,如果发生异常宕机,那这条消息就丢失了

消费者(Customer):

Jedis jedis = new Jedis("192.168.56.31",6379);
System.out.println("开始监听");
while (true)
{
	List<String> msg = jedis.brpop(0,"queue");
	System.out.println("接受消息:");
	//一般来说 一条消息分为两部分,第一部分是list的key,第二部分为value
	for (String m : msg)
	{
		System.out.print(m + " ");
	}
}

生产者(Producer):

Jedis jedis = new Jedis("192.168.56.31",6379);
Scanner sc = new Scanner(System.in);
while (true)
{
	System.out.println("输入发送的消息:");
	String msg = sc.next();
	jedis.lpush("queue",msg);
}

8.2 发布/订阅消息队列

Redis 提供了 PUBLISH / SUBSCRIBE命令,来完成发布、订阅的操作。
在这里插入图片描述
多个消费者,同时消费同一批数据。

//多个客户端同时订阅queue频道
SUBSCRIBE queue

通过生产者,发布一条消息。

PUBLISH queue msg1

客户端接收到消息

SUBSCRIBE queue
// 收到新消息
1) "message"
2) "queue"
3) "msg1"

使用 Pub/Sub这种方案,既支持阻塞式拉取消息,还很好地满足了多组消费者,消费同一批数据的业务需求。
但是该方案会引起消息丢失

  • 消费者下线
  • Redis 宕机
    消费者:通过 jedis 订阅频道,需要一个 JedisPubSub子类对象,并重写 onMessage 方法用于接受消息
public class Customer extends JedisPubSub {
	public void onMessage(String channel, String message) {
		System.out.println("接收到消息:" + channel + ":" + message);
	}
	public static void main(String[] args) {
		Jedis jedis = new Jedis("192.168.56.31",6379);
		//通过jedis订阅频道,需要一个JedisPubSub子类对象,并重写onMessage方法用于接受消息
		jedis.subscribe(new Customer(),"queue");
	}
}

生产者:

Jedis jedis = new Jedis("192.168.56.31",6379); //第一个参数是ip地址,第二个参数是端口
Scanner sc = new Scanner(System.in);
while (true)
{
	System.out.println("输入发送的消息:");
	String msg = sc.next();
	jedis.publish("queue",msg);
}

9 数据一致性解决方案

读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容易出现 缓存(Redis)和数据库(MySQL)间的数据一致性问题。
不管是先写MySQL数据库,再删除 Redis 缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。
例:

  1. 如果删除了缓存 Redis,还没有来得及写库 MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。
  2. 如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。

因为写和读是并发的,没法保证顺序,就会出现缓存和数据库的数据不一致的问题。
在这里插入图片描述

9.1 延时双删策略

  1. 先删除缓存。
  2. 再写数据库。
  3. 休眠500毫秒;
  4. 再次删除缓存。

需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

当然这种策略还要考虑redis和数据库主从同步的耗时。最后的的写数据的休眠时间:则在读数据业务逻辑的耗时基础上,加几百ms即可。比如:休眠1秒。

缺点:结合 双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时。

10 企业级持久化解决方案

在企业中不要仅仅使用 RDB,因为那样会导致丢失很多数据。
也不要仅仅使用 AOF,因为那样有两个问题:

  1. 通过 AOF 做冷备,没有 RDB 做冷备,来的恢复速度更快;
  2. RDB 每次简单粗暴生成数据快照,更加健壮,可以避免 AOF 这种复杂的备份和恢复机制的 bug。

综合使用 AOF 和 RDB 两种持久化机制,用 AOF 来保证数据不丢失,作为数据恢复的第一选择;

用 RDB 来做不同程度的冷备,在 AOF 文件都丢失或损坏不可用的时候,还可以使用 RDB 来进行快速的数据恢复。

如果 RDB 在执行 snapshotting 操作,那么 redis 不会执行 AOF rewrite;如果 redis 再执行 AOF rewrite,那么就不会执行 RDB snapshotting。

如果 RDB 在执行 snapshotting,此时用户执行BGREWRITEAOF命令,那么等 RDB 快照生成之后,才会去执行 AOF rewrite。

10.1 RDB的生成策略

如果希望能确保 RDB 最多丢1分钟的数据,那么尽量就是每隔1分钟都生成一个快照。不过到底是10000条执行一次RDB,还是1000条执行一次 RDB,这个根据需要根据自己的应用和业务的数据量来确定。

10.2 AOF的生成策略

AOF一定要打开,fsync 方式选择 everysec。一般可能会调整的参数可能就是下面俩参数了

auto-aof-rewrite-percentage 100

就是当前 AOF 大小膨胀到超过上次 100%,上次的两倍。

auto-aof-rewrite-min-size 64mb

根据自己的数据量来定,16mb,32mb。

10.3 企业级的数据备份方案

RDB 非常适合做冷备,每次生成之后,就不会再有修改。
数据备份方案:

  1. 写定时调度脚本去做数据备份。
  2. 每小时都 copy一份 rdb 的备份,到一个目录中去,仅仅保留最近48小时的备份。
  3. 每天都保留一份当日的 rdb 的备份,到一个目录中去,仅仅保留最近1个月的备份。
  4. 每次 copy 备份的时候,都把太旧的备份给删了。
  5. 每天晚上将当前服务器上所有的数据备份,发送一份到远程的云服务上去。

MongoDB

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值