Redis学习笔记1


1. Redis简介

B站视频 : https://www.bilibili.com/video/BV1Cb411j7RA?spm_id_from=333.999.0.0

Redis官网

Redis中文网

https://redis.com.cn/

http://www.redis.cn/

https://www.redis.net.cn/

1.1 NoSQL

NoSQL, 泛指非关系型数据库, NoSQL即Not-only SQL, 它可以作为关系型数据库的良好补充, 随着互联网web2.0网站的兴起, 非关系型数据库现在成了一个及其热门的新领域, 非关系型数据库产品的发展非常迅速.

而传统的关系型数据库在应付web2.0网站, 特别是超大规模和高并发的SNS类型的web2.0纯动态网站已经显得力不从心, 暴露了很多难以克服的问题. 比如:

1, High performance - 对数据库高并发读写的需求

web2.0网站要根据用户个性化信息来实时生成动态页面和提供动态信息, 所以基本上无法使用动态页面静态化技术, 因此数据库并发负载非常高, 往往要达到每秒上万次读写请求. 关系型数据库应付上万次SQL查询还勉强能够顶住, 但是应对上万次SQL写数据请求, 硬盘IO已经无法承受了. 其实对于普通的BBS网站, 往往也存在对高并发写请求的需求, 例如网站的实时统计在线用户状态, 记录热门贴子的点击次数, 投票统计等, 因此这是一个相当普遍的需求.

2, Huge Storage - 对海量数据的高效存储和访问的需求

类似Facebook, twitter, Friendfeed这样的SNS网站, 每天用户产生海量的用户动态, 以Friendfeed为例, 一个月就达到了2.5亿条用户动态, 对于关系型数据来说, 在一张2.5亿条记录的表里面进行SQL查询, 效率是及其低下乃至不可忍受的. 再例如大型web网站的用户登录系统, 例如腾讯 数以亿计的账号, 关系型数据库也很难应付.

3, High Scalability && High Availability- 对数据库的高可扩展性和高可用性的需求

在基于web的架构中,数据库是最难进行横向扩展的, 当一个应用系统的用户量和访问量与日俱增时, 你的数据库却没办法像web Server和app Server那样简单的通过添加更多的硬件和服务器节点来扩展性能和负载能力. 对于很多需要提供24小时不断服务的网站来说, 对数据库系统进行升级和扩展是非常痛苦的事情, 往往需要停机维护和数据迁移, 为什么数据库不能通过不断的添加服务器节点来实现扩展呢?

NoSQL数据库的产生就是为了解决大规模数据集合多重数据种类带来的挑战, 尤其是大数据应用难题.

1.2 NoSQL的类别

键值(Key-Value)存储数据库

这一类数据库主要会使用到哈希表, 表中有一个特定的键和一个指向特定的数据.

key-value模型对于IT系统来说, 优势在于简单, 已部署. 但是如果DBA只对部分值进行查询或更新的时候, key-value就显得效率低下的.

相关产品: Tokyo Cabinet , Tyrant, Redis, Voldemort, Berkeley DB

典型应用: 内容缓存, 主要用于处理大量数据的高并发访问负载.

数据模型: 一系列键值对

优势: 快速查询

劣势: 存储的数据缺少结构化

列存储数据库

这部分数据库通常是用来应对分布式存储的海量数据, 键依然存在, 但是它们的特定是指向了多个列, 这些列是由列家族来安排的.

相关产品: Cassandra, HBase, Riak

典型应用: 分布式的文件系统

数据模型: 以列簇式存储, 将同一列数据存在一起

优势: 查找速度快, 可扩展性强, 更容易进行分布式扩展.

劣势: 功能相对局限

文档型数据库

文档型数据库的灵感来自于Lotus Notes办公软件的, 而且它是同第一种键值存储相类似, 该类型的数据库模型是版本化的文档, 半结构化的文档以特定的格式存储, 比如Json, 文档型数据库可以看作是键值数据库的升级版, 允许之间嵌套键值. 而且文档型数据库比键值数据库的查询效率更高.

相关产品: CouchDB, MongDB

典型应用: Web应用(与Key-Value类似, Value是结构化的)

数据模型: 一系列键值对

优势: 数据结构要求不严格

劣势: 查询性能不高, 而且缺乏统一的查询语法.

图形(Graph)数据库

图形结构的数据库同其他行列以及刚性结构的SQL数据库不同, 它是使用灵活的图形模型, 并且能够扩展到多台服务器上. NoSQL数据库没有标准的查询语言(SQL), 因此进行数据库查询需要定制数据模型, 许多NoSQL数据库都有REST式的数据接口或者查询API.

相关数据库产品: Neo4J, InfoGrid, Infinite Graph

典型应用 : 社交网络

数据模型: 图结构

优势: 利用图结构相关算法.

劣势: 需要对整个图做计算才能得出结果, 不容易做分布式的集群方案.

总结

因此, 我们总结NoSQL数据库在以下的几种情况下比较适用:

  • 数据模型比较简单
  • 需要灵活性更强的系统
  • 对数据库性能要求较高
  • 不需要高度的数据一致性
  • 对于给定key, 比较容易映射复杂值的环境

1.3 Redis简介

什么是Redis

Redis是完全开源免费的, 遵守BSD协议, 是一个高性能(NOSQL)的key-value数据库, Redis是一个开源的使用ANSI C语言编写, 支持网络, 可基于内存亦可持久化的日志型, key-value数据库, 并提供多种语言的API.

BSD是”Berkeley Software Distribution”的缩写, 意思是”伯克利软件发行版”.

BSD开源协议是一个给与使用者很大自由的协议, 可以自由的使用, 修改源代码, 也可以将修改后的代码作为开源或者专有软件再发布, BSD代码鼓励代码共享, 但需要尊重代码作者的著作权.

BSD 由于允许使用者修改和重新发布代码, 也允许使用或在BSD代码上开发商业软件发布和销售, 因此是对商业集成很友好的协议.

1.4 Redis历史

2008年, 意大利的一家创业公司Merzia推出了一款基于MySql的网络实时统计系统, 然而没过多久该公司的创始人便对MySQL的性能感到失望, 于是他亲自定做了一个数据库, 并于2009年开发完成, 这个数据库就是Redis. 不过他并不满足只将Redis用于当前产品, 而是希望更多的人使用它, 于是在同一年将Redis开源发布, 并开始和Redis的另一个主要的代码贡献者一起继续Redis的迭代, 直到今天.

VMWare公司从2010开始赞助Redis的开发.

1.5 Redis的应用场景

企业级开发中:

可以用作数据库, 缓存, 热点数据(经常会被查询, 但是不经常被修改和删除的数据).

Redis常用的场景:

  • 缓存

    缓存现在几乎是所有中大型网站都在用的必杀技, 合理的利用缓存不仅能够提升网站访问速度, 还能大大降低数据库的压力. Redis提供了键过期功能, 也提供了灵活的键淘汰策略, 所以, 现在Redis用在缓存的场合非常多.

  • 排行榜

    很多网站都有排行榜应用, 如京东的月度销量榜单等. Redis提供的有序集合数据类型结构能够实现各种复杂的排行榜应用.

  • 计数器

    统计电商网站商品的浏览量, 视频网站的播放量等, 为了保证数据实时性, 每次浏览都要+1, 并发高时如果每次都请求数据库操作是很大压力的, Redis提供的incr命令来实现计数器功能, 内存操作, 性能非常好, 非常适用于这些计数场景.

  • 分布式会话

    集群模式下, 在应用不多的情况下一般使用容器自带的session复制功能就能满足, 当应用增多相对复杂的系统中, 一般都会搭建以Redis等内存数据库为中心的session服务, session不再由容器管理, 而是由session服务以及内存数据库管理.

  • 分布式锁

    在很多互联网公司中都使用了分布式技术, 分布式技术带来的技术挑战是对同一个资源的并发访问. 如: 全局ID, 扣减库存, 秒杀等场景. 并发量不大的场景可以使用数据库的悲观锁, 但在并发量高的场合中, 利用数据库锁来控制资源的并发访问是不太理想的, 大大影响了数据库的性能. 可以利用Redis的setnx功能来编写分布式的锁, 如果设置返回1说明获取锁成功, 否则获取锁失败, 实际应用中要考虑的细节要更多.

  • 社交网络

    点赞, 关注, 共同好友等是社交网络的基本功能, 社交网络的访问量通常来说是比较大, 而且传统的关系数据库类型不适合存储这种类型的数据, Redis提供的哈希, 集合等数据结构能很方便的实现这些功能.

  • 最新列表

    Redis列表结构, lpush可以在列表头部插入一个内容ID作为关键字, ltrim可以用来限制列表的数量, 这样列表永远为N个ID, 无需查询最新的列表, 直接根据ID去到对于的内容页即可.

  • 消息系统

    消息队列是大型网站必用的中间件, 如 ActiveMQ, RbbitMQ, Kafka等流行的消息队列中间件, 主要用于业务解决流量削峰及异步处理实时性低的业务. Redis提供了发布/订阅及阻塞队列的功能, 能实现一个简单的消息队列系统. 另外, 这个不能和专业的消息中间件相比.

1.6 Redis总结

1.6.1 Redis优势

  • 性能极高: Redis读速度是110000次/s, 写的速度是81000次/s.
  • 丰富的数据类型: Redis支持的类型String, Hash, List, Set及Ordered Set 数据类型操作
  • 原子性: Redis所有操作都是原子性的, 意思就是要么成功执行要么失败完全不执行, 单个操作是原子性的. 多个操作也支持事务, 即原子性. 通过multi和exec指令包起来.
  • 丰富的特性: Redis还支持publish/subscribe, 通知, key过期等特性.
  • 高速读写: Redis使用自己实现的分离器, 代码量很短, 没有使用lock(MySql), 因此效率很高.

1.6.2 Redis缺点

  • 持久化: Redis直接将数据存储到内存中, 要将数据保存到磁盘上, Redis可以使用两种方式实现持久化过程.

    第一种方式是定时快照(snapshot) , 每隔一段时间将整个数据库写到磁盘上, 每次均是写全部数据, 代价非常高.

    第二种方式是基于语句追加(aof), 只追踪变化的数据, 但是追加的log可能过大, 同时所有的操作均重新执行一遍, 恢复速度慢.

  • 耗内存, 占用内存过高.

2. Redis安装

Windows系统安装Redis:https://blog.csdn.net/qq_26373925/article/details/109269459

Linux系统安装Redis: https://www.cnblogs.com/xsge/p/13841875.html

3. Redis默认方式启动

3.1 启动Redis服务端

进入对应的安装目录

[root@centos7-01 ~]# cd /usr/local/redis

执行命令

[root@centos7-01 bin]# ./bin/redis-server

请添加图片描述

3.2 启动Redis客户端

进入Redis客户端(clone 一个会话窗口), 进入对应的安装目录

[root@centos7-01 ~]# cd /usr/local/redis

执行命令

[root@centos7-01 bin]# ./bin/redis-cli

在这里插入图片描述
启动Redis客户端命令语法

[root@centos7-01 bin]# redis-cli -h host地址 -p port端口 -a password密码 // 默认本机IP, 端口6379

退出客户端命令: ctrl + c 或 quit

检测服务端是否成功启动

启动redis客户端, 打开终端并输入命令 redis-cli, 该命令会连接本地的redis-server。

[root@centos7-01 bin]# ./redis-cli
127.0.0.1:6379> ping
PONG

以上实例中我们连接到本地的Redis服务, 并执行ping命令, 该命令用于检测redis服务是否启动。

4. Redis配置

Redis默认定义了很多默认配置, 但在实际开发中, 我们都会通过手动配置完成. 回到安装目录下找到解压文件中的redis.conf.

Redis的配置文件位于Redis安装目录下, 文件名为redis.conf.

4.1 配置Redis

命令: 解压目录下的redis.conf配置文件复制到安装目录下:

cp /mnt/resources/redis-6.2.6/redis.conf /usr/local/redis-6.x/

4.2 redis.conf

4.2.1 前10个

  • 当Redis默认不是以守护进程的方式运行, 可以通过该配置项修改, 使用yes启用守护进程.

    daemonize no

  • 当Redis以守护进程方式运行时, Redis默认会把pid写入/var/run/redis.pid文件, 可以通过pidfile指定;

    pidfile /var/run/redis.pid

  • 指定Redis监听端口, 默认端口为6379.

    port 6379

  • 绑定的主机地址

    bind 127.0.0.1 -::1

  • 当客户端闲置多长时间后关闭连接, 如果指定为0, 表示关闭该功能

    timeout 300

  • 指定日志级别, Redis总共支持四个级别: debug, verbose, notice, warning, 默认为verbose .

    loglevel notice

  • 日志记录方式, 默认为标准输出, 如果配置Redis为守护进程方式运行, 而这里又配置为记录方式为标准日志, 将会发送给 /dev/null

    logfile stdout

  • 设置数据库的数量, 默认数据库为0, 可以使用SELECT <dbid> 命令在连接上指定数据库id (0~15).

    databases 16

  • 指定存储至本地数据库时是否压缩数据, 默认为yes, Redis采用LZF(压缩算法)压缩, 如果为了节省cpu时间, 可以关闭该选项, 但会导致数据库文件变的巨大.

    rdbcompression yes

  • 指定在多长时间内, 有多少次更新操作, 就将数据同步到数据文件, 可以多个条件配合

    save <seconds> <changes> Redis默认提供了三个条件:

    save 900 1

    save 300 10

    save 60 10000

    分别表示900s(15min)内有1个更改, 300s(5min)内有10个更改, 60s(1min)内有10000个更改.

3.2.2 中间10个

  • 指定本地数据库文件名, 默认值为dump.rdb

dbfilename dump.rdb

  • 指定本地数据库存放目录

    dir ./

  • 设置当本机为slav服务时, 设置master服务的IP地址及端口, 在Redis启动时, 它会自动从master进行数据同步.

    slaveof <masterip> <masterport>

  • 当Redis服务设置了密码保护时, slav服务连接master的密码

    masterauth <master-password>

  • 设置Redis连接密码, 如果配置了连接密码, 客户端在连接Redis时需要通过 AUTH <password>命令提供密码, 默认关闭.

    requirepass foobared

  • 设置同一时间最大客户端连接数, 默认无限制, Redis可以同时打开的客户端连接数为Redis进程可以打开的最大文件描述符数, 如果设置 maxclients 0, 表示不作限制. 当客户端连接数达到限制时, Redis会关闭新的连接并向客户端返回max number of clients reached 错误信息.

    maxclients 10000

  • 指定Redis最大内存限制, Redis在启动时会把数据加载到内存中, 达到最大内存后, Redis会先尝试清除已到期或即将到期的key, 当此方法处理后, 仍然达到最大内存设置, 将无法再进行写入操作, 但仍然可以进行读操作. Redis新的vm机制, 会把key存放到内存, value存放到swap区.

    maxmemory <bytes>

  • 指定是否在每次更新操作后进行日志记录, Redis在默认情况下是异步的把数据写入磁盘的, 如果不开启, 可能会在断电时导致一段时间内的数据丢失. 因为redis本身同步数据文件是按上面save条件来同步的, 所以有的数据会在一段时间内只存在于内存中, 默认为no.

    appendonly no

  • 指定更新日志文件名, 默认为appendonly.aof

    appendfilename "appendonly.aof"

  • 指定更新日志条件, 共有三个可选值:

    no: 表示等操作系统进行数据缓存同步到磁盘(快);

    always: 表示每次更新操作后手动调用fsync()将数据写到磁盘(慢, 安全);

    everysec: 表示每秒同步一次(折中, 默认值).

    appendfsync everysec

3.2.3 结尾10个

  • 指定是否启用虚拟内存机制, 默认为no, 简单的介绍, VM机制将数据分页存放, 由Redis将访问量较少的页, 即冷数据swap到磁盘上, 访问多的页由磁盘自动切换出到内存中.(后面会仔细分析Redis的VM机制).

    vm-enabled no

  • 虚拟内存文件路径, 默认值 /tmp/redis.swap, 不可多个Redis实例共享

    vm-swap-file /tmp/redis.swap

  • 将所有大于vm-max-memory的数据存入虚拟内存, 无论vm-max-memory设置多小, 所有索引数据都是内存存储的(Redis的索引数据就是keys), 也就是说, 当vm-max-memory设置为0时, 所有value值都存在磁盘, 默认值0.

    vm-max-memory 0

  • Redis swap文件分成了很多的page, 一个对象可以保存在多个page上, 但一个page上不能被多个对象共享, vm-page-size是要根据存储的数据大小来设定的, 建议如果存储很多小对象, page大小最好设置为32或64bytes, 如果存储很多大对象, 则可以使用更大的page, 如果不确定, 就使用默认值.

    vm-page-size 32

  • 设置swap文件中的page数量, 由于页表(一种表示页面空闲或使用的bitmap)是放在内存中的, 在磁盘上每8个page将消耗1bytes的内存.

    vm-pages 134217728

  • 设置访问swap文件的线程数, 最好不要超过机器的核数, 如果设置为0, 那么所有对swap文件的操作都是串行的, 可能会造成比较长时间的延迟, 默认值为4.

    vm-max-threads 4

  • 设置在向客户端应答时, 是否把较小的包含并为一个包发送, 默认为开启.

    glueoutputbut yes

  • 指定在超过一定的数量或者最大的元素超过某一临界值时, 采用一种特殊的哈希算法.

    hash-max-ziplist-entries 512
    hash-max-ziplist-value 64

  • 指定是否激活重置哈希, 默认为开启.

    activerehashing yes

  • 指定包含其他的配置文件, 可以在同一主机上多个Redis实例之间使用一份配置文件, 而同时各个实例又拥有自己的特定配置文件.

    # include /path/to/local.conf

4.3 Redis中的内存维护策略

redis作为优秀的中间缓存件, 经常会存储大量的数据, 即使采取了集群部署来动态扩容, 也应该及时的整理内存, 维持系统性能.

在Redis中有两种解决方案.

第一种: 为数据设置超时时间

expire key seconds # 以秒为单位
# setex(String key, int seconds, String value)  字符串独有的方式
setex key seconds value
ttl key # 查看key的过期时间
  • 除了字符串自己独有设置过期时间的方法外, 其他方法都需要依靠expire方法来设置过期时间.
  • 如果没有设置过期时间, 那缓存就是永不过期.
  • 如果设置了过期时间, 之后又想让缓存永不过期, 使用persist key.

第二种: 采用LRU算法动态将不用的数据删除

内存管理的一种页面置换算法, 对应在内存中但又不同的数据块(内存块), 称为LRU. 操作系统会根据哪些数据属于LRU而将其移出内存来腾出空间加载另外的热点数据.

内存淘汰策略

  • volatile-lru

    设定超时时间的数据中, 删除最不常用的数据.

  • allkeys-lru

    查询所有的key中, 最近最不常用的数据进行删除, 这是应用最广泛的策略.

  • volatile-random

    在已经设定了超时的数据中随机删除.

  • allkeys-random

    查询所有的key, 之后随机删除.

  • noeviction

    如果设置为该属性, 则不会进行删除操作, 如果内存溢出, 则报错返回.

  • volatile-lfu

    从所有配置了过期时间的键中驱逐使用频率最小的键. 4.x版本后新增的策略

  • allkeys-lfu

    从所有键中驱逐使用频率最少的键. 4.x版本后新增的策略

4.4 自定义配置Redis

进入redis安装目录 /usr/local/redis-6.x, 修改redis.conf配置文件, vim redis.conf.

# 修改为yes, 守护进程启动
daemonize yes
# 注释掉下面的配置, 允许除本机外的其他机器访问Redis服务
# bind 127.0.0.1 ::1
# 设定数据库密码, 保证服务安全
requirepass 123456

Redis采用的是单进程多线程的模式. 当redis.conf中选项daemonize设置成yes时,代表开启守护进程模式. 在该模式下, redis会在后台运行, 并将进程pid号写入至redis.conf选项的pidfile设置的文件/var/run/redis.pid中, 此时redis将一直运行, 除非手动kill该进程, 但当daemonize选项设置成no时, 当前界面将进入redis的命令行界面, exit强制退出或者关闭连接工具(xshell)都会导致redis进程退出.

服务端开发的大部分应用都是采用后台运行的模式.

requirepass设置密码. 因为Redis速度相当快, 所以一台比较好的服务器, 一个外部用户在一秒内可以进行15万次密码尝试, 意味着你需要设置非常强大的密码来防止暴力破解.

可以通过redis的配置文件设置密码参数, 这样客户端连接到redis服务就需要密码验证, 这样可以让你的redis服务更安全.

5. Redis指定配置文件方式启动

进入redis安装目录, 服务端启动

[root@centos7-01 redis-6.x]# pwd
/usr/local/redis-6.x
[root@centos7-01 redis-6.x]# ./bin/redis-server redis.conf

查看启动的redis进程, 进程ID 6230

[root@centos7-01 bin]# ps -ef|grep redis
root       6230   1755  0 19:22 ?        00:00:35 ./redis-server *:6379
root       6848   6530  0 22:11 pts/4    00:00:00 grep --color=auto redis

客户端登录, 使用redis-cli密码登录

# 没有使用密码登录
[root@centos7-01 bin]# ./redis-cli
127.0.0.1:6379> get name 
(error) NOAUTH Authentication required. # 无法操作redis命令
127.0.0.1:6379>
127.0.0.1:6379> exit
### 使用密码登录
[root@centos7-01 bin]# ./redis-cli -h localhost -p 6379 -a 123456
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
localhost:6379> get name
(nil) # 使用密码登录后,可以正常使用redis命令

当关闭redis进程后, 就无法完成客户端连接

[root@centos7-01 bin]# ps -ef | grep redis
root       6902      1  0 22:21 ?        00:00:00 ./bin/redis-server *:6379
root       6915   6530  0 22:25 pts/4    00:00:00 grep --color=auto redis
[root@centos7-01 bin]# kill -9 6902
[root@centos7-01 bin]#
[root@centos7-01 bin]# ./redis-cli -h localhost -p 6379 -a 123456
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
### 客户端连接redis服务端失败
Could not connect to Redis at localhost:6379: Connection refused
not connected>

6. Redis关闭

6.1 方式1

断电, 非正常关闭, 容易数据丢失.

查询Redis进程id

[root@centos7-01 bin]# ps -ef | grep -i redis
root       6902      1  0 22:21 ?        00:00:00 ./bin/redis-server *:6379
root       6915   6530  0 22:25 pts/4    00:00:00 grep --color=auto redis

kill对查询的pid进行强制关闭

### kill-9 pid
kill-9 6902

6.2 方式2

通过客户端进行shutdown关闭redis服务. 正常关闭, 数据保存.

如果redis设置了密码, 需要先在客户端通过密码登录, 再进行shutdown即可关闭服务端.

[root@centos7-01 bin]# ./redis-cli -h localhost -p 6379 -a 123456
localhost:6379>
localhost:6379> shutdown
not connected>

7. 远程连接

远程连接, 使用RedisDesktopManager, 目前已经收费.

免费的redis可视化软件下载

默认不允许远程连接, 需要修改信息才允许进行连接.

# 注释掉, 允许本机以外的其他机器访问redis服务
#bind 127.0.0.1 ::1
# 设置数据库密码
requirepass 123456 

8. Docker安装Redis

下载安装redis镜像

# 5.0是版本号
docker pull redis:5.0

创建并运行Redis, 默认配置文件方式

# 启动redis-server
docker run --name redis-6379 -d -it -p 6379:6376 redis:5.0 --requirepass "123456"
# 开启redis客户端
docker exec -it redis-6379 redis-cli
# 如果需要密码, 开启redis客户端
docker exec -it redis-6379 redis-cli -a 123456
# 进入redis容器
docker exec -it redis-6379 bash

方便后续对配置文件做高级修改, 此处用自定义配置文件.

# 将redis.conf配置文件复制到指定目录下
mkdir -p /usr/local/docker/redis
cp redis.conf /usr/local/docker/redis

创建Redis容器, 使用自定义配置文件, 将外面的配置文件映射到容器里面. 并且将数据持久化到容器外的宿主机data目录下面的dump.rdb文件.

docker run -it -d --name redis6379 -p 6379:6379  -v /usr/local/docker/redis/redis.conf:/usr/local/etc/redis/redis.conf -v /usr/local/docker/redis/data:/data redis:5.0 redis-server /usr/local/etc/redis/redis.conf

开启redis客户端

# 开启客户端功能
docker exec -it redis6379 redis-cli
# 如果有密码,使用-a参数
docker exec -it redis6379 redis-cli -h 127.0.0.1 -p 6380 -a 123456

9. Redis命令

Redis命令用于在redis服务上执行操作. 要在redis服务上执行命令需要一个redis客户端.

Redis客户端在我们之前下载的redis安装包中, redis-cli.

Redis支持5种数据类型: string(字符串), hash(哈希) , list(列表), set(集合) , zset(sorted set).

9.1 常用命令key管理

# 返回满足的所有键, 可以模糊匹配, 比如 keys abc* 表示abc开头的key
keys *
# 查看key类型
type key
# 是否存在指定的key, 存在返回1, 不存在返回0
exists key 
# 设置某个key的过期时间, 时间为s
expire key second
# 删除某个key
del key
# 查看某个key的剩余时间, 当key不存在时,返回-2; 存在但没有设置剩余生存时间时, 返回-1; 否则返回key的剩余生存时间, 以秒为单位
ttl key
# 取消过期时间, 永久有效
persist key
# 修改key的过期时间为毫秒
pexpire key millseconds
# 选择数据库, 数据库从0-15(默认一共16个数据库)
# 设计成多个数据库实际上是为了数据安全和备份
select 0
# 将当前数据种key转移到其他数据库
move key dbindex
# 随机返回一个key
randomkey 
# 重命名key
rename key key2
# 打印命令
echo message
# 查看数据库的key数量
dbsize
# 查看数据库信息
info
# 返回相关的配置
config get *
config get requirepass
# 清空当前数据库,谨慎使用
flushdb
# 清空所有数据库,谨慎使用
flushall

9.2 应用场景

  1. 限时的优惠活动信息

  2. 网站数据缓存(对于一些需要定时更新的数据, 例如:积分排行榜)

  3. 手机验证码

  4. 限制网站访客访问频率(例如: 1s内最多访问10次) , 对ip计数

9.3 key的命名建议

redis单个key允许存入512MB大小数据. 非关系型数据库Redis, 数据与数据之间没有关联关系.

  • key不要太长, 尽量不要超过1024字节, 这不仅消耗内存, 而且会降低查询的效率;
  • key也不要太短, 可读性会降低;
  • 在一个项目中, key最好使用统一的命名模式, 例如user:0001:name
  • key名称区分大小写

10. 数据类型

Redis支持五种数据类型: string(字符串), hash(哈希), list(列表), set(集合)及有序集合zset(sorted set)等.

10.1 String

10.1.1 简介

string类型是Redis最基本的数据类型, 一个string类型的键最大能存储512MB.

string数据结构是简单的key-value类型, value不仅可以是string, 也可以是数字, 是包含很多种类型的特殊类型.

string类型是二进制安全的. 意思是redis的string可以包含任何数据. 比如序列化的对象进行存储, 一张图片进行二进制存储等.

10.1.2 String命令

# 赋值, 多次设置相同key, 值会被覆盖
set key value
# 如果key不存在,则设置值并返回1. 如果key存在, 则不设置值并返回0.
# 解决分布式锁问题 setnx简单记 set if not exisit
setnx key value 
# 设置key的值为value, 并设置过期时间为seconds, 单位秒. 过期后,自动删除key
# setex简单记 set expired. 
setex key seconds value
# 从index开始替换为字符串value
setrange key index value
# 获取key的值, 如果key不存在返回nil; 如果key存储的值类型不是string, 返回一个error
get key
# 获取key的字符串值中的子字符串, 截取范围包含start和end偏移量
getrange key start end
# 获取key中指定偏移量上的bit位
getbit key offset
# 设置指定key的值, 并返回key的旧值;当key不存在时返回nil
getset key value
# 返回key的字符串值的长度
strlen key
# 删除key, 如果存在,返回数字类型
del key
# 批量写入多个键值
mset key1 value1 key2 value2 ...
# 批量读取多个键值
mget key1 key2 ...
# 自增/自减 1
incr key
decr key
# 自增 increment
incrby key increment
# 自减 decrement
decrby key decrement
# 在指定key的值末尾追加字符串
append key value

10.1.3 应用场景

  • string通常用于保存单个字符串或json字符串数据
  • 因string是二进制安全的, 所以完全可以把一个图片文件的内容作为字符串来存储
  • 计数器(常规key-value缓存应用, 常规计数: 微博数, 粉丝数)

incr等指令本身就具有原子操作的特性, 所以我们完全可以利用redis的incr, incrby, decr, decrby等指令来实现原子计数的效果. 假如, 在某种场景下有3个客户端同时读取了num的值(为2), 然后对其同时进行了加1操作, 那么最后nun的值一定是5. 很多网站都利用redis的这个特性来实现业务上的统计计数需求.

10.2 hash

10.2.1 简介

hash类型是String类型的field和value的映射表, 或者说是一个string集合, hash特别适合用于存储对象, 相比较而言, 将一个对象类型存储在hash类型要比存储在string类型里占用更少的空间, 且方便对整个对象的存取.

Redis中每个hash可以存储(2^32-1)个键值对(40多亿), 会占用更少的磁盘空间. 对象 Object->Map

比如user对象, 有name, age , gender , salary等属性.

10.2.2 hash命令

常用hash命令

# 为指定的key的field赋值
hset key field value
# 同时给一个key的多个字段赋值
hmset key field1 value1 [field2 value2]...
# 获取对象key中指定字段的值
hget key field 
# 获取对象key中的多个字段的值
hmget key field1 [field2]...
# 获取对象key的所有字段和值
hgetall key
# 获取对象key的所有字段
hkeys key
# 获取对象key的字段数量
hlen key
# 删除一个或多个字段
hdel key feild1 [feild2 feild3] ...
# 只有在字段field不存在时, 才设置哈希表字段的值; hsetnx简单记 hset if not exisit
hsetnx key field value
# 为哈希表中对象key的指定字段的整数值加上增量increment, 返回增加后的值
hincrby key field increment
# 为哈希表中对象key的指定字段的数值加上增量浮点值increment, 返回增加后的值
hincrbyfloat key field increment
# 查看哈希表中对象key是否存在指定的字段,存在返回1, 不存在返回0
hexists key field

10.2.3 应用场景

hash的应用场景: 常用于存储一个对象数据, 比如存储一个用户信息对象数据.

为什么不用string存储一个对象数据呢?

hash是最接近关系型数据库结构的数据类型, 可以将数据库的一条记录或程序中的一个对象转换成HashMap对象存放在redis中.

用户ID为查找的key, 存储的value为用户对象, 用户对象包含name, age , birthday等信息, 如果用普通的key-value结构来存储, 主要有以下2种存储方式:

第一种方式: 将用户ID作为查找key, 把其他信息封装成一个对象以序列化的方式存储, 这种方式的缺点是, 增加了序列化/反序列化的开销, 并且在需要修改其中一项信息时, 需要把整个对象取回来, 并且修改操作需要对并发进行保护, 引入CAS等复杂问题. CAS就是比较并交换.

第二种方式: 这个用户信息对象有多少成员就存成多少个key-value对儿, 以用户ID+对应属性的名称作为唯一标识来取得对应属性的值, 虽然省去了序列化开销和并发问题, 但是用户ID为重复存储, 如果存在大量这样的数据, 会造成内存浪费.

Redis提供的hash很好解决了这个问题, hash实际是内部存储的key-value为一个HashMap.

10.3 List

10.3.1 简介

List类型是一个链表结构的集合, 其主要功能有push, pop, 获取元素等. 更详细的说, List类型是一个双端链表, 我们可以通过相关的操作进行集合的头部或尾部添加和删除元素, List的设计非常精巧, 既可以作为(先进后出), 又可以作为队列(先进先出), 满足大多数的需求.

按照插入顺序排序, 你可以添加一个元素到列表的头部(左边)或者尾部(右边), 一个列表最多可以包含(2^32-1)个元素, 超过40亿个元素. 类似Java中的LinkedList

10.3.2 list命令

# 将一个或多个值插入到列表头部(从左侧添加)
lpush key value1 [value2...] 
# 在列表中添加一个或多个值(从右侧添加)
rpush key value1 [value2...] 
# 将一个值插入到已存在的列表头部. 如果列表不存在, 操作无效.
lpushx key value
# 将一个值插入到已存在的列表尾部. 如果列表不存在, 操作无效.
rpushx key value
# 获取列表长度
llen key 
# 通过索引获取列表中的元素
lindex key index 
# 获取列表指定范围内的元素, 偏移量,0表示第一个元素, 1表示第二个,依次类推
lrange key start stop
# 移出并获取列表的第一个元素(从左侧删除), 如果有count, 就是移出count个元素
lpop key [count]
# 移出并获取列表的最后一个元素(从右侧删除), 如果有count, 就是移出count个元素
rpop key [count]
# 移出并获取列表的元素(从左侧), 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
blpop key1 [key2...] timeout
# 移出并获取列表的元素(从右侧), 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
brpop key1 [key2...] timeout
# 对一个列表进行修剪(trim), 让列表只保留指定区间内的元素, 不在指定区间内的元素都将被删除
ltrim key start stop
# 通过索引修改列表元素的值
lset key index value 
# 在列表的元素前或后插入元素, 将value插入到列表key当中, 位于值word之前或之后
linsert key before|after word value
# 移除列表的最后一个元素, 并将该元素添加到另一个列表, 然后返回
rpoplpush source destination
# 从列表中弹出一个值, 将弹出的元素插入到另一个列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
brpoplpush source destination timeout

10.3.3 应用场景

项目常用于:

1, 对数据量大的集合数据删减

列表数据显示, 粉丝列表, 留言评论等.

利用lrange还可以方便的实现分页的功能, 在博客系统中, 每片博文的评论也可以存入一个单独的list中.

2, 任务队列

list通常用来实现一个消息队列, 而且可以确保先后顺序, 不必像MySql那样还需要通过order by来进行排序.

任务队列介绍(生产者和消费者模式):

在处理web客户端发送的命令请求时, 某些操作的执行时间可能会比我们预期的更长一些, 通过将待执行任务的相关信息放入队列中, 并在之后对队列进行处理, 用户可以推迟执行那些需要一段时间才能完成的操作, 这种将工作交给任务处理器来执行的做法称为"任务队列"(task queue).

10.4 Set

10.4.1 简介

Set是String类型的无序集合, 集合成员是唯一的, 这就意味着集合中不能出现重复的数据. Redis中集合是通过哈希表来实现的. Set是通过hashtable实现的.

集合中最大的成员数为(2^32-1), 即每个集合可以存储40多亿个成员. 类似于Java中的hashtable集合.

10.4.2 set命令

# 向集合添加一个或多个成员
sadd key member1 [member2 ...]
# 获取集合的成员数
scard key
# 返回集合中的所有成员
smembers key
# 判断member元素是否是集合key的成员
sismember key member
# 返回集合中一个或多个随机数
srandmember key [count]
# 移除集合中一个或多个成员
srem key member1 [member2 ...]
# 移除并返回集合中的一个或多个随机元素 
spop key [count]
# 将member元素从source集合移动到destination集合
smove source destination member
# 返回给定所有集合的差集(左侧)
sdiff key1 [key2]
# 返回给定所有集合的差集并存储在destination中
sdiffstore destination key1 [key2]
# 返回给定所有集合的交集(共有数据)
sinter key1 [key2]
# 返回所有给定集合的并集
sunion key1 [key2]
# 返回所有给定集合的并集,且储存在destination集合中
sunionstore key1 [key2]

10.4.3 应用场景

常应用于:

1, 利用集合操作, 可以取不同兴趣圈的交集, 以非常方便的实现如共同关注, 共同喜好, 二度好友等功能. 对上面的所有集合操作, 还可以使用不同的命令将结果返回给客户端还是存储到一个新的集合.

2, 利用唯一性, 可以统计访问网站的所有独立ip, 存取当天或某天的活跃用户列表.

10.5 ZSet

10.5.1 简介

有序集合, 和set一样, 也是string类型元素的集合, 且不允许有重复的成员.

不同的是, zset集合的每个元素都会关联一个double类型的分数. Redis是通过分数来为集合中的成员进行从小到大的排序.

有序集合的成员是唯一的, 但分数(score)可以重复.

集合是通过哈希表实现的, 集合中最大的成员数是(2^32-1), 即每个集合可以存储40多亿个成员.

很多时候, 我们都将redis中的有序集合叫做zsets, 这是因为在redis中, 有序集合相关的操作指令都是以z开头的.

10.5.2 zset命令

# 向有序集合添加一个或多个成员, 或者更新已存在的成员分数
zadd key score1 member1 [score2 member2...]
# 获取有序集合的成员数
zcard key
# 计算在有序集合中指定区间分数的成员数
zcount key min max 
# 返回有序集合中指定成员的索引
zrank key member
# 通过索引区间返回有序集合指定区间内的成员(低到高)
zrange key start stop [withscores]
# 通过分数返回有序集合指定区间的成员
zrangebyscore key min max [withscores] [limit]
# 返回有序集合中指定区间内的成员, 通过索引, 分数从高到低
zrevrange key start stop [withscores]
# 返回有序集合中指定分数区间内的成员, 分数从高到低排序
zrevrangebyscore key max min [withscores]
# 移除集合
del key
# 移除有序集合中的一个或多个成员
zrem key member [member2 ...]
# 移除有序集合中给定的排名区间的所有成员(第一名是0) 低到高排序
zremrangebyrank key start stop
# 移除有序集合中给定的分数区间的所有成员
zremrangebyscore key min max
# 增加member元素的分数increment, 返回值是更改后的分数
zincrby key increment member

10.5.3 应用场景

常应用于: 排行榜, 销量排名, 积分排名等.

比如一个存储全班同学成绩的sorted set, 其集合value可以是同学的学号, 而score就可以是其考试得分, 这样在数据插入集合的时候, 就已经进行了天然的排序.

还可以用sorted set来做带权重的队列, 比如普通消息的score为1, 重要消息的score为2, 然后工作线程可以选择按score的倒序来获取工作任务, 让重要的任务优先执行.

10.6 HyperLogLog

10.6.1 简介

Redis在2.8.9版本添加了HyperLogLog结构.

Redis HyperLogLog 是用来做基数统计的算法, HyperLogLog的优点是, 在输入元素的数量或体积非常大时, 计算基数所需的空间总是固定的, 并且是很小的.

在Redis里面, 每个HyperLogLog键只需要花费12kb内存, 就可以计算接近2^64个不同元素的基数, 这和计算基数时, 元素越多耗费内存就越多的集合形成鲜明对比.

但是, 因为HyperLogLog只会根据输入元素来计算基数, 而不会存储输入元素本身, 所以HyperLogLog不能像集合那样, 返回输入的各个元素.

什么是基数?

比如数据集{1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数基数集为{1, 3, 5, 7, 8}, 基数(不重复元素)为5个. 基数估计就是在误差可接受的范围内, 快速计算基数.

10.6.2 为什么需要HyperLogLog

如果要统计1亿个数据的基数值, 大约需要内存100000000/8/1024/1024约等于12M, 内存减少占用的效果显著.

然而统计一个对象的基数值需要12M, 如果统计10000个对象, 就需要将近120G, 同样不能广泛用于大数据场景.

10.6.3 常用命令

pfadd key element [element2 ...] 添加指定元素到HyperLogLog中.
pfcount key [key2 ...] 返回给定HyperLogLog的基数估算值
pfmerge destkey sourceKey [sourcekey2 ...] 将多个HyperLogLog合并为一个HyperLogLog

10.6.4 应用场景

基数不大, 数据量不大, 就用不上, 会有点大材小用浪费空间.

有局限性, 只能统计基数数量, 而没办法知道具体的内容是什么.

统计IP数

统计每日访问IP数

统计页面实时UV数

统计在线用户数

统计用户每天搜索不同词条的个数

统计真实文章阅读数

10.6.5 总结

HyperLogLog是一种算法, 并非redis独有. 目的是做基数统计, 故不是集合, 不会保存元数据, 只记录数量而不是数值.

耗空间极小, 支持输入非常体积的数据量.

核心是基数估算算法, 主要表现为计算时内存的使用和数据合并的处理, 最终数值存在一定误差.

Redis中每个HyperLogLog占用了12kb的内存用于标记基数.

pfadd命令并不会一次性分配12k内存, 而是随着基数的增加而逐渐增加内存分配, 而pfmerge操作则会将sourcekey合并后存储在12k大小的key中.

误差说明: 基数估计的结果是一个带有0.81%标准错误的近似值, 是可以接受的范围. Redis对HyperLogLog的存储进行了优化, 在计数比较小时, 它的存储空间采用稀疏矩阵存储, 空间占用很小, 仅仅在计数慢慢变大, 稀疏矩阵占用空间渐渐超过了阈值时才会一次性转变成稠密矩阵, 才会占用12k的空间.

11. 常用的Redis客户端

Redis常用客户端有 Jedis , redisson , lettuce , 在SpringBoot2.x之后, 对Redis连接的支持, 默认就采用了lettuce.

11.1 介绍

Jedis :老牌的Redis的Java实现客户端, 提供了比较全面的Redis命令支持.

Redisson: 实现了分布式和可扩展的Java数据结构.

Lettuce: 高级Redis客户端, 用于线程安全同步, 异步和响应使用, 支持集群, 哨兵, 管道和编码器.

11.2 优点

Jedis: 比较全面的提供了Redis的操作特性.

Redisson: 促使使用者对Redis的关注分离, 提供很多分布式相关操作服务, 例如: 分布式锁, 分布式集合, 延迟队列.

Lettuce: 基于Netty框架的事件驱动的通信层, 其方法调用是异步的, Lettuce的API是线程安全的, 所以可以操作单个Lettuce连接来完成各种操作.

11.3 可伸缩

Jedis: 使用阻塞的I/O, 且其方法调用都是同步的, 程序流需要等到sockets处理完I/O才能执行, 不支持异步.

Jedis客户端实例不是线程安全的, 所以需要通过连接池来使用Jedis.

Lettuce: 基于Netty框架的事件驱动的通信层, 其方法调用是异步的, Lettuce的API是线程安全的, 所以可以操作单个Lettuce连接来完成各种操作. Lettuce是基于netty实现的与redis进行同步和异步的通信. Lettuce支持redis4.x, 需要java8及以上.

11.4 jedis,lettuce和Redisson和比较

jedis是直接连接redis server, 如果在多线程环境下是非线程安全的, 只有使用连接池为每个jedis实例增加物理连接.

lettuce的连接是基于netty的, 连接实例(SatefulRedisConnection)可以在多个线程间并发访问, SatefulRedisConnection是线程安全的, 所以一个连接实例可以满足多线程环境下的并发访问, 当然这也是可伸缩的设计, 一个连接实例不够的情况下也可以按需增加连接实例.

Redisson实现了分布式和可扩展的Java数据结构, 和Jedis相比, 功能较为简单, 不支持字符串操作, 不支持排序, 事务,管道, 分区等Redis特性. Redisson的宗旨是促进使用者对Redis的关注分离, 从而让使用者能够将精力更集中在业务逻辑处理上.

总结:

优先使用Lettuce, 如果需要分布式锁, 分布式集合等分布式的高级特性, 添加Redisson结合使用, 因为Redisson本身对字符串的操作支持很差.

在一些高并发场景中, 比如秒杀, 抢票, 抢购这些场景, 都存在对核心资源(商品库存)的争夺, 控制不好会导致库存数量被减少到负数, 出现超卖的情况; 由于web应用部署在多个机器上, 要产生唯一的递增ID ,简单的同步加锁无法实现, 给数据库加锁在高并发场景下可能会从行锁变成表锁, 性能严重下降.

相对而言, redis的分布式锁是个很好的选择, redis官方推荐使用的redisson就提供了分布式锁和相关服务.

12. SpringBoot整合Jedis

12.1 简介

在使用springboot搭建微服务时, 有些场景需要redis的高速缓存来缓存一些数据, 存储一些高频率访问的数据, 如果直接使用redis又比较麻烦, 可以整合Jedis来实现redis缓存目的.

12.2 引入Jedis依赖

创建一个springboot应用, 引入相关依赖.

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

因为springboot默认引用了Jedis版本, 所以我们可以不指定Jedis版本号.

12.3 添加配置

在application.yml文件中配置如下内容:

server:
  port: 8081
spring:
  redis:
    host: 192.168.65.129
    password: 123456
    port: 6379
    jedis:
      pool:
        max-idle: 50 # 最大空闲数
        max-active: 100 #  最大连接数
        min-idle: 10 # 最小空闲数
        max-wait: 10000
    timeout: 2000 # 连接超时时间

12.4 Jedis配置类

/**
 * 类描述:Jedis配置类
 * @author crys
 * @date 2022/1/19 21:49
 * @version 1.0
 */
@Configuration
@Slf4j
public class JedisConfig {

    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port;
    @Value("${spring.redis.password}")
    private String password;
    @Value("${spring.redis.timeout}")
    private int timeout;
    @Value("${spring.redis.jedis.pool.max-active}")
    private int maxActive;
    @Value("${spring.redis.jedis.pool.max-idle}")
    private int maxIdle;
    @Value("${spring.redis.jedis.pool.min-idle}")
    private int minIdle;
    @Value("${spring.redis.jedis.pool.max-wait}")
    private int maxWaitMillSeconds;

    @Bean
    public JedisPool jedisPool(){
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxIdle(maxIdle);
        jedisPoolConfig.setMinIdle(minIdle);
        jedisPoolConfig.setMaxTotal(maxActive);
        jedisPoolConfig.setMaxWaitMillis(maxWaitMillSeconds);

        JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, 
                                            timeout, password);
        log.info("JedisPool连接成功: {}:{}",host,port);

        return jedisPool;
    }
}

测试Jedis配置类, 使用Jedis操作Redis

@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class JedisTest {
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port;
    @Value("${spring.redis.password}")
    private String password;

    @Autowired
    private JedisPool jedisPool;

    @Autowired
    private UserService userService;
    @Test
    public void testJedis(){

        Jedis jedis = new Jedis(host,port);
        System.out.println(jedis);
        jedis.auth(password);
        System.out.println("ping="+jedis.ping());
    }

    @Test
    public void testGetString(){
        String name = userService.getString("name");
        System.out.println("name="+name);
    }
}

12.5 mvc测试

controller

@RestController
@Slf4j
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping(value = "/getString")
    public String getString(String key){
        return userService.getString(key);
    }
}

service

@Service
@Slf4j
public class UserServiceImpl implements UserService {

    @Value("${spring.redis.password}")
    private String password;
    /**
     * Jedis连接池
     */
    @Autowired
    private JedisPool jedisPool;

    /**
     * 需求: 用户输入一个key
     * 先判断Redis中是否存在这个key:
     *      如果存在, 在Redis中查询并返回;
     *      如果不存在, 则在mysql中查询并将结果赋给Redis, 然后返回
     */
    @Override
    public String getString(String key) {
        // 得到Jedis对象
        Jedis jedis = jedisPool.getResource();
        // 如果有密码需要授权
        jedis.auth(password);

        String val = null;

        if (jedis.exists(key)) {
            // 存在key
            val = jedis.get(key);
        }else {
            // 查db, 模拟一个数据, 存入redis,并返回
            val = "333";
            jedis.set(key, val);
        }

        // 关闭jedis
        jedis.shutdown();

        return val;
    }
}

启动应用后, 访问 http://localhost:8081/getString?key=name , 因为redis没有name这个key, 所以第一次返回模拟的数据333.
在这里插入图片描述
并且将模拟数据333存入了redis缓存中, 我们通过redis-cli客户端查看.
在这里插入图片描述
这里其实有遇到一个问题的, 就是从JedisPool中获取Jedis连接失败, 可以从以下几个方面进行问题排查:

  • 检查redis-server是否正常开启;
  • redis.conf中的自我保护模式protected-mode设置为no进行关闭;
  • 注释掉bind 127.0.0.1, 开启其他机器访问权限.
  • 查看远程redis服务器防火墙是否开放了6379端口访问权限

我失败的原因是因为远程服务器防火墙没有开放6379端口访问权限, 通过下面方式添加6379端口到白名单中, 就正常了. 报错现象可以参考资料 Jedis连接失败

# 查看防火墙状态
firewall-cmd --state
# 开放6379端口
firewall-cmd --permanent --add-port=6379/tcp
# 查看端口,执行
firewall-cmd --permanent --query-port=6379/tcp
firewall-cmd --permanent --list-ports
# 重启防火墙
firewall-cmd --reload

13. SpringBoot2.x整合lettuce

13.1 maven配置

新建SpringBoot2.x的web工程, 在pom.xml文件中加入依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </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>

    <!--默认是lettuce客户端-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <!--redis依赖commons-pool,一定要加上-->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
    </dependency>

</dependencies>

13.2 配置文件

在application.yml文件中添加如下配置信息(单机配置):

server:
  port: 8081

spring:
  redis:
    host: 192.168.65.129
    password: 123456
    port: 6379
    timeout: 2000 # 连接超时时间
    lettuce:
      pool:
        max-idle: 50 # 连接池中最大空闲数
        max-active: 100 #  连接池中最大连接数
        min-idle: 10 # 连接池中最小空闲数
        max-wait: 10000 # 连接池最大阻塞等待时间(使用负值表示没有限制)
      shutdown-timeout: 100 # 关闭超时时间

13.3 Redis配置类

RedisTemplate对Redis做了进一步封装, 并使用了lettuce客户端.

13.3.1 简介

编写缓存配置类RedisConfig用于调优缓存默认配置, RedisTemplate<String,Object>的类型兼容性更高, 大家可以看到在redisTemplate()这个方法中用Jackson2JsonRedisSerializer更换掉了Redis默认的序列化方式JdkSerializationRedisSerializer.

spring-data-redis中序列化类有以下几个:

  • JdkSerializationRedisSerializer: 序列化Java对象.
  • Jackson2JsonRedisSerializer: 序列化Object对象为json字符串.
  • GenericToStringSerializer: 可以将任何对象转换为字符串并序列化.
  • StringRedisSerializer: 简单的字符串序列化.

JdkSerializationRedisSerializer, 被序列化对象必须实现Serializable接口, 被序列化的除了属性内容外, 还有其他内容, 长度较长且不易阅读, redis默认采用的这种序列化方式. 存储内容如下:

在这里插入图片描述

Jackson2JsonRedisSerializer, 被序列化对象不需要实现Serializable接口, 被序列化的结果清晰, 容易阅读, 且存储字节少, 读取速度快. 存储内容如下:

"{"userName":"crys","age":"20"}"

StringRedisSerializer, 如果key, value都是String类型的话, 一般就用这个.

13.3.2 Redis自动配置类

SpringBoot中, 已经在RedisAutoConfiguration配置类中默认提供了RedisTemplate的实例配置.

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        return new StringRedisTemplate(redisConnectionFactory);
    }

}

但是, 我们可以自定义redis模板

/**
 * 类描述:Redis配置类
 * @author crys
 * @date 2022/1/22 16:21
 * @version 1.0
 */
@Configuration
public class RedisConfig  extends CachingConfigurerSupport {


    /**
     * 自定义缓存key的生成策略, 默认的生成策略是看不懂的(乱码内容).
     * 通过Spring的依赖注入特性来自定义的配置注入
     * @return
     */
    @Bean
    @Override
    public KeyGenerator keyGenerator() {
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... params) {

                StringBuilder sb = new StringBuilder();
                sb.append(target.getClass().getName());
                sb.append(method.getName());
                for (Object obj : params) {
                    sb.append(obj.toString());
                }
                return sb.toString();
            }
        };
    }

    /**
     * 缓存管理器
     * @param factory
     * @return
     */
    @Bean
    public CacheManager cacheManager(LettuceConnectionFactory factory){
        // 以锁的方式创建RedisCacheWriter对象
        RedisCacheWriter writer = RedisCacheWriter.lockingRedisCacheWriter(factory);
        // 创建默认缓存配置对象
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        RedisCacheManager cacheManager = new RedisCacheManager(writer, config);
        return  cacheManager;
    }

    @Bean
    public RedisTemplate<Object, Object> redisTemplate(LettuceConnectionFactory factory){
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);

        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); // enableDefaultTyping已过期
        om.activateDefaultTyping(om.getPolymorphicTypeValidator(),ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // 在使用注解@Bean返回RedisTemplate时, 同时配置hashKey和hashValue的序列化方式
        // key采用String的序列化方式
        redisTemplate.setKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的key也采用String的序列化方式
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        // hash的value采用jackson的序列化方式
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }
}

13.4 service测试

13.4.1 RedisTemplate

Spring data提供了RedisTemplate模板. 它封装了redis连接池管理的逻辑, 业务代码无须关心连接获取, 释放等逻辑. 在RedisTemplate模板中提供了几个常用的接口, 分别是:

private final ValueOperations<K, V> valueOps = new DefaultValueOperations<>(this);
private final ListOperations<K, V> listOps = new DefaultListOperations<>(this);
private final SetOperations<K, V> setOps = new DefaultSetOperations<>(this);
private final ZSetOperations<K, V> zSetOps = new DefaultZSetOperations<>(this);

13.4.2 创建业务实现

/**
 * 类描述:RedisTemplate实现, 对Jedis/lettuce等做了封装
 * @author crys
 * @date 2022/1/19 22:14
 * @version 1.0
 */
@Service
@Slf4j
public class UserServiceImpl implements UserService {

    @Value("${spring.redis.password}")
    private String password;

    @Autowired
    private RedisTemplate<String,Object> redisTemplate;

    // TODO String类型的演示
    /**
     * 需求: 用户输入一个key
     * 先判断Redis中是否存在这个key:
     *      如果存在, 在Redis中查询并返回;
     *      如果不存在, 则在mysql中查询并将结果赋给Redis, 然后返回
     */
    @Override
    public String getString(String key) {
        String val = "";
        if(redisTemplate.hasKey(key)){
            // 存在
            log.info("从redis缓存中查询...");
            val = (String) redisTemplate.opsForValue().get(key);
        }else {
            // 不存在
            log.info("从mysql数据库中查询,模拟数据...");
            val = "RedisTemplate模板测试数据";
            // 将模拟的数据存入redis
            redisTemplate.opsForValue().set(key,val);
        }
        return val;
    }

    /**
     * 测试String类型
     * 需求:  用户输入一个数据, 该key有效期为28h
     * @param key
     * @param val
     */
    public void expireStr(String key, String val){

        redisTemplate.opsForValue().set(key,val);
        redisTemplate.expire(key, 28, TimeUnit.HOURS);
    }

    //TODO hash类型的演示
    /**
     * 存一个对象
     * 需求分析: 根据用户的ID查询用户信息
     *  用户在前端传入一个ID编号, 根据用户ID查询用户的对象信息, 先判断Redis中是否存在该ID对应的用户信息?
     *      如果存在, 直接返回用户结果.
     *      如果不存在, 查询mysql, 并将查询结果存入redis缓存, 然后返回用户信息
     */
    @Override
    public User selectById(String id) {
        // 实体类名:id
        String key = "user";
        User user = null;
//        redisTemplate.hasKey(key) // 判断整个key是否存在
        // hash对象中的key是否存在
        if (redisTemplate.opsForHash().hasKey(key,id)) {
            // 存在, 查询redis缓存数据返回
            log.info("查询redis缓存数据...");
           user = (User) redisTemplate.opsForHash().get(key , id);
        }else {
            // 不存在, 查询数据库,并存入redis缓存,然后返回
            log.info("查询mysql数据库信息, 模拟数据...");
            user = User.builder()
                    .id(id)
                    .name("crys")
                    .age(25)
                    .build();
            // 存入redis缓存
            redisTemplate.opsForHash().put(key,id,user);
        }
        return user;
    }
}

13.4.3 测试UserService#getString

@SpringBootTest
@RunWith(SpringRunner.class)
class BootLuttuceApplicationTests {

    @Autowired
    private UserService userService;

    @Test
    public void testGetString(){
        String result = userService.getString("redisStr");
        System.out.println("result="+result);
    }
}

执行完成后, 在redis-cli客户端查看结果, 做了序列化.

127.0.0.1:6379> get redisStr
"\"RedisTemplate\xe6\xa8\xa1\xe6\x9d\xbf\xe6\xb5\x8b\xe8\xaf\x95\xe6\x95\xb0\xe6\x8d\xae\""

再次执行,可以查看打印日志, 是从redis缓存中查询并反序列化后将结果返回.

2022-01-23 17:45:21.303  INFO 25620 --- [           main] io.lettuce.core.KqueueProvider           : Starting without optional kqueue library
2022-01-23 17:45:22.527  INFO 25620 --- [           main] c.c.b.service.impl.UserServiceImpl       : 从redis缓存中查询...
result=RedisTemplate模板测试数据

13.4.4 测试UserService#expireStr

编写测试类

@SpringBootTest
@RunWith(SpringRunner.class)
class BootLuttuceApplicationTests {

    @Autowired
    private UserService userService;
   
    @Test
    public void testExpireStr(){
        userService.expireStr("testKey","testVal");
        System.out.println("设置成功");
    }
}

执行单元测试后, 在redis-cli客户端查看结果, 并查看一下key的过期时间.

127.0.0.1:6379> get testKey
"\"testVal\""
127.0.0.1:6379> ttl testKey
(integer) 98141

13.4.5 测试UserService#selectById

编写测试类

@SpringBootTest
@RunWith(SpringRunner.class)
class BootLuttuceApplicationTests {

    @Autowired
    private UserService userService;

    @Test
    public void testSelectById(){
        User user = userService.selectById("1003");
        System.out.println("user=" + user);
    }

}

执行单元测试, 查看执行日志并在redis-cli客户端查看结果.

2022-01-23 17:52:39.081  INFO 17040 --- [           main] io.lettuce.core.KqueueProvider           : Starting without optional kqueue library
2022-01-23 17:52:40.277  INFO 17040 --- [           main] c.c.b.service.impl.UserServiceImpl       : 查询mysql数据库信息, 模拟数据...
user=User(id=1003, name=crys, age=25)
127.0.0.1:6379> hgetall user
1) "1003"
2) "[\"com.crys.bootluttuce.po.User\",{\"id\":\"1003\",\"name\":\"crys\",\"age\":25}]"
127.0.0.1:6379> hget user 1003
"[\"com.crys.bootluttuce.po.User\",{\"id\":\"1003\",\"name\":\"crys\",\"age\":25}]"

再次执行单元测试, 查看打印日志, 已经是从redis缓存中获取数据.

2022-01-23 17:54:21.834  INFO 13484 --- [           main] io.lettuce.core.KqueueProvider           : Starting without optional kqueue library
2022-01-23 17:54:23.046  INFO 13484 --- [           main] c.c.b.service.impl.UserServiceImpl       : 查询redis缓存数据...
user=User(id=1003, name=crys, age=25)

13.4.6 service优化

在UserServiceImpl实现中, 很多重复获取操作redis的接口操作, 可以提取出来.

@Service
@Slf4j
public class UserServiceImpl2 implements UserService {

    @Autowired
    private RedisTemplate<String,Object> redisTemplate;
    // 将操作Redis的接口通过redisTemplate进行注入, 简化操作.
    @Resource(name = "redisTemplate")
    private ValueOperations<String, String> strOps;
    @Resource(name = "redisTemplate")
    private HashOperations<String, String, User> hashOps;

    // TODO String类型的演示
    /**
     * 需求: 用户输入一个key
     * 先判断Redis中是否存在这个key:
     *      如果存在, 在Redis中查询并返回;
     *      如果不存在, 则在mysql中查询并将结果赋给Redis, 然后返回
     */
    @Override
    public String getString(String key) {
        String val = "";
        if(redisTemplate.hasKey(key)){
            // 存在
            log.info("从redis缓存中查询...");
            val = strOps.get(key);
        }else {
            // 不存在
            log.info("从mysql数据库中查询,模拟数据...");
            val = "RedisTemplate模板测试数据";
            // 将模拟的数据存入redis
            strOps.set(key,val);
        }
        return val;
    }

    /**
     * 测试String类型
     * 需求:  用户输入一个数据, 该key有效期为28h
     * @param key
     * @param val
     */
    public void expireStr(String key, String val){

        strOps.set(key,val);
        redisTemplate.expire(key, 28, TimeUnit.HOURS);
    }

    //TODO hash类型的演示
    /**
     * 存一个对象
     * 需求分析: 根据用户的ID查询用户信息
     *  用户在前端传入一个ID编号, 根据用户ID查询用户的对象信息, 先判断Redis中是否存在该ID对应的用户信息?
     *      如果存在, 直接返回用户结果.
     *      如果不存在, 查询mysql, 并将查询结果存入redis缓存, 然后返回用户信息
     */
    @Override
    public User selectById(String id) {
        // 实体类名:id
        String key = "user";
        User user = null;
//        redisTemplate.hasKey(key) // 判断整个key是否存在
        // hash对象中的key是否存在
        if (hashOps.hasKey(key,id)) {
            // 存在, 查询redis缓存数据返回
            log.info("查询redis缓存数据...");
           user = hashOps.get(key , id);
        }else {
            // 不存在, 查询数据库,并存入redis缓存,然后返回
            log.info("查询mysql数据库信息, 模拟数据...");
            user = User.builder()
                    .id(id)
                    .name("crys")
                    .age(25)
                    .build();
            // 存入redis缓存
            hashOps.put(key,id,user);
        }
        return user;
    }
}

14. Redis发布订阅

14.1 简介

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

Redis客户端可以订阅任意数量的频道. 下图展示了频道channel1, 以及订阅这个频道的三个客户端之间的关系.
在这里插入图片描述
当有新消息通过publish命令发送给频道channel1时, 这个消息就会被发送给订阅它的三个客户端.
在这里插入图片描述

14.2 常用命令

# 订阅给定的一个或多个频道的信息
subscribe channel [channel2 ...]
# 订阅一个或多个符合给定模式的频道
psubscribe pattern [pattern2 ...]
# 将信息发送到指定的频道
publish channel message
# 退订给指定的频道
unsubscribe channel [channel2 ...]
# 退订所有给定模式的频道
punsubscribe pattern [pattern2 ...]

14.3 应用场景

这一功能最明显的用法就是构建实时消息系统, 比如普通的即时聊天, 群聊等功能.

在一个博客网站中, 有100个粉丝订阅了你, 当你发布新文章, 就可以推送消息给粉丝.

微信公众号模式.

微博, 每个用户的粉丝都是该用户的订阅者, 当用户发完微博, 所有粉丝都将收到他的动态.

新闻, 资讯站点通常有多个频道, 每个频道就是一个主题, 用户可以通过主题来做订阅, 这样当新闻发布时, 订阅者可以获得更新.

简单的应用场景, 以门户网站为例, 当编辑更新了某推荐板块的内容后:

(1) CMS发布清除缓存的消息到channel(推送者推送消息)

(2) 门户网站的缓存系统通过channel收到清除缓存的消息(订阅者收到消息), 更新了推荐板块的缓存.

(3) 还可以做集中配置中心管理, 当配置信息发生变更后, 订阅配置信息的节点都可以收到通知消息.

15. Redis多数据库

Redis下, 数据库是由一个整数索引标识, 而不是由一个数据库名称. 默认情况下, 一个客户端连接到数据库0.

redis配置文件中下面的参数来控制数据库总数:

# 从0开始, 0,1,2,4,5,..,15
database 16

select 数据库 // 数据库的切换

移动数据(将当前key移动到其他数据库)

move key database数据库

数据库清空

# 清除当前数据库的所有key
flushdb
# 清除整个redis的数据库的所有key
flushall

16. Redis事务

Redis事务可以一次执行多个命令, 按顺序的串行化执行, 执行中不会被其他命令插入, 不许加塞.

16.1 简介

Redis事务可以一次执行多个命令(允许在一次单独的步骤中执行一组命令), 并且带有以下两个重要的保证:

批量操作在发送exec命令前被放入队列缓存.

收到exec命令后进入事务执行, 事务中任意命令执行失败, 其余的命令依然被执行.

Redis会将一个事务中的所有命令序列化, 然后按顺序执行.

执行中不会被其他命令插入, 不许出现加塞行为.

16.2 常用命令

# 取消事务, 放弃执行事务块内的所有命令
discard
# 执行所有事务块内的命令
exec
# 标记一个事务块的开始
multi
# 取消watch命令对所有key的监视
unwatch
# 监视一个(或多个)key, 如果在事务执行之前这个(或这些)key被其他命令所改动,那么事务将被打断
watch key [key2 ...]

一个事务从开始到执行会经历以下三个阶段: 开始事务, 命令入队, 执行事务.

16.3 示例1 multi exec

转账功能, A向B账号转账50元.

一个事务的例子, 它先以multi开始一个事务, 然后将多个命令入队到事务中, 最后由exec命令触发事务.
在这里插入图片描述
(1) 输入multi命令开始事务, 输入的命令都会依次进入命令队列, 但不会执行.

(2) 直到输入exec后, Redis会将之前的命令队列中的命令依次执行.

16.4 示例2 discard放弃队列运行

在这里插入图片描述
(1) 输入multi命令开始, 输入的命令都会依次进入命令队列中,但不会执行.

(2) 直到输入exec后, Redis会将之前的命令队列中的命令依次执行.

(3) 命令队列的过程中可以通过discard来放弃队列运行.

16.5 示例3 事务的错误处理

事务的错误处理:

如果执行的某个命令报出了错误,则只有报错的命令不会被执行, 而其他的命令都会执行, 不会回滚.
在这里插入图片描述

16.6 示例4 事务的错误处理

事务的错误处理:

队列中的某个命令出现了报告错误, 执行时整个的所有队列都会被取消.
在这里插入图片描述

16.7 示例5 事务的WATCH

事务提交前没有设置监控, 会导致数据安全问题. 见下面的操作.
在这里插入图片描述

# 监视一个(或多个)key,如果在事务执行之前这个(或这些)key被其他命令所修改了, 那么事务将被打断.
watch key [key1 ...]

需求: 某一个账户在一个事务内进行操作, 在提交事务前, 另一个进程对该账户进行操作.
在这里插入图片描述

上面可以看出, 因为在事务开启之前, 对a进行了watch监控, 所以在另一个进程对a操作后, 前一个进程的事务(对a操作)就无法提交成功. 一组命令必须同时都执行, 或者都不执行. 要保证一组命令在执行的过程中不被其他命令插入.

17. Redis数据淘汰策略

Redis官方给的警告, 当内存不足时, Redis会根据配置的缓存策略淘汰部分key, 以保证写入成功. 当无淘汰策略时或没有找到合适淘汰的key时, Redis直接返回out of memory错误.

建议: 了解了Redis的淘汰策略之后, 在平时使用应尽量主动设置/更新key的expire时间, 主动剔除不活跃的旧key数据, 有助于提升查询性能.

18. Redis持久化

什么是Redis持久化?

持久化就是把内存的数据写入到磁盘中去, 防止服务器宕机后内存数据的丢失.

Redis提供了两种持久化方式: RDB(默认) 和 AOF.

18.1 简介

数据存放于内存: 高效, 断电(关机)会导致内存数据的丢失.

数据存放于磁盘: 读写速度慢于内存, 断电后不会丢失数据.

Redis持久化存储支持两种方式, RDB 和 AOF. RDB一定时间取存储文件, AOF默认每秒去存储历史命令.

Redis是支持持久化的内存数据库, 也就是说redis需要经常将内存中的数据同步到磁盘来保证持久化.

18.2 RDB

RDB是Redis DataBase缩写. 功能核心函数rdbsave(生成RDB文件)和rdbload(从文件加载内存)两个函数.
在这里插入图片描述
RDB: 是redis的默认持久化机制. 也称快照方式. 这种方式就是将内存中数据以快照的方式写入到二进制文件中, 默认的文件名为 dump.rdb

优点: 快照保存数据极快, 还原数据极快. 适用于灾难备份.

缺点: 小内存机器不适合使用, RDB机制符合要求就会进行快照备份.

快照条件:

1, 服务器正常关闭时 ./bin/redis-cli shutdow

2, key满足一定条件, 会进行快照备份. vim redis.conf 搜索save

save 900 1  // 每900s(15min)至少一个key发生变化, 产生快照.

save 300 10  // 每300s(5min)至少10个key发生变化, 产生快照

save 60 10000 // 每60s(1min)至少10000个key发生变化, 产生快照

18.3 AOF

由于快照方式是在一定间隔时间做一次的, 所以如果redis发生宕机, 就会丢失最后一次快照后的所有修改. 如果应用要求不能丢失任何修改的话, 可以使用AOF持久化方式.

Append-only file: 简称AOF, 比RDB快照方式有更好的持久性, 是由于在使用AOF持久化方式时, redis会将每一个收到的写命令都通过write函数追加到文件中(默认是appendonly.aof), 当redis重启时会通过重新执行文件中保存的写命令在内存中重建整个缓存数据库的内容.
在这里插入图片描述
每当执行服务器(定时)任务或者函数时, flushAppendOnlyFile函数都会被调用, 这个函数执行以下两个工作AOF写入保存.

write: 根据条件, 将aof_buf中的缓存写入到AOF文件.

save: 根据条件, 调用fsync或fdatasync函数, 将AOF文件保存到磁盘中.

启用aof持久化方式 : appendonly yes

aof有三种方式(默认是每秒fsync一次):

  • appendfsync always // 收到写命令就立即写入磁盘, 最慢, 但是保证完全的持久化
  • appendfsync everysec // 每秒写入磁盘一次, 在性能和持久化方面做了很好的折中
  • appendfsync no // 完全依赖os, 性能最好, 持久化没有保证

产生的问题:

AOF的方式也带来了另外一个问题, 持久化文件会变的越来越大, 例如我们调用了100次incr命令, 文件中就会保存全部的100条命令, 其实有99条都是多余的.

解决方案: 开启指令重排配置.(只会保留最后一次修改的命令)

19. Redis缓存与数据库一致性

19.1 实时同步

对强一致性要求比较高的, 应采用实时同步方案, 即查询缓存查询不到再从DB查询, 保存到缓存; 更新缓存时, 先更新数据库, 再设置缓存过期时间 (建议不要去更新缓存内容, 直接设置缓存过期).

@Cacheable : 查询时使用, 注意Long类型需转换为String类型, 否则会抛出异常.

@CachePut : 更新时使用, 使用此注解, 一定会从DB上查询数据.

@CacheEvict : 删除时使用.

@Caching : 组合用法.

19.2 异步队列

对于并发程度较高的, 可采用异步队列的方式同步, 可采用kafka等消息中间件处理消息生产和消费.

非实时同步:

实时: 一方修改, 另一方同步修改.

非实时: 一方修改, 另一方不需要同步修改. 比如: 一个文章, 在1min内被点击100万次.

(1) 定时任务 (凌晨2点触发定时任务), 将redis中的num值查询出来, 同步到mysql

redis incr num num=100万

mysql num:0

(2) 异步队列 : 流量削峰, 异步解耦. 常用的有ActiveMQ, RabbitMq, RocketMq, kafka, ZeroMQ

(3) 电商网站: 注册完成 => 邮箱发送注册成功(或手机号发送注册成功) 异步发送

​ => 将注册的信息插入到数据库

(4) 双11: 5000万人=>支付成功=>生成订单=>一小部分人先发货=>4000万人[中间件异步队列] 生成物流=>物流发货.

19.3 使用阿里的同步工具canal

canal实现方式是模拟mysql slave和master的同步机制, 监控DB bitlog的日志更新来触发缓存的更新, 此种方法可以解放程序员双手, 减少工作量, 但在使用时有些局限性.

mysql主备复制实现
在这里插入图片描述
(1) master将改变记录到二进制日志(binary log)中, 这些记录叫做二进制日志事件. binary log events可以通过show binlog events进行查看.

(2) slave将master的binary log events拷贝到它的中继日志(relay log);

(3) slave重做中继日志的事件, 将改变反映到它自己的数据.

canal的工作原理
在这里插入图片描述

  • canal模拟mysql slave的交互协议, 伪装自己为mysql slave, 向mysql master发送dump协议.
  • mysql master收到dump请求, 开始推送binary log给slave(也就是canal).
  • canal解析binary log对象(原始为byte流).

19.4 采用UDF自定义函数的方式

面对mysql的API进行编程 , 利用触发器进行缓存同步, 但UDF主要是c/c++语言实现, 学习成本高.

20. 缓存问题

20.1 缓存穿透

缓存穿透是指查询一个一定不存在的数据, 由于缓存没有命中时会去数据库查询, 查不到数据则不写入缓存, 这将导致这个不存在的数据每次请求都要到数据库去查询, 造成缓存穿透.

解决办法 : 持久层查询不到就缓存空结果, 第二次查询就直接从缓存返回空, 不会查询数据库.

注意: insert时需清除查询的key, 否则即便DB中有这个key的值, 也会查询不到, 因为缓存中给这个key存了空值, 也可以给这个空结果的key设置缓存过期时间.

20.2 缓存雪崩

雪崩: 缓存大量失效的时候, 引发大量查询数据库操作.

解决办法: 用锁/分布式或者队列串行访问; 设置缓存失效时间均匀分布.

如果缓存集中在同一段时间内失效, 发生大量的缓存穿透, 所有的查询都落在数据库上, 造成了缓存雪崩.

这个没有完美的解决办法, 但可以分析用户行为, 尽量让失效时间点均匀分布, 大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线程(进程)写, 从而避免失效时大量的并发请求落到底层存储系统上.

(1) 加锁排队, 限流

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

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

(2) 数据预热

可以通过缓存reload机制, 预先去更新缓存, 在即将发生大并发访问前手动触发加载缓存不同的key, 设置不同的过期时间, 让缓存失效的时间尽量均匀.

20.3 热点key(缓存击穿)

热点key : 某个key访问非常频繁, 当key失效的时候有大量线程来读取缓存, 导致负载增加, 系统崩溃.

解决办法:

  • 使用锁, 单机用synchronized, lock等, 分布式用分布式锁.
  • 缓存过期时间不设置, 而是设置在key对应的value里, 如果检测到存的时间超过过期时间则异步更新缓存.
  • 在value设置一个比过期时间t0小的过期时间值t1, 当t1过期的时候, 延长t1并做更新缓存操作.
  • 设置标签缓存, 标签缓存设置过期时间, 标签缓存过期后, 需异步更新实际缓存.

20.4 案例

假设并发有10000个请求, 想达到第一次请求从数据库中获取, 其他9999个请求从redis中获取这种效果.

pubic User selectById(@RequestParam("id") String id){
   ExecutorService executor = Executors.newFixedThreadPool(20);
   for(int i = 0 ; i<10000; i++){
      executor.submit(new Runnable(){
         @Override
         public void run(){
            userService.selectById(id);
         }
      });
   }
   return userService.selectById(id);
}

通过测试, 发现有大量的请求去查询了数据库.

解决方案:

方法1: 使用互斥锁排队. 这种方式性能差.

业界普遍的一种做法, 即根据key获取value值为空时, 加上锁, 从数据库中load数据后再释放锁. 若其他线程获取锁失败, 则等待一段时间后重试. 需要注意, 分布式环境中要使用分布式锁, 单机使用普通的锁(synchronized, lock)就够了.

// 伪代码
public sychronized selectById(String id){
   if(key.exists(key)){
      redis...
   }else{
      mysql...
      redis.set(...);
   }
}

方法2: 双重检测锁压测

// 伪代码
public User selectById(String id){
   // 查询缓存
   User user = (User)hash.get("user",id);
   if(null==user){
      synchronized(this){
         user = (User)hash.get("user", id);
         if(null==user){
            System.out.println("===查询数据库===");
            // 缓存为空, 查询数据库
            user = userMapper.selectById(id);
            // 将查询的结果存入Redis中
            hash.put("user", user);
         }
      }
   }
   return user;
}

21. Redis高级配置

21.1 可能的问题

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

  • 从结构上, 单个Redis服务器会发生单点故障, 并且一台服务器要处理所有的请求负载, 压力较大; (容错性)
  • 从容量上, 单个Redis服务器内存容量有限, 就算一台Redis服务器内容容量为256G, 也不能将所有内容用作Redis存储内存, 一般来说, 单台Redis最大使用内存不应该超过20G.
    在这里插入图片描述
    问题:
  • 内存容量有限
  • 处理能力有限
  • 无法高可用

21.2 基本概述

21.2.1 高可用

“高可用性”(High Availablity) 通常来描述一个系统经过专门的设计, 从而减少停工时间, 而保持其服务的高度可用性. (一直都能用)

高可用: 6个9 , 99.9999% , 全年停机不超过32s.

21.2.2 高并发

高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一, 它通常是指, 通过设计保证系统能够同时并行处理很多请求.

高并发相关常用的一些指标有响应时间(Response Time), 吞吐量(Throughput), 每秒查询率QPS(Query Per Second), 并发用户数等.

响应时间: 系统对请求做出的响应时间, 例如系统处理一个HTTP请求需要200ms, 这个200ms就是系统的响应时间.

吞吐量: 单位时间内处理的请求量.

QPS: 每秒响应请求数, 在互联网领域, 这个指标和吞吐量区分的没有这么明显.

并发用户数: 同时承载正常使用系统功能的用户数量, 例如一个即时通讯系统, 同时在线量一定程度上代表了系统的并发用户数.

提升系统并发能力

提高系统并发能力的方式, 方法论上有两种: 垂直扩展(Scale Up) 和 水平扩展(Scale Out).

垂直扩展

垂直扩展: 提升单机处理能力.

垂直扩展的方式又有两种:

(1) 增强单机硬件性能, 例如: 增加CPU核数 (32核), 升级更好的网卡(万兆), 升级更好的硬盘(SSD), 扩充硬盘容量(2T), 扩充系统内存(128G).

(2) 提升单机架构性能, 例如: 使用Cache来减少IO次数, 使用异步来增加单服务吞吐量, 使用无锁数据结构来减少响应时间.

在互联网业务发展迅猛的早期, 如果预算不是问题, 强烈建议使用增强单机硬件性能的方式提升系统并发能力, 因为这个阶段, 公司的战略往往在发展业务抢占市场(时间). 而增强单机硬件性能是最快的方式.

总结: 不管是提升单机硬件性能, 还是提升单机架构性能, 都有一个致命的不足, 单机性能总是有极限的, 所以互联网分布式架构设计高并发终极解决方案还是水平扩展.

水平扩展

水平扩展, 只要增加服务器数量, 就能线性扩充系统性能, 水平扩展对系统架构设计是有要求的, 难点在于: 如何在架构各层进行水平扩展的设计.

21.2.3 高性能

高性能(High Performance) 就是指程序处理速度快, 所占内存少, cup低.

21.3 主从复制

应用场景:

电子商务网站上的商品, 一般都是一次上传, 无数次浏览. 也就是 多读少写.

一个Redis服务可以有多个该服务的复制品, 这个Redis服务称为Master, 其他复制称为 Slaves.
在这里插入图片描述
如图中所示, 我们将一台Redis服务器作为主库(Master), 其他三台作为从库(Slave), 主库只负责写数据, 每次有数据更新都将更新的数据同步到它所有的从库, 而从库只负责读数据. 这样一来, 就有两个好处:

  • 读写分离, 不仅可以提高服务器的负载能力, 并且可以根据读请求的规模自由增加或者减少从库的数量.
  • 数据被复制好几份, 就算有一台机器出现故障, 也可以使用其他机器的数据快速恢复.

需要注意的是, 在Redis主从模式中, 一台主库可以拥有多个从库, 但是一个从库只能隶属于一个主库.

Redis主从复制配置

在Redis中,需要实现主从复制架构非常简单, 只需要在从数据库的配置文件中加上如下命令即可:

(1) 主数据库不需要任何配置, 创建一个从数据库:

redis.conf(配置文件信息)

# 从服务器的端口号
-- port 6380
# 指定主服务器
-- slaveof 127.0.0.1 6379

(2) 启动从数据库

./bin/redis-server ./redis.conf --port=6380 --slaveof 127.0.0.1 6379

加上slaveof参数启动另一个Redis实例作为从库, 并且监听6380端口.

(3) 登录到从服务客户端

./bin/redis-cli -p 6380 -a 123456

21.4 哨兵模式

简介

Redis-Sentinel(哨兵模式) 是高可用解决方案, 当Redis在做master-slave的高可用方案时, 假如master宕机了, redis本身(以及很多客户端)都没有实现自动进行主备切换, 而redis-sentinel本身也是独立运行的进程, 可以部署在其他与redis集群可通讯的机器中监控redis集群.

有了主从复制的实现后, 我们如果想对服务器进行监控, redis提供了一个哨兵机制, 并在2.8版本以后功能稳定起来.

哨兵: 顾名思义, 就是监控Redis系统的运行状况.
在这里插入图片描述

哨兵模式的特点

  • 不时的监控redis是否按照预期良好的运行;
  • 如果发现某个redis节点运行出现状况, 能够通知另外一个进程(例如它的客户端)能够进行自动切换, 当一个master节点不可用时, 能够从多个slave(如果有超过一个slave的话)中选出一个来作为新的master, 其他的slave节点将它所追随的master地址改为被提升为master的slave地址.
  • 哨兵为了客户端提供服务发现, 客户端连接哨兵, 哨兵提供当前master的地址然后提供服务, 如果出现切换, 也就是master宕机, 哨兵会提供客户端一个新地址.

22. Redis Cluster集群

22.1 简介

集群模式是实际应用最多的模式.

Redis Cluster 是社区版推出的Redis分布式集群解决方案, 主要解决Redis分布式方面的需求, 比如, 当遇到单机内存, 并发和流量等瓶颈的时候, Redis Cluster能起到很好的负载均衡的目的.

为什么使用Redis Cluster

为了在大流量访问下提供稳定的业务服务, 集群化是存储的必然形态. 未来的发展趋势肯定是云计算和大数据的紧密结合. 只有分布式架构能够满足要求.

22.2 集群描述

Redis集群搭建方案

  • Twitter开发的twemproxy
  • 豌豆荚开发的codis
  • redis官方的redis-cluster

Redis集群搭建的方式有很多, 但从redis3.0之后版本支持redis-cluster集群, 至少需要3(Master) + 3(Slave)才能建立集群.

Redis-Cluster采用无中心结构, 每个节点保存数据和整个集群状态, 每个节点都和其他所有节点连接, 架构图如下:
在这里插入图片描述

Redis-Cluster 集群节点最小配置6个节点以上(3主3从), 其中主节点提供读写操作, 从节点作为备用节点, 不提供请求, 只作为故障转移使用.

22.3 Redis Cluster集群特点

在这里插入图片描述

  • 所有的redis节点彼此互联(ping-pong机制), 内部使用二进制协议优化传输速度和带宽.
  • 节点的fail是通过集群超过半数的节点检测失效时才生效.
  • 客户端与redis节点直连, 不需要中间proxy层, 客户端不需要连接集群所有节点, 连接集群中任何一个可用节点即可.
  • redis-cluster把所有的物理节点映射到[0-16383] slot上(不一定是平均分配), cluster负责维护.
  • redis集群预分配好16384个哈希槽, 当需要在Redis集群中放置一个key-value时, redis先对key使用crc16算法算出一个结果, 然后把结果对16384求余数, 这样每个key都会对应一个编号在0-16383之间的哈希槽, redis会根据节点数量大致均等的将哈希槽映射到不同的节点.

22.4 Redis Cluster容错

容错性, 是指软件检测应用程序所运行的软件或硬件中发生的错误, 并从错误中恢复的能力, 通常可以从系统的可靠性, 可用性, 可测性等几个方面来衡量.
在这里插入图片描述

什么时候判断master不可用

投票机制, 投票过程中是集群中所有master参与 , 如果半数以上master节点与master节点通信超时(Cluster-node-timeout), 认为当前master节点挂掉.

什么时候整个集群不可用(cluster_state:fail)

如果集群任意master挂掉, 且当前master没有slave, 集群进入fail状态, 也可以理解成集群的slot隐射[0-16383]不完整时进入fail状态. 如果集群超过半数以上master挂掉, 无论是否有slave, 集群进入fail状态.

22.5 Redis Cluster节点分配

Redis Cluster采用虚拟槽分区, 所有的键根据哈希函数映射到0-16383个整数槽内, 每个节点负责维护一部分槽以及槽所映射的键值数据.

三个主节点分别是: A, B ,C , 它们可以是一台机器上的三个端口, 也可以是三台不同的服务器, 那么采用哈希槽(hash slot)的方式来分配16384个slot的话, 它们三个节点分别承担的slot区间是:

节点A覆盖0-5460
节点B覆盖5461-10922
节点C覆盖10923-16383

22.6 Redis Cluster集群搭建

集群搭建参考官网 : https://redis.io/topics/cluster-tutorial

Redis集群需要至少三个master节点, 我们这里搭建三个master节点, 并且给每个master再搭建一个slave节点, 总共6个redis节点, 这里用一台机器(可以多台机器部署, 修改一下IP地址就可以了)部署6个redis实例, 三主三从, 搭建集群的步骤如下:

创建Redis节点安装目录 (Redis5.x及以上版本)

# 指定目录下创建redis_cluster
mkdir /usr/local/redis_cluster

在redis_cluster目录下创建7000, 7001, 7002, 7003, 7004, 7005文件夹

mkdir 7000 7001 7002 7003 7004 7005

并将redis-conf分别拷贝到7000~7005文件夹下.

cp /usr/local/redis-6.x/redis.conf /usr/local/redis_cluster/7000

修改redis配置文件

vi /usr/local/redis_cluster/7000/redis.conf
# 关闭保护模式,用于公网访问
protected-mode no
port 7000
# 开启集群模式
cluster-enabled yes
cluster-config-file nodes-7000.conf
cluster-node-timeout 5000
# 后台启动
daemonize yes
pidfile /var/run/redis_7000.pid
logfile "7000.log"
#dir /redis/data
# 此处绑定ip可以是阿里内网ip和本机ip, 也可以直接注释掉该项
# bind 127.0.0.1
# 用于连接主节点密码
masterauth 123456
# 设置redis密码, 各个节点请保持密码一致
requirepass 123456

依次复制并修改6个redis.conf文件

# 依次进行复制
cp ./7000/redis.conf ./7001/
# 修改配置, 使用命令 :%s/old/new/g全局替换    :wq! 保存退出
vim redis.conf

依次启动6个节点

将安装的redis目录下的src复制到cluster下面, 方便启动服务端.

cp -r /mnt/resources/redis-6.2.6/src/  /usr/local/redis_cluster/

启动集群节点

./src/redis-server ./7000/redis.conf
./src/redis-server ./7001/redis.conf
./src/redis-server ./7002/redis.conf
./src/redis-server ./7003/redis.conf
./src/redis-server ./7004/redis.conf
./src/redis-server ./7005/redis.conf

启动后, 用ps查看进程

ps -ef | grep redis-server

在这里插入图片描述

创建集群

Redis 5.x版本后, 通过redis-cli客户端命令来创建集群.

./src/redis-cli --cluster create -a 123456 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 --cluster-replicas 1

在这里插入图片描述

22.7 Redis Cluster集群验证

在某台机器上(或)连接集群的7001端口的节点:

# 加参数-c可连接到集群
redis-cli -h 127.0.0.1 -c -p 7000 -a 123456

redis cluster在设计的时候, 就考虑到了去中心化, 去中间件, 即集群中的节点都是平等的关系, 每个节点都保存各自的数据和整个集群的状态. 每个节点都和其他所有节点连接, 而且这些连接保持活跃, 这样就保证了我们只需要连接集群中的任意一个节点, 就可以获取到其他节点的数据.

基本命令

info replication 通过Cluster Node命令和Cluster info命令来看看集群效果.
在这里插入图片描述

输入命令 cluster nodes
在这里插入图片描述
每个Redis的节点都有一个ID值, 此ID将被特定redis实例永久使用, 以便实例在集群上下文中具有唯一的名称. 每个节点都会记住使用此ID的每个其他节点, 而不是通过IP或端口. IP地址和端口可能会发生变化, 但唯一的节点标识符在节点的整个生命周期内都不会改变, 我们简单的称这个标识符为节点ID.

测试数据

从下面的写和读数据, 可以看出集群节点之间是共享数据的.
在这里插入图片描述

22.8 Redis总结

Redis Cluster为了保证数据的高可用性, 加入了主从模式, 一个主节点对应一个或多个从节点, 主节点提供数据存取, 从节点则是从主节点拉取数据备份, 当这个主节点挂掉后, 就会选取一个从节点来充当主节点, 从而保证集群不会挂掉.

集群有ABC三个主节点, 如果这三个节点都没有加入从节点, 如果B挂掉, 我们就无法访问整个集群了. A和C的slot也无法访问. 所以在建立集群的时候, 一定要为每个主节点都添加从节点, 比如集群包含主节点 A, B ,C , 以及从节点A1, B1, C1, 即使B节点挂掉, 系统也可以继续正常工作.

B1节点替代了B节点, 所以Redis集群将会选择B1节点作为新的主节点, 集群将会继续正确的提供服务, 当B重新开启后, 它就会变成B1的从节点. 需要注意, 如果节点B和B1同时挂掉了, Redis集群就无法继续正常提供服务了.

22.9 关闭集群(Redis Cluster)

启动集群

开启全部redis节点, start.sh, 添加如下内容:

/usr/local/redis_cluster/src/redis-server ./7000/redis.conf
/usr/local/redis_cluster/src/redis-server ./7001/redis.conf
/usr/local/redis_cluster/src/redis-server ./7002/redis.conf
/usr/local/redis_cluster/src/redis-server ./7003/redis.conf
/usr/local/redis_cluster/src/redis-server ./7004/redis.conf
/usr/local/redis_cluster/src/redis-server ./7005/redis.conf
# 修改脚本执行权限
chmod u+x start.sh
# 当前目录启动
./redisall.sh

启动集群

vim start.sh, 追加如下内容(记得修改自己IP和密码)

./src/redis-cli --cluster create -a 123456 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 --cluster-replicas 1

启动过程中, 发现报错 :
在这里插入图片描述

报错原因: 因为上面启动过依次客户端集群, 且写入过数据了, 生成了配置文件nodes.conf, 以及持久化文件appendonly.aof、dump.rdb, 需要删除后, 再重新启动客户端集群.

删除下面标记的文件后再启动客户端.
在这里插入图片描述
重启成功.
在这里插入图片描述

关闭集群

/usr/local/redis_cluster目录下编写脚本: vim shutdown.sh

内容如下:

/usr/local/redis_cluster/src/redis-cli -a 123456 -c -h 127.0.0.1 -p 7000 shutdown
/usr/local/redis_cluster/src/redis-cli -a 123456 -c -h 127.0.0.1 -p 7001 shutdown
/usr/local/redis_cluster/src/redis-cli -a 123456 -c -h 127.0.0.1 -p 7002 shutdown
/usr/local/redis_cluster/src/redis-cli -a 123456 -c -h 127.0.0.1 -p 7003 shutdown
/usr/local/redis_cluster/src/redis-cli -a 123456 -c -h 127.0.0.1 -p 7004 shutdown
/usr/local/redis_cluster/src/redis-cli -a 123456 -c -h 127.0.0.1 -p 7005 shutdown
# 修改脚本权限为可执行
chmod u+x shutdown.sh
# 在当前目录下关闭
./shutdown.sh 
# 查看
ps aux | grep redis-server
# 官方 /usr/local/redis_cluster/redis-cli -a xxx -c -h xxx.xxx.xxx.xxx -p 9001
# -a表示访问服务端密码, -c表示集群模式, -h指定ip, -p指定端口

22.10 Docker-Compose 集群

redis集群至少需要三个master节点, 我们这里搭建三个master节点, 并且给每个master再搭建一个slave节点, 总共6个redis节点, 这里用一台机器(可以多台机器部署, 修改一下IP地址即可)部署6个redis实例, 三主三从, 搭建集群的步骤如下:

服务名称端口号服务名称端口号
master1127.0.0.16381slave1127.0.0.16384
master2127.0.0.16382slave2127.0.0.16385
master3127.0.0.16383slave3127.0.0.16386

创建Redis节点安装目录

mkdir /opt/docker-redis-cluster

依次创建文件夹, 并依次修改配置文件

# 创建文件夹 6381~6386
mkdir redis-6381
# 复制redis.conf到此目录下
cp redis.conf /opt/docker-redis-cluster/redis-6381
# 关闭保护模式,用于公网访问
protected-mode no
port 6381
# 开启集群模式
cluster-enabled yes
# 不改也行
#cluster-config-file nodes-6381.conf
#cluster-node-timeout 5000
# 后台启动
daemonize yes
# 日志文件
pidfile /var/run/redis_6381.pid
logfile "6381.log"
#dir /redis/data
# 此处绑定ip可以是阿里内网ip和本机ip, 也可以直接注释掉该项
# bind 127.0.0.1
# 用于连接主节点密码
masterauth 123456
# 设置redis密码, 各个节点请保持密码一致
requirepass 123456

编写docker-compose.yml文件

version: '3'
services:
  redis-6381:
    container_name: redis-6381
    image: redis
    command: redis-server /etc/usr/local/redis.conf
    network_mode: "host"
    volumes:
      - /opt/docker-redis-cluster/redis-6381/redis.conf:/etc/usr/local/redis.conf
      - /opt/docker-redis-cluster/redis-6381/data:/data

  redis-6382:
    container_name: redis-6382
    image: redis
    command: redis-server /etc/usr/local/redis.conf
    network_mode: "host"
    volumes:
      - /opt/docker-redis-cluster/redis-6382/redis.conf:/etc/usr/local/redis.conf
      - /opt/docker-redis-cluster/redis-6382/data:/data

  redis-6383:
    container_name: redis-6383
    image: redis
    command: redis-server /etc/usr/local/redis.conf
    network_mode: "host"
    volumes:
      - /opt/docker-redis-cluster/redis-6383/redis.conf:/etc/usr/local/redis.conf
      - /opt/docker-redis-cluster/redis-6383/data:/data

  redis-6384:
    container_name: redis-6384
    image: redis
    command: redis-server /etc/usr/local/redis.conf
    network_mode: "host"
    volumes:
      - /opt/docker-redis-cluster/redis-6384/redis.conf:/etc/usr/local/redis.conf
      - /opt/docker-redis-cluster/redis-6384/data:/data

  redis-6385:
    container_name: redis-6385
    image: redis
    command: redis-server /etc/usr/local/redis.conf
    network_mode: "host"
    volumes:
      - /opt/docker-redis-cluster/redis-6385/redis.conf:/etc/usr/local/redis.conf
      - /opt/docker-redis-cluster/redis-6385/data:/data

  redis-6386:
    container_name: redis-6386
    image: redis
    command: redis-server /etc/usr/local/redis.conf
    network_mode: "host"
    volumes:
      - /opt/docker-redis-cluster/redis-6386/redis.conf:/etc/usr/local/redis.conf
      - /opt/docker-redis-cluster/redis-6386/data:/data

启动并测试

# 先在linux机器上安装docker-compose
curl -L "https://github.com/docker/compose/releases/download/1.25.0/docker-compose-$(uname -s)-$(uname -m)" -o  /usr/local/bin/docker-compose
# 在/opt/docker-compose-cluster目录下启动
docker-compose up -d
# 查看是否全部启动成功, 如果没有成功, 需要排查错误(再执行下一步)
docker-compose ps
# 可以通过远程工具连接任意一节点, 看是否密码有效

创建集群

Redis5.x版本后, 通过redis-cli客户端命令来创建集群.

# 创建集群命令
--cluster create
--cluster-replicas 1
# 例如, 修改ip地址
docker exec -it redis-6381 redis-cli --cluster create -a 123456 127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384 127.0.0.1:6385 127.0.0.1:6386 --cluster-replicas 1

集群验证

在某台机器上(或)连接集群的6381端口的节点

# 需要开放端口6381~6389
firewall -cmd --zone=public --add-port=6381/tcp --permanent
# 普通redis方式 参数-c表示可连接到集群
./redis-cli -h 127.0.0.1 -c -p 6381 -a 123456
# docker方式
docker exec -it redis-6381 redis-cli -h 127.0.0.1 -c -p 6381 -a 123456

Redis cluster在设计的时候, 就考虑到了去中心化, 去中间件, 集群中的每个节点都是平等的关系, 都是对等的, 每个节点都保存各自的数据和整个集群的状态, 每个节点都和其他所有节点连接, 而且这些连接保持活跃, 保证了我们只需要连接集群中的任意一个节点, 就可以获取到其他节点的数据.

23. Java连接Redis集群

23.1 导入pom.xml

SpringBoot2.0 Redis相关jar包

<!--默认是lettuce客户端-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!--redis依赖commons-pool,一定要加上-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

23.2 编写application.yml

server:
  port: 8081

spring:
  redis:
    #    host: 192.168.65.129
    #    port: 6379
    password: 123456
    timeout: 2000 # 连接超时时间
    lettuce:
      pool:
        max-idle: 50 # 连接池中最大空闲数
        max-active: 100 #  连接池中最大连接数
        min-idle: 10 # 连接池中最小空闲数
        max-wait: 10000 # 连接池最大阻塞等待时间(使用负值表示没有限制)
      shutdown-timeout: 100 # 关闭超时时间
    cluster: # 集群配置
      nodes:
        - 192.168.65.129:7000
        - 192.168.65.129:7001
        - 192.168.65.129:7002
        - 192.168.65.129:7003
        - 192.168.65.129:7004
        - 192.168.65.129:7005
      max-redirects: 3

23.3 RedisConfig编写

/**
 * 类描述:Redis配置类
 * @author crys
 * @date 2022/1/22 16:21
 * @version 1.0
 */
@Configuration
public class RedisConfig  extends CachingConfigurerSupport {


    /**
     * 自定义缓存key的生成策略, 默认的生成策略是看不懂的(乱码内容).
     * 通过Spring的依赖注入特性来自定义的配置注入
     * @return
     */
    @Bean
    @Override
    public KeyGenerator keyGenerator() {
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... params) {

                StringBuilder sb = new StringBuilder();
                sb.append(target.getClass().getName());
                sb.append(method.getName());
                for (Object obj : params) {
                    sb.append(obj.toString());
                }
                return sb.toString();
            }
        };
    }

    /**
     * 缓存管理器
     * @param factory
     * @return
     */
    @Bean
    public CacheManager cacheManager(LettuceConnectionFactory factory){
        // 以锁的方式创建RedisCacheWriter对象
        RedisCacheWriter writer = RedisCacheWriter.lockingRedisCacheWriter(factory);
        // 创建默认缓存配置对象
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        RedisCacheManager cacheManager = new RedisCacheManager(writer, config);
        return  cacheManager;
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory factory){
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);

        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); // enableDefaultTyping已过期
        om.activateDefaultTyping(om.getPolymorphicTypeValidator(),ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // 在使用注解@Bean返回RedisTemplate时, 同时配置hashKey和hashValue的序列化方式
        // key采用String的序列化方式
        redisTemplate.setKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的key也采用String的序列化方式
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        // hash的value采用jackson的序列化方式
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }
}

23.4 测试

编写测试类

@SpringBootTest
@RunWith(SpringRunner.class)
class BootLuttuceApplicationTests {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Test
    public void testRedisCluster() {

        System.out.println(redisTemplate);
        // 存取string类型
        redisTemplate.opsForValue().set("redis-cluster", "Redis Cluster test");
        Object string = redisTemplate.opsForValue().get("redis-cluster");
        System.out.println(string);
    }
}

debug测试报无法连接集群的错误
在这里插入图片描述
查看端口开放情况, 需要开放端口访问权限。

[root@centos7-01 redis_cluster]# firewall-cmd --permanent --list-ports
6379/tcp
# 查看防火墙状态
firewall-cmd --state
# 开放7000~7005端口
firewall-cmd --permanent --add-port=7000/tcp
# 查看端口,执行
firewall-cmd --permanent --query-port=7000/tcp
firewall-cmd --permanent --list-ports
# 重启防火墙
firewall-cmd --reload

添加完成后, 查询端口

[root@centos7-01 redis_cluster]# firewall-cmd --permanent --list-ports
6379/tcp 7000/tcp 7001/tcp 7002/tcp 7003/tcp 7004/tcp 7005/tcp

再次测试, 发现还是报上面一样的错误, 仔细查找后, 发现是redis服务中的ip写的127.0.0.1, 外面客户端无法访问当Linux的这个地址, 需要修改成实际的IP地址就ok了。

start.sh

./src/redis-cli --cluster create -a 123456 192.168.65.129:7000 192.168.65.129:7001 192.168.65.129:7002 192.168.65.129:7003 192.168.65.129:7004 192.168.65.129:7005 --cluster-replicas 1

shutdown.sh

/usr/local/redis_cluster/src/redis-cli -a 123456 -c -h 192.168.65.129 -p 7000 shutdown
/usr/local/redis_cluster/src/redis-cli -a 123456 -c -h 192.168.65.129 -p 7001 shutdown
/usr/local/redis_cluster/src/redis-cli -a 123456 -c -h 192.168.65.129 -p 7002 shutdown
/usr/local/redis_cluster/src/redis-cli -a 123456 -c -h 192.168.65.129 -p 7003 shutdown
/usr/local/redis_cluster/src/redis-cli -a 123456 -c -h 192.168.65.129 -p 7004 shutdown
/usr/local/redis_cluster/src/redis-cli -a 123456 -c -h 192.168.65.129 -p 7005 shutdown

在这里插入图片描述

java测试结果如下:

redisTemplate>>>org.springframework.data.redis.core.RedisTemplate@38e00b47
2022-03-06 15:32:51.508  INFO 18352 --- [           main] io.lettuce.core.EpollProvider            : Starting without optional epoll library
2022-03-06 15:32:51.510  INFO 18352 --- [           main] io.lettuce.core.KqueueProvider           : Starting without optional kqueue library
Redis Cluster test

我们再连接redis客户端看看写入的结果

./src/redis-cli -c -h 192.168.65.129 -p 7000 -a 123456

发现数据写入了7001, 从7000客户端查询会重定向到7001获取数据, 说明集群之间实现了数据共享。
在这里插入图片描述

24. 综合案例

24.1 需求

限制登录功能: 用户在2min内,仅允许输入错误密码5次。 如果超过次数, 限制其登录1h。(要求每次登录失败时, 都要给出相应的提示)。

24.2 分析

登录错误次数key : user:loginCount:fail:username

锁定限制登录key: user:loginTime:lock:username

执行登录功能时:

  1. 判断当前登录的用户是否被限制登录

    1. 1 如果没用被限制(执行登录)

      1. 如果登录成功, 清除输入密码错误的次数信息

      2. 如果登录不成功, 记录登录错误的次数(判断Redis中的登录次数key是否存在) user:loginCount:fail:username

        3.1 如果key不存在, 是第一次登录失败, 登录失败次数设置为1,同时设置失效期。

        3.2 如果key存在, 查询登录失败的次数

        if(失败次数<4){

        ​ user:loginCount:fail:username + 1

        }else{

        ​ // =4

        ​ 限制登录key是否存在, 同时设置限制登录时间(1h)

        }

​ 1.2 如果被限制, 做相应的提示

24.3 代码实现

编写controller

@RestController
@RequestMapping(value = "/user")
public class UserController {

    @Autowired
    private UserService userService;

    /**
     * 登录功能
     * @param username 用户名
     * @param password 密码
     * @return 返回登录信息
     */
    @RequestMapping(value = "/login")
    public String login(@RequestParam(name = "username") String username,
                        @RequestParam(name = "password") String password) {

        // 判断当前用户是否被限制登录
        Map<String, Object> lockMap = userService.loginUserLock(username);
        if ((boolean) lockMap.get("isLock")) {
            // 被限制登录
            return "登录失败,因" + username + "用户超过了限制登录次数, 已被禁止登录, 还剩" + lockMap.get("lockTime") + "分钟";
        } else {
            // 没用被锁定, 执行登录功能
            User user = userService.login(username, password);
            // 判断是否登录成功
            if (user != null) {
                // 登录成功
                return "登录成功";
            } else {
                // 登录不成功
                return userService.loginFailHandle(username);
            }
        }
    }
}

编写service

public interface UserService {
    /**
     * 登录功能
     * @param username
     * @param password
     * @return
     */
    User login(String username, String password);

    /**
     * 登录不成功的操作
     * @param username
     * @return
     */
    String loginFailHandle(String username);

    /**
     * 是否被限制登录(锁定账户)
     * @param username
     * @return
     */
    Map<String, Object> loginUserLock(String username);
}

编写service实现

@Service
@Slf4j
public class UserServiceImpl implements UserService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Override
    public User login(String username, String password) {
            if (Objects.equals("crys", username) && Objects.equals("123456", password)) {
            return User.builder()
                    .username(username)
                    .password(password)
                    .build();
        }
        return null;
    }

    @Override
    public Map<String, Object> loginUserLock(String username) {
        // 判断当前登录的用户是否被限制登录了
        // 查询锁定限制登录key是否存在,如果存在,就被限制(需要给出提示:您当前的用户已被限制登录, 还剩多长时间)
        // 如果不存在, 就不限制。
        Map<String, Object> lockMap = new HashMap<>();
        String loginTimeLockKey = User.getLoginTimeLockKey(username);
        if (redisTemplate.hasKey(loginTimeLockKey)) {
            // 存在,被锁定限制登录
            lockMap.put("isLock", true);
            // 还剩多长时间,返回分钟
            lockMap.put("lockTime", redisTemplate.getExpire(loginTimeLockKey, TimeUnit.MINUTES));
        } else {
            // 不存在, 没用锁定,没用被限制登录
            lockMap.put("isLock", false);
        }
        return lockMap;
    }

    @Override
    public String loginFailHandle(String username) {
        // 允许登录失败的次数
        int num = 5;
        // 判断登录失败次数的key是否存在
        String loginCountFailKey = User.getLoginCountFailKey(username);
        if (!redisTemplate.hasKey(loginCountFailKey)) {
            // 不存在, 第一次登录失败, 登录失败次数设置为1,同时设置失效期(2min)。
            redisTemplate.opsForValue().set(loginCountFailKey, "1");
            redisTemplate.expire(loginCountFailKey, 2, TimeUnit.MINUTES);
            return "登录失败,在2min内还允许输入错误" + (num - 1) + "次";
        } else {
            // 存在, 查询登录失败的次数,判断是否<4
            int loginFailCount = Integer.parseInt((String) redisTemplate.opsForValue().get(loginCountFailKey));
            if ((loginFailCount < 4)) {
                // 小于4,继续设置登录失败次数+1
                redisTemplate.opsForValue().increment(loginCountFailKey);
                return "登录失败,在" + redisTemplate.getExpire(loginCountFailKey, TimeUnit.SECONDS) + "秒内还允许输入错误" + (num - 1 - loginFailCount) + "次";
            } else {
                // 超过登录失败的次数(最后一次), 设置锁定限制登录key, 并设置限制登录时间为1h
                String loginTimeLockKey = User.getLoginTimeLockKey(username);
                redisTemplate.opsForValue().increment(loginCountFailKey);
                redisTemplate.opsForValue().set(loginTimeLockKey, "1");
                redisTemplate.expire(loginTimeLockKey, 1, TimeUnit.HOURS);
                return "登录失败,您已经输入错误" + num + "次,账户将被锁定1h,期间限制登录";
            }
        }
    }
}

24.4 测试

首先登录redis客户端集群, 查看登录失败的次数key是否存在

[root@centos7-01 redis_cluster]# ./src/redis-cli -c -h 192.168.65.129 -p 7000 -a 123456
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
192.168.65.129:7000> get user:loginCount:fail:crysw
-> Redirected to slot [13688] located at 192.168.65.129:7002
(nil) // 不存在

启动应用服务后, 首先访问一个登录成功的。 http://localhost:8081/user/login?username=crys&password=123456
在这里插入图片描述

再访问登录失败的, 请求 http://localhost:8081/user/login?username=crysw&password=123456
在这里插入图片描述


文章作者: 王子
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 王子 !
评论
  目录