关系数据库中的逻辑在redis中可不可以redis设计与实现 pdf

感谢您对开源事业的支持!
感谢您的开源项目!
与超过 200 万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
已有帐号?
Spring整合分布式缓存Redis
由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...

Spring-redis
 随着Nosql分布式数据库的不断出现,各种非关系型数据库的重要性日益涌现。redis是一个在内存中运行的键值数据库,是一个业界闻名的分布式缓存。本项目基于Spring平台,整合redis数据库,为我们的业务逻辑添加redis数据缓存的功能。主要特性如下:
1.redis的连接基于Spring-data-redis模块,官网:
2.界面的功能与项目 一模一样,只是在业务逻辑层ActorServiceImpl中添加了缓存支持;
3.一般读取方法需要添加@Cacheable,而删除需要使用@CacheEvict,添加和修改使用@CachePut,以防止失效的缓存数据在前端进行了展示;
4.为了不引起歧义,缓存的名称保持与方法名相同;
5.为了表明一个操作是读取缓存数据还是拉取数据库数据,方法体中有控制台输出,如果是读取缓存则不会有控制台输出:
 
6.当访问了一次actor列表的页面后,我们看到redis数据库中新增了记录:

7.以下是添加了缓存的页面:


附录:个人作品索引目录(持续更新)

基本篇










中级篇






Spring整合Jasig CAS框架实现单点登录(未开源)
Spring框架的session模块实现集中式session管理(未开源)

使用Spring boot整合mybatis,rabbitmq,redis,mongodb实现增删改查(未开源)



高级篇

搭建zookeeper集群提供目录服务(未开源)
使用ubuntu+apache+SVN+SVNadmin+maven+Nexus+Hudson搭建持续集成环境(未开源)
Spring框架整合dubbo框架实现分布式服务治理(SOA架构)(未开源)
Spring框架整合dubbox实现微服务架构(MSA架构)(未开源)
使用Spring Cloud实现微服务架构(MSA架构)(未开源)
使用FastDFS搭建分布式文件系统(高可用、负载均衡)(未开源)
搭建高可用nginx集群和Tomcat负载均衡(未开源)
搭建可扩展的ActiveMQ高可用集群(未开源)
实现Mysql数据库的主从复制、读写分离、分表分库、负载均衡和高可用(未开源)
搭建高可用redis集群实现分布式缓存(未开源)



捐赠,私信索取未开源代码


正在加载...查看:25832|回复:11
助理工程师
做了2年开发,一直没用过缓存技术。最近公司要求使用缓存,经过调研发现应该是redis比较流行,轻量级的nosql技术。我现在的疑问是,怎么在项目在使用的。缓存服务器器开启后,客户端请求数据后,把这次的SQL语句作为key存入 redis,把result作为value存入redis,如果下一次同一条SQL语句的话,就从缓存取。那么同一条SQL数据,数据库里已经有数据更新了,怎么办?redis是怎么知道的。或者我这个想法本身就是不对的呢?
读: 读redis-&没有,读mysql-&把mysql数据写回redis
写: 写mysql-&成功,写redis
关系数据库数据写到kv库,默认从kv库读数据,读不到再去sql读然后同步到kv。非要从kv写入到关系数据库,一般也是用在延迟写入上,写操作先存到kv库里,定时再写入到sql里,哪些key已经写入,哪些还没写入,这个一般是用一个长索引数值控制的
至于缓存,一般都是读缓存(写缓存实现起来很罗嗦,而且也不那么靠谱),与数据库的同步策略需要添加到自己的代码逻辑里。
function get_from_db($condition)
& & $data = get_from_cache($condition);
& & if (!$data)
& && &&&$data = get_from_db_directly($condition);
& && &&&set_to_cache($condition, $data);
& & return $
太阳光芒万丈却不及蜡烛只为一人照亮、、、
助理工程师
还是有点不懂?我再问一下:关系数据库数据写到kv库,把关系型数据库的什么数据作为key,value又是什么?比如下图的相册:
业务需求是查询某个用户的相册:
sql = select * from photo where user_id=23;
redis中没有key=sql,那么去MySQL中取
然后key=sql,value=SQL结果,存入redis:
中间use_id=23,又上传了新相册,先把记录插入MySQL,在更新redis吗?
那是所有的SQL请求结果都做缓存吗
(43.14 KB)
助理工程师
引用:原帖由 yuke198907 于
08:26 发表
听起来好高级~ 求指导,
引用:原帖由 yuke198907 于
08:26 发表
听起来好高级~ 从网上搜的、嘻嘻、、、其实不会
太阳光芒万丈却不及蜡烛只为一人照亮、、、
助理工程师
引用:原帖由 IT妖姬 于
10:29 发表
从网上搜的、嘻嘻、、、其实不会 恩恩,还是谢谢
初级工程师
首先你的需求,完全不需要使用redis,使用hibernate的二级缓存,开始查询条件缓存即可。
其次,使用redis在java中,有相关的技术叫做jedis,是java对redis的封装,类似于jdbc的操作。如果楼主对redis不懂的话,建议还是先学一下redis,然后再使用jedis,如果你会用redis,jedis是很简单的,类似于写一个jdbc的封装。详细,可以私聊
java学习交流:
助理工程师
引用:原帖由 java程序冥 于
14:39 发表
首先你的需求,完全不需要使用redis,使用hibernate的二级缓存,开始查询条件缓存即可。
其次,使用redis在java中,有相关的技术叫做jedis,是java对redis的封装,类似于jdbc的操作。如果楼主对redis不懂的话,建议还是先学一下redis ... 恩,那个jedis,我知道,set,get KV,我的疑问是怎么在web开发中使用reids缓存技术,key,value分别存什么
说说我的理解,
首先说明我也是刚开始看,没用过:P 所以仅供参考。
1.写: 用户A 插入一个photo1
(写mysql-&成功,写redis)
a).insert ... jdbc写入mysql
b).redis里插入key=用户A的photo, value=photo1
& & jedis&&set(&用户A的photo&, photo1)
2.读: 想看用户A的photo
a).jedis调: get(“用户A的photo”)
b).return photo1,返回页面
3.读: 想看用户B的photo
a).jedis调: get(“用户B的photo”)
& &return null,
b).jdbc调mysql
& &return&&photoXXX(假设DB里存在用户B的photo一览)
c).jedis&&set(&用户B的photo&, photoXXX)
本帖最后由 kujo4ever 于
15:05 编辑
引用:原帖由 javaclf 于
10:31 发表
恩恩,还是谢谢 虽然没学过redis,我看到你们的回复,个人理解为 KEY为SQL条件【这个可以根据自己的规则自己封装】,Value为查询结果的ID集合,即List&ids&。这个样的,下次查询,直接从redis中拿到List&ids&,然后再去关系型数据库中取,使用主键id查询,这样速度应该很快吧。
楼主小伙子 你的理解是对的 更新的时候 清空掉这个key及其对应的值 就ok ,下次调sql查询 会继续缓存到radis,因为这个key已经不存在了,所以radis缓存中的数据永远都是最新的,觉着哥说的是对的话 给个发个红包 qq:解决缓存和数据库数据同步问题。
1.缓存的使用方式
读数据:先读取缓存,若不存在则从DB中读取,并将结果写入到缓存中;下次数据读取时便可以直接从缓存中获取数据。
改数据:直接失效缓存数据,再修改DB内容(避免突发情况:避免DB修改成功,但由于网络或者其他问题导致缓存数据没有清理,造成了脏数据)
deleteAndIncVersion接口:此接口并不会真的删除数据,而是给数据打了标签,表明已失效状态,并且增加数据版本号;如果数据不存在则写入NULL,同时也生成随机数据版本号。OCS写入支持原子对比版本号:假设传入的版本号与OCS保存的数据版本号一致或者原数据不存在,则准许写入,否则拒绝修改。
2.缓存与数据库的一致性
方案:MySQL作为主库(写),Redis作为高速数据查询(读)从库的异构读写分离。
解决:专门开发了自己的MySQL复制工具,可以方便的实时同步MySQL中的数据到Redis上。
& &&& &&如果你可以接受定期从redis导入到mysql,那基本上表示你的业务就不需要mysql。
& &&& &&至于缓存,一般都是读缓存(写缓存实现起来很罗嗦,而且也不那么靠谱),与数据库的同步策略需要添加到自己的代码逻辑里。
结论:写操作不缓存。失效缓存数据,再修改DB内容。
其他想法:
& &&& &&首先更新到 Mysql,然后再根据Mysql的更新内容去更新 其他数据库例如redis。有一个问题很明显,就是高并发下写入Mysql是个可怕的事情,所以我之前想到的是直接更新redis然后异步更新Mysql,最后将redis作为缓冲层。
结论:高并发写的情况必须单独特殊处理(直接内存操作,慢回写数据库)。比如第4节:追求响应速度的情况。
3.多IDC的情况下的缓存一致性
不一致的根本原因是异构系统之间无法协同同步,不能保证DB数据先同步,缓存数据后同步。
所以就要考虑缓存系统如何等待DB同步,或者能否做到两者共用一套同步机制?缓存同步也依赖DB BINLOG是一个可行的方案。
4.追求响应速度的情况
如果用关系数据库,大量读写会导致索引无效,读写效率都会比较低下。可以考虑仅仅把那种一旦丢失就影响很大如当前等级、帐号和密码之类的才持久化保存到关系数据库中。
其他的实时数据,可以考虑使用各种NOSQL方案来达成更高的性能,或者干脆自己的前端程序在内存中折腾,10分钟才回写给数据库行不行呢。
假如非得用关系数据库不可,建议采用内存数据库。
& &&& &&如果没有内存数据库可用,可以考虑加大机器内存,反正现在内存也便宜,把各种缓存啊SharedBuffer之类的都开得尽可能的大。 还可以考虑上固态硬盘,提高磁盘I/O速度。
& &&& &&再者,就是考虑看能否把数据分离到不同的各个节点,尽量减少单点处理压力。
阅读(...) 评论()使用 Redis 实现分布式系统轻量级协调技术
在分布式系统中,各个进程(本文使用进程来描述分布式系统中的运行主体,它们可以在同一个物理节点上也可以在不同的物理节点上)相互之间通常是需要协调进行运作的,有时是不同进程所处理的数据有依赖关系,必须按照一定的次序进行处理,有时是在一些特定的时间需要某个进程处理某些事务等等,人们通常会使用分布式锁、选举算法等技术来协调各个进程之间的行为。因为分布式系统本身的复杂特性,以及对于容错性的要求,这些技术通常是重量级的,比如 Paxos 算法,欺负选举算法,ZooKeeper 等,侧重于消息的通信而不是共享内存,通常也是出了名的复杂和难以理解,当在具体的实现和实施中遇到问题时都是一个挑战。Redis 经常被人们认为是一种 NoSQL 软件,但其本质上是一种分布式的数据结构服务器软件,提供了一个分布式的基于内存的数据结构存储服务。在实现上,仅使用一个线程来处理具体的内存数据结构,保证它的数据操作命令的原子特性;它同时还支持基于 Lua 的脚本,每个 Redis 实例使用同一个 Lua 解释器来解释运行 Lua 脚本,从而 Lua 脚本也具备了原子特性,这种原子操作的特性使得基于共享内存模式的分布式系统的协调方式成了可能,而且具备了很大的吸引力,和复杂的基于消息的机制不同,基于共享内存的模式对于很多技术人员来说明显容易理解的多,特别是那些已经了解多线程或多进程技术的人。在具体实践中,也并不是所有的分布式系统都像分布式数据库系统那样需要严格的模型的,而所使用的技术也不一定全部需要有坚实的理论基础和数学证明,这就使得基于 Redis 来实现分布式系统的协调技术具备了一定的实用价值,实际上,人们也已经进行了不少尝试。本文就其中的一些协调技术进行介绍。signal/wait 操作在分布式系统中,有些进程需要等待其它进程的状态的改变,或者通知其它进程自己的状态的改变,比如,进程之间有操作上的依赖次序时,就有进程需要等待,有进程需要发射信号通知等待的进程进行后续的操作,这些工作可以通过 Redis 的 Pub/Sub 系列命令来完成,比如:import redis, time
rc = redis.Redis()
def wait( wait_for ):
ps = rc.pubsub()
ps.subscribe( wait_for )
ps.get_message()
wait_msg = None
while True:
msg = ps.get_message()
if msg and msg['type'] == 'message':
wait_msg = msg
time.sleep(0.001)
ps.close()
return wait_msg
def signal_broadcast( wait_in, data ):
wait_count = rc.publish(wait_in, data)
return wait_count用这个方法很容易进行扩展实现其它的等待策略,比如 try wait,wait 超时,wait 多个信号时是要等待全部信号还是任意一个信号到达即可返回等等。因为 Redis 本身支持基于模式匹配的消息订阅(使用 psubscribe 命令),设置 wait 信号时也可以通过模式匹配的方式进行。和其它的数据操作不同,订阅消息是即时易逝的,不在内存中保存,不进行持久化保存,如果客户端到服务端的连接断开的话也是不会重发的,但是在配置了 master/slave 节点的情况下,会把 publish 命令同步到 slave 节点上,这样我们就可以同时在 master 以及 slave 节点的连接上订阅某个频道,从而可以同时接收到发布者发布的消息,即使 master 在使用过程中出故障,或者到 master 的连接出了故障,我们仍然能够从 slave 节点获得订阅的消息,从而获得更好的鲁棒性。另外,因为数据不用写入磁盘,这种方法在性能上也是有优势的。上面的方法中信号是广播的,所有在 wait 的进程都会收到信号,如果要将信号设置成单播,只允许其中一个收到信号,则可以通过约定频道名称模式的方式来实现,比如:频道名称 = 频道名前缀 (channel) + 订阅者全局唯一 ID(myid)其中唯一 ID 可以是 UUID,也可以是一个随机数字符串,确保全局唯一即可。在发送 signal 之前先使用“pubsub channels channel*”命令获得所有的订阅者订阅的频道,然后发送信号给其中一个随机指定的频道;等待的时候需要传递自己的唯一 ID,将频道名前缀和唯一 ID 合并为一个频道名称,然后同前面例子一样进行 wait。示例如下:import random
single_cast_script="""
local channels = redis.call('pubsub', 'channels', ARGV[1]..'*');
if #channels == 0 then
local index= math.mod(math.floor(tonumber(ARGV[2])), #channels) + 1;
return redis.call( 'publish', channels[index], ARGV[3]);
def wait_single( channel, myid):
return wait( channel + myid )
def signal_single( channel, data):
rand_num = int(random.random() * 65535)
return rc.eval( single_cast_script, 0, channel, str(rand_num), str(data) )分布式锁 Distributed
Locks分布式锁的实现是人们探索的比较多的一个方向,在 Redis 的官方网站上专门有一篇文档介绍,其中提出了 Redlock 算法,并列出了多种语言的实现案例,这里作一简要介绍。Redlock 算法着眼于满足分布式锁的三个要素:
安全性:保证互斥,任何时间至多只有一个客户端可以持有锁
免死锁:即使当前持有锁的客户端崩溃或者从集群中被分开了,其它客户端最终总是能够获得锁。
容错性:只要大部分的 Redis 节点在线,那么客户端就能够获取和释放锁。锁的一个简单直接的实现方法就是用 SET NX 命令设置一个设定了存活周期 TTL 的 Key 来获取锁,通过删除 Key 来释放锁,通过存活周期来保证避免死锁。不过这个方法存在单点故障风险,如果部署了 master/slave 节点,则在特定条件下可能会导致安全性方面的冲突,比如:
客户端 A 从 master 节点获得锁
master 节点在将 key 复制到 slave 节点之前崩溃了
slave 节点提升为新的 master 节点
客户端 B 从新的 master 节点获得了锁,而这个锁实际上已经由客户端 A 所持有,导致了系统中有两个客户端在同一时间段内持有同一个互斥锁,破坏了互斥锁的安全性。在 Redlock 算法中,通过类似于下面这样的命令进行加锁:SET resource_name my_random_value NX PX 30000这里的 my_random_value 为全局不同的随机数,每个客户端需要自己产生这个随机数并且记住它,后面解锁的时候需要用到它。解锁则需要通过一个 Lua 脚本来执行,不能简单地直接删除 Key,否则可能会把别人持有的锁给释放了:if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
end这个 ARGV[1] 的值就是前面加锁的时候的 my_random_value 的值。如果需要更好的容错性,可以建立一个有 N(N 为奇数)个相互独立完备的 Redis 冗余节点的集群,这种情况下,一个客户端获得锁和释放锁的算法如下:
先获取当前时间戳 timestamp_1,以毫秒为单位。
以相同的 Key 和随机数值,依次从 N 个节点获取锁,每次获取锁都设置一个超时,超时时限要保证小于所有节点上该锁的自动释放时间,以免在某个节点上耗时过长,通常都设的比较短。
客户端将当前时间戳减去第一步中的时间戳 timestamp_1,计算获取锁总消耗时间。只有当客户端获得了半数以上节点的锁,而且总耗时少于锁存活时间,该客户端才被认为已经成功获得了锁。
如果获得了锁,则其存活时间为开始预设锁存活时间减去获取锁总耗时间。
如果客户端不能获得锁,则应该马上在所有节点上解锁。
如果要重试,则在随机延时之后重新去获取锁。
获得了锁的客户端要释放锁,简单地在所有节点上解锁即可。Redlock 算法不需要保证 Redis 节点之间的时钟是同步的(不论是物理时钟还是逻辑时钟),这点和传统的一些基于同步时钟的分布式锁算法有所不同。Redlock 算法的具体的细节可以参阅 Redis 的官方文档,以及文档中列出的多种语言版本的实现。选举算法在分布式系统中,经常会有些事务是需要在某个时间段内由一个进程来完成,或者由一个进程作为 leader 来协调其它的进程,这个时候就需要用到选举算法,传统的选举算法有欺负选举算法(霸道选举算法)、环选举算法、Paxos 算法、Zab 算法 (ZooKeeper) 等,这些算法有些依赖于消息的可靠传递以及时钟同步,有些过于复杂,难以实现和验证。新的 Raft 算法相比较其它算法来说已经容易了很多,不过它仍然需要依赖心跳广播和逻辑时钟,leader 需要不断地向 follower 广播消息来维持从属关系,节点扩展时也需要其它算法配合。选举算法和分布式锁有点类似,任意时刻最多只能有一个 leader 资源。当然,我们也可以用前面描述的分布式锁来实现,设置一个 leader 资源,获得这个资源锁的为 leader,锁的生命周期过了之后,再重新竞争这个资源锁。这是一种竞争性的算法,这个方法会导致有比较多的空档期内没有 leader 的情况,也不好实现 leader 的连任,而 leader 的连任是有比较大的好处的,比如 leader 执行任务可以比较准时一些,查看日志以及排查问题的时候也方便很多,如果我们需要一个算法实现 leader 可以连任,那么可以采用这样的方法:import redis
rc = redis.Redis()
local_selector = 0
def master():
global local_selector
master_selector = rc.incr('master_selector')
if master_selector == 1:
# initial / restarted
local_selector = master_selector
if local_selector & 0:
# I'm the master before
if local_selector & master_selector:
# lost, maybe the db is fail-overed.
local_selector = 0
# continue to be the master
local_selector = master_selector
if local_selector & 0:
# I'm the current master
rc.expire('master_selector', 20)
return local_selector & 0这个算法鼓励连任,只有当前的 leader 发生故障或者执行某个任务所耗时间超过了任期、或者 Redis 节点发生故障恢复之后才需要重新选举出新的 leader。在 master/slave 模式下,如果 master 节点发生故障,某个 slave 节点提升为新的 master 节点,即使当时 master_selector 值尚未能同步成功,也不会导致出现两个 leader 的情况。如果某个 leader 一直连任,则 master_selector 的值会一直递增下去,考虑到 master_selector 是一个 64 位的整型类型,在可预见的时间内是不可能溢出的,加上每次进行 leader 更换的时候 master_selector 会重置为从 1 开始,这种递增的方式是可以接受的,但是碰到 Redis 客户端(比如 Node.js)不支持 64 位整型类型的时候就需要针对这种情况作处理。如果当前 leader 进程处理时间超过了任期,则其它进程可以重新生成新的 leader 进程,老的 leader 进程处理完毕事务后,如果新的 leader 的进程经历的任期次数超过或等于老的 leader 进程的任期次数,则可能会出现两个 leader 进程,为了避免这种情况,每个 leader 进程在处理完任期事务之后都应该检查一下自己的处理时间是否超过了任期,如果超过了任期,则应当先设置 local_selector 为 0 之后再调用 master 检查自己是否是 leader 进程。消息队列消息队列是分布式系统之间的通信基本设施,通过消息可以构造复杂的进程间的协调操作和互操作。Redis 也提供了构造消息队列的原语,比如 Pub/Sub 系列命令,就提供了基于订阅/发布模式的消息收发方法,但是 Pub/Sub 消息并不在 Redis 内保持,从而也就没有进行持久化,适用于所传输的消息即使丢失了也没有关系的场景。如果要考虑到持久化,则可以考虑 list 系列操作命令,用 PUSH 系列命令(LPUSH, RPUSH 等)推送消息到某个 list,用 POP 系列命令(LPOP, RPOP,BLPOP,BRPOP 等)获取某个 list 上的消息,通过不同的组合方式可以得到 FIFO,FILO,比如:import redis
rc = redis.Redis()
def fifo_push(q, data):
rc.lpush(q, data)
def fifo_pop(q):
return rc.rpop(q)
def filo_push(q, data):
rc.lpush(q, data)
def filo_pop(q):
return rc.lpop(q)如果用 BLPOP,BRPOP 命令替代 LPOP, RPOP,则在 list 为空的时候还支持阻塞等待。不过,即使按照这种方式实现了持久化,如果在 POP 消息返回的时候网络故障,则依然会发生消息丢失,针对这种需求 Redis 提供了 RPOPLPUSH 和 BRPOPLPUSH 命令来先将提取的消息保存在另外一个 list 中,客户端可以先从这个 list 查看和处理消息数据,处理完毕之后再从这个 list 中删除消息数据,从而确保了消息不会丢失,示例如下:def safe_fifo_push(q, data):
rc.lpush(q, data)
def safe_fifo_pop(q, cache):
msg = rc.rpoplpush(q, cache)
# check and do something on msg
rc.lrem(cache, 1) # remove the msg in cache list.
return msg如果使用 BRPOPLPUSH 命令替代 RPOPLPUSH 命令,则可以在 q 为空的时候阻塞等待。结语使用 Redis 作为分布式系统的共享内存,以共享内存模式为基础来实现分布式系统协调技术,虽然不像传统的基于消息传递的技术那样有着坚实的理论证明的基础,但是它在一些要求不苛刻的情况下不失为一种简单实用的轻量级解决方案,毕竟不是每个系统都需要严格的容错性等要求,也不是每个系统都会频繁地发生进程异常,而且 Redis 本身已经经受了工业界的多年实践和考验。另外,用 Redis 技术还有一些额外的好处,比如在开发过程中和生产环境中都可以直接观察到锁、队列的内容,实施的时候也不需要额外的特别配置过程等,它足够简单,在调试问题的时候逻辑清晰,进行排查和临时干预也比较方便。在可扩展性方面也比较好,可以动态扩展分布式系统的进程数目,而不需要事先预定好进程数目。Redis 支持基于 Key 值 hash 的集群,在集群中应用本文所述技术时建议另外部署专用 Redis 节点(或者冗余 Redis 节点集群)来使用,因为在基于 Key 值 hash 的集群中,不同的 Key 值会根据 hash 值被分布到不同的集群节点上,而且对于 Lua 脚本的支持也受到限制,难以保证一些操作的原子性,这一点是需要考虑到的。使用专用节点还有一个好处是专用节点的数据量会少很多,当应用了 master/slave 部署或者 AOF 模式的时候,因为数据量少,master 和 slave 之间的同步会少很多,AOF 模式实时写入磁盘的数据也少很多,这样子也可以大大提高可用性。本文示例所列 Python 代码在 Python3.4 下运行,Redis 客户端采用 ,Redis 服务端版本为 3.0.1 版。
相关主题Redis 的,上面有非常全面的 Redis 相关文档,包括命令和部署方面的知识。Redlock 算法,上面有详细的文档,包括对该算法的详细分析,以及多种实现的链接。Raft 算法的,上面有丰富的 Raft 算法的资料,包括了该算法作者的以及其它相关论文和教程等,上面还收录了多种实现方案的链接。Wikipedia,上面有 与的介绍。Benjamin Reed 和 Flavio P.Junqueira 所著论文《》以及 ,对 Zab 算法进行了介绍。:查找丰富的操作信息、工具和项目更新,帮助您掌握开源技术并将其用于 IBM 产品。
添加或订阅评论,请先或。
有新评论时提醒我
static.content.url=http://www.ibm.com/developerworks/js/artrating/SITE_ID=10Zone=Open sourceArticleID=1008519ArticleTitle=使用 Redis 实现分布式系统轻量级协调技术publish-date=Redis数据库中实现分布式锁的方法
转载 & & 投稿:goldensun
这篇文章主要介绍了Redis数据库中实现分布式锁的方法,Redis是一个高性能的主存式数据库,需要的朋友可以参考下
分布式锁是一个在很多环境中非常有用的原语,它是不同进程互斥操作共享资源的唯一方法。有很多的开发库和博客描述如何使用Redis实现DLM(Distributed Lock Manager),但是每个开发库使用不同的方式,而且相比更复杂的设计与实现,很多库使用一些简单低可靠的方式来实现。
这篇文章尝试提供更标准的算法来使用Redis实现分布式锁。我们提出一种算法,叫做Relock,它实现了我们认为比vanilla单一实例方式更安全的DLM(分布式锁管理)。我们希望社区分析它并提供反馈,以做为更加复杂或替代设计的一个实现。
在说具体算法之前,下面有一些具体的实现可供参考.
&&&(Ruby实现).
(PHP 实现).
(Go 实现).
(Java 实现).
安全和活跃性保证
从有效分布式锁的最小保证粒度来说,我们的模型里面只用了3个属性,具体如下:
1. 属性安全: 互斥行.在任何时候,只有一个客户端可以获得锁.
2. 活跃属性A: 死锁自由. 即使一个客户端已经拥用了已损坏或已被分割资源的锁,但它也有可能请求其他的锁.
3. 活跃属性B:容错. 只要大部分Redis节点可用, 客户端就可以获得和释放锁.
为何基于容错的实现还不够
要理解我们所做的改进,就要先分析下当前基于Redis的分布式锁的做法。
使用Redis锁住资源的最简单的方法是创建一对key-value值。利用Redis的超时机制,key被创建为有一定的生存期,因此它最终会被释放。而当客户端想要释放时,直接删除key就行了。
一般来说这工作得很好,但有个问题: 这是系统的一个单点。如果Redis主节点挂了呢?当然,我们可以加个子节点,主节点出问题时可以切换过来。不过很可惜,这种方案不可行,因为Redis的主-从复制是异步的,我们无法用其实现互斥的安全特性。
这明显是该模型的一种竞态条件:
&&& 客户端A在主节点获得了一个锁。
&&& 主节点挂了,而到从节点的写同步还没完成。
&&& 从节点被提升为主节点。
&&& 客户端B获得和A相同的锁。注意,锁安全性被破坏了!
有时候,在某些情况下这反而工作得很好,例如在出错时,多个客户端可以获得同一个锁。如果这正好是你想要的,那就可以使用主-从复制的方案。否则,我们建议使用这篇文章中描述的方法。
单实例的正确实现方案
在尝试解决上文描述的单实例方案的缺陷之前,先让我们确保针对这种简单的情况,怎么做才是无误的,因为这种方案对某些程序而言也是可以接受的,而且这也是我们即将描述的分布式方案的基础。
为了获取锁,方法是这样的:
& 代码如下:
SET resource_name my_random_value NX PX 30000
这条指令将设置key的值,仅当其不存在时生效(NX选项), 且设置其生存期为30000毫秒(PX选项)。和key关联的value值是"my_random_value"。这个值在所有客户端和所有加锁请求中是必须是唯一的。
使用随机值主要是为了能够安全地释放锁,这要同时结合这么个处理逻辑:删除key值当且仅当其已存在并且其value值是我们所期待的。看看以下lua代码:
& 代码如下:
if redis.call("get",KEYS[1]) == ARGV[1] then
&&& return redis.call("del",KEYS[1])
&&& return 0
这么做很重要,可以避免误删其他客户端创建的锁。例如某个客户端获得了一个锁,但它的处理时长超过了锁的有效时长,之后它删除了这个锁,而此时这个锁可能又被其他客户端给获得了。仅仅做删除是不够安全的,很可能会把其他客户端的锁给删了。结合上面的代码,每个锁都有个唯一的随机值,因此仅当这个值依旧是客户端所设置的值时,才会去删除它。
那么应该怎样生成这个随机值呢?我们使用的是从/dev/urandom读取的20个字节,但你也可以找个更简单的方法,只要能满足任务就行。例如,可以使用/dev/urandom初始化RC4算法,然后用其产生随机数流。更简单的方法是组合unix时间戳和客户端ID, 这并不安全,但对很多环境而言也够用了。
我们所说的key的时间,是指”锁的有效时长“. 它代表两种情况,一种是指锁的自动释放时长,另一种是指在另一个客户端获取锁之前某个客户端占用这个锁的时长,这被限制在从锁获取后开始的一段时间窗口内。
现在我们已经有好的办法获取和释放锁了。在单实例非分布式系统中,只要保证节点没挂掉,这个方法就是安全的。那么让我们把这个概念扩展到分布式的系统中吧,那里可没有这种保证。
Redlock 算法
在此算法的分布式版本中,我们假设有N个Redis主节点。这些节点是相互独立的,因此我们不使用复制或其他隐式同步机制。我们已经描述过在单实例情况下如何安全地获取锁。我们也指出此算法将使用这种方法从单实例获取和释放锁。在以下示例中,我们设置N=5(这是个比较适中的值),这样我们需要在不同物理机或虚拟机上运行5个Redis主节点,以确保它们的出错是尽可能独立的。
为了获取锁,客户端执行以下操作:
&&& 获取当前时间,以毫秒为单位。
&&& 以串行的方式尝试从所有的N个实例中获取锁,使用的是相同的key值和相同的随机value值。在从每个实例获取锁时,客户端会设置一个连接超时,其时长相比锁的自动释放时间要短得多。例如,若锁的自动释放时间是10秒,那么连接超时大概设在5到50毫秒之间。这可以避免当Redis节点挂掉时,会长时间堵住客户端:如果某个节点没及时响应,就应该尽快转到下个节点。
&&& 客户端计算获取所有锁耗费的时长,方法是使用当前时间减去步骤1中的时间戳。当且仅当客户端能从多数节点(至少3个)中获得锁,并且耗费的时长小于锁的有效期时,可认为锁已经获得了。
&&& 如果锁获得了,它的最终有效时长将重新计算为其原时长减去步骤3中获取锁耗费的时长。
&&& 如果锁获取失败了(要么是没有锁住N/2+1个节点,要么是锁的最终有效时长为负数),客户端会对所有实例进行解锁操作(即使对那些没有加锁成功的实例也一样)。
算法是异步的?
算法依赖于这样一个假定,它在处理的时候不是(基于)同步时钟的,每个处理中仍然使用的是本地的时间,它只是大致地以同样地速率运行,这样它就会有一个小的错误,与之相比会有一个小的自动开合的时钟时间。这个假设很像真正世界的电脑:每一台电脑有一个本地时钟,通常我们使用不同的电脑会有一个很小的时钟差。
基于这个观点,我们需要更好地指明我们共同的互斥法则:这是保证客户端能长时间保持状态锁定,其将会终止它们在有效时间内的工作(在步骤3中获得),减去一些时间(在处理时时间差时减去了一些毫秒用来补偿)。
想要了解关于系统需要一个范围的时间差的内容可以获取更多的信息,这篇论文是很好的参考: Leases: an efficient fault-tolerant mechanism for distributed file cache consistency.
失败时重试
当客户端无法获取锁时,它应该在一个随机延迟后重试,从而避免多个客户端同时试图获取锁,相对应同一的同时请求(这可能会导致崩溃,没人会胜出)。同样的,客户端在大多数场合下尝试获取锁的速度越快,崩溃的窗口就越少(重试的需要也越少),所以实际情况下客户端应尝试采用复用方式发送SET命令到多个实例。
强调客户在获取主锁失败是值得的,释放(或部分)以尽快获得锁,这样没有必要为获取锁锁而去等待键到期(但是如果网络分区发生变化时客户端不能与Redis通信的情况下,需要显性提示和等待超时)。
释放锁是简单的,只需要释放所有实例的锁即可,尽管客户端认为有能力成功锁住一个给出的实例。
要问一个算法是安全的么?那么可以尝试着去理解在不同的情景下发生了什么。我们以假设客户端在大多数情况下都能获得锁来开始,所有的实例都包含相同生存周期的键。由于键是在不同的时间设定的,所以键也将在不同的时间超时。然而,如果第一个节点最迟在t1时刻建立(即样品接触的第一服务器之前),上一个键最迟在T2时刻建立(从上一个服务器获得回复的时间)。可以确定的是第一个键在超时之前将生存至少MIN_VALIDITY=TTL-(T2-T1)-CLOCK_DRIFT。所有其他的钥匙将到期后,钥匙将至少在这一次同时设置。
在过半的键被设置这段时间里,另一个客户端无法获得锁,如果N/2+1个键已经存在,N/2+1 SET NX操作将不能成功。所以一个锁被获取,同一时刻被重复获取是不可能的(违反互斥性)。
然而我们还想让多个客户端在获取锁的时候不能同时成功。
如果一个客户端锁定大部分实例的时间超过了锁的最大有效时间(TTL基本设定) ,它将考虑锁无效了,并解锁。所以我们仅考虑在有效时间内大部分实例获得锁的情况。这种情况已经在上文中讨论过, 对于MIN_VALIDITY没有客户端会重新获取锁。所以只有当锁大多数实例的时间超过TTL时间时,多客户端才能同时锁住N/2+1个实例(在步骤2的“时间”即将结束时),让锁失效。
你是否能提供一个形式化的证明,指出现存的足够相似的算法,或找出些bug? 那我们将感激不尽。
存活性证明
系统的存活性基于以下三个主要特性:
&&& 锁的自动释放(key会到期): 最终所有的key将可以被重新锁住;
&&& 一般来说,客户端如果没有成功获得锁,或者获得了锁并且完成了工作,都会及时释放锁,使得我们无需等待key自动释放以重新获得。
&&& 当客户端重新获取锁之前,它会等待一段时间,这段时间比获取锁本身要长得多,这是为了尽量降低资源竞争引起的脑裂条件的概率。
然而,在网络割裂的情况下,我们得付出等同于"TTL"时间的可用性代价,如果网络持续割裂,我们就得无限的付出这个代价。这发生于当客户端获取了一个锁,而在删除锁之前网络断开了。
基本上,如果网络无限期地持续割裂,那系统将无限期地不可用。
性能、故障恢复和文件同步
许多用户使用Redis作为一个需要高性能的加锁服务器,可以根据延迟动态的获取和释放锁,每秒可以成功执行大量的获取/释放锁操作。为了满足这些需求,一种多路复用策略是协同N台 Redis服务器减少延迟(或者叫做穷人的互助,也就是说,将端口置为non-blocking模式,发送所有的命令,延迟读出所有的命令,假定客户端和每个Redis实例的往返时间是相似的)。
然而,如果我们旨在实现一种故障系统的恢复模式,这里有另一种与持久性相关的思路。
考虑这个基本问题,假定我们完全没有配置Redis的持久性。一个客户端需要锁定5个实例中的3个。其中一个允许客户端获取的锁重新启动,虽然我们可以再次为一些资源锁定3个实例,但其它的客户端同样可以锁定它,违反了排他锁安全性。
如果我们启用AOF持久性,情况就会得到相当的改善。例如我们可以通过发送 SHUTDOWN升级一个服务器并且重启它。因为Redis的期限是通过语义设置的,所以服务器关闭的情况下虚拟时间仍然会流逝,我们所有的需求都得到了满足。不管怎样所有事务都会正常运转只要服务器完全关闭。如果电源中断会怎样?如果Redis进行了相关配置,默认情况下每秒文件都会同步写入磁盘,很有可能在重启后我们的数据会丢失。理论上,如果我们想在任何一种实例重启后保证锁的安全性,我们需要确保在持久性配置中设置fsync=always。这将会在同等级别的CP系统上损失性能,传统上这种方式用来更安全的分配锁。
不管怎样事情比我们初次瞥见他们看起来好些。基本上算法的安全性得到保留,就算是当一个实例在故障后重启,它也将不再参与任何当前活跃的锁的分配。因此当实例重启时,当前所有活动锁的设置将从锁定的实例中获取除它重新加入系统。
为了保证这一点,我们只需要做一个实例,在超过最大TTL后,崩溃,不可用,那么就需要时间去获取所有存在着的锁的钥匙,当实例崩溃时,其就会变得无效,会被自动释放。
使用延时重启可以基本上实现安全,甚至不需要利用任何Redis的持久化特性,但是这存在着另外的副作用。举例来说,如果大量的实例崩溃,系统变得全局不可用,那么TTL(这里的全局意味着根本就没有资源可用,在这个时间内所有的资源都会被锁定)。
让算法更可靠: 扩展锁
如果客户工作的执行是由小步骤组成,那么它就可以在默认时间里默认使用更小的锁,并扩展了算法去实现的一个锁的扩展机制。当锁的有效性接近于一个低值,那么通常是客户端在运算中处于居中位置。当锁被取得时,可能扩展的锁通过发送一个Lua脚本到所有的实例,这个实例是扩展TTL的钥匙,如果钥匙存在,那么它的值就是客户端复制的随机值。
客户端应该仅考虑锁的重新取得,如果它可以被扩展,锁就会在有效时间内进入大量实例(基本的算法使用非常类似于获取锁的使用)。&
虽然这不是从技术上去改变算法,但是无论如何尝试获取锁的最大次数是需要限制的,否则的话会违反活跃性中的一个属性。
您可能感兴趣的文章:
大家感兴趣的内容
12345678910
最近更新的内容
常用在线小工具

我要回帖

更多关于 redis设计与实现 pdf 的文章

 

随机推荐