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. TCP/IP(三)数据链路层~1

    前言 其实前面一堆讲的物理层的概念,会感觉特别的难理解,因为这是一个非常强大的计算机网络体系的底层知识,没有关系!我们大致了解一下就行了. 一.数据链路层概述 这是百度的简介 看图:理解一下,数据链路 ...

  2. HDU2825 Wireless Password

    Description Liyuan lives in a old apartment. One day, he suddenly found that there was a wireless ne ...

  3. UESTC30-最短路-Floyd最短路、spfa+链式前向星建图

    最短路 Time Limit: 3000/1000MS (Java/Others) Memory Limit: 65535/65535KB (Java/Others) 在每年的校赛里,所有进入决赛的同 ...

  4. find the nth digit(二分查找)

    题目连接:http://acm.hdu.edu.cn/showproblem.php?pid=1597 find the nth digit Time Limit: 1000/1000 MS (Jav ...

  5. C语言中%d,%p,%u,%lu等都有什么用处

    %d 有符号10进制整数(%ld 长整型,%hd短整型 )%hu 无符号短整形(%u无符号整形,%lu无符号长整形)%i 有符号10进制整数 (%i 和%d 没有区别,%i 是老式写法,都是整型格式) ...

  6. 调用webService的几种方式

    转自:http://blog.csdn.net/u011165335/article/details/51345224 一.概览 方式1: HttpClient:可以用来调用webservie服务,也 ...

  7. Android初学:Gradle 'HelloWorld' project refresh failed

    Gradle 'HelloWorld' project refresh failed Error:Failed to open zip file.Gradle's dependency cache m ...

  8. 正则API

    正则表达式:规定字符串中字符出现规律的公式 如果备选字符列表中个别字符之间是连续的,可用-省略中间的字符.比如: 匹配1位数字:   [0-9]匹配1位小写字母 : [a-z] 匹配1位大写字母 : ...

  9. 一分钟搭建Vue2.0+Webpack2.0多页面项目

    想要自己一步步搭建的比较麻烦,不是很推荐,最少也要使用vue-cli,在其基础上开始搭建,今天我的主题是一分钟搭建,那么常规方法肯定不能满足的, 而我用的方法也很简单,就是使用已经配置完成的demo模 ...

  10. linux_DNS

    linux其配置文件 : /etc/resolv.conf nameserver 223.5.5.5 nameserver 223.6.6.6 # 这两个解析地址为阿里云解析地址,格式也是这样 什么是 ...