Redis作为一个非常成功的数据库,提供了非常丰富的数据类型和命令。使用这些,我们可以轻松高效地完成很多缓存操作,但总会有一些特殊的问题或需求需要解决。这个时候,可能我们需要定制自己的Redis数据结构和命令。Redis命令问题“线程安全”问题我们都知道Redis是单线程的,但是怎么会有线程安全问题呢?我们通常理解的线程安全问题是指在单进程多线程模型下,多个线程操作进程内共享内存导致的数据资源溢出。Redis的线程安全问题并不是来自于Redis服务器内部。Redis作为一个数据服务器,相当于多个客户端的共享内存。多个客户端相当于同一个进程下的多个线程。如果多个客户端之间没有很好的数据同步策略,就会出现类似的线程安全问题。问题。一个典型的场景是:一个用户的状态存储在Redis中:user5277=idle;客户端连接A读取用户状态,获取用户空闲状态status=get("user5277");客户端连接B也读取到用户状态已设置;客户端连接A给用户安排了一个任务,在Redis中设置用户状态为busyset("user5277","busy");客户端连接B也将用户设置为忙碌状态。但是此时用户同时被分配了两个任务。出现这个问题的原因是,Redis虽然是单线程的,可以保证命令的序列化,但是由于其执行效率高,如果多个客户端的命令不能很好的同步,命令的顺序也会错乱。当然这个问题也很容易解决,锁定用户状态即可,这样同一时间只有一个客户端可以操作用户状态。但是我们在加锁的时候需要考虑锁粒度和死锁等问题,这无疑增加了程序的复杂度,也不利于维护。效率问题Redis是一个非常高效的内存数据服务器,它的命令执行速度非常快。之前看过一个阿里云Redis的压测结果,执行效率可以达到10W写QPS,60W读QPS。那么,它的效率问题就是它从哪里来?答案是网络。做网络的都知道,效率优化是从网络开始的。服务器优化代码并优化数据库。我们要知道,执行一次内存访问大约需要100ns,不同机房之间来回大约需要500,000ns。差距可想而知。Redis在单机上效率极高,但工业部署绝不会把服务器和Redis放在同一台机器上。如果存在效率瓶颈,那就是网络。SpringBoot学习笔记,推荐阅读。一个典型的场景是,我们从Redis中读取了一条数据,然后以这条数据为key,读取另一条数据。这样,就有了两次网络往返。出现这个问题的原因是Redis的普通命令不具备服务端的计算能力,无法在服务端进行复合命令操作。Redis虽然也提供了pipeline特性,但是它要求多个命令的请求和响应之间没有依赖关系。关系。如果想简化多个相互依赖的命令,只能将数据拉回客户端,客户端处理后再请求Redis。综上所述,如果我们想要更加高效便捷的使用Redis,我们需要自己“定制”一些命令。另外整理了Redis面试题及答案。微信搜索公众号:Java技术栈,后台回复面试。嵌入式Lua的执行幸运的是,Redis有一个嵌入式Lua执行环境,支持Lua脚本的执行。通过执行Lua脚本,我们可以将多个命令组合成一个Lua脚本,通过Lua脚本实现上面提到的Redis命令。顺序和Redis服务器端计算。LuaLua是一种简洁、轻量级、可扩展的脚本语言。其特点是:轻量级:源码包只有核心库,编译体积很小。高效:用ANSIC编写,启动快,运行快。嵌入式:可以嵌入到各种编程语言或系统中,提高静态语言的灵活性。例如,OpenResty将Lua嵌入到nginx中执行。而且完全不用担心语法问题。Lua的语法很简单,分分钟上手不是问题。执行步骤Redis2.6版本后,会创建Lua环境,加载Lua库,定义Redis全局表,启动时存放redis.pcall等Redis命令,为执行Lua脚本做准备。一个典型的Lua脚本执行步骤如下:检查脚本是否已经执行,使用脚本的sha1校验和生成一个Lua函数;将超时和错误处理挂钩绑定到函数;创建一个伪客户端,通过这个伪客户端在Lua中执行Redis命令;处理伪客户端的返回值,最终返回给客户端;虽然Lua脚本使用的是伪客户端,但是Redis的处理方式和普通客户端一样,同样执行Redis命令执行rdbaof主从复制等操作。可以通过Redis的EVAL和EVALSHA命令使用Lua脚本。EVAL适用于Lua脚本的单次执行。在执行脚本之前,将从脚本内容生成一个sha1校验和,并查询函数表以查看函数是否已定义。如果没有定义,Redis会在执行成功后将脚本的校验和缓存到全局表中。并作为函数名,以后再执行这条命令,将不会创建新的函数。要使用EVALSHA命令,必须先使用SCRIPTLOAD命令将函数加载到Redis中,Redis会返回函数的sha1校验和,后面可以直接使用这个校验和执行命令。以下是使用上述命令的示例:127.0.0.1:6379>EVAL"return'hello'"00"hello"127.0.0.1:6379>SCRIPTLOAD"returnredis.pcall('GET',ARGV[1])""20b602dcc1bb4ba8fca6b74ab364c05c58161a0a"127.0.0.1:6379>EVALSHA20b602dcc1bb4ba8fca6b74ab364c05c58161a0a0test"zbs"EVAL命令的原型是EVALscriptnumkeyskey[key...]arg[arg...]ARG[arg...]可以使用Lua函数内部引用key和参数,需要注意的是KEYS和ARGV的参数编号都是从1开始的。另外需要注意的是,在Lua脚本中,当Redis返回空的时候,结果是false,而不是零;Lua脚本示例下面写了几个Lua脚本示例,用于介绍语法,仅供参考。Redis中hashSetA的字段B的值为C,取出Redis中key为C的值。//使用:EVALscript2ABlocaltmpKey=redis.call('HGET',KEYS[1],KEYS[2]);returnredis.call('GET',tmpKey);l一次pop出多个值,直到值为n,或者list为空(pipeline也可以很方便的实现);//使用:EVALscript2listcountlocallist={};localitem=false;localnum=tonumber(KEYS[2]);while(num>0)doitem=redis.call('LPOP',KEYS[1]);ifitem==falsethenbreak;end;table.insert(list,item);numnum=num-1;end;returnlist;得到zset中得分最高的n个元素对应hashset中的详细信息;localelements=redis.call('ZRANK',KEYS[1],0,KEY[2]);localdetail={};forindex,eleineelementsdolocalinfo=redis.call('HGETALL',ele);table.insert(细节,信息);结束;返回详细信息;这是基本的使用语法,更多的应用要看具体场景。除了一些思考和实现,还有一些需要思考的地方:使用场景首先总结一下Lua在Redis中的使用场景:可以使用Lua脚本实现原子操作,避免不同客户端访问Redis造成数据冲突服务器。当多个请求的结果存在依赖时,可以使用Lua脚本将多个请求整合为一个请求。注意事项在使用Lua脚本时,我们还需要注意:为保证安全,不要在Lua脚本中使用全局变量,以免污染Lua环境。虽然使用全局变量都会报错,但是Lua脚本停止执行,而是在定义变量关键字的时候加上local。注意Lua脚本的时间复杂度,Redis的单线程也会阻塞Lua脚本的执行。在使用Lua脚本实现原子操作时,需要注意的是,如果Lua脚本报错,则不能回滚之前的命令。一次发出多个Redis请求,但是在请求前后没有依赖关系的情况下,使用管道比Lua脚本更方便。总结最近,我的工作发生了很大的变化。从业务到技术栈,与原来完全不同。所有的代码和业务都不受我控制的感觉真的很不舒服。我的工作充斥着“从搜索引擎入手,语法完整。靠调查”,每天都要熬夜熟悉新事物,有点累,结果发现换工作只是找罪恶感,不过,走出舒适区后的成就感,也让我想起自己在不断进步,这是相当有成就感的。另外,关注公众号Java技术栈,后台回复:面试,可以拿到我整理的Redis系列面试题及答案,很全。
