雁塔区住房和城乡建设局网站,中国品牌500强排名,购物网站建设课程设计报告,个人网站的名字目录
前言
1.分布式锁
1.基于单个节点
2.基于多个节点
3.watch(乐观锁)
2.原子操作
1.单命令操作
2.Lua 脚本(多命令操作)
3.事务
1.执行步骤
2.错误处理
3.崩溃处理
总结 前言
在多个客户端并发访问Redis的时候#xff0c;虽然Redis是单线程执行指令#xff…目录
前言
1.分布式锁
1.基于单个节点
2.基于多个节点
3.watch(乐观锁)
2.原子操作
1.单命令操作
2.Lua 脚本(多命令操作)
3.事务
1.执行步骤
2.错误处理
3.崩溃处理
总结 前言
在多个客户端并发访问Redis的时候虽然Redis是单线程执行指令但是由于客户端指令达到Redis的时序无法保证所以可能出现如下的情况导致并发问题。
2个客户端都执行 get, set指令期望将key的值设置为3结果因为并发问题导致结果为2
client1 get x 1
client2 get x 1
client1 set x 2
client2 set x 2
本文介绍 Redis 并发方面的解决方案。
Redis 的单个命令是原子的但是一个业务操作可能包含多条命令比如以下场景客户端查询值并递增在高并发场景下就可能出现并发问题导致数据不一致。
为了保证并发访问的正确性Redis 提供了三种方法原子操作、分布式锁、事务。
1.分布式锁
与分布式锁相对的是本地锁假如只有一个服务实例就可以直接在该单应用本地使用锁变量来控制多个客户端的访问。
如果使用的是多实例的分布式系统就需要使用分布式锁即将锁保存在一个第三方的共享存储系统中可以被多个客户端共享访问和获取。通常将一个 Redis 实例作为分布式锁的存储系统。
实现分布式锁的关键在于
保证每个加锁、释放锁操作都是原子的保证共享存储系统的可靠性即锁的可靠性
分布式锁相较于 Lua 脚本更简单易用但是可能存在死锁问题。分布式锁的性能不如 Lua 脚本。 1.基于单个节点
Redis 提供了 SETNX 命令在 SET 命令后加上 NX 选项也能达到同样的效果保证了加锁操作的原子性。
同时为了避免客户端加锁后不释放应该给锁变量设置过期时间(set NX EX)且在过期释放锁时判断业务代码是否执行完成如果未完成则给锁续期。如果多次续期后业务仍然未完成再释放锁。仍然存在风险
为了区分不同客户端的操作应该将锁变量设置为随机值或唯一值在释放锁时进行验证。
释放锁的逻辑包含了读取锁变量、判断值、删除锁变量的多个操作所以应该使用 Lua 脚本来保证互斥执行。
单个节点可以实现分布式锁的功能但是无法保证可靠性。
2.基于多个节点
为了避免 Redis 实例故障而导致的锁无法工作的问题Redis 的开发者 Antirez 提出了分布式锁算法 Redlock。
Redlock 算法的基本思路是让客户端和多个独立的 Redis 实例依次请求加锁如果客户端能够和半数以上的实例成功地完成加锁操作就认为客户端成功地获得分布式锁了否则加锁失败。这样一来即使有单个实例发生故障因为锁变量在其它实例上也有保存所以客户端仍然可以正常地进行锁操作锁变量并不会丢失。
加锁过程
客户端获取当前时间客户端按顺序依次向 N 个 Redis 实例执行加锁操作。同样使用 SETNX 命令并设置超时时间。如果请求加锁一直超时则视为加锁失败向下一个实例执行加锁操作。客户端完成所有加锁操作后计算整个加锁过程的总耗时。
客户端只有在满足下面的这两个条件时才能认为是加锁成功
从超过半数大于等于 N/21的实例上成功获取到了锁获取锁的总耗时没有超过锁的有效时间。
在满足了这两个条件后还需要重新计算这把锁的有效时间计算的结果是锁的最初有效时间减去客户端为获取锁的总耗时。如果锁的有效时间已经来不及完成共享数据的操作了可以释放锁以免出现还没完成数据操作锁就过期了的情况。 如果没能同时满足这两个条件则视为加锁失败执行释放锁的过程客户端会向所有节点发起释放锁的操作执行释放锁的 Lua 脚本。
判断是否加锁时需要查询所有节点以半数以上节点的锁状态来判断整个分布式锁的状态。在释放锁之前需要先判断分布式锁的状态。
为了避免 Redis 节点发生崩溃重启后造成锁丢失从而影响锁的安全性antirez 还提出了延时重启的概念即一个节点崩溃后不要立即重启而是等待一段时间后再进行重启这段时间应该大于锁的有效时间。优点是保证了锁不会被多个客户端获取缺点是延长了重启时间可能对系统造成影响。
性能和一致性是冲突的如果为了分布式锁的高可用性可以开启持久化但是会有额外的性能开销需要根据实际场景进行选择。 3.watch(乐观锁)
watch通常跟redis事务配合使用watch某个key在操作过程中有没有被其他指令改变进而做出相应的处理。底层利用了CAS操作后面讲Redis事务会讲到。
2.原子操作
为了实现并发控制要求的临界区代码互斥执行Redis 的原子操作采用了两种方法单命令操作和 Lua 脚本。
1.单命令操作
Redis 的每个操作都是原子性的。
Redis 是使用单线程来串行处理客户端的请求操作命令的所以当 Redis 执行某个命令操作时其他命令是无法执行的这相当于单个操作是原子的。虽然 Redis 的单个操作是原子的但是通常修改数据是包含多个操作的至少包括读数据、修改数据、写回数据这三个操作此时仍然可能出现并发问题。
针对常用的修改数据场景Redis 提供了 INCR/DECR 命令可以对数据进行简单的递增/递减操作它们本身就是单个命令操作在执行时具有互斥性。但是如果要执行更复杂的操作Redis 的单命令操作就无法保证互斥执行了。
2.Lua 脚本(多命令操作)
Redis 可以将多个操作写在 Lua 脚本中然后把整个 Lua 脚本作为一个整体执行在执行的过程中不会被其他命令打断从而保证了 Lua 脚本中操作的原子性。
为什么是 Lua 脚本而不是其他语言的脚本 Lua 是一种高效的轻量级 脚本语言用标准 C 语言编写并以源代码形式开放。其设计目的就是为了嵌入应用程序中从而为应用程序提供灵活的扩展和定制功能。 Lua 脚本可以在服务器端执行不需要将数据传输到客户端再进行处理可以减少网络传输的开销因此性能较高。
使用 Lua 脚本不仅可以实现将多个操作原子执行还能够复用 Lua 脚本。但是使用 Lua 脚本需要额外的语言学习成本还有调试困难、可读性较差的问题。
如果把很多操作都放在 Lua 脚本中原子执行会导致 Redis 执行脚本的时间增加同样也会降低 Redis 的并发性能。所以在编写 Lua 脚本时要避免把不需要做并发控制的操作写入脚本中。
在 Lua 脚本执行过程中崩溃怎么办 Redis 会在内部维护一个已经加载脚本的 哈希表记录了每个脚本的 SHA1 值和对应的 Lua 脚本代码。当 Redis 服务器重启时Redis 会自动重新加载这个哈希表中记录的所有脚本再重新执行此时可能导致部分修改被应用。所以 Lua 脚本并不能严格保证原子性。如果对数据一致性非常严格可以使用 Lua 脚本事务 WATCH 的办法。 3.事务
Redis 事务的本质是一组命令的集合。事务支持一次执行多个命令一个事务中所有命令都会被序列化。在事务执行过程会按照顺序串行化执行队列中的命令其他客户端提交的命令请求不会插入到事务执行命令序列中。
Redis 提供了实现事务的几个命令
MULTI 开启事务redis 会将后续的命令逐个放入队列中然后使用 EXEC 命令来原子化执行这个命令系列。
EXEC执行事务中的所有操作命令。
DISCARD取消事务放弃执行事务块中的所有命令。
WATCH在开启事务之前监视一个或多个 key如果事务在执行前这个 key 或多个 key被其他命令修改则事务被中断不会执行事务中的任何命令一般需要在 EXEC 执行失败后重新执行整个函数。
UNWATCH取消 WATCH 对所有 key 的监视。
为什么 WATCH 是中断事务而不是阻塞其他进程这样不会导致并发量高的时候被 WATCH 的事务一直得不到执行吗 这种机制称为 乐观锁因为在大多数情况下碰撞的概率很小所以选用了更容易实现的方式且影响不大。 在使用事务时可以配合 Pipeline 使用一次性将所有命令打包好再全部发送到服务端。 相比于事务的入队同样是一次性执行这样不仅能减少网络 IO还能保证在开启 WATCH 时不会被其他操作打断。 1.执行步骤
开启事务使用 MULTI 命令开启事务入队接收到命令后并不会立即执行而是放到等待执行的事务队列里执行由 EXEC 命令触发事务执行。
当客户端切换到事务状态之后 服务器会根据这个客户端发来的不同命令执行不同的操作
如果客户端发送的命令为 EXEC 、DISCARD、WATCH、MULTI 四个命令的其中一个 那么服务器立即执行这个命令如果是其他命令 那么服务器并不立即执行命令 而是将这个命令放入一个事务队列里面 然后向客户端返回 QUEUED 回复
2.错误处理
在事务执行过程中可能遇到两种不同类型的错误会有不同的应对方案
编译器错误命令在编译时出错会导致整个事务提交失败即所有命令执行不成功运行时错误命令在运行时检测到错误最终会导致事务提交失败但是事务并不会回滚而是跳过错误命令继续执行并保留结果
为什么 Redis 不支持 事务回滚 Redis 命令只会因为错误的语法而失败并且这些问题不能在入队时发现或是命令用在了错误类型的键上面这也就是说从实用性的角度来说失败的命令是由编程错误造成的而这些错误应该在开发的过程中被发现而不应该出现在生产环境中。 不需要对回滚进行支持所以 Redis 的内部可以保持简单且快速。 3.崩溃处理
Redis 在执行事务时会使用一个单独的内存空间来保存事务中的所有修改操作只有当事务成功提交时这些修改操作才会被应用到 Redis 中。因此如果事务被中止所有的修改操作也都会被撤销从而保证了数据的一致性。
如果开启了 AOF 持久化会先将事务中的所有命令写入 AOF 缓冲区然后执行事务中的命令再将 AOF 缓冲区中的数据写入到 AOF 文件。
如果在写入 AOF 文件前崩溃则持久化失败相当于事务没有发生不会出现数据不一致。
另外RDB 快照不会在事务执行途中进行。
总结
本文介绍了 Redis 应对并发问题的三种方案Redis 中的单条命令都是原子操作而且还有 INCR/DECR 来应对简单的场景。对于复杂的场景Redis 可以使用 Lua 脚本、分布式锁、事务来实现操作的原子性。Lua 脚本是将一系列操作放在一个脚本中原子执行。分布式锁是通过共享的锁变量来限制客户端的并发访问。事务是将一系列操作放到执行队列中再按顺序原子执行。