Redis学习

redis 学习

NoSQL概述(非关系型数据库)

单机的MySQL

一开始单机mysql时代,网站的瓶颈:

  1. 如果数据量过大,一个机器放不下
  2. 数据的索引(B+ tree),一个机器的内存也放不下
  3. 访问量(读写混合),一个服务器也承受不了

Redis学习_第1张图片

Memcached(缓存) + MySQL + 垂直拆分(读写分离)

网站80%的情况下都是在进行读操作,因此如果每次用户在进行读操作的时候都去查询数据库的话,会降低性能,因此为了减轻服务器的压力以及提升性能,我们可以使用缓存来提高读操作的效率

发展过程:优化数据结构和索引–>文件缓存(IO)—>Memcached(缓存技术)

Redis学习_第2张图片

分库分表 + 水平拆分(MySQL集群)

Redis学习_第3张图片

一个基本的互联网项目

Redis学习_第4张图片

为什么使用NoSQL

用户的个人信息,社交网络,地理位置,用户自己产生的数据,用户日志等爆发式增长,传统的关系型数据库没法完成

什么是NoSQL

NoSQL=not only sql(不仅仅是SQL)

关系型数据库:表格,行,列

NoSQL泛指非关系型数据库,NoSQL再当今大数据环境下发展十分迅速,Redis是发展最快的

很多的数据类型使用关系型数据库无法进行存储的,比如社交网络,地理位置,这些数据类型不行要一个固定的格式

Redis采用Map的键值对进行存储的

NoSQL特点

1、方便扩展(数据之间没有关系,易扩展)

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

3、数据类型是多样型的(不需要先设计数据库,随取随用)

传统的RDBMS和NoSQL

传统的RDBMS
-结构化组织
-SQL
-数据和关系都存在单独的表中
-数据定义语言	
-严格的一致性
-基础的事务
NoSQL
-不仅仅是数据
-没有固定的查询语言
-键值对存储,列存储,文档存储,图形数据库
-最终一致性
-CAP定理和BASE(异地多活)
- 高性能,高可用,高可扩

大数据时代的3V和3高

3V:主要是描述问题的

  1. 海量Volume
  2. 多样Variety
  3. 实时Velocity

3高:主要是程序的要求

  1. 高并发
  2. 高可用
  3. 高性能

NoSQL的四大分类

1.KV键值对:
  • 新浪:redis
  • 美团:Redis + Tair
  • 阿里,百度:Redis + Memchache
2.文档型数据库(bson和json的格式一样)
  • MongoDB
    • MongoDB是一个基于分布式文件存储的数据库,由c++编写,主要用来处理大量的文档
    • MongoDB是一个介于关系型数据库和非关系型数据库中间的产品,MongoDB是非关系型数据库中功能最丰富,最像关系型数据库的
  • CouchDB
3.列存储数据库
  • HBase
  • 分布式文件系统
4.图关系数据库

图关系数据库就是表示的是各种关系的拓扑结构的,它存的不是图形,而是各种关系,例如朋友圈

neo4j

Redis学习_第5张图片

Redis入门

什么是redis

Redis(Remote Dictionary Server),远程字典服务

是一个开源的使用ANSI C语言编写,支持网络,可基于内存亦可持久化的日志型,Key—Value数据库,并提供多种语言的API

区别是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加记录文件,并且在此基础上实现了master-slave(主从)同步

免费和开源,是当下最热门的NoSQL技术之一,也被称为结构化数据库

Redis的作用

1.内存存储,持久化,内存中是断电即失,所以持久化很重要(rdb,aof)

2.效率高,可以用于高速缓存

3.发布订阅系统

4.地图信息分析

5.计时器,计数器

redis的特性

1.多样的数据类型

2.持久化

3.集群

4.事务

准备工作

1.redis官网:https://redis.io

2.redis中文官网:http://redis.cn

Windows下安装Redis

步骤:

第一步:github下载redis:https://github.com/tporadowski/redis/releases,解压到指定文件夹

第二步:启动服务端,打开这个文件夹里面有一个redis-server.exe文件,双击运行即可

第三步:启动客户端,双击运行redis-cli.exe文件,我们的操作就在客户端进行即可

在Windows下关闭redis服务器只用在redis服务端dos窗口ctrl + c就可以停止

Linux下安装Redis

步骤:

第一步:从官网上下载压缩包:http://redis.cn

第二步:将压缩包上传到Linux服务器上

第三步:将压缩包解压到指定的文件夹下

第四步:安装c++编译器:

yum install -y gcc

第五步:将压缩包解压到指定的文件夹下面:usr/local/lib

第六步:切换至redis的解压目录下,执行make命令

第七步:make完成之后会生成一个src目录,然后在这个目录下执行

make install prefix=/usr/local/lib/redis

第八步:启动redis

前台启动:
cd /usr/local/lib/redis/bin
./redis-server

后台启动redis
从 redis 的源码目录中复制 redis.conf 到 redis 的安装目录的子目录bin下
cp /usr/local/lib/redis-5.0.13/redis.conf /usr/local/lib/redis/bin/
修改 redis.conf 文件,把 daemonize no 改为 daemonize yes
后台启动 ./redis-server redis.conf

查看redis的进程:

ps -ef|grep redis

开启远程服务,在另一台主机上访问redis服务端

Redis学习_第6张图片

benchmark测试工具

benchmark是一款官方自带的压力测试工具

测试的参数如下:
Redis学习_第7张图片

测试:

# 测试100个并发连接, 100000次请求
redis-benchmark -h localhost -p 6379 -c 100 -n 100000

Redis学习_第8张图片

redis基础知识

redis中默认有16个数据库

Redis学习_第9张图片

默认的数据库索引是从0开始的

切换数据库,使用select命令:

Redis学习_第10张图片

清除当前数据库:

Redis学习_第11张图片

清空之后,数据库的容量又变成0

清空所有数据库:flushall,在任何一个数据库中使用这个命令,那么所有的数据库中的数据都会被清空

redis是单线程的,redis是基于内存操作 ,CPU不是redis性能瓶颈,redis的瓶颈是根据机器的内存和网络带宽来决定的

redis是单线程的,为什么还是这么快

误区1:高性能的服务器不一定都是多线程的

误区2:多线程(CPU会进行上下文切换,这是耗时的操作)

redis是将所有的数据存放在内存之中的,因此使用单线程去操作效率就是最高的,对于内存系统来说,如果CPU没有上下文切换效率就是最高的

五大数据类型

redis五大基本类型

Redis是一个开源,内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理。它支持字符串、哈希表、列表、集合、有序集合,位图,hyperloglogs等数据类型。内置复制、Lua脚本、LRU收回、事务以及不同级别磁盘持久化功能,同时通过Redis Sentinel提供高可用,通过Redis Cluster提供自动分区。

由于redis类型大家很熟悉,且网上命令使用介绍很多,下面重点介绍五大基本类型的底层数据结构与应用场景,以便当开发时,可以熟练使用redis。

1 String(字符串)

1.String类型是redis的最基础的数据结构,也是最经常使用到的类型。 而且其他的四种类型多多少少都是在字符串类型的基础上构建的,所以String类型是redis的基础。
2.String 类型的值最大能存储 512MB,这里的String类型可以是简单字符串、 复杂的xml/json的字符串、二进制图像或者音频的字符串、以及可以是数字的字符串

应用场景

1、缓存功能:String字符串是最常用的数据类型,不仅仅是redis,各个语言都是最基本类型,因此,利用redis作为缓存,配合其它数据库作为存储层,利用redis支持高并发的特点,可以大大加快系统的读写速度、以及降低后端数据库的压力。

2、计数器:许多系统都会使用redis作为系统的实时计数器,可以快速实现计数和查询的功能。而且最终的数据结果可以按照特定的时间落地到数据库或者其它存储介质当中进行永久保存。

3、统计多单位的数量:eg,uid:gongming count:0 根据不同的uid更新count数量。

4、共享用户session:用户重新刷新一次界面,可能需要访问一下数据进行重新登录,或者访问页面缓存cookie,这两种方式做有一定弊端,1)每次都重新登录效率低下 2)cookie保存在客户端,有安全隐患。这时可以利用redis将用户的session集中管理,在这种模式只需要保证redis的高可用,每次用户session的更新和获取都可以快速完成。大大提高效率。

2 List(列表)

list类型是用来存储多个有序的字符串的,列表当中的每一个字符看做一个元素
2.一个列表当中可以存储有一个或者多个元素,redis的list支持存储2^32次方-1个元素。
3.redis可以从列表的两端进行插入(pubsh)和弹出(pop)元素,支持读取指定范围的元素集, 或者读取指定下标的元素等操作。redis列表是一种比较灵活的链表数据结构,它可以充当队列或者栈的角色。
4.redis列表是链表型的数据结构,所以它的元素是有序的,而且列表内的元素是可以重复的。 意味着它可以根据链表的下标获取指定的元素和某个范围内的元素集。

应用场景

1、消息队列:reids的链表结构,可以轻松实现阻塞队列,可以使用左进右出的命令组成来完成队列的设计。比如:数据的生产者可以通过Lpush命令从左边插入数据,多个数据消费者,可以使用BRpop命令阻塞的“抢”列表尾部的数据。

2、文章列表或者数据分页展示的应用。比如,我们常用的博客网站的文章列表,当用户量越来越多时,而且每一个用户都有自己的文章列表,而且当文章多时,都需要分页展示,这时可以考虑使用redis的列表,列表不但有序同时还支持按照范围内获取元素,可以完美解决分页查询功能。大大提高查询效率。

3 Set(集合)

1.redis集合(set)类型和list列表类型类似,都可以用来存储多个字符串元素的集合。
2.但是和list不同的是set集合当中不允许重复的元素。而且set集合当中元素是没有顺序的,不存在元素下标。
3.redis的set类型是使用哈希表构造的,因此复杂度是O(1),它支持集合内的增删改查, 并且支持多个集合间的交集、并集、差集操作。可以利用这些集合操作,解决程序开发过程当中很多数据集合间的问题。

应用场景

1、标签:比如我们博客网站常常使用到的兴趣标签,把一个个有着相同爱好,关注类似内容的用户利用一个标签把他们进行归并。

2、共同好友功能,共同喜好,或者可以引申到二度好友之类的扩展应用。

3、统计网站的独立IP。利用set集合当中元素不唯一性,可以快速实时统计访问网站的独立IP。

数据结构

set的底层结构相对复杂写,使用了intset和hashtable两种数据结构存储,intset可以理解为数组。

4 sorted set(有序集合)

redis有序集合也是集合类型的一部分,所以它保留了集合中元素不能重复的特性,但是不同的是,有序集合给每个元素多设置了一个分数。

redis有序集合也是集合类型的一部分,所以它保留了集合中元素不能重复的特性,但是不同的是,
有序集合给每个元素多设置了一个分数,利用该分数作为排序的依据。

应用场景

1、 排行榜:有序集合经典使用场景。例如视频网站需要对用户上传的视频做排行榜,榜单维护可能是多方面:按照时间、按照播放量、按照获得的赞数等。

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

5 hash(哈希)

Redis hash数据结构 是一个键值对(key-value)集合,它是一个 string 类型的 field 和 value
的映射表, redis本身就是一个key-value型数据库,因此hash数据结构相当于在value中又套了一层key-value型数据。
所以redis中hash数据结构特别适合存储关系型对象

应用场景

1、由于hash数据类型的key-value的特性,用来存储关系型数据库中表记录,是redis中哈希类型最常用的场景。一条记录作为一个key-value,把每列属性值对应成field-value存储在哈希表当中,然后通过key值来区分表当中的主键。

2、经常被用来存储用户相关信息。优化用户信息的获取,不需要重复从数据库当中读取,提高系统性能。

redis的key

127.0.0.1:6379> flushall  # 清空数据库
OK
127.0.0.1:6379> set name lkw # 设置值
OK
127.0.0.1:6379> set name2 ysm
OK
127.0.0.1:6379> keys *  # 显示当前数据库下所有的key
1) "name"
2) "name2"
127.0.0.1:6379> move name 1  # 将当前数据库键为name的数据移动到1号数据库中
(integer) 1
127.0.0.1:6379> keys *
1) "name2"
127.0.0.1:6379> expire name2 10  # 设置某个键的过期时间
(integer) 1
127.0.0.1:6379> ttl name2  #查看某个键剩余的存活时间
(integer) 6
127.0.0.1:6379> ttl name2
(integer) 4
127.0.0.1:6379> ttl name2
(integer) 3
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> keys *
1) "name"

127.0.0.1:6379[1]> type name  # 查看当前key的类型
string
127.0.0.1:6379[1]> exists name  # 判断当前key是否存在
(integer) 1


string

127.0.0.1:6379[1]> strlen name  # 获取字符串的长度
(integer) 3
127.0.0.1:6379[1]> append name2 ",I love you"  # 在指定的字符串后面追加内容
(integer) 14
127.0.0.1:6379[1]> get name2
"ysm,I love you"
127.0.0.1:6379[1]> append name3 "666"    # 如果当前key存在则追加其后,不存在作用相当于set
(integer) 3

# 数据的自增自减以及自定义增长或减少
127.0.0.1:6379[1]> incr num1  # 自增1
(integer) 2
127.0.0.1:6379[1]> decr num1  # 自减1
(integer) 1
127.0.0.1:6379[1]> incrby num1 5  #自增5
(integer) 6
127.0.0.1:6379[1]> decrby num1 5  #自减5
(integer) 1

#######################################################################
截取字符串和字符串替换
127.0.0.1:6379[1]> get name2
"ysm,I love you"
127.0.0.1:6379[1]> getrange name2 4 -1  #截取指定长度的字符串
"I love you"

替换字符串
127.0.0.1:6379[1]> setrange name 0 "lkw,I love you"
(integer) 14
127.0.0.1:6379[1]> get name
"lkw,I love you"
#######################################################################


#######################################################################
设置过期时间和设置值
setex  当存在的时候设置过期时间
setnx  当key不存在的时候set一个值,和set的用法不一样,set是当键存在的时候,再次设置的话,后一个值会覆盖前一个值
127.0.0.1:6379[1]> setex num1 30 "555"  # 设置一个存在的值的存活时间为30s
OK
127.0.0.1:6379[1]> ttl num1
(integer) 26
127.0.0.1:6379[1]> setnx key "redis"  # 设置一个不存在的key,值为redis
(integer) 1
127.0.0.1:6379[1]> setnx key "mysql"  # 再次设置这个key,因为已经存在所以不会对原先的redis进行覆盖
(integer) 0
127.0.0.1:6379[1]> 
127.0.0.1:6379[1]> get key
"redis"
#######################################################################

#######################################################################

127.0.0.1:6379[1]> mset k1 v1 k2 v2 k3 v3   #批量设置值
OK
127.0.0.1:6379[1]> keys *
1) "key"
2) "k3"
3) "k2"
4) "k1"
5) "name"
6) "name3"
7) "name2"
127.0.0.1:6379[1]> mget k1 k2 k3   #批量获取值
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379[1]> msetnx k1 v1 k4 v4   #设置key,如果不存在作用相当于set,如果存在则是一个原子操作,前面失败后买也不会成功,也就是在这里如果k1  v1设置失败的话,那么k4,v4即使符合条件也不能成功
(integer) 0
127.0.0.1:6379[1]> keys *
1) "key"
2) "user:1"
3) "k3"
4) "k2"
5) "k1"
6) "name"
7) "name3"
8) "name2"
127.0.0.1:6379[1]> mset user:1:name zhangsan user:1:age 3  设置一个对象
OK
127.0.0.1:6379[1]> mget user:1:name user:1:age  获取对象的各个属性的值
1) "zhangsan"
2) "3"
#######################################################################
先get再set
127.0.0.1:6379[1]> getset k4 v4  # 先设置一个不存在的键,返回的是空
(nil)
127.0.0.1:6379[1]> get k4
"v4"
127.0.0.1:6379[1]> getset k4 v5   # 在设置一个存在的键,先会获取之前在这个键中保存的数据,然后再将新值设置进去
"v4"
127.0.0.1:6379[1]> get k4
"v5"


#######################################################################

List

list里面的所有命令都是以"l"开头的,list中的元素可以重复

  1. 放入元素

    从list的左边放入元素:lpush list one

    从list的右边放入元素:rpush list two

  2. 获取长度

    llen list

  3. 移除元素

    从左边删除元素:lpop

    从右边删除元素:rpop

    可以一次性移除多个相同元素,因为在list可以存在多个相同的元素,使用:lrem list count element

  4. 截取元素

    ltrim:这个方法会将原来的list给截断,会改变原有的list

  5. 获取元素

    lrange list 0 -1 获取列表中的所有元素

    lrange list 0 1 获取列表中的指定元素

  6. 获取指定下标的元素

    lindex list index

  7. 替换元素

    lset 将列表中的指定元素的值替换成目标元素:lset list 0 item,在下标为0的位置添加item字符串

    如果列表不存在就会提示更新错误,如果存在就会将原来下标的元素进行替换,如果下标不存在或者下标越界也会出错

  8. 插入元素

    将某个元素插入到某个元素的前面或者后面

    linsert list before|after element value

Set(集合)元素不能重复

  1. 添加元素
    127.0.0.1:6379> sadd myset "hello1"
    (integer) 1
    127.0.0.1:6379> sadd myset "hello1"  # 不能添加重复元素
    (integer) 0
    127.0.0.1:6379> sadd myset "hello2"
    (integer) 1
    
  2. 取出元素
    127.0.0.1:6379> smembers myset  #取出所有元素
    1) "hello2"
    2) "hello1"
    127.0.0.1:6379> SRANDMEMBER myset   # 从集合中随机抽取一个元素
    "hello3"
    127.0.0.1:6379> SRANDMEMBER myset
    "hello1"
    127.0.0.1:6379> SRANDMEMBER myset 2   #从集合中随机抽取指定个数的元素
    1) "hello2"
    2) "hello1"
    
    
  3. 删除元素
    127.0.0.1:6379> srem myset hello1
    (integer) 1
    127.0.0.1:6379> srem myset hello2
    (integer) 1
    127.0.0.1:6379> srem myset hello2
    (integer) 0
    127.0.0.1:6379> spop myset  #随机移除一个元素,在后面跟上一个参数表示随机移除多个元素
    "hello2"
    
  4. 查询集合中是否包含某元素
    127.0.0.1:6379> SISMEMBER myset hello1  #有则返回1,没有返回0
    (integer) 1
    127.0.0.1:6379> SISMEMBER myset world
    (integer) 0
    
  5. 查询集合中的元素的个数
    127.0.0.1:6379> scard myset
    (integer) 2
    
  6. 将元素从一个集合移动到另一个集合
    127.0.0.1:6379> smove myset myset2 hello3
    (integer) 1
    127.0.0.1:6379> smembers myset
    (empty list or set)
    127.0.0.1:6379> smembers myset2
    1) "hello3"
    2) "hello4"
    
  7. 集合运算
    127.0.0.1:6379> sdiff key1 key2    # 差集
    1) "c"
    2) "b"
    127.0.0.1:6379> sinter key1 key2   # 交集
    1) "a"
    127.0.0.1:6379> sunion key1 key2   # 并集
    1) "a"
    2) "c"
    3) "b"
    4) "e"
    5) "d"
    

Hash

hash实际上就是一个key-value的集合,但是这里的value是一个map集合

#######################################################################################
127.0.0.1:6379> hset myhash field1 hello  # 向myhash集合中添加一个元素field1,值为hello
(integer) 1
127.0.0.1:6379> hget myhash field1        # 获取值为filed1的值
"hello"
127.0.0.1:6379> hmset myhash name1 lkw name2 ysm   # 向myhash集合中同时设置多个值
OK
127.0.0.1:6379> hmget myhash name1 name2           # 从myhash集合中获取同时获取多个元素的值
1) "lkw"
2) "ysm"
127.0.0.1:6379> hgetall myhash           # 获取myhash集合中所有的键值对
1) "field1"
2) "hello"
3) "name1"
4) "lkw"
5) "name2"
6) "ysm"

#######################################################################################
127.0.0.1:6379> hdel myhash field1   # 删除myhash中的指定元素
(integer) 1
127.0.0.1:6379> hgetall myhash
1) "name1"
2) "lkw"
3) "name2"
4) "ysm"
#######################################################################################
127.0.0.1:6379> hlen myhash   # 获取集合元素的个数
(integer) 2
127.0.0.1:6379> hexists myhash field2   # 判断某个元素在集合中是否存在
(integer) 0
127.0.0.1:6379> hexists myhash name1
(integer) 1
127.0.0.1:6379> hkeys myhash  # 获取集合中所有key
1) "name1"
2) "name2"  
127.0.0.1:6379> hvals myhash  # 获取集合中的所有value
1) "lkw"
2) "ysm"
#######################################################################################
127.0.0.1:6379> hset mykey k1 1
(integer) 1
127.0.0.1:6379> HINCRBY mykey k1 2   # 自增操作
(integer) 3
127.0.0.1:6379> hsetnx mykey k2 2    # 如果不存在则可以设置
(integer) 1
127.0.0.1:6379> hsetnx mykey k2 2    # 如果存在则不能设置
(integer) 0

ZSet

就是在set的基础上增加了一个值

1.向set中添加元素,添加元素的时候需要外加一个参数作为该元素的标记

127.0.0.1:6379> zadd myset 1 one # 向zset集合中添加元素
(integer) 1
127.0.0.1:6379> zadd myset 2 two 
(integer) 1
127.0.0.1:6379> zadd myset 3 three
(integer) 1
127.0.0.1:6379> zrange myset 0 -1  # 遍历获取set中的所有值
1) "one"
2) "two"
3) "three"

2.对zset中的元素进行排序

127.0.0.1:6379> zrangebyscore  myset -inf +inf
1) "one"
2) "two"
3) "three"
127.0.0.1:6379> zrangebyscore  myset -inf +inf withscores   # 升序排列
1) "one"
2) "1"
3) "two"
4) "2"
5) "three"
6) "3"
######################################################################
127.0.0.1:6379> zrange myset 0 -1   # 获取zset集合中的所有元素
1) "one"
2) "two"
3) "three"
127.0.0.1:6379> zrem myset 0 -1    # 移除集合中的所有元素
(integer) 0
127.0.0.1:6379> zrem myset one     # 根据值来移除元素
(integer) 1
127.0.0.1:6379> zcard myset       # 获取有序集合中元素的个数
(integer) 2
##########################################################################
127.0.0.1:6379> zcount myset  1 3  # 获取指定区间成员数量
(integer) 2

以上表示的是从负无穷到正无穷排序

三种特殊的数据类型

geospatial 地理位置

geoadd 将指定的地理空间位置(纬度,精度,名称)添加到指定的key中

127.0.0.1:6379> geoadd china:city 116.40 39.90 beijing
(integer) 1
127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai
(integer) 1
127.0.0.1:6379> geoadd chin:city 106.50 29.53 chongqing 114.05 22.52
127.0.0.1:6379> geoadd chin:city 106.50 29.53 chongqing 114.05 22.52 shenzhen
(integer) 2
127.0.0.1:6379> geoadd china:city 106.50 29.53 chongqing 114.05 22.52 shenzhen
(integer) 2
127.0.0.1:6379> geoadd china:city 120.16 30.24 hangzhou 108.96 34.26 xian
(integer) 2

获取当前定位:是一个由经纬度组成的坐标值

127.0.0.1:6379> geopos china:city beijing
1) "116.39999896287918091"
2) "39.90000009167092543"

geodist 返回两个给定位置之间的距离

单位:

  • m
  • km
  • mi 英里
  • ft 表示单位为英尺
127.0.0.1:6379> geodist china:city beijing shanghai km   北京到上海的直线距离
"1067.3788"
127.0.0.1:6379> geodist china:city beijing shanghai 
"1067378.7564"

georadius 以给定的经度纬度为中心找到附近的地理位置

127.0.0.1:6379> georadius china:city 110 30 1000 km   # 获取距离经纬度为110,30 
1000km的城市
1) "chongqing"
2) "xian"
3) "shenzhen"
4) "hangzhou"
127.0.0.1:6379> georadius china:city 110 30 1000 km withcoord  # 获取城市并且获取经纬度
1) 1) "chongqing"
   2) 1) "106.49999767541885376"
      2) "29.52999957900659211"
2) 1) "xian"
   2) 1) "108.96000176668167114"
      2) "34.25999964418929977"
3) 1) "shenzhen"
   2) 1) "114.04999762773513794"
      2) "22.5200000879503861"
4) 1) "hangzhou"
   2) 1) "120.1600000262260437"
      2) "30.2400003229490224"
127.0.0.1:6379> georadius china:city 110 30 1000 km withdist  # 获取城市并且获取该位置相对于这些城市的距离
1) 1) "chongqing"
   2) "341.9374"
2) 1) "xian"
   2) "483.8340"
3) 1) "shenzhen"
   2) "924.6408"
4) 1) "hangzhou"
   2) "977.5143"

geohash:获取一个城市的哈希值表示的字符串

127.0.0.1:6379> geohash china:city chongqing shanghai beijing
1) "wm5xzrybty0"
2) "wtw3sj5zbj0"
3) "wx4fbxxfke0"

geo 底层的实现原理就是Zset,可以通过zset的命令来实现相应的geo的操作

127.0.0.1:6379> zrange china:city 0 -1
1) "chongqing"
2) "xian"
3) "shenzhen"
4) "hangzhou"
5) "shanghai"
6) "beijing"
127.0.0.1:6379> zrem china:city shanghai
(integer) 1
Hyperloglog 基数

基数:就是指的是一个集合当中不重复的元素

用Hyperloglog技术一般用来统计网页的访问量,因为传统统计页面的访问量的时候都是使用的是保存用户id的方式来统计用户的访问量,但是当用户的数量超过2^64的时候,就无法统计了,但是这时候使用Hyperloglog统计就只用统计数量,因此优先选择使用Hyperloglog技术来实现

127.0.0.1:6379> pfadd mykey a b c d e f g h i j  创建一个hyperloglog
(integer) 1
127.0.0.1:6379> pfcount mykey   统计该集合中的不重复的元素个数
(integer) 10
127.0.0.1:6379> pfadd mykey2 q w e r t y u i o
(integer) 1
127.0.0.1:6379> pfcount mykey2
(integer) 9
127.0.0.1:6379> pfmerge mykey3 mykey mykey2   #将两个hyperloglog合并,mykey3=mykey + mykey2
OK
127.0.0.1:6379> pfcount mykey3  统计整个集合的不重复的元素的个数
(integer) 16
Bitmaps

位存储,就是在计算机中使用比特位来存储数据

bitmaps是位图,是一种数据结构,都是操作二进制来进行记录的,只有0和1两个状态

例:使用bitmaps完成 一周的签到打卡情况

127.0.0.1:6379> setbit sign 0 1
(integer) 0
127.0.0.1:6379> setbit sign 1 1
(integer) 0
127.0.0.1:6379> setbit sign 2 0
(integer) 0
127.0.0.1:6379> setbit sign 3 1 
(integer) 0
127.0.0.1:6379> setbit sign 4 1 
(integer) 0
127.0.0.1:6379> setbit sign 5 0
(integer) 0
127.0.0.1:6379> setbit sign 6 1
(integer) 0
127.0.0.1:6379> bitcount sign 
(integer) 5

事务

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

一次性,顺序性,排他性,执行一系列的命令

-----队列   set  set  set  执行----

redis事务没有隔离级别的概念

所有的命令在事务中,并没有直接被执行,只有发起执行命令的时候才会执行

redis单条命令是原子性的,但是事务不保证原子性

redis的事务:

  1. 开启事务
  2. 命令入队
  3. 执行事务

正常执行事务

127.0.0.1:6379> multi   # 开启事务
OK
127.0.0.1:6379> set k1 v1   # 下面的set命令都是命令入队
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> exec    # 执行事务
1) OK
2) OK
3) OK

注意,每次执行完事务之后,事务都会自动提交,因此下次在使用的时候,需要重新开启事务

放弃事务

127.0.0.1:6379> multi    # 开启事务
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> discard  # 放弃事务
OK
127.0.0.1:6379> get k1
(nil)

编译期异常

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> getset k3  # 在这里出现了编译器异常,那么事务就失效了,整个事务就失效了
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> exec     # 因为前面在事务中就出现了异常,所以在执行事务的时候整个命令队列中的命令都不能成功
(error) EXECABORT Transaction discarded because of previous errors.  
127.0.0.1:6379> get k2    # 因此即便是在事务失效之前执行成功的语句也不能获取到值
(nil)

运行期异常,如果事务队列中存在语法性,其他命令是可以正常执行的,错误命令抛出异常

127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> multi  # 开启事务
OK
127.0.0.1:6379> incr k1  # 因为在这里k1的值是字符串,不能进行自增,但是语法没有错误
QUEUED
127.0.0.1:6379> set k2 v2 # 这之后的命令都没有问题,后面的语句都可以正常执行
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> exec
1) (error) ERR value is not an integer or out of range  # 发现最后只有k1这条命令这里出了问题,但是后面的语句都正常执行了
2) OK
3) OK
4) "v2"

redis实现监视功能

悲观锁:

很悲观,认为什么时候都会出问题,所以任何时候都会加锁

乐观锁:

很乐观,认为什么时候都不会出问题,所以不会上锁,在更新数据的时候去判断一下,是否有人修改过这个数据,如果更新了数据的话,那么在执行更新操作的时候就会更新失败

客户端1:用于转账

127.0.0.1:6379> set money 1000
OK
127.0.0.1:6379> set out 100
OK
127.0.0.1:6379> watch money  # 监视money的变化
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby money 100
QUEUED
127.0.0.1:6379> incrby out 100
QUEUED
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> set money 1000
OK
127.0.0.1:6379> set out 100
OK
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby money 100
QUEUED
127.0.0.1:6379> incrby out 100
QUEUED
127.0.0.1:6379> exec  # 这里因为另一个用户对数据进行了修改,所以这里更新失败,就是现在的余额是800元
(nil)
127.0.0.1:6379> unwatch   # 解锁
OK
127.0.0.1:6379> watch money  # 重新监视money,这个时候的钱就是800
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby money 100
QUEUED
127.0.0.1:6379> incrby out 100
QUEUED
127.0.0.1:6379> exec
1) (integer) 700
2) (integer) 200

客户端2:用于在客户端1在转账的过程中修改账户余额,模拟多线程

127.0.0.1:6379> set money 800
OK

最后发现,redis可以用来实现乐观锁,当客户端1在进行数据的更新的时候也就是执行事务的时候,会先检查数据是否被改变了,在更新之前客户端2对数据进行了修改,因此客户端1在进行数据更新的时候,发现数据被修改了,就不进行数据的更新操作

如果发现修改失败,获取最新的值即可,使用unwatch可以解锁,然后要加锁的话需要重新加

Jedis

使用Java操作redis

导入对应的依赖

<dependencies>
        
        
        <dependency>
            <groupId>redis.clientsgroupId>
            <artifactId>jedisartifactId>
            <version>4.2.3version>
        dependency>
        
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>fastjsonartifactId>
            <version>2.0.10version>
        dependency>
dependencies>

测试连接

public class MyJedis {
    public static void main(String[] args) {
        //创建jedis对象,连接redis
        Jedis jedis = new Jedis("127.0.0.1",6379);
        //测试redis的连通性
        System.out.println(jedis.ping());
    }
}

通过jedis加深对redis的理解

public class TestTransaction {
    public static void main(String[] args) {
        //创建jedis对象
        Jedis jedis = new Jedis("localhost",6379);
        jedis.flushDB();


        JSONObject object = new JSONObject();
        object.put("name","ysm");
        object.put("name2","lkw");

        //开启事务
        Transaction multi = jedis.multi();
        String result = object.toJSONString();

        try {
            multi.set("user1",result);
            multi.set("user2",result);
            int i = 1 / 0;  //在这里代码抛出异常,事务执行失败
            multi.exec();   //执行事务
        } catch (Exception e) {
            multi.discard();    //放弃事务
            e.printStackTrace();
        } finally {
            System.out.println(jedis.get("user1"));//事务执行失败,所有什么也得不到
            System.out.println(jedis.get("user2"));
            jedis.close();  //关闭事务
        }

    }
}

SpringBoot整合redis

SpringBoot操作数据:都是使用Spring-Data实现的

说明:在SpringBoot2.x之后,原来使用的jedis被替换成了lettuce

jedis:采用的直连,多个线程操纵的话,会出现安全问题,如果想要避免不安全,可以使用jedis pool连接池,它更像是BIO模式,也就是阻塞模式

lettuce:采用的netty框架,这个框架是高性能的网络框架,实例可以在多个线程中进行共享,不存在线程不安全的情况,可以减少线程数据了,更像是NIO模式

SpringBoot-reids配置文件

public class RedisAutoConfiguration {

	@Bean
	@ConditionalOnMissingBean(name = "redisTemplate")  // 我们可以自己定义一个redisTemplate来替换这个默认的类,也就是当我们没有自定义redisTemplate的时候使用的就是这个默认的
	@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
	public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // 默认的RedisTemplate没有过多的设置,redis对象都是需要序列化的
        // 两个泛型都是Object, Object类型的,我们后面使用的时候需要进行强制类型转换
		RedisTemplate<Object, Object> template = new RedisTemplate<>();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

	@Bean
	@ConditionalOnMissingBean
	@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
	public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {// 有这个类是因为String在redis中是最常用的数据类型
		return new StringRedisTemplate(redisConnectionFactory);
	}

}
整合过程

1.导入依赖

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redisartifactId>
dependency>

2.配置连接

# 配置redis的连接
spring.redis.host=127.0.0.1
spring.redis.port=6379

3.测试

class Redis02SpringbootApplicationTests {
    //注入redisTemplate类
    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    void contextLoads() {
        //在这里进行redis的操作
        // redisTemplate.opsForValue(); 对字符串进行的操作
        // redisTemplate.opsForHash();  对hash进行操作
        // redisTemplate.opsForList();  对list进行操作
        // ……

        //获取redis的连接
        //RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();

        //先设置一组值用于测试
        redisTemplate.opsForValue().set("name","lkw");
        System.out.println(redisTemplate.opsForValue().get("name"));
        redisTemplate.opsForValue().set("name2","殷思敏");
        System.out.println(redisTemplate.opsForValue().get("name2"));
    }

}
	@Nullable
    private RedisSerializer keySerializer = null;
    @Nullable
    private RedisSerializer valueSerializer = null;
    @Nullable
    private RedisSerializer hashKeySerializer = null;
    @Nullable
    private RedisSerializer hashValueSerializer = null;

我们发现,在使用的时候因为默认的是没有经过序列化的内容,而redis的对象都是需要序列化的,所以中文会出现乱码问题

为了解决乱码问题可以自定义一个可以序列化的类来对对象进行序列化

方式一:使用json

@Test
    public void test() throws JsonProcessingException {
        // 真实开发一般都使用json来传递对象
        User user = new User("lkw",20);
        // 这里在数据进行传输之前先对对象进行序列化,否则不能成功传输数据
        String jsonUser = new ObjectMapper().writeValueAsString(user);
        redisTemplate.opsForValue().set("user",jsonUser);
        System.out.println(redisTemplate.opsForValue().get("user"));
}

方式二:直接进行序列化,但是实体类上面需要实现序列化接口

@Test
    public void test() throws JsonProcessingException {
        // 真实开发一般都使用json来传递对象
        User user = new User("lkw",20);
        // 现在通过实现序列化接口来完成对象的序列化
        redisTemplate.opsForValue().set("user",user);
        System.out.println(redisTemplate.opsForValue().get("user"));
}

方式三:使用自定义的redisTemplate进行序列化

@Component
public class RedisUtil {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    // =============================common============================
    /**
     * 指定缓存失效时间
     * @param key 键
     * @param time 时间(秒)
     * @return
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 根据key 获取过期时间
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }
    /**
     * 判断key是否存在
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 删除缓存
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
            }
        }
    }
    // ============================String=============================
    /**
     * 普通缓存获取
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }
    /**
     * 普通缓存放入
     * @param key 键
     * @param value 值
     * @return true成功 false失败
     */
    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 普通缓存放入并设置时间
     * @param key 键
     * @param value 值
     * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 递增
     * @param key 键
     * @param delta 要增加几(大于0)
     * @return
     */
    public long incr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }
    /**
     * 递减
     * @param key 键
     * @param delta 要减少几(小于0)
     * @return
     */
    public long decr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }
    // ================================Map=================================
    /**
     * HashGet
     * @param key 键 不能为null
     * @param item 项 不能为null
     * @return 值
     */
    public Object hget(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }
    /**
     * 获取hashKey对应的所有键值
     * @param key 键
     * @return 对应的多个键值
     */
    public Map<Object, Object> hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }
    /**
     * HashSet
     * @param key 键
     * @param map 对应多个键值
     * @return true 成功 false 失败
     */
    public boolean hmset(String key, Map<String, Object> map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * HashSet 并设置时间
     * @param key 键
     * @param map 对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map<String, Object> map, long time) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 向一张hash表中放入数据,如果不存在将创建
     * @param key 键
     * @param item 项
     * @param value 值
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 向一张hash表中放入数据,如果不存在将创建
     * @param key 键
     * @param item 项
     * @param value 值
     * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value, long time) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 删除hash表中的值
     * @param key 键 不能为null
     * @param item 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... item) {
        redisTemplate.opsForHash().delete(key, item);
    }
    /**
     * 判断hash表中是否有该项的值
     * @param key 键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String item) {
        return redisTemplate.opsForHash().hasKey(key, item);
    }
    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     * @param key 键
     * @param item 项
     * @param by 要增加几(大于0)
     * @return
     */
    public double hincr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, by);
    }
    /**
     * hash递减
     * @param key 键
     * @param item 项
     * @param by 要减少记(小于0)
     * @return
     */
    public double hdecr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, -by);
    }
    // ============================set=============================
    /**
     * 根据key获取Set中的所有值
     * @param key 键
     * @return
     */
    public Set<Object> sGet(String key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
     * 根据value从一个set中查询,是否存在
     * @param key 键
     * @param value 值
     * @return true 存在 false不存在
     */
    public boolean sHasKey(String key, Object value) {
        try {
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 将数据放入set缓存
     * @param key 键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSet(String key, Object... values) {
        try {
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    /**
     * 将set数据放入缓存
     * @param key 键
     * @param time 时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSetAndTime(String key, long time, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().add(key, values);
            if (time > 0) {
                expire(key, time);
            }
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    /**
     * 获取set缓存的长度
     * @param key 键
     * @return
     */
    public long sGetSetSize(String key) {
        try {
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    /**
     * 移除值为value的
     * @param key 键
     * @param values 值 可以是多个
     * @return 移除的个数
     */
    public long setRemove(String key, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    // ===============================list=================================
    /**
     * 获取list缓存的内容
     * @param key 键
     * @param start 开始
     * @param end 结束 0 到 -1代表所有值
     * @return
     */
    public List<Object> lGet(String key, long start, long end) {
        try {
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
     * 获取list缓存的长度
     * @param key 键
     * @return
     */
    public long lGetListSize(String key) {
        try {
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    /**
     * 通过索引 获取list中的值
     * @param key 键
     * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
     * @return
     */
    public Object lGetIndex(String key, long index) {
        try {
            return redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, Object value) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @param time 时间(秒)
     * @return
     */
    public boolean lSet(String key, Object value, long time) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, List<Object> value) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 将list放入缓存
     *
     * @param key 键
     * @param value 值
     * @param time 时间(秒)
     * @return
     */
    public boolean lSet(String key, List<Object> value, long time) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 根据索引修改list中的某条数据
     * @param key 键
     * @param index 索引
     * @param value 值
     * @return
     */
    public boolean lUpdateIndex(String key, long index, Object value) {
        try {
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 移除N个值为value
     * @param key 键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */
    public long lRemove(String key, long count, Object value) {
        try {
            Long remove = redisTemplate.opsForList().remove(key, count, value);
            return remove;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
}

一般都会推荐使用这种方式进行对象的序列化

Redis.conf详解

redis启动的时候需要加载的文件

单位,对大小写不敏感

Redis学习_第12张图片

网络

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

通用general

daemonize yes   # 以后台守护进程的方式运行,改为yes
pidfile /var/run/redis_6379.pid   # 如果以后台的方式运行,我们就需要指定一个pid文件

# 日志
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably)
# warning (only very important / critical messages are logged)

loglevel notice
logfile ""  日志文件的位置名
databases  16  默认有16个数据库
always-show-logo   # 是否在启动的时候显示logo

快照

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

redis是内存数据库,如果没有持久化,断电即失

# 如果在900s内至少有一个key进行了修改,那么就进行持久化操作
save 900 1
# 假设300s之内,至少有10个key进行了修改,将进行持久化
save 300 10
# 假设60s之内,至少有10000个key进行了修改,将进行持久化
save 60 10000
stop-writes-on-bgsave-error yes  #持久化失败,是否还需要继续工作
rdbcompression yes     # 是否压缩rdb文件(持久化生成的文件),需要消耗一定的cpu资源
rdbchecksum yes  # 保存rdb文件的时候进行错误的校验检查
dir ./ # rdb文件的生成目录 

REPLICATION 复制

SECURITY 安全

redis默认是没有密码的,可以在配置文件里面设置密码也可以通过命令的方式设置密码,一旦设置密码之后,只有登录之后才能对redis进行操作

127.0.0.1:6379> config get requirepass  # 先获取密码发现默认为空
1) "requirepass"
2) ""
127.0.0.1:6379> config set requirepass "lkw20020220"  # 设置密码
OK
127.0.0.1:6379> ping  # 发现在对redis进行操作之前,都需要进行权限的验证,没有权限不能对redis进行操作
(error) NOAUTH Authentication required.
127.0.0.1:6379> auth lkw20020220  # 使用正确的密码进行验证之后就可以成功操作数据库
OK
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) "lkw20020220"

还可以在配置文件里面设置密码

Redis学习_第13张图片

CLIENT 客户

maxclients 10000  # 设置能连接上redis的最大客户数
maxmemory  # redis配置最大的内存容量
maxmemory-policy noeviction  # 内存达到上限之后的处理策略
    # 移除一些过期的key
    # 报错
    # ……
    
  1.volatile-lru:只对设置了过期时间的key进行LRU(默认值)
  2.allkeys-lru:删除lru算法的key
  3.volatile-random:随机删除即将过期的key
  4.allkeys-random:随机删除
  5.volatile-ttl:删除即将国企的key
  6.noeviction:永不过期,返回错误
    

APPEND ONLY 模式 aof配置

appendonly  no  # 默认是不开启aof模式的,默认是使用rdb方式进行持久化的,在大部分情况下rdb够用了
appendfilename "appendonly.aof"  # 持久化之后生成的文件的名字

# appendfsync always  # 每次修改都会写入同步,这个时候速度比较慢
appendfsync everysec  # 每秒执行一次同步,可能会丢失这1s的数据
# appendfsync no      # 不执行同步,这个时候操作系统自己同步数据,速度最快

Redis持久化

RDB

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

RDB(Redis DataBase)

什么是RDB

Redis学习_第14张图片

在指定的时间间隔内,将内存中的数据集体写入到磁盘,也就是snapshot(快照),它恢复时是将快照文件直接读到内存中

Redis会单独创建(fork)一个子进程来进行持久化会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件,整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能,如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是特别敏感,那RDB的方式要比AOF的方式更加高效,RDB的缺点就是最后一次持久化后的数据可能丢失。我们默认使用的就是RDB,一般情况下不需要修改这个配置

rdb保存的文件是dump.rdb

在这里插入图片描述

触发机制

1.save的规则满足的条件下,会自动触发rdb机制

2.执行flushall命令的时候,会生成rdb文件

3.退出redis,生成rdb

备份就自动产生一个dump.rdb

如何恢复rdb文件

1.只需要将redis文件放在我们redis启动目录就可以了,redis启动的时候会自动检查dump.rdb恢复其中的数据

2.查看dump.rdb文件的存放位置

127.0.0.1:6379> config get dir
1) "dir"
2) "/usr/local/bin"  # 如果在这个目录下存在dump.rdb,redis启动的时候就会恢复其中的数据

优点:

1.适合大规模的数据恢复

2.对数据的完整性要求不高

缺点:

1.需要一定的时间间隔进行操作,如果redis意外宕机,最后一次修改的数据就不存在了

2.fork进程的时候,会占用一定的内存空间

save命令的使用方式就是先执行save命令,但是会阻塞所有的客户端进程,直到满足save命令的条件之后,就会执行rdb操作,然后把数据写入到磁盘中,生成dump.rdb文件,只有触发了save命令的条件,save命令才算执行结束,save命令就是用于redis数据安全的,用于备份数据

AOF(Append Only File)

将我们所有的命令记录下来,恢复的时候就是将所有的命令全部执行一遍

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

AOF保存到是appendonly.aof文件

AOF配置文件

Redis学习_第15张图片

默认是不开启的,我们需要手动配置aof,只需要将appendonly改为yes就开启了aof

重启redis就可以生效

Redis学习_第16张图片

因为aof文件就是保存数据库的数据状态的,可以通过aof文件查看执行过的命令
Redis学习_第17张图片

如果我们的aof文件有错位,redis不能正常启动,这时候需要修复aof文件,redis提供了一个工具redis-check-aof --fix,工具可以在 aof文件被恶意修改之后,依然恢复到原来的状态,然后完善aof文件

重写规则说明

aof默认是文件无限追加

在这里插入图片描述

如果aof文件大于64m,就会fork一个进程将我们的文件进行重写

优点和缺点

appendonly  no  # 默认是不开启aof模式的,默认是使用rdb方式进行持久化的,在大部分情况下rdb够用了
appendfilename "appendonly.aof"  # 持久化之后生成的文件的名字

# appendfsync always  # 每次修改都会写入同步,这个时候速度比较慢
appendfsync everysec  # 每秒执行一次同步,可能会丢失这1s的数据
# appendfsync no      # 不执行同步,这个时候操作系统自己同步数据,速度最快

# rewrite 

优点:

  1. 每次修改都同步,文件完整性更好
  2. 每秒同步一次,可能会丢失一秒钟的数据
  3. 从不同步,效率最高

缺点:

  1. 对于数据文件来说,aof远远大于rdb,修复的速度比rdb慢
  2. aof运行效率也要比rdb慢,redis默认的配置就是rdb

Redis发布订阅

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

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

Redis学习_第18张图片

Redis学习_第19张图片

测试

订阅端:

127.0.0.1:6379> subscribe lkw 
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "lkw"
3) (integer) 1
1) "message"
2) "lkw"
3) "hello,lkw"

发送端:

127.0.0.1:6379> publish lkw "hello,lkw"
(integer) 1

原理

redis是使用C实现的,通过分析redis源码里面的pubsub.c文件,了解发布和订阅机制的底层实现,

redis通过PUBLISH,SUBSCRIBE和RSUBSCRIBE等命令实现发布和订阅功能

通过SUBSCRIBE命令订阅某个频道后,redis-server里面维护了一个字典,字典的键就是一个个channel,而字典的值则是一个链表,链表中保存了所有订阅这个channel的客户端,SUBSCRIBE命令的关键,就是将客户端添加到给定channel的订阅链表中

通过PUBLISH命令向订阅者发布消息,redis-server会使用给定的频道作为键,在它所维护的channel字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发送给订阅者

PUB/SUB从字面上就是发布(PUBLISH)和订阅(SUBSCRIBE),在redis中,你可以设定对某一个key值进行消息发布及订阅消息,当一个key值进行了消息发布之后,所有订阅它的客户端就会接收到相应的消息,这一功能明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能

Redis主从复制

概念

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

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

主从复制的作用主要包括:

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

一般来说,要将redis运用于工程项目中,只使用一台redis服务器是不行的:

  1. 从结构上,单个redis服务器会发生单点故障,并且一台服务器需要承载所有的请求负载,压力太大
  2. 从容量上看,单个redis服务器内存容量有限
    Redis学习_第20张图片

主从复制,读写分离,80% 的情况下都是进行的读操作,这种架构可以明显减缓服务器的压力

环境配置

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

127.0.0.1:6379> info replication # 查看当前库的信息
# Replication
role:master   # 角色  master
connected_slaves:0  # 没有从机
master_replid:3fd5fcfeb1996e99058f5975aecac53e99c65670
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

复制3个配置文件,然后修改对应的信息

1.端口

2.pid名字

3.log文件名字

4.dump.rdb名字

修改完毕之后,启动3个redis服务器,可以通过进程信息查看

在这里插入图片描述

一主二从

默认情况下,所有的redis服务器都是主节点

一般情况下只需要配置从机就可以了

一主(79)二从(80,81)

从从机中查看信息

127.0.0.1:6380> info replication
# Replication
role:master
connected_slaves:0
master_replid:3d97b2198b8d980bd7e1d3a84cb6725f98b2bb1a
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
127.0.0.1:6380> slaveof 127.0.0.1 6379
OK
127.0.0.1:6380> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:5
master_sync_in_progress:0
slave_repl_offset:0
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:2a67c542832046072affdcba9350569420f797e9
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
rep_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:0

从主机中查看从机的信息

127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:0
master_replid:287a43adad396d14955863edb8c201463589a288
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6380,state=online,offset=56,lag=0
slave1:ip=127.0.0.1,port=6381,state=online,offset=56,lag=1
master_replid:2a67c542832046072affdcba9350569420f797e9
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:70
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:70

真实的配置是在配置文件中进行配置,这里使用的是命令行的方式进行配置的,不使永久生效的

细节

主机可以写,从机只能读不能写,主机中的所有信息和数据都会自动被从机保存

127.0.0.1:6379> set k1 v1  # 主机可以进行写
OK
127.0.0.1:6380> get k1  # 从机只能读
"v1"
127.0.0.1:6381> get k1
"v1"
127.0.0.1:6381> set k2 v2
(error) READONLY You can't write against a read only replica.  # 从机不可以写

测试

主机宕机,从机依然可以获取保存的数据,两个从机并没有确认新的主机,还是将原来的主机视为主机,主机从断开又重新连接,主机依然可以设置值

如果是使用命令行配置的主从关系,如果从机断开了,这时候如果使用主机设置了新的值,那么从机再次连接上的时候,从机获取不到新设置的值,但是使用配置文件的形式的话就可以获取最新设置的值,要想使用命令行的方式在从机中获取新设置的值,那么需要在这个从机中使用slaveof命令再次绑定主机才可以,因此主从复制的原因,从机就可以获取到最新设置的值

复制原理

slave命令启动成功,连接到master后会发送一个sync同步命令

master接到命令,启动后台的存盘进程,同时收集到所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master将传送整个数据文件到slave,并完成一次完全同步

全量复制:当slave接收到数据库文件数据之后,将其存盘并加载到内存中

增量复制:master继续将新的所有收集到的修改命令依次传递给slave,完成同步

但是只要是重新连接master,一次完全同步(全量复制)将被自动执行

代代相传模式
Redis学习_第21张图片

6379作为这条链的主节点,下面有一个从节点6380,然后6380之后又有一个从节点6381,这个时候,6380既作为主节点,又作为从节点

主节点宕机,选举新的主节点

手动方式:如果断开了连接,可以使用slaveof no one将自己作为主节点,如果这个时候最开始的主节点又重新连接上了,不会作为新的主节点,只有重新配置才可以

哨兵模式

自动选取主节点的模式

概述

主从切换技术的方法是:当主服务器宕机之后,需要手动把一台服务器切换为主服务器,这就需要进行人工干预,费时费力,还会造成一段时间内服务不可用,这不是一种推荐的方式,优先考虑哨兵模式,redis 2.8之后提供了sentinel(哨兵)来解决这个问题

能够后台监控主机是否故障,如果出现故障就会根据票数自动将从库切换为主库

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

这里的哨兵有两个作用

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

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

Redis学习_第23张图片

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

测试

1.配置哨兵配置文件sentinel.conf

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

后面这个数字1代表当主机宕机的时候,slave进行投票,然后获得票数最多的slave就会成为新的主机

2.启动哨兵

Redis学习_第24张图片

规则

如果主机回来了,只能归并到新的主机下,当作从机

哨兵模式的优缺点

优点:

  1. 哨兵集群,基于主从复制模式,所有的主从配置优点,它都有
  2. 主从可以切换,故障可以转移,系统的可用性更好
  3. 哨兵模式就是主从模式的升级,手动到自动,更加健壮

缺点:

  1. redis不好在线扩容,集群容量一旦达到上限,在线扩容就很麻烦
  2. 实现哨兵模式的配置很麻烦

哨兵模式的全部配置

# Example sentinel.conf
 
 
# 1、哨兵sentinel 实例运行的端口 默认26379
port 26379
 
# 2、 哨兵 sentinel 的工作目录
dir "/usr/local/bin"
 
# 3、哨兵sentinel监控的redis主节点 host port
#   - master-name 可以自己对 主节点 明明
#   - quorum      配置多少个sentinel 哨兵认为master 主节点失联,那么这个时候就客观的认为失联了
# sentinel monitor master-name host port quorum
sentinel monitor myredis 127.0.0.1 7371 1
 
 
# 4、在Redis实例中开启了密码,这时,所有连接Redis的客户端都需要密码
#    - 设置了哨兵sentinel 连接上主从的密码,注意必须设置一样的验证码
#    sentinel auth-pass master-name password
sentinel auth-pass myredis 123456
 
 
# 5、指定多少毫秒后 主节点没有回答哨兵sentinel 此时 哨兵主观上认为主节点离线  默认30秒
#   sentinel down-after-milliseconds  
sentinel down-after-milliseconds myredis 30000
 
# 6、这个配置指定了在发生failover 主备切换时最多可以由多少个slave同时对新的master进行同步
    - 这个数字越小,完成failover 所需的时间越长
    - 这个数字越大,就意味着越多的slave 因为repkication 而不可用
    - 可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态
#    sentinel parallel-syncs  
sentinel parallel-syncs mymaster 1
 
# 7、故障转移的时间 failover-timeout 可以用一下这些方面
   同一个sentinel 对同一个master 两次failover  之间的间隔时间
   当想要取消一个正在进行的fai1over所需要的时间,直到slave 被纠正为向正确的master那里同步数据时
    当进行fai1over时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,s1aves依然会被正确配置为指向 master,但是就不按para11e1-syncs所配置的规则来了
# sentinel failover-timeout   默认三分钟
sentinel failover-timeout mymaster 180000
 
# 8、配置当某一个事件发生时需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时,发送
    邮件通知相关人员
    - 对于脚本的运行结果有以下规则:
        1.若脚本执行后返回1,那么该脚本稍后会重新执行,重复次数默认为10
        2.若脚本执行后返回2,或者是比2更高的返回值,脚本将不会执行
        3.若脚本在执行过程中由于收到系统中断信号被终止了,则同返回值1的时候的相同
        4.一个脚本执行的最大时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,重新执行
    - 通知型脚本:当sentine1有任何警告级别的事件发生时(比如说re dis实例的主观失效和客观失效等等),将会去调用这个脚本,这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行行的信息。调用该脚本时,将传给脚本两个参数,一个是事件的类型,一个是事件的描述。如果sentine1.conf配置文件中配置了这个脚本路路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则 sentine1无法正常启动成功。
#     sentinel notification-script  
sentinel notification-script mymaster /var/redis/notify.sh
 
# 9、客户端重新配置主节点参数脚本
    - 当一个master 发生改变时,这个脚本就会被调用,通知相关的客户端关于 master 地址已经发生改变
    - 一下参数将会在调用脚本的时候传给脚本
        1. <master-name> <role> <state> <from-ip><from-port><to-ip><to-port>
        2. 目前<state>总是"failover”
        3. 是"leader"或者"observer”中的一个。
        4. 参数from-ip,from-port,to-ip,to-port是用来和旧的master和新的master(即旧的s1ave)通信的
        5. 这个脚本应该是通用的,能被多次调用,不是针对性的
#    sentinel client-reconfig-script  
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh

缓存穿透和雪崩

redis缓存的使用,极大提升了应用程序的性能和效率,特别是数据查询方面,但同时也带来了一些问题,其中,最要害的问题就是数据的一致性问题,从严格意义上讲,这个问题无解,如果对数据的一致性要求很高,就不能使用缓存

缓存穿透

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

解决方案

布隆过滤器

布隆过滤器是一种数据结构,对所有可能查询的参数以hash的形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力

Redis学习_第25张图片

缓存空对象

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

Redis学习_第26张图片

这种方式存在的问题:

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

缓存击穿

概述

缓存击穿是指一个key非常热点,在不停承受着大的并发,大并发集中这一个点进行访问,当这个key再失效的瞬间,持续的大并发就穿破缓存,直接请求数据库

当某个key在过期的瞬间,在大量的请求并发访问 ,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会使数据库瞬间压力过大

解决方案

设置热点数据缓存永不过期

从缓存层面来说,没有设置过期时间,所以不会出现热点key过期后产生的问题

加互斥锁

分布式锁:使用分布式锁,保证对于每个key,同时只有一个线程去查询后端数据,其他线程没有获取到分布式锁的权限,因此只需要等待即可,这种方式将压力转移到分布式锁,因此对分布式锁的考验很大

缓存雪崩

概述

缓存雪崩,是指某一段时间内,缓存集中过期失效,redis宕机

产生雪崩的原因之一,就是对缓存设置了一定的过期时间,然后在某一段时间段内刚好有大量的请求打到服务器上,但是这个时候缓存集体过期了,这个时候,缓存就被穿透了,会直接达到存储层,这个时候缓存层的压力就会很大

Redis学习_第27张图片

其中集中过期不是最致命的,最致命的是缓存服务器某个节点宕机或断网,因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,这个时候,数据库也是可以顶住压力的,而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,有可能瞬间把数据库压垮

解决方案

redis高可用

多增加几台redis服务器,这样一台宕机,还有其他的服务器可以继续工作,其实就是搭建一个集群

限流降级

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

数据预热

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

你可能感兴趣的:(redis,学习,数据库)