Redis(二)
Redis扩展功能
分布式锁
watch
利用Watch实现Redis乐观锁
乐观锁基于CAS(Compare And Swap)思想(比较并替换),是不具有互斥性,不会产生锁等待而消
耗资源,但是需要反复的重试,但也是因为重试的机制,能比较快的响应。因此我们可以利用redis来实
现乐观锁。具体思路如下:
1、用redis的watch功能,监控这个redisKey的状态值
2、获取redisKey的值
3、创建redis事务
4、给这个key的值+1
5、然后去执行这个事务,如果key的值被修改过则回滚,key不加1
当 Redis 使用 exec 命令执行事务的时候,它首先会去比对被 watch 命令所监控的键值对,如果没有发生变化,那么它会执行事务队列中的命令,提交事务;如果发生变化,那么它不会执行任何事务中的命令,而去事务回滚。无论事务是否回滚,Redis 都会去取消执行事务前的 watch 命令。
public class Second {
public static void main(String[] arg) {
String redisKey = "lock";
ExecutorService executorService = Executors.newFixedThreadPool(20);
try {
Jedis jedis = new Jedis("127.0.0.1", 6378); // 初始值
jedis.set(redisKey, "0");
jedis.close();
} catch (Exception e) {
e.printStackTrace();
}
for (int i = 0; i < 1000; i++) {
executorService.execute(() -> {
Jedis jedis1 = new Jedis("127.0.0.1", 6378);
try {
jedis1.watch(redisKey);
String redisValue = jedis1.get(redisKey);
int valInteger = Integer.valueOf(redisValue);
String userInfo = UUID.randomUUID().toString();
// 没有秒完
if (valInteger < 20) {
Transaction tx = jedis1.multi();
tx.incr(redisKey);
List list = tx.exec(); // 秒成功 失败返回空list而不是空
if (list != null && list.size() > 0) {
System.out.println("用户:" + userInfo + ",秒杀成功! 当前成功人数:" + (valInteger + 1));
}// 版本变化,被别人抢了。
else {
System.out.println("用户:" + userInfo + ",秒杀失 败");
}
}// 秒完了
else {
System.out.println("已经有20人秒杀成功,秒杀结束");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
jedis1.close();
}
});
}
executorService.shutdown();
}
}
setnx
实现原理
共享资源互斥
共享资源串行化
单应用中使用锁:(单进程多线程)
synchronized、ReentrantLock
分布式应用中使用锁:(多进程多线程)
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。
利用Redis的单线程特性对共享资源进行串行化处理
实现方式
获取锁
/**
* 使用redis的set命令实现获取分布式锁
* * @param lockKey 可以就是锁
* * @param requestId 请求ID,保证同一性 uuid+threadID
* * @param expireTime 过期时间,避免死锁
* * @return
*/
public boolean getLock(String lockKey, String requestId, int expireTime) {
//NX:保证互斥性
// hset 原子性操作 只要lockKey有效 则说明有进程在使用分布式锁
String result = jedis.set(lockKey, requestId, "NX", "EX", expireTime);
if ("OK".equals(result)) {
return true;
}
return false;
}
Redisson分布式锁
加锁
"if (redis.call('exists',KEYS[1])==0) then "+ --看有没有锁
"redis.call('hset',KEYS[1],ARGV[2],1) ; "+ --无锁 加锁
"redis.call('pexpire',KEYS[1],ARGV[1]) ; "+
"return nil; end ;" +
"if (redis.call('hexists',KEYS[1],ARGV[2]) ==1 ) then "+ --我加的锁
"redis.call('hincrby',KEYS[1],ARGV[2],1) ; "+ --重入锁
"redis.call('pexpire',KEYS[1],ARGV[1]) ; "+
"return nil; end ;" +
"return redis.call('pttl',KEYS[1]) ;" --不能加锁,返回锁的时间
ua的作用:保证这段复杂业务逻辑执行的原子性。
lua的解释:
KEYS[1]) : 加锁的key
ARGV[1] : key的生存时间,默认为30秒
ARGV[2] : 加锁的客户端ID (UUID.randomUUID()) + “:” + threadId)
第一段if判断语句,就是用“exists myLock”命令判断一下,如果你要加锁的那个锁key不存在的话,你就
进行加锁。如何加锁呢?很简单,用下面的命令:
hset myLock
8743c9c0-0795-4907-87fd-6c719a6b4586:1 1
通过这个命令设置一个hash数据结构,这行命令执行后,会出现一个类似下面的数据结构:
myLock :{“8743c9c0-0795-4907-87fd-6c719a6b4586:1”:1 }
上述就代表“8743c9c0-0795-4907-87fd-6c719a6b4586:1”这个客户端对“myLock”这个锁key完成了加
锁。
接着会执行“pexpire myLock 30000”命令,设置myLock这个锁key的生存时间是30秒。
释放锁机制
# 如果key已经不存在,说明已经被解锁,直接发布(publish)redis消息
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end;" +
# key和field不匹配,说明当前客户端线程没有持有锁,不能主动解锁。 不是我加的锁 不能解锁
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" + "end; " +
# 将value减1
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
# 如果counter>0说明锁在重入,不能删除key
"if (counter > 0) then " + "redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
# 删除key并且publish 解锁消息
"else " + "redis.call('del', KEYS[1]); " +
# 删除锁 "redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; "+
"end; " +
"return nil;"
redisson代码
//加锁并设置有效期
if(redis.lock("RDL",200)){
//判断库存
if (orderNum<getCount()){ //加锁成功 ,可以下单
order(5); //释放锁
redis,unlock("RDL");
}
}
慢日志查询
# 执行时间超过多少微秒的命令请求会被记录到日志上 0 :全记录 <0 不记录
slowlog-log-slower-than 10000 #slowlog-max-len 存储慢查询日志条数
slowlog-max-len 128
Redis使用列表存储慢查询日志,采用队列方式(FIFO)
config set的方式可以临时设置,redis重启后就无效
config set slowlog-log-slower-than 微秒
config set slowlog-max-len 条数
监视器
Redis核心原理分析
Redis持久化
Redis是内存数据库,宕机后数据会消失。
Redis重启后快速恢复数据,要提供持久化机制
Redis持久化是为了快速的恢复数据而不是为了存储数据
Redis有两种持久化方式:RDB和AOF
注意:Redis持久化不保证数据的完整性。
当Redis用作DB时,DB数据要完整,所以一定要有一个完整的数据源(文件、mysql)
在系统启动时,从这个完整的数据源中将数据load到Redis中
数据量较小,不易改变,比如:字典库(xml、Table)
通过info命令可以查看关于持久化的信息
RDB
RDB(Redis DataBase),是redis默认的存储方式,RDB方式是通过快照( snapshotting )完成的。
这一刻的数据,不关注过程
触发快照的方式
- 符合自定义配置的快照规则
save 900 1 # 表示15分钟(900秒钟)内至少1个键被更改则进行快照。
save 300 10 # 表示5分钟(300秒)内至少10个键被更改则进行快照。
save 60 10000 # 表示1分钟内至少10000个键被更改则进行快照。
- 执行save或者bgsave命令
- 执行flushall命令
- 执行主从复制操作 (第一次)
配置参数定期执行
在redis.conf中配置:save 多少秒内 数据变了多少
在客户端调用bgsave命令
- Redis父进程首先判断:当前是否在执行save,或bgsave/bgrewriteaof(aof文件重写命令)的子
进程,如果在执行则bgsave命令直接返回。 - 父进程执行fork(调用OS函数复制主进程)操作创建子进程,这个复制过程中父进程是阻塞的,
Redis不能执行来自客户端的任何命令。 - 父进程fork后,bgsave命令返回”Background saving started”信息并不再阻塞父进程,并可以响
应其他命令。 - 子进程创建RDB文件,根据父进程内存快照生成临时快照文件,完成后对原有文件进行原子替换。
(RDB始终完整) - 子进程发送信号给父进程表示完成,父进程更新统计信息。
- 父进程fork子进程后,继续工作。
RDB的优缺点
优点
RDB是二进制压缩文件,占用空间小,便于传输(传给slaver)
主进程fork子进程,可以最大化Redis性能,主进程不能太大,Redis的数据量不能太大,复制过程中主进程阻塞
缺点
不保证数据完整性,会丢失最后一次快照以后更改的所有数据
AOF
# 可以通过修改redis.conf配置文件中的appendonly参数开启
appendonly yes
# AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的。
dir ./
# 默认的文件名是appendonly.aof,可以通过appendfilename参数修改 appendfilename appendonly.aof
AOF原理
AOF文件中存储的是redis的命令,同步命令到 AOF 文件的整个过程可以分为三个阶段:
命令传播:Redis 将执行完的命令、命令的参数、命令的参数个数等信息发送到 AOF 程序中。
缓存追加:AOF 程序根据接收到的命令数据,将命令转换为网络通讯协议的格式,然后将协议内容追加到服务器的 AOF 缓存中。
文件写入和保存:AOF 缓存中的内容被写入到 AOF 文件末尾,如果设定的 AOF 保存条件被满足的话,fsync 函数或者 fdatasync 函数会被调用,将写入的内容真正地保存到磁盘中。
命令传播
当一个 Redis 客户端需要执行命令时, 它通过网络连接, 将协议文本发送给 Redis 服务器。服务器在
接到客户端的请求之后, 它会根据协议文本的内容, 选择适当的命令函数, 并将各个参数从字符串文
本转换为 Redis 字符串对象( StringObject )。每当命令函数成功执行之后, 命令参数都会被传播到
AOF 程序。
缓存追加
命令被传播到 AOF 程序之后, 程序会根据命令以及命令的参数, 将命令从字符串对象转换回原来的
协议文本。协议文本生成之后, 它会被追加到 redis.h/redisServer 结构的 aof_buf 末尾。
redisServer 结构维持着 Redis 服务器的状态, aof_buf 域则保存着所有等待写入到 AOF 文件的协议文本(RESP)。
文件写入和保存
每当服务器常规任务函数被执行、 或者事件处理器被执行时,aof.c/flushAppendOnlyFile 函数都会被调用, 这个函数执行以下两个工作:
WRITE:根据条件,将 aof_buf 中的缓存写入到 AOF 文件。
SAVE:根据条件,调用 fsync 或 fdatasync 函数,将 AOF 文件保存到磁盘中。
AOF保存模式
Redis 目前支持三种 AOF 保存模式,它们分别是:
AOF_FSYNC_NO :不保存。
AOF_FSYNC_EVERYSEC :每一秒钟保存一次。(默认)
AOF_FSYNC_ALWAYS :每执行一个命令保存一次。(不推荐)
以下三个小节将分别讨论这三种保存模式。
AOF_FSYNC_EVERYSEC 每一秒钟保存一次(推荐)
在这种模式中, SAVE 原则上每隔一秒钟就会执行一次, 因为 SAVE 操作是由后台子线程(fork)调用的, 所以它不会引起服务器主进程阻塞(其他模式会阻塞)。
AOF重写、触发方式、混合持久化
AOF记录数据的变化过程,越来越大,需要重写“瘦身”
Redis可以在 AOF体积变得过大时,自动地在后台(Fork子进程)对 AOF进行重写。重写后的新 AOF文件包含了恢复当前数据集所需的最小命令集合。 所谓的“重写”其实是一个有歧义的词语, 实际上,AOF 重写并不需要对原有的 AOF 文件进行任何写入和读取, 它针对的是数据库中键的当前值。
缓存过期淘汰策略
-
惰性删除:Redis的惰性删除策略由 db.c/expireIfNeeded 函数实现,所有键读写命令执行之前都会调用 expireIfNeeded 函数对其进行检查,如果过期,则删除该键,然后执行键不存在的操作;未过期则不作操作,继续执行原有的命令。
-
定期删除:由redis.c/activeExpireCycle 函数实现,函数以一定的频率运行,每次运行时,都从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键。
1)随机测试100个设置了过期时间的key
2)删除所有发现的已过期的key
3)若删除的key超过25个则重复步骤1
注意:并不是一次运行就检查所有的库,所有的键,而是随机检查一定数量的键。
定期删除函数的运行频率,在Redis2.6版本中,规定每秒运行10次,大概100ms运行一次。在Redis2.8版本后,可以通过修改配置文件redis.conf 的 hz 选项来调整这个次数。
- 内存淘汰策略
1、最大内存参数
2、内存淘汰策略
(1)volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。
(2)volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。
(3)volatile-random:从已设置过期时间的数据集中任意选择数据淘汰。
(4)volatile-lfu:从已设置过期时间的数据集挑选使用频率最低的数据淘汰。
(5)allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
(6)allkeys-lfu:从数据集中挑选使用频率最低的数据淘汰。
(7)allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
(8) no-enviction(驱逐):禁止驱逐数据,这也是默认策略。意思是当内存不足以容纳新入数据时,新写入操作就会报错,请求可以继续进行,线上任务也不能持续进行,采用no-enviction策略可以保证数据不被丢失。
这八种大体上可以分为4中,lru、lfu、random、ttl。
Redis主从功能
主从复制
主从集群,将数据库分为两中角色,一种是主数据库(master),另一种是从数据库(slave)。主数据库可以进行读写操作,从数据库只能有读操作(并不一定,只是推荐这么做,后续会说明)。当主数据库有数据写入,会将数据同步复制给从节点,一个主数据库可以同时拥有多个从数据库,而从数据库只能拥有一个主数据库。值得一提的是,从节点也可以有从节点,级联结构。
在2.8之后,主从复制不再发送SYNC命令,取而代之的是PSYNC,格式为:“PSYNC ID offset”。
当从节点和主节点断开重连之后,会把从节点维护的offset,也就是上一次同步到哪里的这个值告诉主节点,同时会告诉主节点上次和当前从节点连接的主节点的runid,满足下面两个条件,Redis不会全量复制,也就是说,不满足以下条件还是会全量复制。
1.从节点传递的run id和master的run id一致。
2.主节点在环形队列上可以找到对应offset的值。
主从模式是三种集群方式里最简单的。它主要是基于Redis的主从复制特性架构的。通常我们会设置一个主节点,N个从节点;默认情况下,主节点负责处理使用者的IO操作,而从节点则会对主节点的数据进行备份,并且也会对外提供读操作的处理。主要的特点如下:
- 主从模式下,当某一节点损坏时,因为其会将数据备份到其它Redis实例上,这样做在很大程度上可以恢复丢失的数据。
- 主从模式下,可以保证负载均衡,这里不再叙说了
- 主从模式下,主节点和从节点是读写分离的。使用者不仅可以从主节点上读取数据,还可以很方便的从从节点上读取到数据,这在一定程度上缓解了主机的压力。
- 从节点也是能够支持写入数据的,只不过从从节点写入的数据不会同步到主节点以及其它的从节点下。
哨兵模式
哨兵模式:是基于主从模式做的一定变化,它能够为Redis提供了高可用性。在实际生产中,服务器难免不会遇到一些突发状况:服务器宕机,停电,硬件损坏等。这些情况一旦发生,其后果往往是不可估量的。而哨兵模式在一定程度上能够帮我们规避掉这些意外导致的灾难性后果。其实,哨兵模式的核心还是主从复制。只不过相对于主从模式在主节点宕机导致不可写的情况下,多了一个竞选机制——从所有的从节点竞选出新的主节点。竞选机制的实现,是依赖于在系统中启动一个sentinel进程。
哨兵与每一个Redis节点相连接并通过PING命令检测它自己和主、从库的网络连接情况,用来判断实例的状态。每一个Redis节点的状态可以分为”主观下线“和”客观下线“。
如果检测的是从库,那么,哨兵简单地把它标记为“主观下线”就行了,因为从库的下线影响一般不太大,集群的对外服务不会间断。
如果检测的是主库,那么,哨兵还不能简单地把它标记为“主观下线”,开启主从切换。因为很有可能存在这么一个情况:那就是哨兵误判了,其实主库并没有故障。可是,一旦启动了主从切换,后续的选主和通知操作都会带来额外的计算和通信开销。
在判断主库是否下线时,不能由一个哨兵说了算,只有大多数的哨兵实例,都判断主库已经“主观下线”了,主库才会被标记为“客观下线”,这个叫法也是表明主库下线成为一个客观事实了。这个判断原则就是:少数服从多数。同时,这会进一步触发哨兵开始主从切换流程。
当主库挂了以后,哨兵会在从库中进行选主,选主主要包括以下三个原则。
首先,优先级最高的从库得分高。
其次,和旧主库同步程度最接近的从库得分高。
最后,ID 号小的从库得分高。
集群模式
- 数据分布
分布式数据库首先要解决地把整个数据集按照分区规则映射到多个节点地问题。即把数据集划分到多个节点上,每个节点负责整体数据的一个子集。HASH_SLOT = CRC16(key) mod 16384 - 查询路由
Redis没有选择使用代理,而是客户端直接连接每个节点。Redis的每个节点中都存储着整个集群的状态,集群状态中一个重要的信息就是每个桶的负责节点。在具体的实现中,Redis用一个大小固定为CLUSTER_SLOTS的clusterNode数组 slots来保存每个桶的负责节点。在集群模式下,Redis接收任何键相关命令时首先计算键对应的桶编号,再根据桶找出所对应的节点,如果节点是自身,则处理键命令;否则回复MOVED重定向错误,通知客户端请求正确的节点,这个过程称为MOVED重定向。重定向信息包含了键所对应的桶以及负责该桶的节点地址,根据这些信息客户端就可以向正确的节点发起请求。 - 集群伸缩
本节主要介绍了集群的扩缩容,从上面的内容中我们也可以看出,扩缩容最核心的地方还是数据的迁移,扩容和缩容的最大区别除了数据迁移的方向之外,就是扩容添加新节点后,需要向整个集群广播slot被新的节点负责的信息,而集群收缩下线节点时,需要向集群广播让所有节点forget掉下线节点。同时在数据迁移过程中,我们需要解决的很重要的问题就是数据路由问题,Redis通过ASK重定向和MOVED重定向解决了该问题。 - 故障转移
整体过程为,首先每个节点都会自主判断其他节点状态,当一个节点A与自己断连时间过长,则判定该节点为PFAIL状态,并将A节点的判定状态Gossip给集群的其他节点。其他节点接收到消息后,会累计节点A的下线报告数,如果节点A的下线报告数目超过了cluster_size/2,即说明一半以上的节点都认为A下线,那么就判定A真正的下线,标记为FAIL。判定结束后,向集群广播节点A下线消息,其他节点都会更新自己维护的节点A的状态信息,标记A为FAIL。
当节点3故障后,我们要采用的故障恢复的方案就是让节点3的子节点代替节点3继续向外提供服务。
- 子节点竞选资格检查
- 子节点休眠时间计算
- 子节点发起拉票,其他主节点投票
- 获得多数选票的子节点替换其主节点并向集群所有节点广播该信息
- 其他节点接收信息后更新配置
- 原先的主节点以及该主节点的其他节点自动成为新的主节点的Slave节点。
Redis和其他缓存区别memCache,mongo
性能对比:
三者的性能都比较高,性能对比的话:Memcache和Redis差不多,要高于MongoDB。
便捷性对比
memcache数据结构单一;
redis丰富一些,数据操作方面,redis更好一些,较少的网络IO次数;
mongodb支持丰富的数据表达、索引,最类似关系型数据库,支持的查询语言非常丰富。
存储空间对比
redis在2.0版本后增加了自己的VM特性,突破物理内存的限制,可以对key value
设置过期时;
memcache可以修改最大可用内存,采用LRU算法;
mongoDB适合大数据量的存储,依赖操作系统VM做内存管理,吃内存也比较厉害,服务不要和别的服务在一起。
可用性对比
redis依赖客户端来实现分布式读写。主从复制时,每次从节点重新连接主节点都要依赖整个快照,无增量复制,因性能和效率问题,所以单点问题比较复杂,不支持自动sharding,需要依赖程序设定一致hash机制。一种替代方案是不用redis本身的复制机制,采用自己做主动复制(多份存储),或者改成增量复制的方式(需要自己实现),一致性问题和性能的权衡。
Memcache本身没有数据冗余机制,也没必要;对于故障预防,采用依赖成熟的hash或者环状的算法,解决单点故障引起的抖动问题。
mongoDB支持master-slave replicaset (内部采用paxos选举算法,自动故障恢复)、auto sharding机制,对客户端屏蔽了故障转移和切分机制。
可靠性对比:
redis支持(快照、AOF)依赖快照进行持久化,aof增强了可靠性的同时,对性能有所影响;
memcache不支持,通常用在做缓存,提升性能;
MongoDB从1.8版本开始采用binlog方式支持持久化的可靠性。
一致性对比
Memcache在并发场景下,用cas保证一致性;
redis事务支持比较弱,只能保证事务中的每个操作连续执行;
mongoDB 4.0后支持事务,之前不支持。
数据分析
mongoDB内置了数据分析的功能(mapreduce),其他两者不支持。
应用场景:
redis:数据量较小的更性能操作和运算上;
memcache:用于在动态系统中减少数据库负载,提升性能。做缓存,提高性能(适合读多写少,对于数据量比较大,可以采用sharding);
MongoDB:主要解决海量数据的访问效率问题。