Redis如何实现事务ACID
什么是ACID
Redis能否实现事务ACID属性呢?
我们可以先来解释一下什么是ACID
- A原子性:要不全部成功,要不全部失败
- C一致性:事务执行前后是一致的,不能因为事务A执行时看到的字段A是1,准备提交时,字段A已经被事务B改成了2。这样是不行的。
- I隔离性:执行事务时,其他操作无法存取到正在执行事务访问的数据。
- D持久性:数据库执行事务后,数据的修改要被持久化保存下来。当数据库重启后,数据的值需要是被修改后的值。
Redis如何实现事务
事务执行的过程我们可以分为三步走
- 客户端要下达一个命令表示一个事务的开启
MULTI
- 客户端把本身要执行的操作和指令发给服务器端,这些也就是读写命令。服务器接收读写命令把他暂存在命令队列中
业务代码 set get incr 等
- 客户端向服务器端发起一个提交事务的命令让Redis去消化刚刚命令队列里的命令。
EXEC
事务机制的ACID的分析
原子性
对于Redis的原子性操作,主要分两种情况 执行报错 和 入队报错
执行报错: 执行报错的话,说明入队的时候是不报错的。执行过程中必然会有正确的指令,Redis在执行过程中正确的会正常执行,报错的指令会返回报错。原子性就无法保证了
入队报错: 入队报错的话,Redis就不会执行这段指令,所以直接返回错误,可以保证原子性!
扩展一下MySQL,MySQL事务中报错的话会有回滚机制,Redis中是不存在回滚机制的。一旦使用过程中Redis发生了这种情况,我们可以使用Redis提供的
redis-check-aof
工具检查 AOF 日志文件,这个工具可以把未完成的事务操作从 AOF 文件中去除。这样一来,我们使用 AOF 恢复实例后,事务操作不会再被执行,从而保证了原子性。如果AOF,RDB都不开启就不要谈数据安全性持久化这些概念了
一致性
事务的一致性保证会受到错误命令、实例故障的影响。所以,我们按照命令出错和实例故障的发生时机,分成三种情况来看。
- 入队就报错,Redis会放弃执行,同时也保证了数据库的一致性。
- 执行就报错,有错误的命令不会被执行,正确的命令可以正常执行,也不会改变数据库的一致性。
- 实例故障报错,实例故障重启后我们要根据用户是否开启了AOF和RDB进行分情况讨论。
如果我们使用了 RDB 快照,因为 RDB 快照不会在事务执行时执行,所以,事务命令操作的结果不会被保存到 RDB 快照中,使用 RDB 快照进行恢复时,数据库里的数据也是一致的。
如果我们使用了 AOF 日志,而事务操作还没有被记录到 AOF 日志时,实例就发生了故障,那么,使用 AOF 日志恢复的数据库数据是一致的。如果只有部分操作被记录到了 AOF 日志,我们可以使用 redis-check-aof 清除事务中已经完成的操作,数据库恢复后也是一致的。
Redis事务机制对一致性是有保证的
隔离性
事务的隔离性主要和并发有关。并发过程中我们还可以细分两个执行阶段。EXEC执行前 和 EXEC执行后
执行前: 我们可以通过Redis提供的watch机制来实现隔离性
执行后: 无法保证
什么是watch机制?
在事务执行前,监控一个或多个键的值变化情况,当事务调用 EXEC 命令执行时,WATCH 机制会先检查监控的键是否被其它客户端修改了。如果修改了,就放弃事务执行,避免事务的隔离性被破坏。然后,客户端可以再次执行事务,此时,如果没有并发修改事务数据的操作了,事务就能正常执行,隔离性也得到了保证。
如果在执行前我们 没有使用watch机制,同时发生了并发请求,就会对数据进行读写,隔离性就没有得到保障
如果在EXEC执行后,虽然无法保证,但是Redis的单线程的。按照入队的先后顺序执行,所以后一个请求不会排的前面一个请求。于是 也不会破坏事务的隔离性。
持久化
Redis 是内存数据库,所以,数据是否持久化保存完全取决于 Redis 的持久化配置模式。
- 如果 Redis 没有使用 RDB 或 AOF,那么事务的持久化属性肯定得不到保证。
- 如果 Redis 使用了 RDB 模式,那么,在一个事务执行后,而下一次的 RDB 快照还未执行前,如果发生了实例宕机,这种情况下,事务修改的数据也是不能保证持久化的。
- 如果 Redis 采用了 AOF 模式,因为 AOF 模式的三种配置选项 no、everysec 和 always 都会存在数据丢失的情况,所以,事务的持久性属性也还是得不到保证。
Redis主从同步的那些问题
主从数据不一致
- 主从同步时,采用的是异步同步。所以无法保证主从库数据的实时一致性。
- 主从同步时,网络因素导致主从数据实时性的延迟
- 主从同步时,从库接收到了主库的命令。但是从库正在处理其他复杂度过高的命令而阻塞,从库只有处理完当前任务后才能处理主库的新命令。这就造成了主从延迟
解决方案
- 在硬件方面,我们要尽量保证主从库间的网络连接状况良好。例如,我们要避免把主从库部署在不同的机房,或者是避免把网络通信密集的应用和Redis 主从库部署在一起。
- 监控主从库间的复制差值,如果主从库差值过大我们就可以通过设置阈值的方式。干预解决主从同步的延迟问题
Redis 的 INFO replication 命令可以查看主库接收写命令的进度信息(master_repl_offset)和从库复制写命令的进度信息(slave_repl_offset),所以,我们就可以开发一个监控程序,先用 INFO replication 命令查到主、从库的进度,然后,我们用 master_repl_offset 减去 slave_repl_offset,这样就能得到从库和主库间的复制进度差值了
读到过期数据
平时应用中读到过期数据是比较常见的,我们分析一下为什么会读到过期数据。
假如一个key的过期时间是19:51:49,刚好有个请求访问了这个key,访问时间是19:51:50。
key过期了正等待被回收,但是还没有回收这段期间就被读取了。这主要是由Redis的过期策略引起的。
过期策略分 惰性删除和 定期删除
惰性删除。当一个数据的过期时间到了以后,并不会立即删除数据,而是等到再有请求来读写这个数据时,对数据进行检查,如果发现数据已经过期了,再删除这个数据。
这个策略的好处是尽量减少删除操作对 CPU 资源的使用,对于用不到的数据,就不再浪费时间进行检查和删除了。但是,这个策略会导致大量已经过期的数据留存在内存中,占用较多的内存资源。所以,Redis 在使用这个策略的同时,还使用了第二种策略:定期删除策略。
定期删除策略 是指Redis 每隔一段时间(默认 100ms),就会随机选出一定数量的数据,检查它们是否过期,并把其中过期的数据删除,这样就可以及时释放一些内存。
清楚了这两个删除策略,我们再来看看它们为什么会导致读取到过期数据。
首先,虽然定期删除策略可以释放一些内存,但是,Redis 为了避免过多删除操作对性能产生影响,每次随机检查数据的数量并不多。如果过期数据很多,并且一直没有再被访问的话,这些数据就会留存在 Redis 实例中。业务应用之所以会读到过期数据,这些留存数据就是一个重要因素。
其次,惰性删除策略实现后,数据只有被再次访问时,才会被实际删除。如果客户端从主库上读取留存的过期数据,主库会触发删除操作,此时,客户端并不会读到过期数据。但是,从库本身不会执行删除操作,如果客户端在从库中访问留存的过期数据,从库并不会触发数据删除。那么,从库会给客户端返回过期数据吗?这就和版本有关了!
版本问题
- 3.2 之前的版本,从库在服务读请求时,并不会判断数据是否过期,而是会返回过期数据。
- 3.2 版本后,如果读取的数据已经过期了,从库虽然不会删除,但是会返回空值,这就避免了客户端读到过期数据
除了版本的问题还有设置过期时间的命令有关,有些命令给数据设置的过期时间在从库上可能会被延后,导致应该过期的数据又在从库上被读取到了。
当主从库全量同步时,如果主库接收到了一条 EXPIRE 命令,那么,主库会直接执行这条命令。这条命令会在全量同步完成后,发给从库执行。而从库在执行时,就会在当前时间的基础上加上数据的存活时间,这样一来,从库上数据的过期时间就会比主库上延后了。
为了避免这种情况,我给你的建议是,在业务应用中使用 EXPIREAT/PEXPIREAT 命令,把数据的过期时间设置为具体的时间点,避免读到过期数据。
结尾
大概总结了
- Redis在事务机制ACID的相关实现保证
- 分析了使用Redis时,Redis主从同步的那些问题
由主从同步问题展开了主从数据不一致的原因以及解决方案,过期数据的原因以及解决方案。
这篇文章大概算是Redis第二阶段的一个收尾吧。下面将从RocketMQ或者Mybatis进行技术的分享!