大多数场景下用户的写操作次数远远小于读操作次数,例如:用户使用淘宝的时候,绝大部分时间是查询商品,下单购买的操作频率相对较低,将频繁读取的数据,写入到reids中,可以减少读取数据库的次数,降低数据库的负载,同时也能加快读取速度
当系统部署多个节点的时候,每个节点的缓存数据独立管理,会导致系统之间的数据出现不一致,交互不及时的情况,使用redis统一对这部分数据进行关联,可以降低维护代价和通知代价
Ehcache | Memcache | Redis | |||
---|---|---|---|---|---|
优点 | 缺点 | 优点 | 缺点 | 优点 | 缺点 |
基于java开发 | 不支持集群 | key-value存储 | 无法容灾 | 数据结构丰富 | 单线程 (某些时候是优势) |
基于JVM缓存 | 不支持分布式 | 内存使用率高 | 无法持久化 | 可持久化 | 单核 |
简单、轻巧、方便 | — | 多核、多线程 | — | 主从同步、故障转移 | — |
下载地址:https://redis.io/download
yum install gcc-c++
tar -zxvf redis-5.0.5.tar.gz
cd redis-5.0.5
make && make install
在redis根目录下
#打开配置文件进行修改
vim redis.conf
修改配置
#后台运行
daemonize yes
#工作目录,一定是个目录,不是文件(需要自己创建文件夹)
dir /usr/local/redis/working
#可以访问redis的ip(这里表示所有ip都可以访问)
bind 0.0.0.0
#redis的密码(需要自己添加这个配置项,严重的后门)
requirepass 123456
#redis的端口
port 6379
#redis启动的进程号保存的位置
pidfile /var/run/redis_6379.pid
在redis的utils目录下
#复制到init.d目录下为方便开机启动
cp redis_init_script /etc/init.d/
cd /etc/init.d/
vim edis_init_script
修改配置
#redis的端口(建议在redis.config中修改)
REDISPORT=6379
#指定redis的服务启动文件
EXEC=/usr/local/bin/redis-server
#指定redis的客户端启动文件
CLIEXEC=/usr/local/bin/redis-cli
#redis启动的进程号保存的位置(建议在redis.config中修改)
PIDFILE=/var/run/redis_${REDISPORT}.pid
#redis默认使用的核心配置文件(redis.config),如果不换核心配置不需要修改
CONF="/usr/local/redis/redis.conf"
如果redis设置了密码
在脚本中找到如下这句话
$CLIEXEC -p $REDISPORT shutdown
修改成
$CLIEXEC -a 这里写密码 -p $REDISPORT shutdown
修改执行权限
chmod 777 redis_init_script
添加自启动脚本,在redis_init_script文件的注释下,配置前的位置上添加
注意:保留前面的“#”号
#chkonfig: 22345 10 90
#description: Start and Stop redis
保存并退出redis_init_script,配置开机启动
注意:redis_init_script文件必须存放到/etc/init.d目录下
chkconfig redis_init_script on
./redis_init_script start
命令 | 注释 |
---|---|
auth [password] | 登录 |
keys [*key*] | 查询key列表,查询所有的key使用 keys * |
ttl [key] | 查看过期时间 |
expire [key] [time] | 设置过期时间 |
select [num] | 切换库,默认0-15 |
flushdb | 清空当前库 |
flushall | 清空所有库 |
命令 | 注释 |
---|---|
set [key] [value] | 设置键值对(类似java Map的put方法) |
get [key] | 获取键对应的值 |
del [key] | 删除键值对 |
setnx [key] [value] | 当key不存的时候,设置value |
append [key] [value] | 将值,拼接到原本值的后面 |
strlen [key] | 查看字符串长度 |
incr [key] | 累加,自加一(纯数字字符串) |
decr [key] | 累减,自减一(纯数字字符串) |
getrange [key] [start] [end] | 截取值的一段内容 |
setrange [key] [start] [value] | 替换值中的一段内容 |
mset [key] [value] [key value …] | 同时设置多个键值对 |
mget [key] [key …] | 同时获取多个键值对 |
msetnx [key] [value] [key value …] | 当key不存的时候,同时设置多个value |
命令 | 注释 |
---|---|
hset [key] [field] [value] | 设置hash对象 |
gset [key] [field] | 获取指定hash对象的指定属性值 |
hmset [key] [field] [value] [field value …] | 同时设置hash对象的多个属性 |
hmget [key] [field] [field …] | 同时获取hash对象的多个属性 |
hgetall [key] | 获取hash对象里面所有的属性和值 |
hlen [key] | 获取hash对象里属性的个数 |
hkeys [key] | 获取hash对象里属性名 |
hvak [key] | 获取hash对象里属性值 |
hexists [key] [field] | 判断某个属性是否存在 |
hdel [key] [field] | 删除指定属性 |
命令 | 注释 |
---|---|
lpush [key] [vlaue] [value …] | 设置list对象,将值存到list的左边 |
rpush [key] [vlaue] [value …] | 设置list对象,将值存到list的右边 |
lrange [key] [start] [end] | 获取指定list的某个区间里面的对象 |
lpop [key] | 弹出列表最左侧的值 |
rpop [key] | 弹出列表最右侧的值 |
llen [key] | 获取list里面的元素个数 |
lindex [key] [index] | 获取list中指定下标的值 |
lset [key] [index] [value] | 修改指定下标的元素 |
linsert [key] [before | after] [value] [newVlaue] |
lrem [key] [count] [value] | 删除几个指定值的元素 |
命令 | 注释 |
---|---|
sadd [key] [vlaue] [value …] | 添加不重复的值 |
smembers [key] | 查看set里面的所有值 |
scard [key] | 查看set里面值的个数 |
sismember [key] [value] | 查看值是否存在 |
spop [key] | 弹出第一个元素 |
spop [key] [num] | 弹出前几个元素 |
srandmember [key] [count] | 从set里面随机获取几个对象 |
smove [oldkey] [newkey] [value] | 将oldkey里面的value元素移到newkey中 |
sdiff [key1] [key2] | 获取两set的差集,key1中有但key2中没有的 |
sunion [key1] [key2] | 获取两set的并集 |
sinter [key1] [key2] | 获取两set的交集 |
命令 | 注释 |
---|---|
zadd [key] [score] [value] [score value …] | 设置member和对应的分数 |
zrange [key] [start] [end] | 查看set中的内容 |
zrank [key] [value] | 获取元素对应的下标 |
zscore [key] [value] | 获取元素对应的分数 |
zcard [key] | 统计元素个数 |
zcount [key] [score1] [score2] | 统计指定分数的个数 |
zrangebyscore [key] [score1] [score2] | 查询指定分数之间的member(包含分数1 分数2) |
zrangebyscore [key] [score1] [score2] limit [start] [end] | 查询指定分数之间的member(包含分数1 分数2),做分页 |
zrem [zset] [value] | 删除指定元素 |
org.springframework.boot
spring-boot-starter-data-redis
spring:
redis:
#使用redis的库
database: 0
#连接地址
host: 127.0.0.1
#连接端口
post: 6379
#密码
password: 123456
这里以一个Controller为例
@RestController
@RequestMapping("/redis")
public class RedisController{
//注入模板
@Autowired
private RedisTemplate redis;
//也可以使用StringRedisTemplate
@GetMapping("/set")
public Object set(String key,String value){
redis.opsForValue.set(key,value);
return "OK";
}
@GetMapping("/get")
public String get(String key){
return (String)redis.opsForValue.get(key);
}
}
准备2个客户端,1台为发布,1台为订阅(这两个客户端要连接同一个redis)
注意:这里只是演示,实际使用的时候,我们应当选择专业的MQ
打开一个redis客户端,用于订阅
#订阅一个叫food的频道,food只是一个标识,可以填写任意的字符串
subscribe food
#通配符订阅,这样所有的以chat为标识的消息,都会被订阅
psubscribe chat*
执行命令之后,客户端会处于一个阻塞的监听状态
打开一个redis客户端,用于发布
#向订阅了food频道的客户端发送一个汉堡
publish food burger
redis的数据是存在内存中的,如果突然断电我们的数据就会就是,使用持久化让数据保存在硬盘上
每个一段时间,进行对redis数据进行快照,是一种全量的持久化
优势:
缺点:
配置RDB备份
打开核心配置文件(redis.conf),找到SNAPSHOTTING的区域
#保存规则,如果发生了一次key的变化,那么900秒之后就会备份一次
save 900 1
#如果发生了十次key的变化,那么300秒之后就会备份一次
save 300 10
#如果发生了一万次key的变化,那么60秒之后就会备份一次
save 60 10000
#默认保存的名称
dbfilename dump.rdb
#保存的位置
dir /usr/local/redis/working
以日志的方式存在,地方进行写操作的时候就会触发AOF
优势:
缺点:
配置AOF备份
打开核心配置文件(redis.conf),找到APPEND ONLY MODE的区域
#开启AOF
appendonly yes
#备份文件名称
appendfilename "appendonly.aof"
#同步策略
#一共有三种 always(每次写操作),everysec(每秒),no(关闭)
appendfsync always
#重写日志的时候是否将新的操作同步到日志中,建议用no
no-appendfsync-on-rewrite no
#重写日志的触发添加
#当文件大小超过上次文件备份之后大小的100%后,触发
auto-aof-rewrite-percentage 100
#当文件小于指定大小时,不触重写操作
auto-aof-rewrite-min-size 64mb
恢复的时候上一次误删除的数据,只需要删除AOF日志中的最后一条命令,然后重启就可以了
在redis中如果key设置了过期时间,并且key已经过期,此时已经无法查询value了,但是这个数据依旧会被保留在内存中,这是因为redis的内存清理策略造成的
定时随机的检查的key,如果过期则清理删除。
在redis.conf中修改
每秒随机抽取10个key进行检查
hz 10
当客户端请求一个已经过期的key的时候,那么redis会检查这个key是否过期,如果过期了,则删除,然后返回一个nil。这种策略对cpu比较友好,不会有太多的损耗,但是内存占用会比较高
在redis.conf中添加
#当redis的内存超过这个值的时候会真正释放掉redis中的某些键值对(单位:bytes)
maxmemory 4096
#内存满载的清理策略 默认为noeviction
maxmemory_policy noeviction
noeviction:旧缓存永不过期,新缓存设置不了,返回错误
allkeys-lru:清除最少用的旧缓存,然后保存新的缓存(推荐使用)
allkeys-random:在所有的缓存中随机删除(不推荐)
volatile-lru:在那些设置了expire过期时间的缓存中,清除最少用的旧缓存,然后保存新的缓存
volatile-random:在那些设置了expire过期时间的缓存中,随机删除缓存
volatile-ttl:在那些设置了expire过期时间的缓存中,删除即将过期的
准备3台装有redis的机器,1台作为master,2台作为slave
进入redis-cli,登录后输入
info replication
显示以下数据
#这台机器为master
role:master
#连接的从机个数
connected_slaves:0
master_replid:505dd5ab28e221e8fdff546a1ebbfb4219147693
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
主机是不需要修改的,我们只需要核心配置文件(redis.conf)将其设置为从机并且连接到主机上就可以了
vim redis.conf
/REPLICATION
在模块的任意位置(实际上在配置文件的任意位置上都可以),添加配置
#连接master的地址和端口 replicaof
replicaof 192.168.85.200 6379
# #连接master的密码 masterauth
masterauth 123456
找下面的配置并修改
#从机的数据设置为只读
replica-read-only yes
重启之后当前的这个redis就会变成从节点连接上master,并且同步master的数据,如果master上有数据且数据量较大,可能会有点慢
进入redis-cli,登录后输入
info replication
显示以下数据
#这里已经变从节点了
role:slave
#连接的主机地址
master_host:192.168.85.200
#连接的主机端口
master_port:6379
master_link_status:up
master_last_io_seconds_ago:7
master_sync_in_progress:0
slave_repl_offset:252
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:82067392216d7336830be1a9e27ad0a6a188bb2a
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:252
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:252
redis的从节点不宜太多,不然可能会占用过多的网络带宽和性能
一般有三种配法
1、1主1从
2、1主2从(现在的公司一般都使用这个)
3、在1主2从的基础上给从节点再增加从节点
在主节点上添加数据,查看两个从节点上数据是否同步
在配置好从机之后,重启redis,从机会到master上复制数据文件,实际上就是master提供一个RDB给从机进行下载,是下载到在硬盘上的,如果数据量较大,且是内网环境的时候,普通的磁盘可能会限制传输速度。此时我们就需要同到无磁盘户复制,让redis将这个过程从硬盘上移到内存上
#启动无磁盘化复制
repl-diskless-sync yes
#等待时间 单位秒
repl-diskless-sync-delay 5
redis在主从分离的模式下,如果master发生了宕机,redis自己是不会像其他的中间件一样自动的推选出新的master的,这是就需要哨兵进程的帮助。
哨兵(Sentinel)是用于监控Redis集群中Master状态的工具,是 Redis 高可用解决方案,哨兵可以监视一个或者多个redis master服务,以及这些master服务的所有从服务;当某个master服务宕机后,会把这个master下的某个从服务升级为master来替代已宕机的master继续工作。
在redis的根目录下找到sentinel.conf,修改其配置
有些配置是需要自己添加或者去掉注解的
#这个配置和redis.conf是一样,绑定的ip
bind 127.0.0.1
#是否启动保护模式,如果启动了保护模式那么只有绑定的ip才能操作redis
protected-mode no
#端口号
port 26379
#是否后台运行
daemonize yes
#哨兵进程的保存文件
pidfile /var/run/redis-sentinel.pid
#哨兵的日志
logfile /usr/local/redis/sentinel/redis-sentinel.log
#工作路径
dir /tmp
#连接master的配置
#sentinel moitor <故障转移触发的哨兵数量>
#master的别名:只能使用A-z,0-9,“.-_”这些字符
#故障转移触发的哨兵数量:例如部署了5个哨兵对这个master进行监控,当有2个哨兵发现master没有响应的时候,开始进行故障转移
sentinel monitor master-200 192.168.85.200 6479 2
#master登录密码
sentinel auth-pass master-200 123456
# master被sentinel认定为失效的间隔时间
sentinel down-after-milliseconds master-200 30000
#剩余的slaves重新和新的master做同步的并行个数
sentinel parallel-syncs master-200 1
# 主备切换的超时时间,哨兵要去做故障转移,这个时候哨兵也是一个进程,如果他没有去执行,超过这个时间后,会由其他的哨兵来处理
sentinel failover-timeout master-200 180000
之前配置了日志文件位置的话 需要创建对应的文件夹
redis-sentinel sentinel.conf
如果你仔细的操作个上面的配置,会发现原来的Master恢复成Slave后,状态为master_link_status:down,实际上是因为我们没有配置它的masterauth属性,所以只需要修改redis.conf中的masterauth为对应的密码即可,这里为123456。
在redis-cli中可以使用命令让哨兵查看各节点的信息
# 查看master节点信息
sentinel master
# 查看slaves节点信息
sentinel slaves
# 查看哨兵节点信息
sentinel sentinels
原先spring整合单机单节的配置
spring:
redis:
database: 1
host: 192.168.85.200
port: 6379
password: 123456
将配置修改成哨兵的模式
spring:
redis:
database: 1
password: 123456
sentinel:
#master的别名
master: master-200
#所有的哨兵节点
nodes: 192.158.85.200:26379,192.158.85.201:26379,192.158.85.202:26379
在主从复制的基础上,哨兵引入了主节点的自动故障转移,进一步提高了Redis的高可用性;但是哨兵的缺陷同样很明显:哨兵无法对从节点进行自动故障转移,在读写分离场景下,从节点故障会导致读服务不可用,需要我们对从节点做额外的监控、切换操作。
此外,哨兵仍然没有解决写操作无法负载均衡、及存储能力受到单机限制的问题;这些问题的解决需要使用集群。
主从复制以及哨兵,他们可以提高读的并发,但是单个master容量有限,数据达到一定程度会有瓶颈,这个时候可以通过水平扩展为多master-slave成为集群。支持海量数据,实现高可用与高并发。哨兵模式其实也是一种集群,他能够提高读请求的并发,但是容错方面可能会有一些问题,比如master同步数据给slave的时候,这其实是异步复制,这个时候master挂了,那么slave上的数据就没有master新,数据同步需要时间的,1-2秒的数据会丢失。master恢复并转换成slave后,新数据则丢失。
构建Redis集群,需要至少3个节点作为master,以此组成一个高可用的集群,此外每个master都需要配备一个slave,所以整个集群需要6个节点,这也是最经典的Redis集群,也可以称之为三主三从,容错性更佳。所以在搭建的时候需要有6台虚拟机。请各自准备6台虚拟机,可以通过克隆去构建,使用单实例的Redis 去克隆即可,如果之前配置了主从或是哨兵建议删除 。
这里演示是新版本的集群搭建,老版本需要使用redis-trib.rb这里不做演示,使用机器的配置如下
主从一 | 主从二 | 主从三 | |
---|---|---|---|
主节点 | 192.168.85.200 | 192.168.85.201 | 192.168.85.202 |
从节点 | 192.168.85.203 | 192.168.85.204 | 192.168.85.205 |
配置前保证redis是关闭的
在redis的核心配置文件(redis.conf)里面找到REDIS CLUSTER区域
#启动Redis-Cluster 集群
cluster-enabled yes
#每个节点的配置文件
#这里保存着对应节点的配置信息,和其他节点的对应关系
#这是redis自己进行管理的,不需要我们进行干预,只需要打开原本的注释就行了
cluster-config-file nodes-6379.conf
#节点超时的时间 (单位:秒)
cluster-node-timeout 5000
#开启AOF持久化模式
appendonly yes
如果配置出现问题,可以删除掉原先的AOF持久化文件,但是要注意文件删除,持久化的数据也就消失了
进入redis目录里的src文件夹
每一个节点之间使用空格分开
redis-cli -a 密码 --cluster create 192.168.85.200:6379 192.168.85.201:6379 192.168.85.202:6379 192.168.85.203:6379 192.168.85.204:6379 192.168.85.205:6379 --cluster-replicas 1
执行后,会打印出集群配置信息,如果没有问题就输入yes,回车
redis-cli --cluster check 192.168.85..200:6380
redis-cli -c -a 密码 -h 节点ip -p 节点端口
#连接后,查看当前节点信息
cluster info
#查看所有节点信息
cluster nodes
当我们查询集群信息的时候,可以看到这么一段话
[OK] All 16384 slots covered
这里的意思就是 一共有16384个槽节点,那么什么是槽节点
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hOQnhwV1-1577677344717)(en-resource://database/2525:1)]
这里可以看到3个master是平分了所有槽节点,而从节点是没有槽节点的
当来了一条数据,redis就会对数据的key进行计算 "hash(key)%槽数量 "这样就可以算出一个对应的slot,经过计算所有值相同的数据都会被完整的保存在这个slot中。如果在200的机器上查询keys * 是查看不到其他slot里的数据,但是get [key]是会自动切换节点查询到的
当然存入主节点的时候,同时会复制一份到对应的从节点上,当主节点宕机后,从节点就会接替主节点的位置。
spring:
redis:
password: 123456
cluster:
#所有的节点,nodes后面是不换行的,页面上显示的有问题
nodes: 192.158.85.200:6379,192.158.85.201:6379,192.158.85.202:6379,192.158.85.203:6379,192.158.85.204:6379,192.158.85.205:6379
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。
缓存空对象. 将 null 变成一个值.
可以采用一个简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
缓存空对象会有两个问题:
第一,空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间 ( 如果是攻击,问题更严重 ),比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。
第二,缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象。
布隆过滤
对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃。还有最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
布隆过滤器的问题:
布隆过滤器的原理是,当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。
bloom filter之所以能做到在时间和空间上的效率比较高,是因为牺牲了判断的准确率、删除的便利性
缓存击穿
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
缓存雪崩
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库
缓存击穿
缓存雪崩
对于需要一次性获取多个key的value时,例如下面代码
public Object getAlot(String... keys){
List<String> resutl = new ArrayList<>();
for(String k:keys){
resutl.add(redisOperator.get(k));
}
return resutl;
}
修改成使用MultiGet,对应的就是mget命令
public Object getAlot(String... keys){
List<String> keysList = Arrays.asList(keys);
return redisTemplate.opsForValue().multGet(keys);
}
每次对redis发起请求,都需要创建相互的链接,这样就有额外的开销,使用pipeline建立管道,进行优化
public List<Object> getAlot(String... keys){
List<Object> resutl = redisTemplate.executePipelined(new RedisCallback<String>() {
@Override
public String doInRedis(RedisConnection connection) throws DataAccess{
StringRedisConnection src = (StringRedisConnection)connection;
for(String k:keys){
src.get(k);
}
return null;
}
});
return resutl;
}