Redis的线程模型
Redis的原子性是如何保证的?
Redis 是一个非常快的内存数据库,它的操作默认是 原子性的,意思是每个操作要么完全成功,要么完全不做,中间不会被打断或停止。也就是说,每次操作要么完全按计划执行完,要么什么都不做,这样可以保证数据的一致性和完整性。
Redis 的原子性主要靠这几个机制:
单线程模型 🧵:Redis 每次只做一个操作,确保操作按顺序执行,不会被其他操作打断。
事务机制(MULTI 和 EXEC) 🔒:Redis 可以把多个操作放一起做,要么全都成功,要么全都不做,确保操作过程中不会被中断。
Lua 脚本 📜:Redis 允许你写脚本,把复杂的操作作为一个整体来执行,确保不会被其他操作影响。这些机制帮助 Redis 在高并发的情况下保持数据的正确性和一致性。
复合指令
Redis内部提供了很多复合指令,他们是一个指令,可是明显干着多个指令的活。
比如 MSET(同时设置多个键值对)
(HMSET(设置哈希类型的多个字段值(已废弃,推荐使用HSET)))、
GETSET(设置新值并返回旧值)、
SETNX(键不存在时设置值,常用于分布式锁)、
SETEX(设置值并指定过期时间(秒),适用于缓存或会话管理)。
这些复合指令都能很好的保持原子性。
Redis事务
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
public class TransactionExample {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
// 开启事务
Transaction transaction = jedis.multi();
// 添加多个操作
transaction.set("key1", "value1");
transaction.incr("counter");
transaction.set("key2", "value2");
// 提交事务
transaction.exec();
System.out.println("Key1: " + jedis.get("key1"));
System.out.println("Counter: " + jedis.get("counter"));
}
}
127.0.0.1:6379> help @transactions
DISCARD (null) -- 放弃事务
summary: Discards a transaction.
since: 2.0.0
EXEC (null) -- 执行事务
summary: Executes all commands in a transaction.
since: 1.2.0
MULTI (null) -- 开启事务
summary: Starts a transaction.
since: 1.2.0
UNWATCH (null) --去掉监听
summary: Forgets about watched keys of a transaction.
since: 2.2.0
WATCH key [key ...] --监听某一个key的变化。key有变化后,就执行当前事务
summary: Monitors changes to keys to determine the execution of a transaction.
since: 2.2.0
- 解析 :
- MULTI开启事务,EXEC提交事务
- 在事务执行期间,其他操作无法插入,确保事务的原子性
总结 :
Redis事务可以通过Watch机制进一步保证在某个事务执行前,某个key不被修改。
注意:UNWATCH取消监听,只在当前客户端有效。
Redis事务失败如何回滚
Redis中的事务回滚,不是回滚数据,而是回滚操作
1.如果事务实在EXEC执行前失败(比如事务中的指令敲错了,或者指令的参数不对),那么整个事务的操作都不会执行。
2.如果事务是在EXEC执行之后失败(比如指令操作的key类型不对),那么事务中的其他操作都会正常执行,不受影响。
事务执行过程中出现失败了怎么办
1.只要客户端执行了EXEC指令,那么就算之后客户端的连接断开了,事务就会一直进行下去。
2.事务有可能造成数据不一致。当EXEC指令执行后,Redis会先将事务中的所有操作都先记录到AOF文件中,然后再执行具体的操作。这时有一种可能,Redis保存了AOF记录后,事务的操作在执行过程中,服务就出现了非正常宕机(服务崩溃了,或者执行进程被kill -9了)。这就会造成AOF中记录的操作,与数据不符合。如果Redis发现这种情况,那么在下次服务启动时,就会出现错误,无法正常启动。这时,就要使用redis-check-aof工具修复AOF文件,将这些不完整的事务操作记录移除掉。这样下次服务就可以正常启动了。
事务机制优缺点,什么时候用事务
Pipeline
- 什么是管道
当客户端执行一个指令,数据包需要通过网络从Client传到Server,然后再从Server返回到Client。这个中间的时间消耗,就称为RTT(Rount Trip Time)。
redis的原生复合指令和事务,都是原子性的。但是pipeline不具备原子性。pipeline只是将多条命令发送到服务端,最终还是可能会被其他客户端的指令加塞的,虽然这种概率通常比较小。所以在pipeline中通常不建议进行复杂的数据操作。同时,这也表明,执行复合指令和事务,会阻塞其他命令执行,而执行pipeline不会。
pipeline的执行需要客户端和服务端同时完成,pipeline在执行过程中,会阻塞当前客户端。在pipeline中不建议拼装过多的指令。因为指令过多,会使客户端阻塞时间太长,同时服务端需要回复这个很繁忙的客户端,占用很多内存。
总体来说,pipeline机制适合做一些在非热点时段进行的数据调整任务。
LUA脚本
在 Lua 脚本中封装复杂业务逻辑,减少客户端的复杂性。
使用 Lua 脚本保证多个操作的原子性。
Redis Function
- 什么是Function
如果你觉得开发lua脚本有困难,那么在Redis7之后,提供了另外一种让程序员解脱的方法-Redis Function。
Redis Function允许将一些功能声明成一个统一的函数,提前加载到Redis服务端(可以由熟悉Redis的管理员加载)。客户端可以直接调用这些函数,而不需要再去开发函数的具体实现。
Redis Function更大的好处在于在Function中可以嵌套调用其他Function,从而更有利于代码复用。相比之下,lua脚本就无法进行复用。
- Function注意点
1》Function同样也可以进行只读调用。
2》如果在集群中使用Function,目前版本需要在各个节点都手动加载一次。Redis不会在集群中进行Function同步
3》Function是要在服务端缓存的,所以不建议使用太多太大的Function。
4》Function和Script一样,也有一系列的管理指令。使用指令 help @scripting 自行了解。
总结
Redis的线程模型整体还是多线程的,只是后台执行指令的核心线程是单线程的。整个线程模型可以理解为还是以单线程为主。基于这种单线程为主的线程模型,不同客户端的各种指令都需要依次排队执行。
Redis这种以单线程为主的线程模型,相比其他中间件,还是非常简单的。这使得Redis处理线程并发问题,要简单高效很多。甚至在很多复杂业务场景下,Redis都是用来进行线程并发控制的很好的工具。但是,这并不意味着Redis就没有线程并发的问题。这时候选择合理的指令执行方式,就非常重要了。
另外,Redis这种比较简单的线程模型其实本身是不利于发挥多线程的并发优势的。而且Redis的应用场景又通常与高性能深度绑定在一起,所以,在使用Redis的时候,还是要时刻思考Redis的这些指令执行方式,这样才能最大限度发挥Redis高性能的优势。