目录
一、Redis事务、锁机制
定义:
Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。Redis事务的主要作用就是串联多个命令防止别的命令插队。
1、Redis中关于事务的三个命令
multi exec discard
从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。
组队的过程中可以通过discard来放弃组队。
事务执行失败有两种情况
1、添加命令期间有命令错误,则全部都不会执行,如下
2、添加命令期间没有命令错误,但是有逻辑错误,类似java运行时错误,则只执行成功的
2、事务的冲突问题
场景:三个人想用一个账户去商场购物
一个请求想给金额减8000
一个请求想给金额减5000
一个请求想给金额减1000
这时就会产生余额变为负数的情况,显然这时不合实际的,Redis中有两种锁可以解决这些问题
①悲观锁
悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
但是这种锁的效率非常低。
②乐观锁
乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。
3、演示乐观锁和事务特性
①watch key1 [key2]
在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
②unwatch
取消 WATCH 命令对所有 key 的监视。
如果在执行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了。
3、Redis中事务的三个特性
- 单独的隔离操作
事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
- 没有隔离级别的概念
队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
- 不保证原子性
事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
二、Redis持久化
Redis 提供了2个不同形式的持久化方式。在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里
1、RDB(Redis DataBase)
进入redis.conf文件进行编辑,修改持久化规则
在redis.conf配置文件中修改持久化操作文件保存位置,如图保存到根目录下
查看根目录下目前还没有文件
连续保存6个key后(规则为30秒内5个key改变即持久化),生成了dump.rdb文件
备份执行过程
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。若redis正在执行最后一次的写入操作时崩溃,则造成了数据的丢失
Fork
- Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
- 在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux中引入了“写时复制技术”
- 一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。
验证redis的恢复备份
清空redis
将redis中存入7个字符串并备份一份dump.rdb文件
删除dump.rdb文件,关闭redis服务,然后将dump.rdb.bak更名为dump.rdb,启动redis
此时,可以看到,k7已经丢失。
RDB优势劣势
优势
- 适合大规模的数据恢复
- 对数据完整性和一致性要求不高更适合使用
- 节省磁盘空间
- 恢复速度快
劣势
- Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑
- 虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。
- 在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。
2、AOF(Append Of File)
①什么是AOF
以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
②AOF持久化流程
(1)客户端的请求写命令会被append追加到AOF缓冲区内;
(2)AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;
(3)AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;
(4)Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;
③AOF默认状态
默认不开启
可以在redis.conf中配置文件名称,默认为 appendonly.aof
AOF文件的保存路径,同RDB的路径一致。
修改配置文件后重启redis,生成了appendonly.aof文件
注意:当aof和rdb同时开启,redis默认会从aof中读取数据
当在redis中保存数据后,发现 原本0大小的aof文件大小改变,说明生效
④aof恢复数据过程
操作同rdb部分一样,aof并不会造成数据的丢失
⑤aof的异常修复
- 修改默认的appendonly no,改为yes
- 如遇到AOF文件损坏,通过/usr/local/bin/redis-check-aof--fix appendonly.aof进行恢复
- 备份被写坏的AOF文件
- 恢复:重启redis,然后重新加载
⑥aof同步频率设置
appendfsync always
始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好
appendfsync everysec
每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。
appendfsync no
redis不主动进行同步,把同步时机交给操作系统。
⑥aof优缺点
优点
- 备份机制更稳健,丢失数据概率更低。
- 可读的日志文本,通过操作AOF稳健,可以处理误操作。
缺点
- 比起RDB占用更多的磁盘空间。
- 恢复备份速度要慢。
- 每次读写都同步的话,有一定的性能压力。
- 存在个别Bug,造成恢复不能。
总结
官方推荐两个都启用。
如果对数据不敏感,可以选单独用RDB。
不建议单独用 AOF,因为可能会出现Bug。
如果只是做纯内存缓存,可以都不用。
三、主从复制
什么是主从复制
主机数据更新后根据配置和策略, 自动同步到备机的master/slaver机制,Master以写为主,Slave以读为主
好处是
- 读写分离,性能扩展
- 容灾快速恢复
1、配置一个一主两从的数据库
①创建myredis文件夹
②复制redis.conf到文件夹中
③配置一主两从,创建三个配置文件
reids6379.conf,内容如下
注意:如果主redis设置有密码,配置文件需要加一行:masterauth 密码
reids6380.conf
reids6381.conf
这两份复制过来对比改一下内容即可
④启动三个redis
分别连接一下三台服务器
使用info replication命令查看主从关系,现在并没有主从关系
⑤设置主从关系
使用在从机的命令行使用命令连接三个数据库
slaveof 主机ip 主机端口
接着在主机中用
info replication
命令查看从机是否连接成功,若成功则显示从机信息
2、常用知识
①主从复制原理
②一主二仆
若从服务器在主服务器写的期间挂掉,那么重启该从服务器之后,该服务器变成主的状态,但是仍然可以读到挂掉之前主服务器写入的key,并且,在重新连接为主服务器的从机后,所有key都可以读到
③薪火相传
假设6379为主服务器,6380设置主服务器为6379,6381设置主服务器为6380,那么,他们的主从关系类似于树形结构的父亲孩子结点关系,如果出现宕机的情况那么数据保存仍然遵循一主二仆
④反客为主
假如主机宕机从机可以使用下面的命令变为主机,若它本身就有从机,那么从机继续作为它的从机
salve no one
3、主从复制哨兵模式
先搭建一主二从的环境
①创建哨兵配置文件
在myredis下创建sentinel.conf配置文件,名称不能错误
注意:如果主机设有密码,那么配置文件需要加入
sentinel auth-pass mymaster 密码
且主从密码一致,否则哨兵机制会失效
编写完成后
redis-sentinel sentinel
启动哨兵,当主机宕机后,哨兵会自动选举一个从机作为新的主机,即使宕机的主机在后来重新启动,仍然会作为从机。
②宕机后从机优先级
在redis.conf文件中,600多行处有这样一个配置来更改优先级
replica-priority 100
优先级从0到100,数字越小优先级越高,宕机后优先级高的从机作为主机
4、小结
复制延时:
由于所有的写操作都是先在Master上操作,然后同步更新到Slave上,所以从Master同步到Slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重。
故障恢复
哨兵模式下,当主机故障后,从从机中挑选一个作为主机,选择条件依次为:
- 选择优先级靠前的
- 选择偏移量最大的
- 选择runid最小的
挑选出新的主机后,sentinel向其他从机发送slaveof命令,让它们服从新的master,当宕机的原主机重新上线后,sentinel会向其发送slaveof让其服从新主机
注意:
- 优先级在redis.conf中默认:slave-priority 100,值越小优先级越高
- 偏移量是指获得原主机数据最全的
- 每个redis实例启动后都会随机生成一个40位的runid
IDEA中主从复制代码
private static JedisSentinelPool jedisSentinelPool=null;
public static Jedis getJedisFromSentinel(){
if(jedisSentinelPool==null){
Set<String> sentinelSet=new HashSet<>();
sentinelSet.add("192.168.11.103:26379");
JedisPoolConfig jedisPoolConfig =new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(10); //最大可用连接数
jedisPoolConfig.setMaxIdle(5); //最大闲置连接数
jedisPoolConfig.setMinIdle(5); //最小闲置连接数
jedisPoolConfig.setBlockWhenExhausted(true); //连接耗尽是否等待
jedisPoolConfig.setMaxWaitMillis(2000); //等待时间
jedisPoolConfig.setTestOnBorrow(true); //取连接的时候进行一下测试 ping pong
jedisSentinelPool=new JedisSentinelPool("mymaster",sentinelSet,jedisPoolConfig);
return jedisSentinelPool.getResource();
}else{
return jedisSentinelPool.getResource();
}
}
四、集群
问题:
- 容量不够,redis如何进行扩容?
- 并发写操作, redis如何分摊?
- 另外,主从模式,薪火相传模式,主机宕机,导致ip地址发生变化,应用程序中配置需要修改对应的主机地址、端口等信息。之前通过代理主机来解决,但是redis3.0中提供了解决方案。就是无中心化集群配置。
什么是集群
Redis 集群实现了对Redis的水平扩容,即启动N个redis节点,将整个数据库分布存储在这N个节点中,每个节点存储总数据的1/N。
Redis 集群通过分区(partition)来提供一定程度的可用性(availability): 即使集群中有一部分节点失效或者无法进行通讯, 集群也可以继续处理命令请求。
1、搭建6台主机的集群
端口号为6379,6389,6380,6390,6381,6391
相同颜色小号为主机,大号为从机
①修改redis.conf配置文件
增加如下:
cluster-enabled yes 打开集群模式
cluster-config-file nodes-6379.conf 设定节点配置文件名
cluster-node-timeout 15000 设定节点失联时间,超过该时间(毫秒),集群自动进行主从切换。
然后复制5份,一共六份配置文件,然后启动配置好的6个服务
全部打开,并生成对应结点文件
②将6个结点合成集群
命令:
redis-cli --cluster create --cluster-replicas 1 192.168.131.128:6379 192.168.131.128:6380 192.168.131.128:6381 192.168.131.128:6389 192.168.131.128:6390 192.168.131.128:6391
注意:此处ip不要用127.0.0.1,用真实ip,并且老版本redis中需要在安装目录下的src文件夹中运行该命令
效果如下:
然后用如下代码以集群方式启动
利用
cluster nodes
查看集群结点信息
至此集群已经搭建完毕
2、什么是slot
我们注意到,搭建集群时系统为我们分配了16384个插槽
一个 Redis 集群包含 16384 个插槽(hash slot), 数据库中的每个键都属于这 16384 个插槽的其中一个,
集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和 。
集群中的每个节点负责处理一部分插槽。 举个例子, 如果一个集群可以有主节点, 其中:
节点 A 负责处理 0 号至 5460 号插槽。
节点 B 负责处理 5461 号至 10922 号插槽。
节点 C 负责处理 10923 号至 16383 号插槽。
①在集群中录入值
当我们存储一个value时,它会计算这个value属于哪个槽
当要一次性插入多个数据时,会插入失败,显示不能计算多个hash值,因此要以组的形式插入
取值时也应带上组名
②查询集群中的值
CLUSTER GETKEYSINSLOT <slot><count>
返回 count 个 slot 槽中的键。
3、故障恢复
- 如果主节点挂掉,超时时间过后仍然没有恢复,那么从机会取代它成为主节点,当原先主节点恢复后,作为新主节点的从机
- 如果某一段插槽的主从都挂掉,而cluster-require-full-coverage 为yes ,那么 ,整个集群都挂掉
- 如果某一段插槽的主从都挂掉,而cluster-require-full-coverage 为no ,那么,该插槽数据全都不能使用,也无法存储。
- redis.conf中的参数 cluster-require-full-coverage
4、集群的Jedis开发
即使连接的不是主机,集群会自动切换主机存储。主机写,从机读。
无中心化主从集群。无论从哪台主机写的数据,其他主机上都能读到数据。
public class JedisClusterTest {
public static void main(String[] args) {
Set<HostAndPort>set =new HashSet<HostAndPort>();
set.add(new HostAndPort("192.168.31.211",6379));
JedisCluster jedisCluster=new JedisCluster(set);
jedisCluster.set("k1", "v1");
System.out.println(jedisCluster.get("k1"));
}
}
5、集群的优缺点
优点
- 实现扩容
- 分摊压力
- 无中心配置相对简单
缺点
- 多键操作是不被支持的
- 多键的Redis事务是不被支持的。lua脚本不被支持
- 由于集群方案出现较晚,很多公司已经采用了其他的集群方案,而代理或者客户端分片的方案想要迁移至redis cluster,需要整体迁移而不是逐步过渡,复杂度较大。
五、Redis应用问题解决
1、缓存穿透
描述:
缓存穿透的概念很简单,用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。
发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库。
这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。
一般场景为黑客对服务器进行攻击,发起大量查询数据库中没有的值,让redis缓存命中率大幅下降,导致数据库查询压力变大,最后崩溃。
一个一定不存在缓存及查询不到的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
解决方案:
1、对空值缓存:如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟
2、设置可访问的名单(白名单):
使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。
3、采用布隆过滤器:
(布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。
布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。)
将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被 这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。
4、进行实时监控:当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务
2、缓存击穿
描述:
key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题。
解决方案:
(1)预先设置热门数据:在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长
(2)实时调整:现场监控哪些数据热门,实时调整key的过期时长
(3)使用锁:
- 就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db。
- 先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX)去set一个mutex key
- 当操作返回成功时,再进行load db的操作,并回设缓存,最后删除mutex key;
- 当操作返回失败,证明有线程在load db,当前线程睡眠一段时间再重试整个get缓存的方法。
3、缓存雪崩
描述:
key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
缓存雪崩与缓存击穿的区别在于前者同一时间大量key过期,后者热门key过期
缓存失效时的雪崩效应对底层系统的冲击非常可怕!
解决方案:
1、构建多级缓存架构:nginx缓存 + redis缓存 +其他缓存(ehcache等)
2、使用锁或队列:
用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况
3、设置过期标志更新缓存:
记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存。
4、将缓存失效时间分散开:
比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
六、分布式锁
1、设置一个简单的分布式锁
描述:
随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!
分布式锁主流的实现方案:
1. 基于数据库实现分布式锁
2. 基于缓存(Redis等)
3. 基于Zookeeper
每一种分布式锁解决方案都有各自的优缺点:
1. 性能:redis最高
2. 可靠性:zookeeper最高
基于redis实现分布式锁
利用命令
set key value nx ex time
nx:同setnx效果
ex:同setex效果
time:过期时间,单位秒
2、UUID防误删
问题:删除操作缺乏原子性。
场景:
- index1执行删除时,查询到的lock值确实和uuid相等
uuid=v1
set(lock,uuid);
- index1执行删除前,lock刚好过期时间已到,被redis自动释放
在redis中没有了lock,没有了锁。
- index2获取了lock
index2线程获取到了cpu的资源,开始执行方法
uuid=v2
set(lock,uuid);
- index1执行删除,此时会把index2的lock删除
index1 因为已经在方法中了,所以不需要重新上锁。index1有执行的权限。index1已经比较完成了,这个时候,开始执行
删除的index2的锁!
七、Redis6.0新功能
1、ACL
Redis ACL是Access Control List(访问控制列表)的缩写,该功能允许根据可以执行的命令和可以访问的键来限制某些连接。
在Redis 5版本之前,Redis 安全规则只有密码控制 还有通过rename 来调整高危命令比如 flushdb , KEYS* , shutdown 等。Redis 6 则提供ACL的功能对用户进行更细粒度的权限控制 :
(1)接入权限:用户名和密码
(2)可以执行的命令
(3)可以操作的 KEY
①使用acl list命令展现用户权限列表
②使用acl cat命令
(1)查看添加权限指令类别
(2)加参数类型名可以查看类型下具体命令
③使用acl whoami命令查看当前用户
④使用aclsetuser命令创建和编辑用户ACL
(1)ACL规则
下面是有效ACL规则的列表。某些规则只是用于激活或删除标志,或对用户ACL执行给定更改的单个单词。其他规则是字符前缀,它们与命令或类别名称、键模式等连接在一起。
ACL规则 | ||
类型 | 参数 | 说明 |
启动和禁用用户 | on | 激活某用户账号 |
off | 禁用某用户账号。注意,已验证的连接仍然可以工作。如果默认用户被标记为off,则新连接将在未进行身份验证的情况下启动,并要求用户使用AUTH选项发送AUTH或HELLO,以便以某种方式进行身份验证。 | |
权限的添加删除 | +<command> | 将指令添加到用户可以调用的指令列表中 |
-<command> | 从用户可执行指令列表移除指令 | |
+@<category> | 添加该类别中用户要调用的所有指令,有效类别为@admin、@set、@sortedset…等,通过调用ACL CAT命令查看完整列表。特殊类别@all表示所有命令,包括当前存在于服务器中的命令,以及将来将通过模块加载的命令。 | |
-@<actegory> | 从用户可调用指令中移除类别 | |
allcommands | +@all的别名 | |
nocommand | -@all的别名 | |
可操作键的添加或删除 | ~<pattern> | 添加可作为用户可操作的键的模式。例如~*允许所有的键 |
(2)通过命令创建新用户默认权限
acl setuser user1
在上面的示例中,我根本没有指定任何规则。如果用户不存在,这将使用just created的默认属性来创建用户。如果用户已经存在,则上面的命令将不执行任何操作。
(3)设置有用户名、密码、ACL权限、并启用的用户
acl setuser user2 on >password ~cached:* +get
(4)切换用户,验证权限
auth username password
2、IO多线程
Redis6终于支撑多线程了,告别单线程了吗?
IO多线程其实指客户端交互部分的网络IO交互处理模块多线程,而非执行命令多线程。Redis6执行命令依然是单线程+IO多路复用
另外,多线程IO默认也是不开启的,需要再配置文件中配置
io-threads-do-reads yes
io-threads 4