一 Redis概述
1 为什么要用NoSQL
单机Mysql的美好年代
在90年代,一个网站的访问量一般都不大,用单个数据库完全可以 轻松应付。在那个时候,更多的都是静态网页,动态交互类型的网站不多。
遇到问题: 随着用户数的增长,Tomcat和数据库之间竞争资源,单机性能不足以支撑业务。
Tomcat与数据库分开部署
Tomcat和数据库分别独占服务器资源,显著提高两者各自性能。
新的问题: 随着用户数的增长,并发读写数据库成为瓶颈。
引入本地缓存和分布式缓存
通过缓存能把绝大多数请求在读写数据库前拦截掉,大大降低数据库压力。其中涉及的技术包括:使用memcached作为本地缓存, 使用Redis作为分布式缓存。
注意: 缓存抗住了大部分的访问请求,随着用户数的增长,并发压力主要落在单机的Tomcat上,响应逐渐变慢。
引入反向代理实现负载均衡
在多台服务器上分别部署Tomcat,使用反向代理软件(Nginx)把请求均匀分发到每个Tomcat中。
新的挑战: 反向代理使应用服务器可支持的并发量大大增加,但并发量的增长也意味着更多请求穿透到数据库,单机的数据库最终成为瓶颈。
数据库读写分离
由于数据库的写入压力增加,Memcached只能缓解数据库的读取压力。读写集中在一个数据库上让数据库不堪重负,大部分网站开始使用主从复制技术来达到读写分离,以提高读写性能和读库的可扩展性。Mysql的master-slave模式成为这个时候的网站标配了
新的挑战: 业务逐渐变多,不同业务之间的访问量差距较大,不同业务直 接竞争数据库,相互影响性能。
数据库按业务分库
把不同业务的数据保存到不同的数据库中,使业务之间的资源竞争降低,对于访问量大的业务,可以部署更多的服务器来支撑。
为什么用NoSQL
用户的个人信息,社交网络,地理位置,用户生成的数据和用户操作日志已经成倍的增加。我们如果要对这些用户数据进行挖掘,那 SQL数据库已经不适合这些应用了, NoSQL数据库的发展也却能很好的处理这些大的数据。
2 什么是NoSQL
什么是NoSQL
NoSQL(NoSQL = Not Only SQL),意即“不仅仅是SQL”,泛指非关系型的数据库。随着互联网web2.0网站的兴起,传统的关系数据库在应付特别是超大规模和高并发类型纯动态网站已经显得力不从心,暴露了很多难以克服的问题。
结构化数据和非结构化数据
-
结构化数据指的是由二维表结构来逻辑表达和实现的数据,严格遵循数据格式与长度规范,也称作为行数据。
-
非结构化数据,指的是数据结构不规则或不完整,没有任何预定义的数据模型,不方便用二维逻辑表来表现的数据,例如办公文档(Word)、文本、图片、HTML、各类报表、视频音频等。
NoSQL的四大分类
KV型NoSql(代表----Redis)
KV型NoSql顾名思义就是以键值对形式存储的非关系型数据库,是最简单、最容易理解也是大家最熟悉的一种NoSql,因此比较快地带过。
特点:
- 数据基于内存,读写效率高
- KV型数据,时间复杂度为O(1),查询速度快
注意:
KV型NoSql最大的优点就是高性能,利用Redis自带的 BenchMark做基准测试,TPS可达到10万的级别,性能非常强劲。
列式NoSql(代表----HBase)
列式NoSql,大数据时代最具代表性的技术之一了,以HBase为代表。
关系行数据库数据
注意:
看到每行有name、phone、address三个字段,这是行式存储 的方式,且可以观察id = 2的这条数据,即使phone字段没有, 它也是占空间的。
列式数据库数据
注意:
- 查询时只有指定的列会被读取,不会读取所有列
- 列数据被组织到一起,一次磁盘IO可以将一列数据一次性读取到内存中
文档型NoSql(代表----MongoDB)
什么是文档型NoSql呢,文档型NoSql指的是将半结构化数据存储为文档的一种NoSql,文档型NoSql通常以JSON或者XML格式存储数据。
注意:
关系型数据库是按部就班地每个字段一列存,在MongDB里面就是一个JSON字符串存储。
搜索型NoSql(代表----ElasticSearch)
传统关系型数据库主要通过索引来达到快速查询的目的,但是在全文搜索的场景下,索引是无能为力的,like查询一来无法满足所有模糊匹配需求,二来使用限制太大且使用不当容易造成慢查询,搜索型NoSql的诞生正是为了解决关系型数据库全文搜索能力较弱的问题,ElasticSearch是搜索型NoSql的代表产品。
关系型数据库和非关系型数据及其区别
关系型数据库
关系型数据库最典型的数据结构是表,由二维表及其之间的联系所组成的一个数据组织
优点:
- 易于维护:都是使用表结构,格式一致;
- 使用方便:SQL语言通用,可用于复杂查询;
- 复杂操作:支持SQL,可用于一个表以及多个表之间非常复杂的查询。
缺点:
- 读写性能比较差,尤其是海量数据的高效率读写;
- 固定的表结构,灵活度稍欠;
非关系型数据库
优点:
- 格式灵活:存储数据的格式可以是key,value形式、文档形式、图片形式等等,文档形式、图片形 式等等,使用灵活,应用场景广泛,而关系型数据库则只支持基础类型。
- 速度快:nosql可以使用硬盘或者随机存储器作为载体,而关系型数据库只能使用硬盘;
- 高扩展性;
- 成本低:nosql数据库部署简单,基本都是开源软件。
缺点:
- 不提供sql支持,学习和使用成本较高;
- 无事务处理;
- 数据结构相对复杂,复杂查询方面稍欠。
3 当下NoSQL经典应用
当下应用是SQL和NoSQL一起使用
淘宝商品信息如何存放
商品基本信息
名称、价格、出厂信息、生产厂商,商家信息等, 关系型数据库就可以解决。
注意:淘宝内部用的Mysql是里面的大牛自己改造过的。
商品描述、详情、评论
多文件信息描述类,IO读写性能变差不能使用Mysql数据库,使用 MongDB。
商品的图片
分布式文件系统:
- 淘宝自己的TFS
- Google的GFS
- Hadoop的HDFS
- 阿里云的OSS
商品关键字
搜索引擎 elasticsearch 或者 ISerach
商品热门的波段信息
内存数据库 Redis Tair Memache
遇到的问题:
- 数据类型太多
- 数据源繁多
- 数据要改造
发现问题
难点:
- 数据类型多样性
- 数据源多样性和变化重构
- 数据源改造而数据服务平台不需要大面积重构
解决问题
UDSL统一数据服务平台
UDSL热点缓存设计
4 Redis是什么
Redis是什么
Redis是一个使用ANSI C编写的开源、包含多种数据结构、支持网络、基于内存、可选持久性的键值对存储数据库。
特性:
- 基于内存运行,性能高效
- 支持分布式,理论上可以无限扩展
- key-value存储系统
- 开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、KeyValue数据库,并提供多种语言的API
谁在用Redis
Github 京东 微博 阿里巴巴 百度 美团 搜狐
二 Redis安装
1 Linux下安装Redis
下载地址
Redis官方网址:https://redis.io/
下载Redis
redis-6.2.6.tar.gz上传至CentOS并解压,解压后得到redis-6.2.4目 录
解压命令:
tar -zxvf redis-6.2.6.tar.gz -C /usr/local
安装GCC
安装C语言编译环境
yum install -y gcc
通过使用 gcc --version 命令打印 GCC 版本,来验证 GCC 编译器是否被成功安装:
gcc --version
安装Redis
编译Redis
在redis目录下(/usr/local/redis-6.2.6)执行:
make
安装Redis
在redis目录下(/usr/local/redis-6.2.6)执行:
make install
安装目录: /usr/local/redis-6.2.6/src
注意:
redis-benchmark:Redis自带的基准性能测试工具
redis-check-aof:对有问题的 AOF 文件进行修复,AOF和RDB 文件后面会说明
redis-check-rdb:对有问题的 RDB文件进行修复
redis-sentinel:Redis集群使用
redis-cli:客户端
redis-server:服务器启动
服务启动
前台启动:/usr/local/redis-6.2.6/src下执行
./redis-server
后台启动
修改redis.conf文件
daemonize yes #由no改为yes
启动服务
./redis-server ../redis.conf
客户端启动
/usr/local/redis-6.2.6/src下执行
./redis-cli
ping命令可以检测服务器是否正常(服务器返回PONG)
127.0.0.1:6379> ping
PONG
2 Docker下安装Redis
下载最新Redis镜像
docker pull redis
注意:
可以用docker pull redis命令下载最新版本的Redis镜像,也可 以用“docker pull redis:标签”命令下载指定版本的Redis。
启动Redis容器
docker run -itd --name myFirstRedis -p 6379:6379 redis:latest
观察Redis启动效果
docker logs myFirstRedis
注意:
如果直接在Linux等环境上启动Redis服务器,就能直接看到启动后的效果。
查看Redis的版本
先确保myFirstRedis容器处于Up状态。进入容器的命令行交互窗口。
docker exec -it myFirstRedis /bin/bash redis-server --version
Redis服务器和客户端
Redis是基于键值对存储的NoSQL数据库,其中的数据是存储在 Redis服务器里的。和传统的MySQL数据库服务器相似,一个Redis 服务器可以同多个客户端创建连接。
docker exec -it myFirstRedis /bin/bash redis-cli
3 基本知识
默认16数据库
Redis是一个字典结构的存储服务器,一个Redis实例提供了多个用来存储数据的字典,客户端可以指定将数据存储在哪个字典中。
这与在一个关系数据库实例中可以创建多个数据库类似(如下图所 示),所以可以将其中的每个字典都理解成一个独立的数据库。
Redis默认支持16个数据库,可以通过调整Redis的配置文件 redis/redis.conf中的databases来修改这一个值,设置完毕后重启 Redis便完成配置。
Redis 使用的到底是多线程还是单线程?
因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈 最有可能是机器内存的大小或者网络带宽。既然单线程容易实现, 而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。
IO多路复用技术
redis 采用网络IO多路复用技术来保证在多连接的时候, 系统的高吞吐量。
大白话解释
假设你是一个机场的空管, 你需要管理到你机场的所有的航线, 包括进港,出港, 有些航班需要放到停机坪等待,有些航班需要去登机口接乘客。
最简单的做法,就是你去招一大批空管员,然后每人盯一架飞机, 从进港,接客,排位,出港,航线监控,直至交接给下一个空港, 全程监控。
遇到的问题:
- 很快你就发现空管塔里面聚集起来一大票的空管员,交通稍微繁忙一点,新的空管员就已经挤不进来了。
- 空管员之间需要协调,屋子里面就1, 2个人的时候还好,几十号人以后 ,基本上就成菜市场 了。
- 空管员经常需要更新一些公用的东西,比如起飞显示屏,比如下一个小时后的出港排期,最 后你会很惊奇的发现,每个人的时间最后都花在了抢这些资源上。
怎么解决
这个东西叫flight progress strip. 每一个块代表一个航班,不同的槽代表不同的状态,然后一个空管员可以管理一组这样的块(一组航班),而他的工作,就是在航班信息有新的更新的时候,把对应的块放到不同的槽子里面。
结论
这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就 了Redis具有很高的吞吐量。
切换数据库
语法结构:
select number
示例:
# 默认使用 0 号数据库
redis 127.0.0.1:6379> SET db_number 0
OK
# 使用 1 号数据库
redis 127.0.0.1:6379> SELECT 1
OK
清空当前库
Redis Flushdb 命令用于清空当前数据库中的所有 key。
语法结构:
127.0.0.1:6379> FLUSHDB
示例:
127.0.0.1:6379> FLUSHDB
通杀全部库
Redis Flushall 命令用于清空整个 Redis 服务器的数据(删除所有数 据库的所有key )。
语法结构:
redis 127.0.0.1:6379> FLUSHALL
示例:
# 清空所有数据库的所有 key
redis 127.0.0.1:6379>flushall
OK
为什么默认端口6379
意大利的一位广告女郎名字叫Merz全名Alessia Merz。
6379 = Merz
三 Redis数据类型
1 key键
keys
查看当前库中所有的key 。
语法结构:
keys *
有3个通配符 *, ? ,[]
- *: 通配任意多个字符
- ?: 通配单个字符
- []: 通配括号内的某1个字符
示例:
keys *
注意: 生产已经禁止。因为长时间阻塞redis而导致其他客户端的命令请求一直处于阻塞状态。 更安全的做法是采用scan。
新版本也进行了替代:
root@6c068b3fbf29:/data# redis-cli --scan
"u*"
"user1"
"user"
exists
判断某个key是否存在,返回1表示存在,0不存在。
语法结构:
exists key
示例:
#查看k1是否存在,如果存在返回1
exists k1
# 查看k1 k2 k3是否存在,如果k1 k2存在,k3不存在,则
返回2
exists k1 k2 k3
注意: 可以设置多个key,只返回存在的个数,但不返回哪一个存在/ 不存在。
type
查看当前key 所储存的值的类型。返回当前key所储存的值的类型, 如string 、list等。
语法结构:
type key
示例:
type k1
del
删除已存在的key,不存在的 key 会被忽略。
语法结构:
del key
示例:
可以设置多个key,返回删除成功的个数。
# 删除k1,如果成功返回1,失败返回0
del k1
# 删除k1 k2 k3,如果k1 k2存在,k3不存在,则返回2
del k1 k2 k3
expire
给key设置time秒的过期时间。设置成功返回 1 。 当 key 不存在返回 0。
语法结构:
expire key time
示例:
# 给k1设置10秒后过期
expire k1 10
ttl
以秒为单位返回 key 的剩余过期时间。
语法结构:
ttl key
示例:
ttl k1
注意: 当 key 不存在时,返回 -2 。 当 key 存在但没有设置剩余生存时间时,返回 -1 。 否则,以秒为单位,返回 key 的剩余生存时间。
persist
移除给定 key 的过期时间,使得 key 永不过期。
语法结构:
persist key
示例:
persist k1
注意: 当过期时间移除成功时,返回 1 。 如果 key 不存在或 key 没有设置过期时间,返回 0 。
2 String
简介
String是Redis最基本的类型,一个key对应一个value。String是二进制安全的,意味着String可以包含任何数据,比如序列化对象或者一张图片。String最多可以放512M的数据。
常用命令
set
用于设置给定 key 的值。如果 key 已经存储其他值, set 就重写旧值,且无视类型。
语法格式:
set key value
示例:
127.0.0.1:6379> set k1 v1
OK
get
用于获取指定 key 的值。如果 key 不存在,返回 nil 。
语法格式:
get key
示例:
127.0.0.1:6379> get k1
"v1"
append
将给定的value追加到key原值末尾。
语法格式:
append key value
示例:
127.0.0.1:6379> APPEND k1 k1
(integer) 4
127.0.0.1:6379> APPEND k1 k2
(integer) 6
注意:
- 如果 key 已经存在并且是一个字符串, append 命令将 value 追加到 key 原来的值的末尾。
- 如果 key 不存在, append 就简单地将给定 key 设为 value ,就像执行 set key value 一 样。
strlen
获取指定 key 所储存的字符串值的长度。当 key 储存的不是字符串值时,返回一个错误。
语法格式:
strlen key
示例:
127.0.0.1:6379> strlen k1
(integer) 6
setex
给指定的 key 设置值及time 秒的过期时间。如果 key 已经存在, setex命令将会替换旧的值,并设置过期时间。
语法格式:
setex key time value
示例:
#向Redis中设置一个k1的键值对并且10秒后过期
127.0.0.1:6379> setex k1 10 v1
OK
setnx
只有在key不存在时设置key的值
语法格式:
setnx key value
示例:
127.0.0.1:6379> setnx k1 v1
(integer) 0
127.0.0.1:6379> setnx k4 v4
(integer) 1
getrange
获取指定区间范围内的值,类似between…and 的关系
语法格式:
getrange key start end
示例:
127.0.0.1:6379> set k5 abcd123xxx
OK
127.0.0.1:6379> getrange k5 2 4
"cd1"
setrange
设置指定区间范围内的值,类似between…and 的关系
语法结构:
setrange key offset value
示例:
127.0.0.1:6379> set k6 abcd1234
OK
127.0.0.1:6379> setrange k6 1 xxx
(integer) 8
127.0.0.1:6379> get k6
"axxx1234"
incr
将 key 中储存的数字值增一。
语法格式:
incr key
示例:
#因为Redis中不存在k1,所以先初始化为0,再递增,值为1
127.0.0.1:6379> incr k1
(integer) 1
# incr k1 存在k1,递增后k1的值为2
127.0.0.1:6379> incr k1
(integer) 2
# 如果value不是数字就会报错
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> incr k2
(error) ERR value is not an integer or out of range
注意:
- 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 incr 操作。
- 如字符串类型的值不能表示为数字、或者是其他类型,那么返回一个错误。
decr
将 key 中储存的数字值减一。
语法格式:
decr key
示例:
127.0.0.1:6379> decr k1
(integer) 1
127.0.0.1:6379> decr k1
(integer) 0
127.0.0.1:6379> decr k1
(integer) -1
127.0.0.1:6379> decr k1
(integer) -2
#如果
set k2 v2
decr k2 因为k2不为数值,Redis返回一个错误
注意:
- 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 decr 操作。
- 如字符串类型的值不能表示为数字、或者是其他类型,那么返回一个错误。
incrby/decrby key step
将key存储的数字值按照step进行增减。
127.0.0.1:6379> incrby k1 10
(integer) 20
注意:
- 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 incrby/decrby 命令。
- 如字符串类型的值不能表示为数字、或者是其他类型,那么返回一个错误。
mset
同时设置一个或多个 key-value 。
语法格式:
mset key1 value1 key2 value2
示例:
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3
OK
mget
返回所有(一个或多个)给定 key 的值。
语法格式:
mget key1 key2
示例:
127.0.0.1:6379> mget k1 k2 k3
1) "v1"
2) "v2"
3) "v3"
注意: 如果给定的 key 里面,有某个 key 不存在,那么这个 key 返回 特殊值 nil 。
getset
将给定key值设为value,并返回key的旧值(old value),简单一 句话(先get然后立即set)。
语法格式:
getset key value
示例:
127.0.0.1:6379> getset k1 wcc
"v1"
127.0.0.1:6379> get k1
"wcc"
使用场景
value 除了是字符串以外还可以是数字。
- 计数器
- 统计多单位的数量
- 粉丝数
- 对象缓存存储
- 分布式锁
3 List
简介
List是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。底层是一个双向链表, 对两端操作性能极高,通过索引操作中间的节点性能较差。
一个List最多可以包含 2 32 − 1 2^{32}-1 232−1个元素 ( 每个列表超过40亿个元素)。
常用命令
lpush/rpush
从左边(头部)/右边(尾部)插入一个或多个值。
语法结构:
lpush/rpush key1 value1 value2 value3……
示例:
#从左边放入v1 v2 v3
127.0.0.1:6379> lpush k1 v1 v2 v3
(integer) 3
#从右边放入v4 v5 v6
127.0.0.1:6379> rpush k1 v4 v5 v6
(integer) 6
lrange
返回key列表中的start和end之间的元素(包含start和end)。 其 中 0 表示列表的第一个元素,-1表示最后一个元素。
语法结构:
lrange key start end
示例:
#取出列表里前3个值,结果为v3 v2 v1
127.0.0.1:6379> lrange k1 0 2
#取出列表里全部值,结果为v3 v2 v1 v4 v5 v6
127.0.0.1:6379> lrange k1 0 -1
lpop/rpop
移除并返回第一个值或最后一个值。
语法格式:
lpop/rpop key
示例:
lpop k1 从列表中删除v3,并返回,当前列表全部值v2 v1 v4 v5 v6
rpop k1 从列表中删除v6,并返回,当前列表全部值v2 v1 v4 v5
注意: 值在键在,值光键亡。
lindex
获取列表index位置的值(从左开始)。
语法结构:
lindex key index
示例:
lindex k1 0
llen
获取列表长度。
语法结构:
llen key
示例:
127.0.0.1:6379> llen k1
(integer) 6
lrem
从左边开始删除与value相同的count个元素。
语法结构:
lrem key count value
示例:
#从左边开始删除k1列表中2个v1元素
lrem k1 2 v1
linsert
在列表中value值的前边/后边插入一个new value值(从左开始)。
语法结构:
linsert key before/after value newvalue
示例:
linsert k1 before v1 v5 在v1前面插入一个v5
lset
将索引为index的值设置为value
语法结构:
lset key index value
示例:
lset k1 0 v11
使用场景
- 消息队列
- 排行榜
- 最新列表
4 Set
简介
与List类似是一个列表功能,但Set是自动排重的,当需要存储一个列表数据,又不希望出现重复数据时,Set是一个很好的选择。
Set是String类型的无序集合,它底层其实是一个value为null的 hash表,所以添加、删除、查找的时间复杂度都是O(1)。
常用命令
sadd
将一个或多个元素添加到集合key中,已经存在的元素将被忽略。
语法结构:
sadd key value1 value2……
示例:
#向集合中添加值,最终只有v1 v2 v3 v4 v5 v6
127.0.0.1:6379> sadd k1 v1 v2 v2 v3 v4 v5 v6
smembers
取出该集合的所有元素。
语法结构:
smembers key
示例:
127.0.0.1:6379> smembers k1
sismember
判断集合key中是否含有value元素,如有返回1,否则返回0。
语法结构:
sismember key value
示例:
sismember k1 v1
scard
返回该集合的元素个数。
语法结构:
scard key
示例:
scard k1
srem
删除集合中的一个或多个成员元素,不存在的成员元素会被忽略。
语法结构:
srem key value1 value2……
示例:
# 删除v1 v2
srem k1 v1 v2
spop
随机删除集合中一个元素并返回该元素。
语法结构:
spop key
示例:
spop k1 随机删除一个元素,并返回
srandmember
随机取出集合中count个元素,但不会删除。
语法结构:
srandmember key count
示例:
#随机取出集合中的2个元素
srandmember k1 2
smove
将value元素从sourcekey集合移动到destinationkey集合中。
语法结构:
smove sourcekey destinationkey value
示例:
smove k1 k2 v5 将元素v5从集合k1中移动到集合k2
注意:
如果 sourcekey集合不存在或不包含指定的 value元素,则 smove 命令不执行任何操作,仅返回 0 。
sinter
返回两个集合的交集元素。
语法结构:
sinter key1 key2
示例:
sinter k1 k2
sunion
返回两个集合的并集元素。
语法结构:
sunion key1 key2
示例:
sunion k1 k2
sdiff
返回两个集合的差集元素(key1中的,不包含key2)
语法结构:
sdiff key1 key2
示例:
sdiff k1 k2
使用场景
- 黑白名单
- 随机展示
- 好友
- 关注人
- 粉丝
- 感兴趣的人集合
5 Hash
简介
Hash是一个键值对的集合。Hash 是一个 String 类型的 field(字 段) 和 value(值) 的映射表,hash 特别适合用于存储对象。
Hash存储结构优化
- 如果field数量较少,存储结构优化为类数组结构
- 如果field数量较多,存储结构使用HashMap结构
常用命令
hset
给key集合中的field赋值value。
语法结构:
hset key field value
示例:
127.0.0.1:6379> hset user name baizhan
(integer) 1
127.0.0.1:6379> hset user age 3
(integer) 1
注意:
- 如果哈希表不存在,一个新的哈希表被创建并进行 HSET 操作。
- 如果字段已经存在于哈希表中,旧值将被重写。
hget
从key哈希中,取出field字段的值。
语法结构:
hget key field
示例:
127.0.0.1:6379> hget user name
"baizhan"
hmset
批量设置哈希的字段及值。
语法结构:
hmset key field1 value1 field2 value2……
示例:
127.0.0.1:6379> hmset user1 name baizhan age 15
OK
hexists
判断指定key中是否存在field
语法结构:
hexists key field
示例:
127.0.0.1:6379> hexists user1 name
(integer) 1
127.0.0.1:6379> hexists user1 xxx
(integer) 0
注意: 如果哈希表含有给定字段,返回 1 。 如果哈希表不含有给定字段,或 key 不存在,返回 0 。
hkeys
获取该哈希中所有的field。
语法结构:
hkeys key
示例:
127.0.0.1:6379> hkeys user1
1) "name"
2) "age"
hvals
获取该哈希中所有的value。
语法结构:
hvals key
示例:
127.0.0.1:6379> hvals user1
1) "baizhan"
2) "15"
hincrby
为哈希表key中的field字段的值加上增量increment
语法结构:
hincrby key field increment
示例:
127.0.0.1:6379> hincrby user1 age 10
(integer) 25
注意:
- 增量也可以为负数,相当于对指定字段进行减法操作。
- 如果哈希表的 key 不存在,一个新的哈希表被创建并执行 hincrby 命令。
- 如果指定的字段不存在,那么在执行命令前,字段的值被初始化为 0 。
- 对一个储存字符串值的字段执行 hincrby 命令将造成一个错误。
hincrby user1 age 10 对user中的age字段做运算,增加10
hdel
删除哈希表 key 中的一个或多个指定字段,不存在的字段将被忽略。
语法结构:
hdel key field1 field2……
示例:
127.0.0.1:6379> hdel user1 age
(integer) 1
hsetnx
给key哈希表中不存在的的字段赋值 。
语法结构:
hsetnx key field value
示例:
127.0.0.1:6379> hsetnx user1 age 10
(integer) 1
注意:
如果哈希表不存在,一个新的哈希表被创建并进行 hsetnx 操作。
如果字段已经存在于哈希表中,操作无效。
如果 key 不存在,一个新哈希表被创建并执行 hsetnx 命令。
使用场景
- 购物车
- 存储对象
6 Zset
简介
Zset与Set非常相似,是一个没有重复元素的String集合。不同之处是Zset的每个元素都关联了一个分数(score),这个分数被用来按照从低分到高分的方式排序集合中的元素。集合的元素是唯一的, 但分数可以重复。
注意: 因为元素是有序的,所以可以根据分数(score)或者次序 (position)来获取一个范围内的元素。
常用命令
zadd
将一个或多个元素(value)及分数(score)加入到有序集key中。
语法结构:
zadd key score1 value1 score2 value2……
示例:
zadd k1 100 java 200 c++ 300 python 400 php
注意:
如果某个元素已经是有序集的元素,那么更新这个元素的分数值,并通过重新插入这个元素,来保证该元素在正确的位置上。
分数值可以是整数值或双精度浮点数。
如果有序集合 key 不存在,则创建一个空的有序集并执行 zadd 操作。
zrange
返回key集合中的索引start和索引end之间的元素(包含start和 end)。
语法结构:
zrange key start end [withscores]
示例:
zrange k1 0 -1 返回集合中所有元素
zrange k1 0 -1 withscores 返回集合中所有元素,并携带元素分数
注意:
- 其中元素的位置按分数值递增(从小到大)来排序。 其中 0 表示列表的第一个元素,-1表示最 后一个元素。
- withscores是可选参数,是否返回分数。
zrangebyscore
返回key集合中的分数minscore 和分数maxscore 之间的元素(包 含minscore 和maxscore )。其中元素的位置按分数值递增(从小 到大)来排序。
语法结构:
zrangebyscore key minscore maxscore [withscores]
示例:
zrangebyscore k1 200 400 返回200-400分之间的元素递增排序
zincrby
为元素value的score加上increment的值。
语法结构:
zincrby key increment value
示例:
zincrby k1 50 java 给java元素加上50分
zrem
删除该集合下value的元素
语法结构
zrem k1 php 删除php
zcount
统计该集合在minscore 到maxscore分数区间中元素的个数。
语法结构:
zcount key minscore maxscore
示例:
zcount k1 100 300 统计100分到300分中间元素的个数
zrank
返回value在集合中的排名,从0开始。
语法结构:
zrank key value
示例:
zrank k1 c++ 返回c++排名
使用场景
- 延时队列
- 排行榜
- 限流
7 Bitmaps
简介
在计算机中,用二进制(位)作为存储信息的基本单位,1个字节等于8位。
例如 “abc” 字符串是由 3 个字节组成,计算机存储时使用其二进制表示,"abc"分别对应的ASCII码是97、98、99,对应的二进制是 01100001、01100010、01100011,在内存中表示如下:
合理地使用位能够有效地提高内存使用率和开发效率。
Redis提供了Bitmaps这个 “数据结构” 可以实现对位的操作:
常用命令
setbit
设置Bitmaps中某个偏移量的值。
语法结构:
setbit key offset value
示例:
redis中bitmaps可以用来统计用户信息,eg:活跃天数、打卡天 数、登录天数
bitmaps位图,都是操作二进制来进行记录,就只有0和1两个状态
127.0.0.1:6379> setbit zhangsan 1 1 # 往zhangsan中添加数据,第1天打卡
(integer) 1
127.0.0.1:6379> setbit zhangsan 2 0 # 第2天未打卡
(integer) 0
127.0.0.1:6379> setbit zhangsan 3 1 # 第3天打卡
(integer) 0
127.0.0.1:6379> setbit zhangsan 4 0 # 第4天未打卡
(integer) 0
127.0.0.1:6379> setbit zhangsan 5 1 # 第5天打卡
(integer) 0
127.0.0.1:6379> setbit zhangsan 6 0 # 第6天未打卡
(integer) 0
127.0.0.1:6379> setbit zhangsan 7 1 # 第7天打卡
(integer) 0
127.0.0.1:6379> getbit zhangsan 1 #获取第一天的打卡状态
(integer) 1
127.0.0.1:6379> bitcount zhangsan # 统计所有打卡天数
(integer) 4
getbit
获取Bitmaps中某个偏移量的值。
语法结构:
getbit key offset
示例:
获取key的offset 的值。
getbit sign 3
如果偏移量未设置值,则也返回0。
getbit sign 99 获取偏移量为99的值,结果为0
bitcount
统计字符串被设置为1的bit数量。一般情况下,给定的整个字符串都会被进行统计,可以选择通过额外的start和end参数,指定字节 组范围内进行统计(包括start和end),0表示第一个元素,-1表示 最后一个元素。
语法结构:
bitcount key [start end]
示例:
bitcount sign 获取整个字符串被设置为1的bit数量
bitop
将多个bitmaps通过求交集/并集方式合并成一个新的bitmaps。
语法结构:
bitop and/or destkey sourcekey1 sourcekey2……
示例:
bitop and k3 k1 k2 通过求交集将k1 k2合并成k3
bitop or k3 k1 k2 通过求并集将k1 k2合并成k3
8 Geospatia
简介
GEO,Geographic,地理信息的缩写。该类型就是元素的二维坐标,在地图上就是经纬度。Redis基于该类型,提供了经纬度设置、 查询、范围查询、距离查询、经纬度Hash等常见操作。
常用命令
geoadd
用于存储指定的地理空间位置,可以将一个或多个经度 (longitude)、纬度(latitude)、位置名称(member)添加到指定的 key 中。
语法结构:
geoadd key longitude latitude member
示例:
# 将北京的经纬度和名称添加到china
geoadd china 116.405285 39.904989 beijing
# 将成都和上海的经纬度、名称添加到china
geoadd china 104.065735 30.659462 chengdu 121.472644 31.231706 shanghai
geopos
从给定的 key 里返回所有指定名称(member)的位置(经度和纬 度),不存在的返回 nil
语法结构:
geopos key member [member ……]
示例:
返回china中名称为shanghai和beijing的经纬度
geopos china shanghai beijing
geodist
用于返回两个给定位置之间的距离。
语法结构:
geodist key member1 member2 [m|km|ft|mi]
参数说明:
- m :米,默认单位。
- km :千米。
- mi :英里。
- ft :英尺。
示例:
# 返回shanghai和beijing之间的距离,结果1067597.9668,单位米
geodist china shanghai beijing
# 返回shanghai和chengdu之间的距离,结果1660.0198,单位是千米
geodist china shanghai chengdu km
georadius
以给定的经纬度(longitude latitude)为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离(radius )的所有位 置元素。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ICFVaTu8-1692870460036)(F:\001-after-end\笔记\12-高并发处理与数据缓存\02-assets\56.PNG)]
语法结构:
georadius key longitude latitude radius m|km|ft|mi
示例:
#获取经纬度110 30为中心,在china内1200公里范围内的所有元素。
georadius china 110 30 1200 km
使用场景
- 附近的电影院
- 附近的好友
- 离最近的火锅店
9 Hyperloglog
简介
在我们做站点流量统计的时候一般会统计页面UV(独立访客:unique visitor)和PV(即页面浏览量:page view)。redis HyperLogLog是用来做基数统计的算法,HyperLogLog的优点是:在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
什么是基数
比如数据集{1,3,5,7,5,7,8},那么这个数据集的基数集为{1,3,5,7,8}, 基数(不重复元素)为5.基数估计就是在误差可接受的范围内,快速计算基数。
常用命令
pfadd
将所有元素参数添加到 Hyperloglog 数据结构中。
语法结构:
pfadd key element1 element2……
示例:
如果至少有个元素被添加返回 1, 否则返回 0。
pfadd book1 uid1 uid2 uid3
注意: 添加元素到HyperLogLog中,如果内部有变动返回1,没有返回 0。
pfcount
计算Hyperloglog 近似基数,可以计算多个Hyperloglog ,统计基数总数。
语法结构:
pfcount key1 key2……
示例:
pfcount book1 #计算book1的基数,结果为3
pfadd book2 uid3 uid4 #添加两个元素到book2中
pfcount book1 book2 #统计两个key的基数总数,结果为5
pfmerge
将一个或多个Hyperloglog(sourcekey1) 合并成一个 Hyperloglog (destkey )。
语法结构:
pfmerge destkey sourcekey1 sourcekey2……
示例:
比如每月活跃用户可用每天活跃用户合并后计算。
#将book1和book2合并成book,结果为5
pfmerge book book1 book2
使用场景
基数不大,数据量不大就用不上,会有点大材小用浪费空间,有局限性,就是只能统计基数数量,而没办法去知道具体的内容是什 么,和bitmap相比,属于两种特定统计情况,简单来说, HyperLogLog 去重比 bitmaps 方便很多,一般可以bitmap和 hyperloglog配合使用,bitmap标识哪些用户活跃。
- 网站PV统计
- 网站UV统计
- 统计访问量(IP数)
- 统计在线用户数
- 统计每天搜索不同词条的个数
- 统计文章真实阅读数
四 Redis可视化工具
下载Redis Desktop Manager
注意: 官网https://rdm.dev/pricing
选择安装路径
连接Redis服务
关闭防火墙
systemctl stop firewalld.service
关闭保护模式
protected-mode no
开启远程访问
redis默认只允许本地访问,要使redis可以远程访问可以修改 redis.conf。
注释掉bind 127.0.0.1 可以使所有的ip访问redis
配置连接服务
配置信息
五 Java整合Redis
1 Jedis操作
什么是Jedis
Jedis是Redis官方推荐的Java连接开发工具。
引入Jedis
创建maven工程
引入maven依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.6.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
Jedis连接到redis
//第一个参数是ip地址,第二个参数是端口
Jedis jedis = new Jedis("192.168.56.31",6379);
注意:
在连接之前,需要开放redis连接服务,redis.conf中注释掉 bind 127.0.0.1 ,然后 protected-mode no,关闭防火墙。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mpRsMXrP-1692870460042)(F:\001-after-end\笔记\12-高并发处理与数据缓存\02-assets\65.PNG)]
测试相关数据类型
public class RedisTest {
private Jedis jedis;
@Before
public void conn() {
jedis = new Jedis("192.168.66.100", 6379);
}
@After
public void close() {
//jedis使用完毕需要关闭
jedis.close();
}
@Test
public void testConnection() {
//通过ping()方法向redis发送一个ping命令,服务器返回一个Pong
String msg = jedis.ping();
System.out.println(msg);
}
@Test
public void testString() {
//设置一个key
jedis.set("k1", "v1");
//设置一个key
jedis.set("k2", "1");
//获取一个key
String res = jedis.get("k1");
//对某一个key自增
Long ires = jedis.incr("k2");
}
@Test
public void testKeys() {
//返回所有的key
Set<String> keys = jedis.keys("*");
keys.stream().forEach(System.out::println);
//返回该key剩余过期时间
Long time = jedis.ttl("k1");
System.out.println(time);
}
@Test
public void testList() {
//向list中添加数据
jedis.lpush("list1", "v1", "v2", "v3");
//返回list全部数据
List<String> list = jedis.lrange("list1", 0, -1);
list.stream().forEach(System.out::println);
}
@Test
public void testSet() {
//向set中添加数据
jedis.sadd("set1", "v1", "v2", "v2", "v3");
//查看该集合中有多少个元素
Set<String> set = jedis.smembers("set1");
set.stream().forEach(System.out::println);
}
@Test
public void testHash() {
//设置一个hash
jedis.hset("user", "age", "25");
jedis.hset("user", "name", "lxx");
//获取该key的所有value
List<String> list = jedis.hvals("user");
list.stream().forEach(System.out::println);
}
@Test
public void testZset() {
//向zset中添加一条数据
jedis.zadd("zset1", 100, "java");
jedis.zadd("zset1", 200, "c++");
//获取所有的值
Set<String> set = jedis.zrange("zset1", 0, -1);
set.stream().forEach(System.out::println);
}
@Test
public void testBitmaps() {
//将b1偏移量为0的位设置为1
jedis.setbit("b1", 0, "1");
//将b1偏移量为1的位设置为0
jedis.setbit("b1", 1, "0");
//统计字符串被设置为1的bit数量
Long bitCount = jedis.bitcount("b1");
System.out.println(bitCount);
}
@Test
public void testGeospatia() {
//添加一条地理信息数据
jedis.geoadd("china", 116.40400, 39.92800, "kunming");
}
@Test
public void testHyperloglog() {
//将所有元素参数添加到 Hyperloglog 数据结构中。
jedis.pfadd("book", "uid1", "uid2", "uid3");
}
}
注意: 其实jedis中的方法基本同redis命令一致。
2 Spring-Data-Redis
简介
Spring-Data-Redis是spring大家族的一部分,通过简单的配置访问 Redis服务,对Reids底层开发包(Jedis, JRedis, and RJC)进行了高度封装,RedisTemplate提供了Redis各种操作、异常处理及序列化, 支持发布订阅。
RedisTemplate介绍
Spring封装了RedisTemplate对象来进行对Redis的各种操作,它支 持所有的Redis原生的api。
org.springframework.data.redis.core Class RedisTemplate<K,V>
注意:
K:模板中的Redis key的类型,模板中的Redis key的类型(通常为String)
如: RedisTemplate<String,Object>。
V:模板中的Redis value的类型
RedisTemplate中定义了对5种数据结构操作
redisTemplate.opsForValue();//操作字符串
redisTemplate.opsForHash();//操作hash
redisTemplate.opsForList();//操作list
redisTemplate.opsForSet();//操作set
redisTemplate.opsForZSet();//操作有序set
StringRedisTemplate与RedisTemplate
- 两者的关系是StringRedisTemplate继承RedisTemplate。
- 两者的数据是不共通的;也就是说StringRedisTemplate只能管理StringRedisTemplate里面的数据,RedisTemplate只能管理 RedisTemplate中的数据。
- SDR默认采用的序列化策略有两种,一种是String的序列化策略,一种是JDK的序列化策略。StringRedisTemplate默认采用的是String的序列化策略,保存的key和value都是采用此策略序列化保存的。 RedisTemplate默认采用的是JDK的序列化策略,保存的key和 value都是采用此策略序列化保存的。
pom.xml添加依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
在application.properties中配置
#Redis服务器连接地址
spring.redis.host=192.168.66.100
#Redis服务器连接端口
spring.redis.port=6379
#连接池最大连接数(使用负值表示没有限制)
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
自定义序列化
/**
* 自定义序列化方式
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object>
redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
}
使用redisTemplate进行各类型的CURD操作
String数据类型操作
@SpringBootTest
public class StringTest {
@Autowired
private RedisTemplate redisTemplate;
//添加元素
@Test
public void testSet() {
redisTemplate.opsForValue().set("k1", "v1");
}
//获取元素
@Test
public void testGet() {
Object k1 = redisTemplate.opsForValue().get("k1");
System.out.println(k1);
}
//添加元素并设置过期时间
@Test
public void testSetex() {
redisTemplate.opsForValue().set("k1", "v1", 10, TimeUnit.SECONDS);
}
}
Hash类型的操作
@SpringBootTest
public class HashTest {
@Autowired
private RedisTemplate redisTemplate;
//添加元素
@Test
public void testHset() {
redisTemplate.opsForHash().put("user", "name", "lxx");
}
//获取数据
@Test
public void testHget() {
Object o = redisTemplate.opsForHash().get("user", "name");
System.out.println(o);
}
}
set类型的操作
@SpringBootTest
public class SetTest {
@Autowired
private RedisTemplate redisTemplate;
//添加元素
@Test
public void testSset() {
redisTemplate.opsForSet().add("k1", "v1", "v2", "v2", "v3");
}
//获取set的长度
@Test
public void testSgetSize() {
Long size = redisTemplate.opsForSet().size("k1");
System.out.println(size);
}
//获取元素
@Test
public void testSgetAll() {
Set set = redisTemplate.opsForSet().members("k1");
set.stream().forEach(System.out::println);
}
}
zset类型的操作
@SpringBootTest
public class ZsetTest {
@Autowired
private RedisTemplate redisTemplate;
//添加元素
@Test
public void testZadd() {
redisTemplate.opsForZSet().add("s1", "java", 100);
redisTemplate.opsForZSet().add("s1", "python", 200);
}
//获取元素
@Test
public void testZrange() {
Set set = redisTemplate.opsForZSet().range("s1", 0, -1);
set.stream().forEach(System.out::println);
}
}
list类型的操作
@SpringBootTest
public class ListTest {
@Autowired
private RedisTemplate redisTemplate;
//添加元素
@Test
public void testLrpush() {
redisTemplate.opsForList().rightPush("l1", "v1");
redisTemplate.opsForList().rightPush("l1", "v2");
}
//获取元素
@Test
public void testLrange() {
List list = redisTemplate.opsForList().range("l1", 0, -1);
list.stream().forEach(System.out::println);
}
}
3 Redis构建web应用实践_网页缓存
创建springboot项目
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatisPlus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.0</version>
</dependency>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.48</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.23</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
编写配置文件application.yml
spring:
# 数据源
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/student?characterEncoding=utf-8
username: root
password: 123456
#Redis
redis:
host: 192.168.66.100 #Redis服务器连接地址
port: 6379 #Redis服务器连接端口
pool:
max-active: 8 #连接池最大连接数(使用负值表示没有限制)
max-wait: -1 #连接池最大阻塞等待时间(使用负值表示没有限制)
max-idle: 8 #连接池中的最大空闲连接
min-idle: 0 #连接池中的最小空闲连接
timeout: 3000 #连接超时时间(毫秒)
创建实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
private Integer id;
private String name;
private String sex;
private String address;
}
编写持久层
public interface StudentMapper extends BaseMapper<Student> {
}
编写业务层
@Service
public class StudentService {
@Autowired
private StudentMapper studentMapper;
@Autowired
private StringRedisTemplate redisTemplate;
public Student findById(Integer id) {
// 从Redis中获取缓存
String studentStr = redisTemplate.opsForValue().get("student:id:" + id);
// 判断是否有缓存
if (StringUtils.isEmpty(studentStr)) {
//如果没有
//根据id查询学生信息
Student student = studentMapper.selectById(id);
//添加缓存
redisTemplate.opsForValue().set("student:id:" + id, JSON.toJSONString(student));
return student;
} else {
//如果有
// 直接把json数据转为goods对象
Student student = JSON.parseObject(studentStr, Student.class);
return student;
}
}
}
编写控制层
@RestController
@RequestMapping("/student")
public class StudentController {
@Autowired
private StudentService studentService;
@GetMapping("/{id}")
public Student findById(@PathVariable Integer id) {
return studentService.findById(id);
}
}
使用Jmeter进行压力测试
六 Redis配置文件
在Redis的解压目录下有个很重要的配置文件 redis.conf ,关于 Redis的很多功能的配置都在此文件中完成的,一般为了不破坏安装的文件,出厂默认配置最好不要去改。
units单位
配置大小单位,开头定义基本度量单位,只支持bytes,大小写不敏感。
INCLUDES
Redis只有一个配置文件,如果多个人进行开发维护,那么就需要多个这样的配置文件,这时候多个配置文件就可以在此通过 include /path/to/local.conf 配置进来,而原本的 redis.conf 配置文件就作为一个总闸。
NETWORK
参数:
- bind:绑定redis服务器网卡IP,默认为127.0.0.1,即本地回环地址。这样的话,访问redis服务只能通过本机的客户端连接,而无法通过远程连接。如果bind选项为空的话,那会接受所有 来自于可用网络接口的连接。
- port:指定redis运行的端口,默认是6379。由于Redis是单线程模型,因此单机开多个 Redis进程的时候会修改端口。
- timeout:设置客户端连接时的超时时间,单位为秒。当客户端在这段时间内没有发出任何指令,那么关闭该连接。默认值为0,表示不关闭。
- tcp-keepalive :单位是秒,表示将周期性的使用SO_KEEPALIVE检测客户端是否还处于健康 状态,避免服务器一直阻塞,官方给出的建议值是300s,如果设置为0,则不会周期性的检测。
GENERAL
具体配置详解:
daemonize:设置为yes表示指定Redis以守护进程的方式启动(后台启动)。默认值为 no
pidfile:配置PID文件路径,当redis作为守护进程运行的时候,它会把 pid 默认写到 /var/redis/run/redis_6379.pid 文件里面
loglevel :定义日志级别。默认值为notice,有如下4种取值:
debug(记录大量日志信息,适用于开发、测试阶段) verbose(较多日志信息) notice(适量日志信息,使用于生产环境) warning(仅有部分重要、关键信息才会被记录)
logfile :配置log文件地址,默认打印在命令行终端的窗口上
databases:设置数据库的数目。默认的数据库是DB 0 ,可以在每个连接上使用select 命令 选择一个不同的数据库,dbid是一个介于0到databases - 1 之间的数值。默认值是 16,也就 是说默认Redis有16个数据库。
SNAPSHOTTING
这里的配置主要用来做持久化操作。
参数:
save:这里是用来配置触发 Redis的持久化条件,也就是什么时候将内存中的数据保存到硬盘
save 900 1:表示900 秒内如果至少有 1 个 key 的值变化,则保存
save 300 10:表示300 秒内如果至少有 10 个 key 的值变化, 则保存
save 60 10000:表示60 秒内如果至少有 10000 个 key 的值变 化,则保存
REPLICATION
参数:
slave-serve-stale-data:默认值为yes。当一个 slave 与 master 失去联系,或者复制正在进行的时候,
slave 可能会有两种表现:
- 如果为 yes ,slave 仍然会应答客户端请求,但返回的数据 可能是过时,或者数据可能是空的在第一次同步的时候
- 如果为 no ,在你执行除了 info he salveof 之外的其他命令时,slave 都将返回一个 “SYNC with master in progress” 的错误
slave-read-only:配置Redis的Slave实例是否接受写操作,即Slave是否为只读Redis。默认 值为yes。
repl-diskless-sync:主从数据复制是否使用无硬盘复制功能。默认值为no。
repl-diskless-sync-delay:当启用无硬盘备份,服务器等待一段时间后才会通过套接字向从 站传送RDB文件,这个等待时间是可配置的。
repl-disable-tcp-nodelay:同步之后是否禁用从站上的TCP_NODELAY 如果你选择yes, redis会使用较少量的TCP包和带宽向从站发送数据。
SECURITY
requirepass:设置redis连接密码。
比如: requirepass 123 表示redis的连接密码为123。
CLIENTS
参数:
maxclients :设置客户端最大并发连接数,默认无限制,Redis 可以同时打开的客户端连接数为Redis进程可以打开的最大文件。 描述符数-32(redis server自身会使用一些),如果设置 maxclients为0 。表示不作限制。当客户端连接数到达限制时, Redis会关闭新的连接并向客户端返回max number of clients reached错误信息
MEMORY MANAGEMENT
参数:
maxmemory:设置Redis的最大内存,如果设置为0 。表示不作限制。通常是配合下面介绍 的maxmemory-policy参数一起使用。
maxmemory-policy :当内存使用达到maxmemory设置的最大值时,redis使用的内存清除 策略。有以下几种可以选择:
- volatile-lru 利用LRU算法移除设置过过期时间的 key (LRU:最近使用 Least Recently Used )
- allkeys-lru 利用LRU算法移除任何key
- volatile-random 移除设置过过期时间的随机key
- allkeys-random 移除随机ke
- volatile-ttl 移除即将过期的key(minor TTL)
- noeviction noeviction 不移除任何key,只是返回 一个写错误 ,默认选项
maxmemory-samples :LRU 和 minimal TTL 算法都不是精准的算法,但是相对精确的算法(为了节省内存)。随意你可以选择样本大小进行检,redis默认选择3个样本进行检测,你可 以通过maxmemory-samples进行设置样本数。
APPEND ONLY MODE
参数:
- appendonly:默认redis使用的是rdb方式持久化,这种方式在许多应用中已经足够用了。但 是redis如果中途宕机,会导致可能有几分钟的数据丢失,根据save来策略进行持久化, Append Only File是另一种持久化方式, 可以提供更好的持久化特性。Redis会把每次写入 的数据在接收后都写入appendonly.aof文件,每次启动时Redis都会先把这个文件的数据读 入内存里,先忽略RDB文件。默认值为no。
- appendfilename :aof文件名,默认是"appendonly.aof"
- appendfsync:aof持久化策略的配置;no表示不执行fsync,由操作系统保证数据同步到磁 盘,速度最快;always表示每次写入都执行fsync,以保证数据同步到磁盘;everysec表示每 秒执行一次fsync,可能会导致丢失这1s数据
LUA SCRIPTING
参数:
lua-time-limit:一个lua脚本执行的最大时间,单位为ms。默 认值为5000.
REDIS CLUSTER
参数:
- cluster-enabled:集群开关,默认是不开启集群模式。
- cluster-config-file:集群配置文件的名称。
- cluster-node-timeout :可以配置值为15000。节点互连超时的阀值,集群节点超时毫秒数
- cluster-slave-validity-factor :可以配置值为10。
七 Redis其他功能
1 发布与订阅
什么是发布与订阅
Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发 送消息,订阅者 (sub) 接收消息。
什么时候用发布订阅
看到发布订阅的特性,用来做一个简单的实时聊天系统再适合不过 了。再比如,在一个博客网站中,有100个粉丝订阅了你,当你发布新文章,就可以推送消息给粉丝们。
Redis的发布与订阅
发布订阅命令行实现
订阅
语法格式:
subcribe 主题名字
示例:
127.0.0.1:6379> subcribe channel-1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel-1"
3) (integer) 1
发布命令
语法格式:
publish channel-1 hello
示例:
打开另一个客户端,给channel1发布消息hello
127.0.0.1:6379> puhlish channel-1 hello
(integer) 1
注意: 返回的1是订阅者数量。
打开第一个客户端可以看到发送的消息
127.0.0.1:6379> subcribe channel-1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel-1"
3) (integer) 1
1) "message"
2) "channel-1"
3) "hello"
注意: 发布的消息没有持久化,如果在订阅的客户端收不到hello,只能收到订阅后发布的消息。
2 慢查询
什么是慢查询
慢查询,顾名思义就是比较慢的查询,但是究竟是哪里慢呢?
Redis命令执行的整个过程
两点说明:
- 慢查询发生在第3阶段
- 客户端超时不一定慢查询,但慢查询是客户端超时的一个可能因素
- 慢查询日志是存放在Redis内存列表中。
什么是慢查询日志
慢查询日志是Redis服务端在命令执行前后计算每条命令的执行时长,当超过某个阈值是记录下来的日志。日志中记录了慢查询发生的时间,还有执行时长、具体什么命令等信息,它可以用来帮助开发和运维人员定位系统中存在的慢查询。
如何获取慢查询日志
可以使用 slowlog get 命令获取慢查询日志,在 slowlog get 后面还可以加一 个数字,用于指定获取慢查询日志的条数,比如,获取3条慢查询日志:
127.0.0.1:6379> SLOWLOG get 3
1) 1) (integer) 0
2) (integer) 1640056567
3) (integer) 11780
4) 1) "FLUSHALL"
5) "127.0.0.1:43406"
6) ""
参数:
- 唯一标识ID
- 命令执行的时间戳
- 命令执行时长
- 执行的命名和参数
如何获取慢查询日志的长度
可以使用 slowlog len 命令获取慢查询日志的长度。
> slowlog len
(integer) 121
注意:当前Redis中有121条慢查询日志。
怎么配置慢查询的参数
-
命令执行时长的指定阈值 slowlog-log-slower-than。
slowlog-log-slower-than的作用是指定命令执行时长的阈值,执行 命令的时长超过这个阈值时就会被记录下来
-
存放慢查询日志的条数 slowlog-max-len。
slowlog-max-len的作用是指定慢查询日志最多存储的条数。实际上,Redis使用了一个列表存放慢查询日志,slowlog-max-len就是 这个列表的最大长度。
如何进行配置
查看慢日志配置
查看redis慢日志配置,登陆redis服务器,使用redis-cli客户端连接 redis server
127.0.0.1:6379> config get slow*
1) "slowlog-max-len"
2) "128"
3) "slowlog-log-slower-than"
4) "10000"
慢日志说明: 10000阈值,单位微秒,此处为10毫秒,128慢日志记录保存数量的阈值,此处保存128条。
修改Redis配置文件
比如,把slowlog-log-slower-than设置为1000,slowlog-max-len 设置为1200:
slowlog-log-slower-than 1000
slowlog-max-len 1200
使用 config set 命令动态修改。
比如,还是把slowlog-log-slower-than设置为1000,slowlog-maxlen设置为1200:
> config set slowlog-log-slower-than 1000
OK
> config set slowlog-max-len 1200
OK
> config rewrite
OK
实践建议
slowlog-max-len配置建议
- 线上建议调大慢查询列表,记录慢查询时Redis会对长命令做截断操作,并不会占用大量内存。
- 增大慢查询列表可以减缓慢查询被剔除的可能,例如线上可设置为1000以上。
slowlog-log-slower-than配置建议
- 默认值超过10毫秒判定为慢查询,需要根据Redis并发量调整该值。
- 由于Redis采用单线程响应命令,对于高流量的场景,如果命令执行时间在1毫秒以上,那么Redis 最多可支撑OPS不到1000。因此对于高OPS场景的Redis建议设置为1毫秒。
3 流水线pipeline
1次网络命令通信模型
经历了1次时间 = 1次网络时间 + 1次命令时间。
批量网络命令通信模型
经历了 n次时间 = n次网络时间 + n次命令时间
什么是流水线?
经历了 1次pipeline(n条命令) = 1次网络时间 + n次命令时间,这大大减少了网络时间的开销,这就是流水线。
案例展示
从北京到上海的一条命令的生命周期有多长?
执行一条命令在redis端可能需要几百微秒,而在网络光纤中传输只 花费了13毫秒。
注意:
在执行批量操作而没有使用pipeline功能,会将大量的时间耗费 在每一次网络传输的过程上;而使用pipeline后,只需要经过一 次网络传输,然后批量在redis端进行命令操作。这会大大提高了效率。
pipeline-Jedis实现
首先,引入jedis依赖包:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
没有pipeline的命令执行
Jedis jedis = new Jedis("127.0.0.1",6379);
for ( int i = 0 ; i < 10000 ; i ++ ){
jedis.hset("hashkey:" + i , "field" + i , "value" + i);
}
注意: 在不使用pipeline的情况下,使用for循环进行每次一条命令的执行操作,耗费的时间可能达到 1w 条插入命令的耗时为50s。
使用pipeline
Jedis jedis = new Jedis("127.0.0.1",6379);
for ( int i = 0; i < 100 ; i++) {
Pipeline pipeline = jedis.ppipelined();
for (int j = i * 100 ; j < (i + 1) * 100 ; j++) {
pipeline.hset("hashkey:" + j,"field" + j, "value" + j);
}
pipeline.syncAndReturnAll();
}
八 Redis数据安全
1 持久化机制概述
由于Redis的数据都存放在内存中,如果没有配置持久化,Redis重 启后数据就全丢失了,于是需要开启Redis的持久化功能,将数据保 存到磁盘上,当Redis重启后,可以从磁盘中恢复数据。
持久化机制概述
对于Redis而言,持久化机制是指把内存中的数据存为硬盘文件, 这样当Redis重启或服务器故障时能根据持久化后的硬盘文件恢复数据。
持久化机制的意义
redis持久化的意义,在于故障恢复。比如部署了一个redis,作为 cache缓存,同时也可以保存一些比较重要的数据。
Redis提供了两个不同形式的持久化方式
- RDB(Redis DataBase)
- AOF(Append Only File)
2 RDB持久化机制实战
RDB是什么
在指定的时间间隔内将内存的数据集快照写入磁盘,也就是行话讲的快照,它恢复时是将快照文件直接读到内存里。
注意: 这种格式是经过压缩的二进制文件。
配置dump.rdb文件
RDB保存的文件,在redis.conf中配置文件名称,默认为 dump.rdb。
439
440 # The filename where to dump the DB
441 dbfilename dump.rdb
442
rdb文件的保存位置,也可以修改。默认在Redis启动时命令行所在的目录下。
dir ./
触发机制-主要三种方式
RDB配置
快照默认配置:
- save 3600 1:表示3600秒内(一小时)如果至少有1个key的值变化,则保存。
- save 300 100:表示300秒内(五分钟)如果至少有100个 key 的值变化,则保存。
- save 60 10000:表示60秒内如果至少有 10000个key的值变化,则保存。
配置新的保存规则
给redis.conf添加新的快照策略,30秒内如果有5次key的变化,则触发快照。
配置修改后,需要重启Redis服务。
save 3600 1
save 300 100
save 60 10000
save 30 5
flushall
执行flushall命令,也会触发rdb规则。
save与bgsave
手动触发Redis进行RDB持久化的命令有两种:
- save 该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,直到RDB过程完成为止,不建议使用。
- bgsave 执行该命令时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。
高级配置
stop-writes-on-bgsave-error
默认值是yes。当Redis无法写入磁盘的话,直接关闭Redis的写操作。
rdbcompression
默认值是yes。对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis会采用LZF算法进行压缩。如果你不想消耗 CPU来进行压缩的话,可以设置为关闭此功能,但是存储在磁盘上的快照会比较大。
rdbchecksum
默认值是yes。在存储快照后,我们还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。
恢复数据
只需要将rdb文件放在Redis的启动目录,Redis启动时会自动加载 dump.rdb并恢复数据。
优势
- 适合大规模的数据恢复
- 对数据完整性和一致性要求不高更适合使用
- 节省磁盘空间
- 恢复速度快
劣势
- 在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。
3 AOF持久化机制实战
AOF是什么
以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来。
AOF默认不开启
可以在redis.conf中配置文件名称,默认为appendonly.aof。
注意:
AOF文件的保存路径,同RDB的路径一致,如果AOF和RDB同时启动,Redis默认读取AOF的数据。
AOF启动/修复/恢复
开启AOF
设置Yes:修改默认的appendonly no,改为yes
appendonly yes
注意:
修改完需要重启redis服务。
设置数据。
set k11 v11
set k12 v12
set k13 v13
set k14 v14
set k15 v15
AOF同步频率设置
参数:
appendfsync always
始终同步,每次Redis的写入都会立刻记入日志,性能较差但数据完整性比较好。
appendfsync everysec
每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。
appendfsync no
redis不主动进行同步,把同步时机交给操作系统。
优势
- 备份机制更稳健,丢失数据概率更低。
- 可读的日志文本,通过操作AOF稳健,可以处理误操作。
劣势
- 比起RDB占用更多的磁盘空间。
- 恢复备份速度要慢。
- 每次读写都同步的话,有一定的性能压力。
4 如何选用持久化方式
不要仅仅使用RDB
RDB数据快照文件,都是每隔5分钟,或者更长时间生成一次,这个时候就得接受一旦redis进程宕机,那么会丢失最近5分钟的数据。
也不要仅仅使用AOF
- 你通过AOF做冷备,没有RDB做冷备,来的恢复速度更快。
- RDB每次简单粗暴生成数据快照,更加健壮,可以避免AOF这种复杂的备份和恢复机制的bug。
综合使用AOF和RDB两种持久化机制
用AOF来保证数据不丢失,作为数据恢复的第一选择,用RDB来做不同程度的冷备,在AOF文件都丢失或损坏不可用的时候,还可以使用RDB来进行快速的数据恢复。
九 Redis事务
1 事务的概念与ACID特性
数据库层面事务
在数据库层面,事务是指一组操作,这些操作要么全都被成功执行,要么全都不执行。
数据库事务的四大特性
- A:Atomic,原子性,将所有SQL作为原子工作单元执行,要么全部执行,要么全部不执行;
- C:Consistent,一致性,事务完成后,所有数据的状态都是一致的,即A账户只要减去了100,B 账户则必定加上了100;
- I:Isolation,隔离性,如果有多个事务并发执行,每个事务作出的修改必须与其他事务隔离;
- D:Duration,持久性,即事务完成后,对数据库数据的修改被持久化存储。
Redis事务
Redis事务是一组命令的集合,一个事务中的所有命令都将被序列化,按照一次性、顺序性、排他性的执行一系列的命令。
Redis事务三大特性
- 单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断;
- 没有隔离级别的概念:队列中的命令没有提交之前都不会实际的被执行,因为事务提交前任何指令都不会被实际执行,也就不存在”事务内的查询要看到事务里的更新,在事务外查询不能看到”。
- 不保证原子性:redis同一个事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚;
Redis事务执行的三个阶段
- 开启:以 MULTI 开始一个事务;
- 入队:将多个命令入队到事务中,接到这些命令并不会立即执行,而是放到等待执行的事务队列里面;
- 执行:由 EXEC 命令触发事务;
2 Redis事务基本操作
Multi、Exec、discard
事务从输入Multi命令开始,输入的命令都会依次压入命令缓冲队列 中,并不会执行,直到输入Exec后,Redis会将之前的命令缓冲队列 中的命令依次执行。组队过程中,可以通过discard来放弃组队。
例子
正常执行
127.0.0.1:6379> set t1 1
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set id 12
QUEUED
127.0.0.1:6379(TX)> get id
QUEUED
127.0.0.1:6379(TX)> incr t1
QUEUED
127.0.0.1:6379(TX)> incr t1
QUEUED
127.0.0.1:6379(TX)> get t1
QUEUED
127.0.0.1:6379(TX)> EXEC
1) OK
2) "12"
3) (integer) 2
4) (integer) 3
5) "3"
放弃事务
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set name z3
QUEUED
127.0.0.1:6379(TX)> set age 29
QUEUED
127.0.0.1:6379(TX)> incr t1
QUEUED
127.0.0.1:6379(TX)> DISCARD
OK
全体连坐
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set name z3
QUEUED
127.0.0.1:6379(TX)> get name
QUEUED
127.0.0.1:6379(TX)> incr t1
QUEUED
127.0.0.1:6379(TX)> get t1
QUEUED
127.0.0.1:6379(TX)> set email
(error) ERR wrong number of arguments for
'set' command
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded
because of previous errors.
注意:
命令集合中含有错误的指令(注意是语法错误),均连坐,全部失败。
冤有头,债有主
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set age 11
QUEUED
127.0.0.1:6379(TX)> incr t1
QUEUED
127.0.0.1:6379(TX)> set email abc@163.com
QUEUED
127.0.0.1:6379(TX)> incr email
QUEUED
127.0.0.1:6379(TX)> get age
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (integer) 5
3) OK
4) (error) ERR value is not an integer or
out of range
5) "11"
注意:
运行时错误,即非语法错误,正确命令都会执行,错误命令返 回错误。
十 Redis主从复制
1 主从复制概述
概述
在现有企业中80%公司大部分使用的是redis单机服务,在实际的场景当中单一节点的redis容易面临风险。
面临问题:
- 机器故障。我们部署到一台 Redis 服务器,当发生机器故障时,需要迁移到另外一台服务器并且要保证数据是同步的。
- 容量瓶颈。当我们有需求需要扩容 Redis 内存时,从 16G 的内存升到 64G,单机肯定是满足 不了。当然,你可以重新买个 128G 的新机器。
解决办法
要实现分布式数据库的更大的存储容量和承受高并发访问量,我们会将原来集中式数据库的数据分别存储到其他多个网络节点上。
注意:
Redis 为了解决这个单一节点的问题,也会把数据复制多个副本部署到其他节点上进行复制,实现 Redis的高可用,实现对数据的冗余备份从而保证数据和服务的高可用。
什么是主从复制
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave),数据的复制是单向的,只能由主节点到从节点。
主从复制的作用
- 数据冗余: 主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
- 故障恢复: 当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
- 负载均衡: 在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务 (即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
- 高可用基石: 除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是 Redis高可用的基础。
2 主从复制环境搭建
编写配置文件
新建redis6379.conf
include /usr/local/redis-6.2.6/redis.conf
pidfile /var/run/redis_6379.pid
port 6379
dbfilename dump6379.rdb
redis6380.conf
include /usr/local/redis-6.2.6/redis.conf
pidfile /var/run/redis_6380.pid
port 6380
dbfilename dump6380.rdb
建redis6381.conf
include /usr/local/redis-6.2.6/redis.conf
pidfile /var/run/redis_6381.pid
port 6381
dbfilename dump6381.rdb
启动三台redis服务器
./redis-server ../redis6379.conf
./redis-server ../redis6380.conf
./redis-server ../redis6381.conf
查看系统进程
[root@localhost src]# ps -ef |grep redis
root 40737 1 0 22:05 ?
00:00:00 ./redis-server *:6379
root 40743 1 0 22:05 ?
00:00:00 ./redis-server *:6380
root 40750 1 0 22:05 ?
00:00:00 ./redis-server *:6381
root 40758 40631 0 22:05 pts/0
00:00:00 grep --color=auto redis
查看三台主机运行情况
#打印主从复制的相关信息
./redis-cli -p 6379
./redis-cli -p 6380
./redis-cli -p 6381
127.0.0.1:6379> info replication
127.0.0.1:6380> info replication
127.0.0.1:6381> info replication
配从库不配主库
语法格式:
slaveof <ip> <port>
示例:
在6380和6381上执行。
127.0.0.1:6380> slaveof 127.0.0.1 6379
OK
127.0.0.1:6381> slaveof 127.0.0.1 6379
OK
在主机上写,在从机上可以读取数据
set k1 v1
3 主从复制原理剖析
主从复制可以分为3个阶段
- 连接建立阶段(即准备阶段)
- 数据同步阶段
- 命令传播阶段
复制过程大致分为6个过程
1、保存主节点(master)信息。
执行 slaveof 后 查看状态信息
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
2、从节点(slave)内部通过每秒运行的定时任务维护复制相关逻辑,当定时任务发现存在新的主节点后,会尝试与该节点建立网络连接
3、从节点与主节点建立网络连接
从节点会建立一个 socket 套接字,从节点建立了一个端口为51234 的套接字,专门用于接受主节点发送的复制命令。
4、发送ping命令
连接建立成功后从节点发送 ping 请求进行首次通信。
作用:
- 检测主从之间网络套接字是否可用。
- 检测主节点当前是否可以接受命令 。
4、权限验证。
如果主节点设置了 requirepass 参数,则需要密码验证,从节点必 须配置 masterauth 参数保证与主节点相同的密码才能通过验证; 如果验证失败复制将终止,从节点重新发起复制流程。
5、同步数据集。
主从复制连接正常通信后,对于首次建立复制的场景,主节点会把持有的数据全部发送给从节点,这部分操作是耗时最长的步骤。
主从同步策略
主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。 redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。
6、命令持续复制。
当主节点把当前的数据同步给从节点后,便完成了复制的建立流程。接下来主节点会持续地把写命令发送给从节点,保证主从数据 一致性。
十一 Redis哨兵机制
1 哨兵监控概述
Redis主从复制缺点
当主机 Master 宕机以后,我们需要人工解决切换。
暴漏问题:一旦主节点宕机,写服务无法使用,就需要手动去切换,重新选取主节点,手动设置主从关系。
主从切换技术
当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。
哨兵概述
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是 一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。
哨兵作用
- 集群监控:负责监控redis master和slave进程是否正常工作
- 消息通知:如果某个redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员
- 故障转移:如果master node挂掉了,会自动转移到slave node上
- 配置中心:如果故障转移发生了,通知client客户端新的master地址
2 哨兵监控环境搭建
新建sentinel-26379.conf文件
#端口
port 26379
#守护进程运行
daemonize yes
#日志文件
logfile "26379.log"
sentinel monitor mymaster 127.0.0.1 6379 2
参数:
sentinel monitor mymaster 192.168.92.128 6379 2 配置的含义是:
该哨兵节点监控192.168.92.128:6379这个主节点,该主节点的名称是mymaster,最后的2的含义与主节点的故障判定有关:至少需要2个哨兵节点同意,才能判定主节点故障并进行故障转移。
新建sentinel-26380.conf文件
#端口
port 26380
#守护进程运行
daemonize yes
#日志文件
logfile "26380.log"
sentinel monitor mymaster 127.0.0.1 6379 2
新建sentinel-26381.conf文件
#端口
port 26381
#守护进程运行
daemonize yes
#日志文件
logfile "26381.log"
sentinel monitor mymaster 127.0.0.1 6379 2
哨兵节点的启动两种方式
redis-sentinel ../sentinel-26379.conf
redis-sentinel ../sentinel-26380.conf
redis-sentinel ../sentinel-26381.conf
查看哨兵节点状态
[root@localhost src]# ./redis-cli -p 26379
127.0.0.1:26379>
127.0.0.1:26379>
127.0.0.1:26379> info sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=192.
168.66.100:6379,slaves=2,sentinels=3
3 哨兵工作原理详解
监控阶段
注意:
sentinel(哨兵1)----->向master(主)和slave(从)发起info,拿到全信息。
sentinel(哨兵2)----->向master(主)发起info,就知道已经存在的sentinel(哨兵1)的信息,并且 连接slave(从)。
sentinel(哨兵2)----->向sentinel(哨兵1)发起subscribe(订阅)。
通知阶段
sentinel不断的向master和slave发起通知,收集信息。
故障转移阶段
通知阶段sentinel发送的通知没得到master的回应,就会把master 标记为SRI_S_DOWN,并且把master的状态发给各个sentinel,其他 sentinel听到master挂了,说我不信,我也去看看,并把结果共享 给各个sentinel,当有一半的sentinel都认为master挂了的时候, 就会把master标记为SRI_0_DOWN。
问题来了: 这时就要把master给换掉了,到底谁当Master呢。
投票方式
方式:
自己最先接到哪个sentinel的竞选通知就会把票投给它。
剔除一些情况:
不在线的
响应慢的
与原来master断开时间久的
优先级原则
4 故障转移
概述
演示当主节点发生故障时,哨兵的监控和自动故障转移功能。
演示故障转移
使用kill命令杀掉主节点
ps aux |grep redis
kill -9 pid
查看哨兵节点信息
如果此时立即在哨兵节点中使用info Sentinel命令查看。
[root@localhost src]# ./redis-cli -p 26379
127.0.0.1:26379> info sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=127.0.0.1:6380,slaves=2,sentinels=3
注意: 哨兵发现主节点故障并转移,需要一段时间。
重启6379节点
[root@localhost src]# ./redis-cli info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6380
master_link_status:up
配置文件都会被改写
故障转移阶段,哨兵和主从节点的配置文件都会被改写
include /usr/local/redis-6.2.6/redis.conf
pidfile /var/run/redis_6379.pid
port 6379
dbfilename dump6379.rdb
# Generated by CONFIG REWRITE
daemonize yes
protected-mode no
appendonly yes
slowlog-max-len 1200
slowlog-log-slower-than 1000
save 5 1
user default on nopass ~* &* +@all
dir "/usr/local/redis"
replicaof 127.0.0.1 6380
结论
- 哨兵系统中的主从节点,与普通的主从节点并没有什么区别,故障发现和转移是由哨兵来控制和完成的。
- 哨兵节点本质上是redis节点。
- 每个哨兵节点,只需要配置监控主节点,便可以自动发现其他的哨兵节点和从节点。
- 在哨兵节点启动和故障转移阶段,各个节点的配置文件会被重写(config rewrite)。
十二 Redis集群
1 Cluster模式概述
Redis有三种集群模式
- 主从模式
- Sentinel模式
- Cluster模式
哨兵模式的缺点
缺点:
- 当master挂掉的时候,sentinel 会选举出来一个 master,选举的时候是没有办法去访问 Redis的,会存在访问瞬断的情况;
- 哨兵模式,对外只有master节点可以写,slave节点只能用于读。尽管Redis单节点最多支持 10W的QPS,但是在电商大促的时候,写数据的压力全部在master上。
- Redis的单节点内存不能设置过大,若数据过大在主从同步将会很慢;在节点启动的时候,时间特别长;
Cluster模式概述
Redis集群是一个由多个主从节点群组成的分布式服务集群,它具有复制、高可用和分片特性。
Redis集群的优点
- Redis集群有多个master,可以减小访问瞬断问题的影响
- Redis集群有多个master,可以提供更高的并发量
- Redis集群可以分片存储,这样就可以存储更多的数据
2 Cluster模式搭建
Redis的集群搭建最少需要3个master节点,我们这里搭建3个 master,每个下面挂一个slave节点,总共6个Redis节点;
环境准备
第1台机器: 192.168.66.108 8001端口 8002端口
第2台机器: 192.168.66.109 8001端口 8002端口
第3台机器: 192.168.66.110 8001端口 8002端口
创建文件夹
mkdir -p /usr/local/redis/redis-cluster/8001 /usr/local/redis/redis-cluster/8002
拷贝配置文件
将redis安装目录下的 redis.conf 文件分别拷贝到8001目录下
cp /usr/local/redis/redis.conf /usr/local/redis/redis-cluster/8001
修改redis.conf文件以下内容
/usr/local/redis/redis-cluster/8001/redis.conf
port 8001
daemonize yes
pidfile "/var/run/redis_8001.pid"
#指定数据文件存放位置,必须要指定不同的目录位置,不然
会丢失数据
dir /usr/local/redis/redis-cluster/8001/
#启动集群模式
cluster-enabled yes
#集群节点信息文件,这里800x最好和port对应上
cluster-config-file nodes-8001.conf
# 节点离线的超时时间
cluster-node-timeout 5000
#去掉bind绑定访问ip信息
#bind 127.0.0.1
#关闭保护模式
protected-mode no
#启动AOF文件
appendonly yes
#如果要设置密码需要增加如下配置:
#设置redis访问密码
#requirepass baizhan
#设置集群节点间访问密码,跟上面一致
#masterauth baizhan
文件拷贝到8002文件夹
#将8001修改为8002:
cp /usr/local/redis/redis-cluster/8001/redis.conf /usr/local/redis/redis-cluster/8002
# 批量修改字符串
:%s/8001/8002/g
将本机机器上的文件拷贝到另外两台机器上
# 第二台机器
scp /usr/local/redis/redis-cluster/8001/redis.conf root@192.168.66.109:/usr/local/redis/redis-cluster/8001/
scp /usr/local/redis/redis-cluster/8002/redis.conf root@192.168.66.109:/usr/local/redis/redis-cluster/8002/
# 第三台机器
scp /usr/local/redis/redis-cluster/8001/redis.conf root@192.168.66.110:/usr/local/redis/redis-cluster/8001/
scp /usr/local/redis/redis-cluster/8002/redis.conf root@192.168.66.110:/usr/local/redis/redis-cluster/8002/
分别启动这6个redis实例
/usr/local/redis/src/redis-server /usr/local/redis/redis-cluster/8001/redis.conf
/usr/local/redis/src/redis-server /usr/local/redis/redis-cluster/8002/redis.conf
检查是否启动成功
ps -ef |grep redis
使用redis-cli创建整个redis集群
/usr/local/redis/src/redis-cli --cluster create --cluster-replicas 1 \
192.168.66.108:8001 192.168.66.108:8002 \
192.168.66.109:8001 192.168.66.109:8002 \
192.168.66.110:8001 192.168.66.110:8002 \
参数:
-a:密码
–cluster-replicas 1:表示1个master下挂1个slave; --cluster-replicas 2:表示1个master 下挂2个slave。
注意:记得关闭防火墙
查看帮助命令
redis-cli --cluster help
参数:
- create:创建一个集群环境host1:port1 … hostN:portN
- call:可以执行redis命令
- add-node:将一个节点添加到集群里,第一个参数为新节点的ip:port,第二个参数为集群中任意一个已经存在的节点的ip:port
- del-node:移除一个节点
- reshard:重新分片
- check:检查集群状态
验证集群
连接任意一个客户端
/usr/local/redis/src/redis-cli -a redis-pw -c -h 192.168.66.108 -p 8001
参数:
- ‐a表示服务端密码
- ‐c表示集群模式
- -h指定ip地址
- -p表示端口号
查看集群的信息
cluster info
3 Cluster模式原理分析
Redis Cluster将所有数据划分为16384个slots(槽位),每个节点负 责其中一部分槽位。槽位的信息存储于每个节点中。只有master节点会被分配槽位,slave节点不会分配槽位。
槽位定位算法: k1 = 127001
Cluster 默认会对 key 值使用 crc16 算法进行 hash 得到一个整 数值,然后用这个整数值对 16384 进行取模来得到具体槽位。
HASH_SLOT = CRC16(key) % 16384
命令执行
set k1 v1
注意: 根据k1计算出的槽值进行切换节点,并存入数据。不在一个slot 下的键值,是不能使用mget、mset等多建操
可以通过{}来定义组的概念,从而是key中{}内相同内容的键值对放 到同一个slot中。
mset k1{test} v1 k2{test} v2 k3{test} v3
故障恢复
查看节点
192.168.66.108:8001> cluster nodes
杀死Master节点
lsof -i:8001
kill -9 pid
观察节点信息
4 Java操作Redis集群
SpringBoot 整合 Redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring‐boot‐starter‐data‐redis</artifactId>
</dependency>
配置文件
##单服务器
spring.redis.cluster.nodes=192.168.159.129:7
001,192.168.159.129:7002,192.168.159.129:700
3,192.168.159.129:7004,192.168.159.129:7005,
192.168.159.129:7006
## 连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=300
## Redis数据库索引(默认为0)
spring.redis.database=0
## 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
## 连接池中的最大空闲连接
spring.redis.pool.max-idle=100
## 连接池中的最小空闲连接
spring.redis.pool.min-idle=20
## 连接超时时间(毫秒)
spring.redis.timeout=60000
java代码
@RestController
public class TestController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/test_cluster")
public void testCluster() throws
InterruptedException {
stringRedisTemplate.opsForValue().set("user:name", "baizhan");
System.out.println(stringRedisTemplate.opsForValue().get("user:name"));
}
}
十三 Redis企业级解决方案
1 Redis脑裂
什么是Redis的集群脑裂
Redis的集群脑裂是指因为网络问题,导致Redis Master节点跟 Redis slave节点和Sentinel集群处于不同的网络分区,此时因为 sentinel集群无法感知到master的存在,所以将slave节点提升为 master节点。
注意:
此时存在两个不同的master节点,就像一个大脑分裂成了两 个。集群脑裂问题中,如果客户端还在基于原来的master节点继续写入数据,那么新的Master节点将无法同步这些数据,当网络问题解决之后,sentinel集群将原先的Master节点降为 slave节点,此时再从新的master中同步数据,将会造成大量的数据丢失。
解决方案
redis.conf配置参数:
min-replicas-to-write 1
min-replicas-max-lag 5
参数:
第一个参数表示最少的slave节点为1个
第二个参数表示数据复制和同步的延迟不能超过5秒
配置了这两个参数:如果发生脑裂原Master会在客户端写入操作的时候拒绝请求。这样可以避免大量数据丢失。
2 缓存预热
缓存冷启动
缓存中没有数据,由于缓存冷启动一点数据都没有,如果直接就对外提供服务了,那么并发量上来Mysql就裸奔挂掉了。
缓存冷启动场景
新启动的系统没有任何缓存数据,在缓存重建数据的过程中,系统性能和数据库负载都不太好,所以最好是在系统上线之前就把要缓存的热点数据加载到缓存中,这种缓存预加载手段就是缓存预热。
解决思路
- 提前给redis中灌入部分数据,再提供服务
- 如果数据量非常大,就不可能将所有数据都写入redis,因为数据量太大了,第一是因为耗费的时间太长了,第二根本redis容纳不下所有的数据
- 需要根据当天的具体访问情况,实时统计出访问频率较高的热数据
- 然后将访问频率较高的热数据写入redis中,肯定是热数据也比较多,我们也得多个服务并行读取数据去写,并行的分布式的缓存预热
3 缓存穿透
概念
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请 求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
解释:
缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。
解决方案
- 对空值缓存:如果一个查询返回的数据为空(不管数据是否存在),我们仍然把这个空结果缓存, 设置空结果的过期时间会很短,最长不超过5分钟。
- 布隆过滤器:如果想判断一个元素是不是在一个集合里,一般想到的是将集合中所有元素保存起来,然后通过比较确定。
布隆过滤器
什么是布隆过滤器
布隆过滤器是一种数据结构,比较巧妙的概率型数据结构 (probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。
注意: 布隆说不存在一定不存在,布隆说存在你要小心了,它有可能不存在。
代码实现
引入hutool包
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.17</version>
</dependency>
java代码实现
// 初始化 注意 构造方法的参数大小10 决定了布隆过滤器BitMap的大小
BitMapBloomFilter filter = new BitMapBloomFilter(10);
filter.add("123");
filter.add("abc");
filter.add("ddd");
boolean abc = filter.contains("abc");
System.out.println(abc);
4 缓存击穿
概念
某一个热点 key,在缓存过期的一瞬间,同时有大量的请求打进来,由于此时缓存过期了,所以请求最终都会走到数据库,造成瞬时数据库请求量大、压力骤增,甚至可能打垮数据库。
解决方案
- 互斥锁:在并发的多个请求中,只有第一个请求线程能拿到锁并执行数据库查询操作,其他的线程拿不到锁就阻塞等着,等到第一个线程将数据写入缓存后,其他线程直接查询缓存。
- 热点数据不过期:直接将缓存设置为不过期,然后由定时任务去异步加载数据,更新缓存。
代码实现
public String get(String key) throws InterruptedException {
String value = jedis.get(key);
// 缓存过期
if (value == null){
// 设置3分钟超时,防止删除操作失败的时候 下一次缓存不能load db
Long setnx = jedis.setnx(key +"mutex", "1");
jedis.pexpire(key + "mutex", 3 * 60);
// 代表设置成功
if (setnx == 1){
// 模拟数据库查询
value = db.get(key);
//保存缓存
jedis.setex(key,3*60,value);
jedis.del(key + "mutex");
return value;
}else {
// 这个时候代表同时操作的其他线程已经load db并设置缓存了。 需要重新获取缓存
Thread.sleep(50);
// 重试
return get(key);
}
}else {
return value;
}
}
5 缓存雪崩
概念
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
缓存正常从Redis中获取,示意图如下:
缓存失效瞬间示意图如下:
解决方案
- 过期时间打散:既然是大量缓存集中失效,那最容易想到就是让他们不集中生效。可以给缓存的过期时间时加上一个随机值时间,使得每个 key 的过期时间分布开来,不会集中在同一时刻失效。
- 热点数据不过期:该方式和缓存击穿一样,也是要着重考虑刷新的时间间隔和数据异常如何处理的情况。
- 加互斥锁: 该方式和缓存击穿一样,按 key 维度加锁,对于同一个 key,只允许一个线程去计算, 其他线程原地阻塞等待第一个线程的计算结果,然后直接走缓存即可。
加锁排队代码如下:
public Object GetProductListNew(String cacheKey) {
int cacheTime = 30;
String lockKey = cacheKey;
// 获取key的缓存
String cacheValue = jedis.get(cacheKey);
// 缓存未失效返回缓存
if (cacheValue != null) {
return cacheValue;
} else {
// 枷锁
synchronized(lockKey) {
// 获取key的value值
cacheValue = jedis.get(cacheKey);
if (cacheValue != null) {
return cacheValue;
} else {
//这里一般是sql查询数据
// db.set(key)
// 添加缓存
jedis.set(cacheKey,"");
}
}
return cacheValue;
}
}
注意:
加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。
6 Redis开发规范
key设计技巧
- 把表名转换为key前缀,如 tag:
- 把第二段放置用于区分key的字段,对应msyql中主键的列名,如 user_id
- 第三段放置主键值,如 2,3,4
- 第四段写存储的列名
user_id | name | age |
---|---|---|
1 | baizhan | 18 |
2 | itbaizhan | 20 |
示例
# 表名 主键 主键值 存储列名字
set user:user_id:1:name baizhan
set user:user_id:1:age 20
#查询这个用户
keys user:user_id:9*
value设计
拒绝bigkey
防止网卡流量、慢查询,string类型控制在10KB以内,hash、 list、set、zset元素个数不要超过5000。
命令使用
1、禁用命令
禁止线上使用keys、flushall、flushdb等,通过redis的rename机 制禁掉命令,或者使用scan的方式渐进式处理。
2、合理使用select
redis的多数据库较弱,使用数字进行区分,很多客户端支持较差, 同时多业务用多数据库实际还是单线程处理,会有干扰。
3、使用批量操作提高效率
- 原生命令:例如mget、mset。
- 非原生命令:可以使用pipeline提高效率。
注意:
但要注意控制一次批量操作的元素个数(例如500以内,实际也和元素字节数有关)。
4、不建议过多使用Redis事务功能
Redis的事务功能较弱(不支持回滚),而且集群版本(自研和官方)要 求一次事务操作的key必须在一个slot上。
客户端使用
- Jedis :https://github.com/xetorthio/jedis 重点推荐
- Spring Data redis :https://github.com/spring-projects/spring-data-redis 使用Spring框架时推荐
- Redisson :https://github.com/mrniko/redisson 分布式锁、阻塞队列的时重点推荐
1、避免多个应用使用一个Redis实例
不相干的业务拆分,公共数据做服务化。
2、使用连接池
可以有效控制连接,同时提高效率,标准使用方式:
//执行命令如下:
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//具体的命令
jedis.executeCommand()
} catch (Exception e) {
logger.error("op key {} error: " + e.getMessage(), key, e);
} finally {
//注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。
if (jedis != null) {
jedis.close();
}
}
7 数据一致性
缓存已经在项目中被广泛使用,在读取缓存方面,大家没啥疑问, 都是按照下图的流程来进行业务操作。
缓存说明: 从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。
三种更新策略
-
先更新数据库,再更新缓存
-
先删除缓存,再更新数据库
-
先更新数据库,再删除缓存
先更新数据库,再更新缓存
这套方案,大家是普遍反对的。为什么呢?
线程安全角度
同时有请求A和请求B进行更新操作,那么会出现:
(1)线程A更新了数据库 (2)线程B更新了数据库 (3)线程B更新了缓存 (4)线程A更新了缓存
这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据,因此不考虑。
先删缓存,再更新数据库
该方案会导致不一致的原因是。同时有一个请求A进行更新操作,另 一个请求B进行查询操作。那么会出现如下情形:
(1)请求A进行写操作,删除缓存 (2)请求B查询发现缓存不存在 (3)请求B去数据库查询得到旧值 (4)请求B将旧值写入缓存 (5)请求A将新值写入数据库
注意: 该数据永远都是脏数据。
先更新数据库,再延时删缓存
这种情况存在并发问题吗?
(1)缓存刚好失效 (2)请求A查询数据库,得一个旧值 (3)请求B将新值写入数据库 (4)请求B删除缓存 (5)请求A将查到的旧值写入缓存
发生这种情况的概率又有多少?
发生上述情况有一个先天性条件,就是步骤(3)的写数据库操作比步骤(2)的读数据库操作耗时更短,才有可能使得步骤 (4)先于步骤(5)。可是,大家想想,数据库的读操作的速 度远快于写操作的,因此步骤(3)耗时比步骤(2)更短,这 一情形很难出现。