首页 > 数据库 > 数据库开发技术 > 重新认识redis(十)





最近学习的redis内容与事务和并发相关,分为三部分,并发操作的控制,事务特性的支持,分布式锁的实现,感觉这三部分比较相关,就放到一块了。针对这些内容,简单做了一个归纳,如下,可针对每个部分单独学习,也可总体学习。




在生产环境中,不可避免会遇到并发访问问题,例如高铁抢票,双十一抢购。此时如果使用redis,如何保证并发访问的正确性呢?这个需要我们实现并发访问控制。实现并发访问控制推荐的做法是原子操作。下面思考这几个问题:
1.什么是并发访问控制?
2.redis数据修改流程?
3.原子操作实现方法?
1.什么是并发访问控制?
在redis中,并发访问控制指的是对多个客户端访问操作服务端的同一份数据的过程进行控制,以保证任何一个客户端的操作在redis实例上执行时具有互斥性。
2.redis数据修改流程?
redis数据修改流程简称为RMW(Read-Modify-Write),即客户端修改数据时,需要先将数据读取到本地,在本地完成修改,修改完成后再写回redis服务端。当多个客户端对同一份数据进行RMW操作时,需要让RMW操作以原子性方式执行,否则可能会导致执行结果异常。考虑以下场景:
你有一个糖果,张三给你一个,李四给你一个,正常来说你是不是有三个,如果糖果数量结果存在redis中,把张三和李四当作客户端,给你糖果后,他们想知道你现在有多少糖果,显然最后的结果是错误的。

3.原子操作实现方法?
redis实现原子操作有两种方法,第一种,操作合并,即将多命令操作合并为单命令操作,例如INCR/DECR操作。第二种,如果没有对应的单个命令操作可以实现多命令操作相同的效果,可以将多个操作写到一个Lua脚本,Lua脚本能够保证多个操作以原子性方式执行。但是Lua脚本会降低redis的并发性能,因此如果需要使用Lua脚本时,建议最小粒度的把需要进行并发控制的操作写入脚本。执行Lua脚本时在使用客户端端时加上eval选项即可。
示例:
redis-cli --eval lua.script key,args


在关系型数据库中,例如mysql,在事务执行时都支持ACID属性,以保证事务正确可靠。那么,redis支持ACID属性嘛,从以下两个部分来了解:
1.redis对事务机制的支持?
2.redis对ACID属性的支持?
1.redis对事务机制的支持?
redis中提供了MULTI,EXEC,DISCARD和WATCH四个命令来支持事务机制,在redis中,开启事务后,提交的命令不会马上执行,所有的命令会进入执行队列,等到执行EXEC命令后一起执行。具体作用如下:

2.redis对ACID属性的支持?
分别从原子性,一致性,隔离性,持久性四个方面来看吧:
原子性:
第一种情况,在执行EXEC命令前客户端发送的操作命令错误:此时在命令入队时会报错并记录操作,可以继续提交命令,但是到执行EXEC命令时,redis会拒绝执行所有提交的命令操作,事务失败,此时满足原子性属性。
第二种情况,在执行EXEC命令前客户端发送的命令和操作的数据类型不匹配:此时redis对错误命令报错,但是会执行正确的命令,此时不满足原子性属性。
第三种情况,在执行事务的EXEC命令时实例故障导致事务失败:此时如果开启AOF日志,可以通过redis-check-aof工具将已完成的事务操作从AOF文件中去除,则使用AOF恢复实例后事务操作不会再被执行,此时满足原子性属性,如果未开启AOF日志,谈原子性也无意义了。
一致性:
第一种情况,在执行EXEC命令前客户端发送的操作命令错误:该情况事务会放弃执行,此时满足一致性属性。
第二种情况,在执行EXEC命令前客户端发送的命令和操作的数据类型不匹配:此时redis对错误命令报错,但是会执行正确的命令,此时满足一致性属性。
第三种情况,在执行事务的EXEC命令时实例故障:此时无论是否开启持久化,都可以保证数据前后一致,此时满足一致性属性。
隔离性:
第一种情况:执行EXEC操作前使用了WATCH机制并有并发操作:此时在事务执行前watch会监控一或多个键的值是否改变,如果事务执行EXEC时发现监控的值发生修改,则放弃事务执行,此时满足隔离性属性。
第二种情况:执行EXEC操作前未使用WATCH机制并有并发操作:此时不满足隔离性属性。
第三种情况:执行EXEC命令后有并发操作:提交EXEC命令后redis会将命令队列中的所有命令执行完后才能执行其他的命令,因此其他事务不会对当前事务造成影响。此时满足隔离性属性。
持久化:
redis是内存数据库,数据存储在内存中,即使使用持久化,也无法百分比保证数据持久化,因此,不满足持久化属性。
简单记忆可参考下面的图:



前面讲到并发操作,我们知道redis提供了单命令操作和Lua操作,其实,还可以通过使用分布式锁实现加锁,虽然不建议通过加锁控制并发操作,因为会对性能有一定的损耗,但是redis如何实现分布式锁还是要学习的,接下来从以下几个方面了解下吧。
1.分布式锁的实现要求?
2.单节点redis实现分布式锁?
3.多节点实现分布式锁?
分布式锁的实现要求?
分布式锁的实现要求总体来说需要这两点:第一,原子性,分布式锁的加锁和释放锁的过程中涉及多个操作,需要保证这些锁操作的原子性。第二,共享存储系统保存锁变量,因此需要考虑共享存储系统的高可用性,即共享存储系统发生故障或宕机时客户端依然可以正常进行锁操作,以保证锁的可靠性。
2.单节点redis实现分布式锁?
单节点redis实现分布式锁实现简单,性能较好,但不保证可靠性。redis使用SETNX+DEL实现分布式锁。实现思想是key值存在即代表存在锁,释放锁时删除该值即可。考虑两个问题,第一,如果加锁后客户端异常了怎么办?第二,其他客户端也能执行DEL命令,如何区分不同客户端的锁操作?
最终解决方案是:设置锁时使用SET key unique_value [EX seconds|PX milliseconds] [NX],unique_value代表客户端端的唯一标识,可通过随机函数生成,PX表示key过期时间,保证一段时间后锁能自动释放。释放锁时通过判断锁变量是否相符来决定是否能删除锁变量,这期间涉及读取锁变量,判断值,删除锁变量多个操作,因此需要在Lua脚本中实现以保证锁释放操作的原子性。
3.多节点实现分布式锁?
单节点实现分布式锁虽然实现简单,性能较好,但是无法保证可靠性,这在敏感业务中是不可忍受的。因此可以基于多节点实现高可靠的分布式锁。高可用分布式锁基于分布式锁算法Readlock实现,核心思想是客户端和多个独立的Redis实例依次请求加锁,如果和半数以上实例成功完成加锁操作,则认为加锁成功,否则加锁失败。注意,在真正实现时,开始加锁时客户端会记录当前时间,而且加锁过程中会给加锁操作设置超时时间,如果在超时时间内加锁失败,会跳到下一个redis节点执行,另外,会计算获取锁的总耗时,如果锁的最终有效时间(开始有效时间-加锁总耗时)不足以完成数据操作,则向所有节点发起释放锁操作。
SETNX命令和SET命令加NX参数效果相同,执行该命令时,如果key不存在,则key会被创建,如果key已经存在,则不做任何操作。
就通用性来说,肯定是Lua脚本更好,Lua脚本可以包含多个操作,这些操作以原子性方式执行,而单命令原子操作的适用性较少,只有部分RMW操作能转变成单命令的操作。
就性能来说,单命令操作肯定更好,因为redis是单线程执行,Lua脚本涉及多个操作原子执行,会导致执行时间增加,并发性能也会受到一定影响。
可以从三种情况考虑:
第一种,事务执行中途故障,此时RDB中不包含事务所做的修改,能保证事务的原子性属性。
第二种,事务执行完且RDB快照已生成,此时重启后RDB恢复即可,能保证事务的原子性属性。
第三种,事务执行完未生成RDB快照,此时修改的数据丢失,无所谓原子性属性。

以上即为redis中并发操作的控制,事务特性的支持,分布式锁的实现这几个点的知识学习了。你get了嘛。

END





原文作者:DBA修行之路;如有侵权,请联系管理员删除。