基于redis的cas实现
cas是我们常用的一种解决并发问题的手段,小到CPU指令集,大到分布式存储,都能看到cas的影子。本文假定你已经充分理解一般的cas方案,如果你还不知道cas是什么,请自行百度
我们在进行关系型数据库的更新操作时,基于cas的更新常常是保证数据业务逻辑语义下的一致性的终极手段,一般用来解决“写偏序”问题。关系型数据库有基于where的条件更新,一些NoSQL也都有对cas的支持,可为什么redis在原生语义上不支持cas操作呢?例如:
setcas key oldvalue newvalue
很多人不理解,redis处理速度本就很快,还需要cas么?我承认redis对于单个指令的处理速度很快,但很多时候我们要解决的是网络问题,和应用程序STW(stop the world,一般指java那种长时间GC)
一旦发生这种问题,形成了 get->判断->停顿 ->set,就可能出现写偏序或者更新丢失,redis也没办法帮你了
为什么redis不支持原生的cas?
这种功能对redis来说实现起来几乎不费力气:原本对数据处理的操作就是基于单线程的,压根不会出现像其他语言的那种内存不可见问题,或者什么性能损失
我找到了09年redis的一个mail list (要翻墙),redis的作者Salvatore Sanfilippo 开始解释了为什么他不想加入cas功能,理由是至少没法说服我,社区中很多人也表示“我们只需要关于string类型的cas操作就好啦”。然而时至今日你依旧没有在redis.io的command列表中找到cas操作的踪迹
幸好,我们有两种方式可以自己实现cas,且并不费力
基于Lua脚本的cas实现
目前我们使用的redis版本,都支持lua脚本的执行,并且性能非常好。甚至对于比较复杂的功能,redis-cli还提供了lua脚本的调试工具。下面是我自己实现的一个string的cas功能,相信已经能满足大多数场景了:
local v = redis.call("get", KEYS[1]) local r = 1 if v == KEYS[2] or v == false then redis.call("set", KEYS[1], KEYS[3]) else r = 0 end return {r, v}
不好意思,我用空格代替了换行,因为语句实现在是太简单。此脚本中的KEYS[1](lua的数组从1开始)代表你要修改的key, KEYS[2]代表原值,KEYS[3]代表要修改为的值。最终返回两个值:第一个值为1或者0,1代表修改成功,0代表修改失败,无论成功失败,第二个值会返回原值,这是为了方便你直接在cas失败后重新进行计算,而不需要再get一下
调用时依照一下方式:
eval 'lua脚本' 3 key oldvalue newvalue
但我更建议你将这个脚本加载到redis中,在shell中执行:
> redis-cli script load 'lua脚本'
> "74ff40a09af2913b2651bfbc68d7bab7220daecd"
第二行返回的就是这个脚本的sha1的哈希码,下次调用这个脚本你可以直接:
evalsha 74ff40a09af2913b2651bfbc68d7bab7220daecd 3 key oldvalue newvalue
你可能疑惑脚本中 v==false的意义,原因是,如果你调用redis.call去获取一个不存在的key,会返回false。由于我使用的go-redis中无法把nil作为old value发送给redis (redis-clie也不行),所以这个脚本会在key不存在的情况下cas成功,无论你把oldvalue赋予了什么值。我想这在大多数场景中都不成问题。对于任意语言的redis框架,对应参数传个空字符串就可以了。对于第二个返回值,这种情况下会返回nil, 能被框架成功解析成对应语言的null值(比如go就是nil)
以下是实际的例子, 在redis-cli下:
> evalsha 74ff40a09af2913b2651bfbc68d7bab7220daecd 3 nosuchkey a b
> 1) (integer) 1
> 2) (nil)
> evalsha 74ff40a09af2913b2651bfbc68d7bab7220daecd 3 nosuchkey b c
> 1) (integer) 1
> 2) "b"
基于Watch和Multi的cas实现
如果你尝试过自己搜索一下redis cas的解决方案,我想你看到的大多数文章都是基于“redis 事务”的,即watch和multi。曾经我做面试官的时候,询问面试者一个他们解决方案,我说既然用到了redis,为什么不尝试用“redis 事务”解决一下这个问题。他表示“不知道redis 事务”,而且根据“事务”二字顺理成章的认为“事务会大大影响redis性能”
实际上所谓的redis事务并不像关系型数据库的事务那么复杂,举个例子, 使用了redis 某种语言框架的伪代码:
client = redis.newClient() //创建客户端
client.watch("teacher") // 对应redis的指令 watch teacher
client.multi() // 对应redis的指令 multi
a = client.get("teacher") // 对应redis的指令 get teacher
if a == "annie"
client.set("teacher", "joe") // 对应redis的指令 set teacher joe
else
client.set("teacher", "han") // 对应redis的指令 set teacher han
client.exec() // 对应redis的指令 exec
服务器为每一个被watch的key维护了一个链,当你的客户端执行到watch teacher时,会被加到这个链上去。之后exec之前的所有get, set操作其实仅仅是进入了一个指令队列,待到exec时,如果watch 的key 没发生变更,则一起执行,否则不执行
拿这种机制与数据库事务对比,会发现无论这个所谓的"redis事务"中间隔了多长时间,其实也并不影响其他指令或者事务,而且一旦队列中的指令执行,也是无法插入其他指令的,保证了隔离性
性能上的对比
好了,现在我们有两个方案了,那个更好一点呢?我倾向于lua脚本的方案,一是因为这个脚本相对易读,通用,减小开发人员代码量。二就是因为性能。我进行了两个简单的实验, 基于我的笔记本上的虚拟机中的docker...,虚拟机分配了2核2G内存
单线程实验
三种交互方式:set——直接对测试key进行set操作, cas——通过lua脚本进行set,并且故意设计成一半成功一半不成功,watch——先watch,再set,最后exec
并发数:1
循环次数:10000
跑了若干次的结果:
set | cas | watch |
2.0s-2.3s | 2.1s-2.9s | 4.3s-4.9s |
并发实验
三种交互方式:set——对测试key先get后set操作, cas——先get,再通过lua脚本进行set,watch——先watch,再get,再set,最后exec
并发数:500
循环次数:1000
跑了若干次的结果:
set | cas | watch |
1m13s-1m33s | 1m30s-1m49s | 2m23s-2m32s |
从以上结果可以看出来,在模拟对一个key进行高并发的操作时,lua脚本会略微比set耗时一些,但事务的方式要远高于其他两个
对于这个试验我要做个说明:
- 为了减小语言本身多线程并发的开销,我选择了go语言
- 测试前做了预热
- 没把建立连接的时间算进去
- 看似500并发的测试,其实还是受物理机CPU核数影响比较大,所以并不能真正模拟出实际高并发的场景
- 两个结果中,网络的延迟应该比redis处理速度占时更多,甚至远多于
- 这是一个非正式的测试结果,仅供横向对比
- 即使4,5两条成立,依旧不会影响lua脚本更好的结论,因为毕竟同样的功能都跑了50w次,lua要比事务省时间
最后留下测试代码以供参考: github地址
作者:cz
基于redis的cas实现的更多相关文章
- 基于Redis的CAS服务端集群
为了保证生产环境CAS(Central Authentication Service)认证服务的高可用,防止出现单点故障,我们需要对CAS Server进行集群部署. CAS的Ticket默认是以Ma ...
- 基于 Redis 实现 CAS 操作
基于 Redis 实现 CAS 操作 Intro 在 .NET 里并发情况下我们可以使用 Interlocked.CompareExchange 来实现 CAS (Compare And Swap) ...
- 基于redis的cas集群配置(转)
1.cas ticket统一存储 做cas集群首先需要将ticket拿出来,做统一存储,以便每个节点访问到的数据一致.官方提供基于memcached的方案,由于项目需要,需要做计入redis,根据官方 ...
- 基于redis的cas集群配置
1.cas ticket统一存储 做cas集群首先需要将ticket拿出来,做统一存储,以便每个节点访问到的数据一致.官方提供基于memcached的方案,由于项目需要,需要做计入redis,根据官方 ...
- 基于Redis的CAS集群
单点登录(SSO)是复杂应用系统的基本需求,Yale CAS是目前常用的开源解决方案.CAS认证中心,基于其特殊作用,自然会成为整个应用系统的核心,所有应用系统的认证工作,都将请求到CAS来完成.因此 ...
- 基于redis分布式缓存实现(新浪微博案例)
第一:Redis 是什么? Redis是基于内存.可持久化的日志型.Key-Value数据库 高性能存储系统,并提供多种语言的API. 第二:出现背景 数据结构(Data Structure)需求越来 ...
- 基于redis分布式缓存实现
Redis的复制功能是完全建立在之前我们讨论过的基 于内存快照的持久化策略基础上的,也就是说无论你的持久化策略选择的是什么,只要用到了Redis的复制功能,就一定会有内存快照发生,那么首先要注意你 的 ...
- 基于Redis的分布式锁真的安全吗?
说明: 我前段时间写了一篇用consul实现分布式锁,感觉理解的也不是很好,直到我看到了这2篇写分布式锁的讨论,真的是很佩服作者严谨的态度, 把这种分布式锁研究的这么透彻,作者这种技术态度真的值得我好 ...
- 基于redis的分布式锁(转)
基于redis的分布式锁 1 介绍 这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁.会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁. 本篇文章会将分布式锁的实现分 ...
随机推荐
- Mysql ibd文件恢复指南
背景 mysql在使用的过程中,难免遇到数据库表误操作,基于此,作者亲力亲为,对mysql数据表ibd文件的恢复做以下详细的说明,对开发或者初级dba提供一定的指导作用,博客中如若存在相关问题,请指明 ...
- linux下的外网木马前期要的工具
这周看了demon哥的博客弄msf外网马,自己试了一下 首先去ngrok官网下载对应的系统版本,如果对应下载那个标签是绿色的(这里不支持windows) 下载好后在linux提取出来 让我们来看看他的 ...
- CDOJ 1330 柱爷与远古法阵【高斯消元,卡精度】
柱爷与远古法阵 Time Limit: 125/125MS (Java/Others) Memory Limit: 240000/240000KB (Java/Others) Submit S ...
- [bzoj1500 维修数列](NOI2005) (splay)
真的是太弱了TAT...光是把代码码出来就花了3h..还调了快1h才弄完T_T 号称考你会不会splay(当然通过条件是1h内AC..吓傻)... 黄学长的题解:http://hzwer.com/28 ...
- 从零开始学习前端JAVASCRIPT — 2、JavaScript基础ES5
1:ES5简介 ECMAScript 5.1 (或仅 ES5) 是ECMAScript(基于JavaScript的规范)标准的修正. 与HTML5规范进程本质类似,ES5通过对现有JavaScript ...
- {style}/index_article.htm {style}表示什么意思啊
LS有点安全意识好不好.... 在你的后台系统设置有个"模板默认风格:________ cfg_df_style " 默认是default也就是 {style}=模板路径+模板默 ...
- 手把手教你搭建 vue 环境
第一步 node环境安装 1.1 如果本机没有安装node运行环境,请下载node 安装包进行安装1.2 如果本机已经安装node的运行换,请更新至最新的node 版本下载地址:https://nod ...
- 月薪20k以上的高级程序员需要学习哪些技术呢?
课程内容: 源码分析.分布式架构.微服务架构.性能优化.团队协作效率.双十一项目实战 适用对象: 1-5年或更长软件开发经验,没有工作经验但基础非常扎实,对java工作机制,常用设计思想,常用java ...
- jquery ui-----弹出窗口 dialog
jquery ui 提供了强大的dialog功能,基本能满足开发的功能. 先上一个简单的例子: [代码] <script> $(function() { $( "#dialo ...
- bootstrap 响应式图片自适应图片大小
<img src="..." class="img-responsive center-block" > 或者 $(window).load(fun ...