Redis基础

Redis

一、数据库

1、关系型数据库

采用了关系模型来组织数据的数据库。

关系模型指的就是二维表格模型,而一个关系型数据库就是由二维表及其之间的联系所组成的一个数据组织。

关系模型中常用的概念:

关系:一张二维表,每个关系都具有一个关系名,也就是表名

元组:二维表中的一行,在数据库中被称为记录

属性:二维表中的一列,在数据库中被称为字段

域:属性的取值范围,也就是数据库中某一列的取值限制

关键字:一组可以唯一标识元组的属性,数据库中常称为主键,由一个或多个列组成

关系模式:指对关系的描述。其格式为:关系名(属性1,属性2, … … ,属性N),在数据库中成为表结构

优点:

  1. 容易理解:二维表结构是非常贴近逻辑世界的一个概念,关系模型相对网状、层次等其他模型来说更容易理解

  2. 使用方便:通用的SQL语言使得操作关系型数据库非常方便

  3. 易于维护:丰富的完整性(实体完整性、参照完整性和用户定义的完整性)大大减低了数据冗余和数据不一致的概率

不足:

  1. 网站的用户并发性非常高,往往达到每秒上万次读写请求,对于传统关系型数据库来说,硬盘I/O是一个很大的瓶颈
  2. 网站每天产生的数据量是巨大的,对于关系型数据库来说,在一张包含海量数据的表中查询,效率是非常低的
  3. 在基于web的结构当中,数据库是最难进行横向扩展的,当一个应用系统的用户量和访问量与日俱增的时候,数据库却没有办法像web server和app server那样简单的通过添加更多的硬件和服务节点来扩展性能和负载能力。当需要对数据库系统进行升级和扩展时,往往需要停机维护和数据迁移。
  4. 性能欠佳:在关系型数据库中,导致性能欠佳的最主要原因是多表的关联查询,以及复杂的数据分析类型的复杂SQL报表查询。为了保证数据库的ACID特性,必须尽量按照其要求的范式进行设计,关系型数据库中的表都是存储一个格式化的数据结构。

2、非关系型数据库

nosql: not only sql,泛指非关系型数据库。

指非关系型的,分布式的,且一般不保证遵循ACID原则的数据存储系统。

非关系型数据库以键值对存储,且结构不固定,每一个元组可以有不一样的字段,每个元组可以根据需要增加一些自己的键值对,不局限于固定的结构,可以减少一些时间和空间的开销。

优点:

  1. 用户可以根据需要去添加自己需要的字段,为了获取用户的不同信息,不像关系型数据库中,要对多表进行关联查询。仅需要根据id取出相应的value就可以完成查询。
  2. 适用于SNS(Social Networking Services)中,例如facebook,微博。系统的升级,功能的增加,往往意味着数据结构巨大变动,这一点关系型数据库难以应付,需要新的结构化数据存储。由于不可能用一种数据结构化存储应付所有的新的需求,因此,非关系型数据库严格上不是一种数据库,应该是一种数据结构化存储方法的集合。

不足:

  • 只适合存储一些较为简单的数据,对于需要进行较复杂查询的数据,关系型数据库显的更为合适。不适合持久存储海量数据

二、Redis概述

Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

redis支持各种不同方式的排序,与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。

作用:

  1. 内存存储,持久化。
  2. 效率高,可以用于高速缓存。
  3. 发布订阅系统。
  4. 地图信息分析。
  5. 计时器,计数器(浏览量)

特性:

  1. 多样的数据类型
  2. 持久化
  3. 集群
  4. 事务

基本知识:

  • redis有16个数据库,默认为0号数据库,可通过select index切换数据库
  • keys *查看所有key
  • 清空当前数据库:flushdb
  • 清空全部数据库:flushall
  • redis是单线程的。官方表示,redis是基于内存操作,CPU不是性能瓶颈,redis的瓶颈是根据机器的内存和网络带宽
  • redis是将所有的数据放在内存中的,由于多线程操作进行上下文切换会消耗不必要的时间,所以redis使用单线程

三、安装

1、Windows

  1. 下载并解压
  2. 打开服务:redis-server.exe
  3. 打开客户端:redis-cli.exe

2、Linux

  1. 下载至usr/local并解压,将文件夹改名为redis
  2. 安装gcc环境:yum -y install gcc
  3. 进入解压后的Redis目录,编译:make
  4. 进入src目录,安装:make PREFIX=/usr/local/redis install,PREFIX为指定安装目录,默认路径为/usr/local/bin
  5. usr/local/redis/src/bin目录下即可看到redis-server等文件即为安装成功
  6. usr/local/redis/src/bin目录下创建文件夹configmkdir config
  7. usr/local/redis下的redis.conf配置文件复制到新建的config文件夹中:cp /usr/local/redis/redis.conf config
  8. 修改配置文件,使redis后台启动:daemonize no改成daemonize yes
  9. 启动:在usr/local/redis/src/bin目录下,将文件复制到usr/local/bin中,并将权限改成755,回到usr/local/redis/src/bin,执行:redis-server config/redis.conf
  10. 运行:redis-cli -p 6379
  11. 退出:shutdownexit

设置密码:

  1. 修改配置文件:将配置文件中的requirepass的注释取消,在该字段后面修改成自己的密码
  2. 重启redis
  3. auth password登录

在jedis中:jedis.auth("password");

四、性能测试

redis-benchmark为官方的压力测试工具。

redis 性能测试工具可选参数如下所示:

序号选项描述默认值
1-h指定服务器主机名127.0.0.1
2-p指定服务器端口6379
3-s指定服务器 socket
4-c指定并发连接数50
5-n指定请求数10000
6-d以字节的形式指定 SET/GET 值的数据大小2
7-k1=keep alive 0=reconnect1
8-rSET/GET/INCR 使用随机 key, SADD 使用随机值
9-P通过管道传输 请求1
10-q强制退出 redis。仅显示 query/sec 值
11–csv以 CSV 格式输出
12*-l*(L 的小写字母)生成循环,永久执行测试
13-t仅运行以逗号分隔的测试命令列表。
14*-I*(i 的大写字母)Idle 模式。仅打开 N 个 idle 连接并等待。

示例:

启动

redis-server config/redis.conf

测试

# 主机为localhost,端口号为6379,并发量为100,请求数100000,执行命令为set
redis-benchmark -h localhost -p 6379 -c 100 -n 100000 -t set

结果

====== SET ======                                                     
  100000 requests completed in 0.93 seconds   #100000个请求花费的时间
  100 parallel clients   #100个客户端请求,即100并发
  3 bytes payload   #每次写入3字节
  keep alive: 1   #每次请求结束后不中断连接
  host configuration "save": 3600 1 300 100 60 10000
  host configuration "appendonly": no
  multi-thread: no

Latency by percentile distribution:
0.000% <= 0.103 milliseconds (cumulative count 1)
50.000% <= 0.471 milliseconds (cumulative count 58106)
75.000% <= 0.487 milliseconds (cumulative count 77202)
87.500% <= 0.519 milliseconds (cumulative count 88643)
93.750% <= 0.599 milliseconds (cumulative count 94069)
96.875% <= 0.663 milliseconds (cumulative count 97013)
98.438% <= 0.775 milliseconds (cumulative count 98444)
99.219% <= 0.951 milliseconds (cumulative count 99244)
99.609% <= 1.175 milliseconds (cumulative count 99614)
99.805% <= 1.359 milliseconds (cumulative count 99805)
99.902% <= 1.671 milliseconds (cumulative count 99904)
99.951% <= 2.079 milliseconds (cumulative count 99953)
99.976% <= 2.455 milliseconds (cumulative count 99976)
99.988% <= 2.719 milliseconds (cumulative count 99988)
99.994% <= 2.855 milliseconds (cumulative count 99994)
99.997% <= 2.911 milliseconds (cumulative count 99997)
99.998% <= 2.935 milliseconds (cumulative count 99999)
99.999% <= 2.943 milliseconds (cumulative count 100000)
100.000% <= 2.943 milliseconds (cumulative count 100000)

Cumulative distribution of latencies:
0.001% <= 0.103 milliseconds (cumulative count 1)
0.062% <= 0.207 milliseconds (cumulative count 62)
0.253% <= 0.303 milliseconds (cumulative count 253)
1.374% <= 0.407 milliseconds (cumulative count 1374)
85.212% <= 0.503 milliseconds (cumulative count 85212)
94.496% <= 0.607 milliseconds (cumulative count 94496)
97.755% <= 0.703 milliseconds (cumulative count 97755)
98.716% <= 0.807 milliseconds (cumulative count 98716)
99.081% <= 0.903 milliseconds (cumulative count 99081)
99.394% <= 1.007 milliseconds (cumulative count 99394)
99.525% <= 1.103 milliseconds (cumulative count 99525)
99.648% <= 1.207 milliseconds (cumulative count 99648)
99.760% <= 1.303 milliseconds (cumulative count 99760)
99.822% <= 1.407 milliseconds (cumulative count 99822)
99.850% <= 1.503 milliseconds (cumulative count 99850)
99.883% <= 1.607 milliseconds (cumulative count 99883)
99.909% <= 1.703 milliseconds (cumulative count 99909)
99.916% <= 1.807 milliseconds (cumulative count 99916)
99.925% <= 1.903 milliseconds (cumulative count 99925)
99.941% <= 2.007 milliseconds (cumulative count 99941)
99.955% <= 2.103 milliseconds (cumulative count 99955)
100.000% <= 3.103 milliseconds (cumulative count 100000)

Summary:
  throughput summary: 107181.13 requests per second   #每秒处理107181
  latency summary (msec):
          avg       min       p50       p95       p99       max
        0.487     0.096     0.471     0.623     0.887     2.943

五、五大数据类型

1、String

string 是 redis 最基本的类型,一个 key 对应一个 value。

string 类型是二进制安全的。意思是 redis 的 string 可以包含任何数据。比如jpg图片或者序列化的对象。

string 类型是 Redis 最基本的数据类型,string 类型的值最大能存储 512MB。

命令描述示例
get获取key的valueget key
set设置keyset key value
exists判断一个key是否存在exists key
append追加字符串,若该key不存在则创建append key “string”
strlen获取字符串长度strlen key
incr自增1incr key
decr自减1decr key
incrby自增指定数额incrby key 10
decrby自减指定数额decrby key 10
getrange截取字符串getrange key 0 3
getrange key 0 -1
setrange替换自定位置开始的字符串setrange key 3 xx
setex设置过期时间setex key 30 “string”
setnx当该key不存在是才可以设置值,当该key存在时设置值会失败setnx key “string”
mset设置多个键值mset k1 v1 k2 v2 k3 v3
mget获取多个键的值mget k1 k2 k3
msetnx类似setnx,当key不存在才可以设置值,该命令为原子性,要么全部成功,要么全部失败msetnx k1 v1 k2 v2
设置对象可以利用mset,mget设置与获取对象mset user:1:name zhangsan user:1:age 18
mget user:1:name user:1:age
getset先获取key的值再设置key的值getset key “string”

2、Hash

Redis hash 是一个键值(key=>value)对集合。

Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。

每个 hash 可以存储 2^32 -1 键值对(40多亿)。

命令描述示例
hset设置一个具体的key-valuehset hash field “string”
hget获取一个具体的字段值hget hash field
hmset设置多个key-valuehmset hash field1 v1 field2 v2
hmget获取多个字段值hmget hash field1 field2
hgetall获取全部字段值hgetall hash
hdel删除指定字段hdel hash field
hlen获取hash表的字段数量hlen myhash
hexists判断hash中指定字段是否存在hexists hash field
hkeys获取全部字段hkeys hash
hvals获取全部valuehvals hash
hincrby指定增量,类似incrbyhincrby hash field 1
hsetnx类似setnxhsetnx hash field “string”

3、List

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

根据使用的不同,可以把List当成队列或者栈。

列表最多可存储 2^32 - 1 元素 (40多亿)。

命令描述示例
lpush将一个或多个值插入到列表头部(左边)lpush list one two three
rpush将一个或多个值插入到列表尾部(右边)rpush list one two three
lrange获取list中的值lrange list 0 -1
lrange list 0 2
lpop移除list的第一个元素(最左边)lpop list
rpop移除list的最后一个元素(最右边)rpop list
lindex通过下标获取list的某一个值lindex list 2
llen获取列表的长度llen list
lrem移除list中指定个数的value,精确匹配lrem list 2 three
ltrim获取指定范围的下标元素ltrim list 2 5
rpoplpush移除list中最后一个元素到另一个listrpoplpush list1 list2
lset将list中指定下标的值替换为另一个值lset list 0 value
linsert将一个值添加到指定的value的前面或后面linsert list after value value1
linsert list before value value2

4、Set

Redis 的 Set 是 string 类型的无序集合。

Set集合里的元素不允许重复。

集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。

集合中最大的成员数为 2^32 - 1(40多亿)。

命令描述示例
sadd向set中添加值sadd set “string”
smembers获取指定set的所有值smembers set
sismember判断set中是否存在该值sismember set value
scard获取set中元素个数scard set
srem移除set中的指定元素srem set value
srandmember随机获取set中的一个或指定个数元素srandmember set
srandmember set 2
spop随机删除并返回set中的一个元素spop set
smove将一个指定的值,移动到另一个set中smove set set1
sdiff获取多个set中的差集sdiff set1 set2
sinter获取多个set中的交集sinter set1 set 2
sunion获取多个set中的并集sunion set1 set2

5、Zset

Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。

不同的是Zset每个元素都会关联一个double类型的分数score。redis正是通过分数来为集合中的成员进行从小到大的排序。

zset的成员是唯一的,但分数(score)却可以重复。

命令描述示例
zadd设置一个或多个值zadd set 1one 2 two
zrangebyscore从小到大输出全部元素
withscores:附带分数
zrangebyscore zset -inf + inf
zrangebyscore zset -inf + inf withscores
zrem移除指定元素zrem zset value
zcard获取集合中的个数zcard zset
zcount获取指定区间的元素数量zcount zset 1 5

6、应用场景

类型简介特性场景
String(字符串)二进制安全可以包含任何数据,比如jpg图片或者序列化的对象,一个键最大能存储512M任何场景
Hash(字典)键值对集合,即编程语言中的Map类型适合存储对象,并且可以像数据库中update一个属性一样只修改某一项属性值(Memcached中需要取出整个字符串反序列化成对象修改完再序列化存回去)存储、读取、修改用户属性
List(列表)链表(双向链表)增删快,提供了操作某一段元素的API1,最新消息排行等功能(比如朋友圈的时间线) 2,消息队列
Set(集合)哈希表实现,元素不重复1、添加、删除,查找的复杂度都是O(1) 2、为集合提供了求交集、并集、差集等操作1、共同好友 2、利用唯一性,统计访问网站的所有独立ip 3、好友推荐时,根据tag求交集,大于某个阈值就可以推荐
Sorted Set(有序集合)将Set中的元素增加一个权重参数score,元素按score有序排列数据插入集合时,已经进行天然排序1、排行榜 2、带权重的消息队列

六、三大特殊数据类型

1、Geospatial

Geospatial:地理位置。

将指定的地理空间位置(纬度、经度、名称)添加到指定的key中。这些数据将会存储到sorted set,其目的是为了方便使用GEORADIUS或者GEORADIUSBYMEMBER命令对数据进行半径查询等操作。

该命令以采用标准格式的参数x,y,所以经度必须在纬度之前。这些坐标的限制是可以被编入索引的,区域面积可以很接近极点但是不能索引。具体的限制,由EPSG:900913 / EPSG:3785 / OSGEO:41001 规定如下:

  • 有效的经度从-180度到180度。
  • 有效的纬度从-85.05112878度到85.05112878度。

当坐标位置超出上述指定范围时,该命令将会返回一个错误。

场景:朋友圈定位,附近的人,打车距离

该类型只有六个命令。

1.GEOADD

添加地理位置,可添加单个也可多个。

南极和北极无法添加。

geoadd China:city 114.09 22.55 shenzhen
geoadd China:city 120.15 30.29 hangzhou 120.62 31.30 suzhou

2.GEOPOS

key里返回所有给定位置元素的位置(经度和纬度)。

geopos China:city shenzhen
geopos China:city guangzhou shanghai

3.GEODIST

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

如果两个位置之间的其中一个不存在, 那么命令返回空值。

指定单位的参数 unit 必须是以下单位的其中一个:

  • m 表示单位为米。
  • km 表示单位为千米。
  • mi 表示单位为英里。
  • ft 表示单位为英尺。

如果用户没有显式地指定单位参数, 那么 GEODIST 默认使用米作为单位。

GEODIST 命令在计算距离时会假设地球为完美的球形, 在极限情况下, 这一假设最大会造成 0.5% 的误差。

geodist China:city beijing shanghai km

4.GEORADIUS

以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。

范围可以使用以下其中一个单位:

  • m 表示单位为米。
  • km 表示单位为千米。
  • mi 表示单位为英里。
  • ft 表示单位为英尺。

在给定以下可选项时, 命令会返回额外的信息:

  • WITHDIST: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。 距离的单位和用户给定的范围单位保持一致。
  • WITHCOORD: 将位置元素的经度和维度也一并返回。
  • WITHHASH: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。

命令默认返回未排序的位置元素。 通过以下两个参数, 用户可以指定被返回位置元素的排序方式:

  • ASC: 根据中心的位置, 按照从近到远的方式返回位置元素。
  • DESC: 根据中心的位置, 按照从远到近的方式返回位置元素。

在默认情况下, GEORADIUS 命令会返回所有匹配的位置元素。 虽然用户可以使用 COUNT <count> 选项去获取前 N 个匹配元素, 但是因为命令在内部可能会需要对所有被匹配的元素进行处理, 所以在对一个非常大的区域进行搜索时, 即使只使用 COUNT 选项去获取少量元素, 命令的执行速度也可能会非常慢。 但是从另一方面来说, 使用 COUNT 选项去减少需要返回的元素数量, 对于减少带宽来说仍然是非常有用的。

场景:附近的人。

#获取China:city集合中,中心经纬度为110 30,半径为1000km的所有位置
georadius China:city 110 30 1000 km

#获取China:city集合中,中心经纬度为110 30,半径为1000km的所有位置,并将经纬度一并返回
georadius China:city 110 30 1000 km withcoord

#获取China:city集合中,中心经纬度为110 30,半径为1000km的所有位置,并返回与中心的直线距离
georadius China:city 110 30 1000 km withdist

#获取China:city集合中,中心经纬度为110 30,半径为1000km的所有位置,只返回其中的2个
georadius China:city 110 30 1000 km count 2

5.GEORADIUSBYMEMBER

这个命令和 GEORADIUS命令一样, 都可以找出位于指定范围内的元素, 但是 GEORADIUSBYMEMBER 的中心点是由给定的位置元素决定的, 而不是像 GEORADIUS那样, 使用输入的经度和纬度来决定中心点。

georadiusbymember China:city shanghai 500 km

6.GEOHASH

返回一个或多个位置元素的 Geohash 表示。

该命令会返回11位的Geohash字符串,将二维的经纬度转换为一维的字符串,如果这两个字符串越接近,则距离越近。

geohash China:city beijing shanghai

1) "wx4fbzx4me0"
2) "wtw3sj5zbj0"

GEO的底层原理是Zset,所以可以用Zset的命令来操作GEO

2、Hyperloglog

基数:多个集合中不重复的元素的个数。

基数估计:在误差可接受的范围内,快速计算基数。

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

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

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

HyperLogLog的错误率仅为0.81%,在统计UV中,几乎可以忽略不计。

场景:网站的UV(访问网站的用户数,单个用户访问网站多次算一个用户)。

1.PFADD

添加指定元素到 HyperLogLog 中。

pfadd num 1 3 4 5 6 7 8
pfadd num2 3 5 7 4 2 8

2.PFCOUNT

统计key的基数数量。

pfcount num

3.PFMERGE

合并多个key到一个key中。

pfmerge num3 num num2

3、Bitmap

bitmap位图,操作二进制位来记录,只有0和1。

场景:登录,打卡。

1.SETBIT

添加bitmap

#设置一周的签到
#setbit key id value(0或1)

setbit sign 1 1
setbit sign 2 0
setbit sign 3 1
setbit sign 4 1
setbit sign 5 0
setbit sign 6 1
setbit sign 7 1

2.GETBIT

获取bitmap

#getbit key id

getbit sign 2

3.BITCOUNT

统计一个key中value为1的数量

bitcount sign

七、事务

redis事务:一组命令的集合。

一个事务中的所有命令会被序列化,在事务执行过程中,会按顺序执行。

redis事务具有:一次性顺序性排他性

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

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

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

事务执行完毕就结束了,是一次性的。

1.redis事务:

  • 开启事务(multi)
  • 命令入队(自定义命令)
  • 执行事务(exec)
127.0.0.1:6379> multi			#开启事务
OK
127.0.0.1:6379(TX)> set k1 v1			#命令入队
QUEUED
127.0.0.1:6379(TX)> set k2 v2			#命令入队
QUEUED
127.0.0.1:6379(TX)> get k2				#命令入队
QUEUED
127.0.0.1:6379(TX)> set k3 v3			#命令入队
QUEUED
127.0.0.1:6379(TX)> exec		#执行事务
1) OK
2) OK
3) "v2"
4) OK

2.放弃事务:discard

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set v1 k1
QUEUED
127.0.0.1:6379(TX)> set v2 k2
QUEUED
127.0.0.1:6379(TX)> discard
OK
127.0.0.1:6379> get v2
(nil)

3.异常

  • 编译型异常,代码有问题,命令有错误,事务中所有命令都不会执行。
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> setget 3		#错误的命令
(error) ERR unknown command `setget`, with args beginning with: `3`, 
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> exec		#所有的命令都不会执行
(error) EXECABORT Transaction discarded because of previous errors.
  • 运行时异常,如果事务中某个命令发生了运行时异常,那么执行事务时该命令报错,其他命令会正常执行。
127.0.0.1:6379> set k1 "v1"			#设置k1为字符串
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incr k1			#将k1自增1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> exec
1) (error) ERR value is not an integer or out of range
2) OK

4.watch

watch在redis中意为监视,即给某个key加上乐观锁。

正常操作如下:

127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> watch money			#监视money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby money 20
QUEUED
127.0.0.1:6379(TX)> incrby out 20
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 80
2) (integer) 20

触发锁:

127.0.0.1:6379> watch money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby money 10
QUEUED
127.0.0.1:6379(TX)> incrby out 10
QUEUED
127.0.0.1:6379(TX)> exec			#在未执行事务前开启另一个客户端修改money的值:set money 1000
(nil)								#返回空,证明事务执行失败

事务执行失败后,可通过unwatch解锁后再次加锁执行

127.0.0.1:6379> unwatch
OK
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby money 10
QUEUED
127.0.0.1:6379(TX)> incrby out 10
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 990
2) (integer) 30
127.0.0.1:6379> 

八、Jedis

jedis是redis官方推荐的的java连接开发工具。

依赖:

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

1、连接远程redis

1.修改远程redis配置

#1.在redis.conf中注释掉bind 127.0.0.1 -::1

#2.修改protected-mode yes为no
protected-mode no

#3.关闭防火墙
systemctl stop firewalld.service

#4.关闭redis服务再重新启动

2.连接

public class Test {
    public static void main(String[] args) {
        //创建redis对象
        Jedis jedis = new Jedis("ip", 6379);
        //使用,输出pong即为连接成功
        System.out.println(jedis.ping());
    }
}

2、常用API

System.out.println(jedis.ping());
System.out.println("清空数据库:" + jedis.flushDB());
System.out.println("判断某个键是否存在:" +jedis.exists("k1"));
System.out.println("新增k1,v1键值对:"+jedis.set("k1","v1"));
System.out.println("获取k1的值:"+jedis.get("k1"));
System.out.println("系统中所有的键如下:");
Set<String> keys = jedis.keys("*");
System.out.println(keys);
System.out.println("删除键k2:"+jedis.del("k2"));
System.out.println("判断键k2是否存在:"+jedis.exists("k2"));
System.out.println("查看键 k1 所存储的数据类型:"+jedis.type("k1"));
System.out.println("随机返回 key 空间的一个:"+jedis.randomKey());
System.out.println("重命名 key :"+jedis.rename("k1","newk1"));
System.out.println("取出改后的 newk1 :"+jedis.get("newk1"));
System.out.println("按索引查询:"+jedis.select(0));
System.out.println("删除当前选择数据库中的所有键:"+jedis.flushDB());
System.out.println("返回当前数据库中 key 的数量:"+jedis.dbSize());
System.out.println("删除所有数据库中的所有 key:"+jedis.flushAll());
jedis.close();

其他API与redis中的命令一致。

3、事务

public class TestTX {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("119.91.154.95", 6379);
        System.out.println(jedis.ping());
        jedis.flushDB();
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("hello", "world");
        jsonObject.put("name", "srx");
        //开启事务
        Transaction transaction = transaction = jedis.multi();
        String result = jsonObject.toJSONString();
        try {
            transaction.set("user1", result);
            transaction.set("user2", result);
            //执行事务
            transaction.exec();
        } catch (Exception e) {
            //异常,放弃事务
            transaction.discard();
            e.printStackTrace();
        } finally {
            System.out.println(jedis.get("user1"));
            System.out.println(jedis.get("user2"));
            //关闭事务
            jedis.close();
        }
    }
}

九、springboot集成redis

在springboot2.x之后,原来使用的jedis被替换成了lettuce。

jedis:采用的是直连,多个线程操作的话是不安全的,若想避免不安全的情况,则使用jedis pool连接池,类似BIO模式。

lettuce:采用netty,实例可以在多个线程中共享,不存在线程不安全的情况,类似NIO模式。

开始集成:

1.创建springboot项目

在创建springboot项目时在非关系型数据库选项卡中添加Spring Data Redis

2.配置连接

在application配置文件中添加如下配置

redis:
  host: ip
  port: 6379
  password: password

3.测试连接

@SpringBootTest
class SpringRedisApplicationTests {

    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    void contextLoads() {
        /*
        opsForValue()  操作字符串
        opsForList()   操作List
        opsForSet()    操作Set
        。。。
        常用的一些方法可以直接通过 redisTemplate 操作
        redisTemplate.multi();
         */

        //创建连接
//        RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
//        connection.flushAll();
//        connection.flushDb();

        //使用
        redisTemplate.opsForValue().set("name", "zhangsan");
        System.out.println(redisTemplate.opsForValue().get("name"));
    }

}

1、自定义RedisTemplate

1.创建配置类

@Configuration
public class RedisConfig {
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory){
        //使用 <String, Object>类型
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.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);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        //String 的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        //key 采用String 的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        //hash 的key 也采用String 的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        //value 序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //hash 的value序列化方式采用Jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();

        return template;
    }
}

2.使用

@Autowired
private RedisTemplate redisTemplate;

使用时需要确保装配到了该配置类。

2、自定义工具类

前往查看工具类代码及使用

3、注解使用

使用注解时若是方法的参数为对象,则该类需要序列化,否则无法存入redis缓存导致报错。

缓存配置

在RedisConfig配置类中加入如下配置:

@Configuration
public class RedisConfig {
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory){
        //使用 <String, Object>类型
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.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);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        //String 的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        //key 采用String 的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        //hash 的key 也采用String 的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        //value 序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //hash 的value序列化方式采用Jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();

        return template;
    }
    
    /**
     * 基于SpringBoot2 对 RedisCacheManager 的自定义配置
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        //初始化一个RedisCacheWriter
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
        //设置CacheManager的值序列化方式为json序列化
        RedisSerializer<Object> jsonSerializer = new GenericJackson2JsonRedisSerializer();
        RedisSerializationContext.SerializationPair<Object> pair = RedisSerializationContext.SerializationPair.fromSerializer(jsonSerializer);
        RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(pair);

        //设置默认超过时期是1天
        defaultCacheConfig.entryTtl(Duration.ofDays(1));
        //初始化RedisCacheManager
        return new RedisCacheManager(redisCacheWriter, defaultCacheConfig);
    }
}

@EnableCaching

该注解放于启动类上,用于启动redis缓存

@Cacheable

该注解主要针对方法配置,能够根据方法的请求参数对其结果进行缓存

参数解释example
value缓存的名称,在 spring 配置文件中定义,必须指定至少一个@Cacheable(value=“mycache”)
@Cacheable(value={“cache1”,“cache2”}
key缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合@Cacheable(value=“testcache”,key=“#uid”)
@Cacheable(value=“testcache”,key=“#user.getUid()”)
condition缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存@Cacheable(value=“testcache”,condition=“”#uid.length()>2")
//@Cacheable(value=”user”,key="#uid"),这个注释的意思是,当调用这个方法的时候,会从一个名叫 user::uid 的缓存中查询,如果没有,则执行实际的方法(即查询数据库),
//并将执行的结果存入缓存中,否则返回缓存中的对象。这里的缓存中的 key 就是参数 uid,value 就是 user 对象。
@Cacheable(value="user",key="#uid")
public User getUser(Integer uid) {
    return getUserByUid(uid);
}

@CachePut

该注解的作用主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用

注意:该注解的value和key需要和@Cacheable里的一致才可以对缓存进行更新

参数解释example
value缓存的名称,在 spring 配置文件中定义,必须指定至少一个@CachePut(value=“mycache”)
key缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合@CachePut(value=“testcache”,key=“#uid”)
condition缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存@CachePut(value=“testcache”,condition="#uid.length()>2)
//@CachePut 注释,这个注释可以确保方法被执行,同时方法的返回值也被记录到缓存中,实现缓存与数据库的同步更新。
@CachePut(value="user",key="#user.getUid()")// 更新 user 缓存
public User updateUser(User user) { 
  return updateUser(user); 
} 

@CacheEvict

该注解的作用主要针对方法配置,能够根据一定的条件对缓存进行清空

参数解释example
value缓存的名称,在 spring 配置文件中定义,必须指定至少一个@CacheEvict(value=“my cache”)
key缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合@CacheEvict(value=“testcache”,key=“#uid”)
condition缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存@CacheEvict(value=“testcache”,condition=“#uidlength()>2”)
allEntries是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存@CachEvict(value=“testcache”,allEntries=true)
beforeInvocation是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存@CachEvict(value=“testcache”,beforeInvocation=true)
//@CacheEvict(value="user",key="#user.getUid()")// 清空 user 缓存 
public void updpateUser(User user) {
   updateUser(user); 
} 
 
//@CacheEvict(value="user",allEntries=true)// 清空 user 缓存
public void reload() {
   reloadAll()
}
 
//@Cacheable(value="user",condition="#uid.length() <=4")// 缓存名叫 user 
public User getUser(Integer uid) { 
	return getUserByUid(uid); 
}

@CacheConfig

该注解的作用的做一些配置,例如@CacheEvict、@CachePut和@Cacheable中都有一个value的配置,可以在@CacheConfig注解中统一设置,这样就不需要每一个注解都设置一遍了。

@CacheConfig("user")
public class UserServiceImpl implements UserService {
  @Cacheable
  public User getUser(Integer uid) {...}
}

条件缓存

//@Cacheable将在执行方法之前( #result还拿不到返回值)判断condition,如果返回true,则查缓存; 
@Cacheable(value = "user", key = "#id", condition = "#id lt 10")
public User conditionFindById(final Long id) 
 
//@CachePut将在执行完方法后(#result就能拿到返回值了)判断condition,如果返回true,则放入缓存; 
@CachePut(value = "user", key = "#id", condition = "#result.username ne 'zhang'") 
public User conditionSave(final User user)  
 
//@CachePut将在执行完方法后(#result就能拿到返回值了)判断unless,如果返回false,则放入缓存;(即跟condition相反)
@CachePut(value = "user", key = "#user.id", unless = "#result.username eq 'zhang'")
public User conditionSave2(final User user)  
 
//@CacheEvict, beforeInvocation=false表示在方法执行之后调用(#result能拿到返回值了);且判断condition,如果返回true,则移除缓存;
@CacheEvict(value = "user", key = "#user.id", beforeInvocation = false, condition = "#result.username ne 'zhang'") 
public User conditionDelete(final User user) 

@Caching

有时候我们可能组合多个Cache注解使用。比如用户新增成功后,我们要添加id–>user;username—>user;email—>user的缓存;此时就需要@Caching组合多个注解标签了。

@Caching(put = {
    @CachePut(value = "user", key = "#user.id"),
    @CachePut(value = "user", key = "#user.username"),
    @CachePut(value = "user", key = "#user.email")
})
public User save(User user) {
 	//TODO
}

十、redis.conf

网络

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

通用设置

daemonize yes   #后台运行,默认为no

#日志
# Specify the server verbosity level.
# This can be one of:
# 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   #默认数据库数量

快照

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

#在900s内,若是有超过1个 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文件
rdbchecksum yes   #保存rdb文件时,是否进行错误检验
dir ./   #rdb文件保存目录

安全

requirepass foobared   #设置密码,默认不设置,foobared为默认密码

客户端

maxclients 10000   #能连接redis的最大客户端数量
maxmemory <bytes>   #配置最大内存容量
maxmemory policy noeviction   #内存达到上限的处理策略

APPEND ONLY模式,aof配置

appendonly no   #是否开启aof模式,默认是使用rdb模式的
appendfilename "appendonly.aof"   #aof持久化的文件名

# appendfsync always   #每次修改都同步
appendfsync everysec   #每秒执行一次同步,可能会丢失这1s的数据
# appendfsync no	   #不同步

十一、持久化

redis是内存数据库,若没有持久化,当redis异常关闭时数据会丢失。

1、RDB(redis database)

在指定的间隔时间内将内存中的数据集快照写入磁盘,恢复时是将快照文件直接读到内存里。

Redis会单独创建(fork)一个子进程来进行持久化,会将数据写入到一个临时文件中,待持久化结束了,再将这个临时文件替换上次的持久化文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能。如果需要进行大规模的数据恢复,且对于恢复的数据完整性不是特别的敏感,那RDB模式比AOF模式要高效得多。

rdb保存的文件是:dump.rdb

触发机制

  1. 满足save配置的情况会自动持久化
  2. 退出redis

恢复

只需要将rdb文件放在redis的启动目录下,当redis启动时,会自动检测rdb文件进行数据恢复。

优点:

  1. 适合大规模的数据恢复
  2. 对数据的完整性要求不高

缺点:

  1. 需要一定的时间间隔进行操作,若是在操作过程中宕机了,则数据就消失了
  2. fork进程的时候,会占用一定的内存空间

2、AOF(addend only file)

以日志的形式来记录每个写操作,将redis执行过的所有写命令记录下来(读命令不记录),只允许追加文件不可以改写文件,redis启动时会读取该文件重新构建数据,即redis启动时会将日志里的命令重新执行一次,达到回复数据的目的。

aof保存的文件是:appendonly.aof

aof默认是不开启的,需要手动开启。

如果aof文件出错了,或被异常更改了,redis将会无法启动,此时可执行:redis-check-aof --fixe appendonly.aof进行修复操作,该修复操作会将错误的命令删除。

十二、发布订阅

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

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

发布订阅命令:

命令描述
psubscribe pattern [pattern …]订阅一个或多个符合给定模式的频道
pubsub subcommand [argument [argument …]]查看订阅与发布系统状态
publish channel message将信息发送到指定的频道
punsubscribe [pattern [pattern …]]退订所有给定模式的频道
subscribe channel [channel …]订阅给定的一个或多个频道的信息
unsubscribe [channel [channel …]]退订给定的频道

示例

订阅端:

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

发布端:

127.0.0.1:6379> publish boke "hello,word"
(integer) 2

原理

Redis通过PUBLISHSUBSCRIBEPSUBSCRIBE等命令实现发布和订阅功能。

通过SUBSCRIBE命令订阅某频道后,redis-server里维护了一个字典,字典的键就是一个个channel,而字典的值则是一个链表,链表中保存了所有订阅这个channel的客户端。SUBSCRIBE 命令的关键,就是将客户端添加到给定channel的订阅链表中。通过PUBLISH命令向订阅者发送消息,redis-server 会使用给定的频道作为键,在它所维护的channel字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者。

Pub/Sub从字面上理解就是发布( Publish)与订阅( Subscribe ) ,在Redis中,可以设定对某一个key值进行消息发布及消息订阅,当一个key值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,微信公众号,B站关注等。

十三、主从复制

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

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

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

  1. 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
  2. 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
  3. 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用主节点,读Redis数据时应用从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
  4. 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。一般来说,要将Redis运用于工程项目中,只使用一台Redis是万万不能的,原因如下:
    1、从结构上,单个Redis服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大;
    2、从容量上,单个Redis服务器内存容量有限,就算一台Redis服务器内存容量为256G,也不能将所有内存用作Redis存储内存,一般来说,单台Redis最大使用内存不应该超过20G

其架构如下:

配置

只需要配置从机,不需要配置主机。

127.0.0.1:6379> info replication			#查看当前信息
# Replication
role:master			#角色为master,主机
connected_slaves:0  #从机为0个
master_failover_state:no-failover
master_replid:8aeb661d961cfcd1c694c156fda704a996437c7d
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

从机需要修改的配置:

port 6379		#端口号
pidfile /var/run/redis_6379.pid		#pid
logfile ""		#日志文件名
dbfilename dump.rdb			#rdb文件名
replicaof <masterip> <masterport>		#配置所对应的主机
masterauth <yourpassword>	#主机的密码

配置修改完成后,分别开启redis服务即可。

在主机客户端中即可查看从机连接数:

127.0.0.1:6379> info replication
# Replication
role:master			#主机
connected_slaves:3	#从机连接数
slave0:ip=127.0.0.1,port=6380,state=online,offset=294,lag=0		#从机信息
slave1:ip=127.0.0.1,port=6381,state=online,offset=294,lag=1		#从机信息
slave2:ip=127.0.0.1,port=6382,state=online,offset=294,lag=0		#从机信息
master_failover_state:no-failover
master_replid:60aa9d0ed7a7d529a639bc5394260cb137bab528
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:294
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:294

细节

  1. 主机用于处理写请求,从机用于处理读请求
  2. 主机断开连接,从机仍可以连接上主机,但是不能进行写操作。若主机恢复了,则从机仍可以读取到主机写入的数据
  3. 从机断开连接又恢复后,仍可以获取到断开期间主机写入的数据

复制原理

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

Master接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后, master将传送整个数据文件到slave,并完成一次完全同步。
全量复制:slave服务在接收到数据库文件数据后,将其存盘并加载到内存中
增量复制:Master继续将新的所有收集到的修改命令依次传给slave,完成同步

只要是重新连接master , 一次完全同步(全量复制)将被自动执行!数据一定可以在从机中看到

十四、哨兵模式

概述

主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。Redis从2.8开始正式提供了Sentinel (哨兵)架构来解决这个问题。

哨兵模式,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库

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

这里的哨兵有两个作用

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

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

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

配置

1、首先配置从机

port 6379		#端口号
pidfile /var/run/redis_6379.pid		#pid
logfile ""		#日志文件名
dbfilename dump.rdb			#rdb文件名
replicaof <masterip> <masterport>		#配置所对应的主机
masterauth <yourpassword>	#主机的密码

2、配置哨兵

在redis安装目录中,有一个哨兵配置文件:sentinel.conf,将其复制一份用作哨兵配置文件,可以跟redis.conf放在一起

# 禁止保护模式
protected-mode no
# 配置监听的主服务器,这里sentinel monitor代表监控,master代表服务器的名称,可以自定义,192.168.11.128代表监控的主服务器,6379代表端口,2代表只有两个或两个以上的哨兵认为主服务器不可用的时候,才会进行failover操作。
sentinel monitor master 192.168.11.128 6379 2
# sentinel author-pass定义服务的密码,mymaster是服务名称,123456是Redis服务器密码
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster 123456

3、启动

注意启动的顺序。首先是主机的Redis服务进程,然后启动从机的服务进程,最后启动3个哨兵的服务进程。

如果主机断开了连接(或者服务发生异常),这个时候就会通过哨兵模式投票出一个从机充当主机。若之后当时的主机重新恢复连接,则会归并到新的主机下,充当从机。

spring boot使用哨兵模式

完成上述步骤后在properties配置文件中加入以下配置即可:

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    sentinel:
      #哨兵集群的名称
      master: master
      #哨兵节点
      nodes: 127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381
      #设置密码
      password: gggd1234

十五、穿透、击穿与雪崩

1、缓存穿透

概念

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

解决方案

1.布隆过滤器(推荐)
布隆过滤器是一种数据结构,对所有可能查询的参数以hash的形式存储,在控制层先进行校验,不符合则丢弃,从而减少了数据库查询的压力。

代码实现:

1.引入依赖

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>25.1-jre</version>
</dependency>

2.在redisUtil中使用

String get(String key) {
    String value = redisTemplate.opsForValue().get(key);     
    if (value  == null) {
        if(!bloomfilter.mightContain(key)){
            return null; 
        }else{
            //向数据库中获取数据
            value = db.get(key); 
            redisTemplate.opsForValue().set(key, value); 
        }    
    }
    return value;
}

2.缓存空对象

当缓存未命中后,若是数据库中也没有该信息,则会将该key存起来,并设置一个null的value,同时设置一个过期时间,之后再次访问该数据则会返回一个null。

但是这种方法会存在两个问题:

1、如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;

2.即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。

2、缓存击穿

概念

这里需要注意和缓存击穿的区别:缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。

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

解决方案

1.设置热点永不过期

从缓存层面看,没有设置过期时间,则不会产生key过期后的缓存击穿问题。

2.加互斥锁

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

3、缓存雪崩

雪崩:指在某一个时间段,缓存集中过期失效。

产生雪崩的原因之一,,比如马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。

解决方案

1.redis高可用
这个思想的含义是:既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。(异地多活! )
**2.限流降级 **
这个解决方案的思想是:在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
3.数据预热
数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。

1.访问量前十的商品
    zset
    将商品存入zset中,浏览量每增加1则score加1
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值