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耗时一些,但事务的方式要远高于其他两个

  对于这个试验我要做个说明:

  1. 为了减小语言本身多线程并发的开销,我选择了go语言
  2. 测试前做了预热
  3. 没把建立连接的时间算进去
  4. 看似500并发的测试,其实还是受物理机CPU核数影响比较大,所以并不能真正模拟出实际高并发的场景
  5. 两个结果中,网络的延迟应该比redis处理速度占时更多,甚至远多于
  6. 这是一个非正式的测试结果,仅供横向对比
  7. 即使4,5两条成立,依旧不会影响lua脚本更好的结论,因为毕竟同样的功能都跑了50w次,lua要比事务省时间

  最后留下测试代码以供参考: github地址

  

  作者:cz

基于redis的cas实现的更多相关文章

  1. 基于Redis的CAS服务端集群

    为了保证生产环境CAS(Central Authentication Service)认证服务的高可用,防止出现单点故障,我们需要对CAS Server进行集群部署. CAS的Ticket默认是以Ma ...

  2. 基于 Redis 实现 CAS 操作

    基于 Redis 实现 CAS 操作 Intro 在 .NET 里并发情况下我们可以使用 Interlocked.CompareExchange 来实现 CAS (Compare And Swap) ...

  3. 基于redis的cas集群配置(转)

    1.cas ticket统一存储 做cas集群首先需要将ticket拿出来,做统一存储,以便每个节点访问到的数据一致.官方提供基于memcached的方案,由于项目需要,需要做计入redis,根据官方 ...

  4. 基于redis的cas集群配置

    1.cas ticket统一存储 做cas集群首先需要将ticket拿出来,做统一存储,以便每个节点访问到的数据一致.官方提供基于memcached的方案,由于项目需要,需要做计入redis,根据官方 ...

  5. 基于Redis的CAS集群

    单点登录(SSO)是复杂应用系统的基本需求,Yale CAS是目前常用的开源解决方案.CAS认证中心,基于其特殊作用,自然会成为整个应用系统的核心,所有应用系统的认证工作,都将请求到CAS来完成.因此 ...

  6. 基于redis分布式缓存实现(新浪微博案例)

    第一:Redis 是什么? Redis是基于内存.可持久化的日志型.Key-Value数据库 高性能存储系统,并提供多种语言的API. 第二:出现背景 数据结构(Data Structure)需求越来 ...

  7. 基于redis分布式缓存实现

    Redis的复制功能是完全建立在之前我们讨论过的基 于内存快照的持久化策略基础上的,也就是说无论你的持久化策略选择的是什么,只要用到了Redis的复制功能,就一定会有内存快照发生,那么首先要注意你 的 ...

  8. 基于Redis的分布式锁真的安全吗?

    说明: 我前段时间写了一篇用consul实现分布式锁,感觉理解的也不是很好,直到我看到了这2篇写分布式锁的讨论,真的是很佩服作者严谨的态度, 把这种分布式锁研究的这么透彻,作者这种技术态度真的值得我好 ...

  9. 基于redis的分布式锁(转)

    基于redis的分布式锁 1 介绍 这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁.会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁. 本篇文章会将分布式锁的实现分 ...

随机推荐

  1. Java Web应用集成OSGI

    对OSGI的简单理解 就像Java Web应用程序需要运行在Tomcat.Weblogic这样的容器中一样.程序员开发的OSGI程序包也需要运行在OSGI容器中.目前主流的OSGI容器包括:Apach ...

  2. Linux 权限、磁盘操作命令-Linux基础环境命令学习笔记

    1.创建用户和用户组 1)用户和用户组 /etc/passwd 保存系统用户的基本信息 /etc/group 保存用户组信息 用户名:x:UID:GID 2)基本命令 useradd 增加用户 gro ...

  3. c语言中标识符的作用域

    1.代码块作用域(block scope) 位于一对花括号之间的所有语句称为一个代码块,在代码块的开始位置声明的标识符具有代码块作用域,表示它们可以被这个代码中的所有语句访问.函数定义的形式参数在函数 ...

  4. 《TensorFlow深度学习应用实践》

    http://product.dangdang.com/25207334.html 内容 简 介 本书总的指导思想是在掌握深度学习的基本知识和特性的基础上,培养使用TensorFlow进行实际编程以解 ...

  5. [国嵌攻略][071][Coredump故障分析]

    Core Dump定义 Core Dump又叫核心转存.当程序在运行过程中发生异常,Linux系统可以把程序出错时的内存内容存储在一个core文件中,这个过程叫Core Dump. Core Dump ...

  6. oracle创建触发器及作用举例

    --创建触发器及作用举例 create or replace trigger tri before delete on emp --在删除emp表数据之前需要做的事根据自己的业务去写,before是在 ...

  7. 用.net中的SqlBulkCopy类批量复制数据 (转载)

    在软件开发中,把数据从一个地方复制到另一个地方是一个普遍的应用. 在很多不同的场合都会执行这个操作,包括旧系统到新系统的移植,从不同的数据库备份数据和收集数据. .NET 2.0有一个SqlBulkC ...

  8. HDU 1233 还是畅通工程(模板——克鲁斯卡尔算法)

    题目链接: http://acm.hdu.edu.cn/showproblem.php?pid=1233 题意描述: 输入n个城镇以及n*(n-1)/2条道路信息 计算并输出将所有城镇连通或者间接连通 ...

  9. git gui提交无法获知你的身份 20

    刚刚学习,请说的详细一些,谢谢 callct | 浏览 3382 次 我有更好的答案 1条回答 你没有定义你的名字和邮箱.你打开git console/shell, #输入下面两句,并且替换成你的名字 ...

  10. J.U.C JMM. pipeline.指令重排序,happen-before(续MESI协议)

    缓存(Cache)       CPU的读/写(以及取指令)单元正常情况下甚至都不能直接访问内存——这是物理结构决定的:CPU都没有管脚直接连到内存.相反,CPU和一级缓存(L1 Cache)通讯,而 ...