解决使用redisTemplate-set方法保存出现x00问题

在项目有个需求要保存一个字符串到redis,并设置一个过期时间。这个需求一看非常简单,使用redisTemplate一行代码搞定,代码如下 redisTemplate.opsForValue().set("userKey", data, 10000); 但保存后,查看redis发现value的前缀多出了 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x一开始以为是redis的序列化问题,于是就修改了redisTemplate的序列化方式,终于还是没能解决问题。那问题出在哪里?翻看源码,发现redisTemplate.opsForValue().set()有重载方法,一个是 void set(K key, V value, long offset) 另外一个是 void set(K key, V value, long timeout, TimeUnit unit)调用set(K key, V value, long offset)这个方法,其底层调用的是redis的setrange命令,这个命令看官网介绍 Overwrites part of the string stored at key, starting at the specified offset, for the entire length of value. If the offset is larger than the current length of the string at key, the string is padded with zero-bytes to make offset fit. Non-existing keys are considered as empty strings, so this command will make sure it holds a string large enough to be able to set value at offset其含义是从指定的偏移量开始,覆盖整个值范围内从key存储的字符串的一部分。如果偏移量大于key处字符串的当前长度,则该字符串将填充零字节以使偏移量适合。不存在的键被视为空字符串,因此此命令将确保它包含足够大的字符串以能够将值设置为offset。 ...

June 18, 2020 · 1 min · jiezi

英雄之旅行走在开源领域的一个自叙故事

作者介绍潘娟,京东数科高级DBA&Apache ShardingSphere PMC,主要负责京东数科分布式数据库开发、数据库运维自动化平台开发等工作。曾负责京东数科数据库自动化平台设计与开发,现专注于Apache ShardingSphere分布式数据库中间件平台的开发。主要在分布式数据库、开源、分布式架构等相关领域进行探索。多次受邀参加数据库&架构领域的相关会议并进行分享交流。 前序《英雄之旅》是由美国神话学家约瑟夫·坎贝尔提出的。好莱坞很多经典IP巨作即是把《英雄之旅》的套路搬上了巨幕。 最近在读一些心理学书籍,随即发现我们每个人的一生都是一场英雄之旅。从被使命召唤、踏上艰辛的考验之旅、接收他人恩赐、发现自我、到达胜利顶点、回归自我。然而,很多人可能一生都行走在发现自我的路上,若是有幸,则能到达顶点,回归自我。 这篇分享以”英雄之旅“为开始,是希望把自上篇文章-程序媛成长纪:从DBA到研发工程师以来到现在的经历,提取出经验点,与所有行走在英雄之旅的朋友交流,碰撞火花。毕竟我们每个人面对工作、生活、家庭、自我都有着太多冲撞和思考,需要被他人理解和照亮。:-) 由点及面上篇的自叙文谈到我从运维DBA转成了Java开发工程师。准确的说,应该是开源分布式数据库开发工程师。因为在过去一年半多的时间里,我更多地是在分布式数据库中间件平台、分布式事务框架、分布式治理等领域做相关的研发工作。主要从事的项目是Apache分布式数据库中间件平台Apache ShardingSphere, 和京东数科主导的分布式事务平台JDTX(未开源)。 之所以说”由点及面”,是因为工作的内容开始从最初新手关注的一个点,开始渗透到整个项目的各个领域。从最初做的metaData初始化加载模块到现在Apache ShardingSphere的11个一级模块中,有8个模块都是深耕接触过,此外还有公司内部的分布式事务平台JDTX。从这里可以看出一个新手开始逐渐变成了”老司机“。这让我突然想到之前很多人问过的一个问题— 类似像ShardingSphere这样的大型项目,参与的正确姿势是什么?有人选择通读文档,有人选择把模块间架构关系梳理清楚……不过在我看来,从一个点入手,由浅入深,由点及面是参与大型开源项目相对容易的方式。因为一上来就啃一本厚书,很容易厌倦和恐惧,停留在表面,最后兴趣被各种事情冲淡,最后不了了之。而从社区一个很小的任务入手,不仅可以渐渐熟悉项目、获得成就感,更能够积累社区的信赖感,树立自我品牌,最终一览众山小。而在这个过程中,你也会发现自己的知识、技能、人际关系都在崎岖的路上不断上升! 由深入广这个小节用来讨论大家争执已久的一个问题:究竟是先进行深度学习,还是先进行广度学习。标准答案似乎是:同时进行。但是从实践的角度讲,我们的精力、学习阶段、难易程度等因素都会让这个做法变得不太容易进展、很耗时、短期内没有产出。每个人从事的工作阶段、内容都有所不同,自然看法各异。从我个人角度来看,我更倾向于先深度后广度,同时尽可能兼顾彼此。 先深度,是为了帮助我们打下坚实的基础。试想一座空中楼阁,每次风吹雨打都会引发你的惊慌,又如何风轻云淡地向四周望去?正是由于有了扎实的地基,才让你觉得能够更容易理解别的高楼的建筑风格和特色,即在同一个交流层次能帮助我们快速了解他人的核心要点和提出自己的见解。在我刚入行的时候,我很不理解为什么要去听其他人的分享?我自己的一亩三分地还没有耕作完,日日新增的工作还不够我操心,我真的有时间再关注他人吗?当时,我的思想和视野都是狭窄的,我的更多精力是聚焦在了基石的打造,从GitHub提交记录可以看到我疯狂地学习和贡献,此时我没有多余的精力,也没有欲望去关注别人,于是我的视野也很有限。而当我不断熟悉了这个项目的架构与细节,这个领域的知识体系之后,突然发现我可以抽出一部分时间和精力去了解整个行业,而不仅仅是整个项目。而且此刻,我特别有欲望想看看别的楼阁是怎么搭建的、别人的项目是什么情况,我们和别人的异同在哪里。这增大我的视野,也意味着我开始转向对广度的寻求。 其实最好的做法,还是在深耕的同时,关注广度,了解别人在做什么。这样可以有效帮助我们在世界的大地图里,知道自己在哪个位置、自己的水准如何。不过,建议是建议,实践是实践。最好的方式还是问问自己,想不想,要做不做。因为,我选择,我自由,我存在。 由内向外作为开源分布式数据库开发工程师,开源是赋予这个工作岗位的神奇力量。因为在开源的世界里,有开放、自由、平等、自我提升、品牌打造,也有竞争和资本的力量。这让我发现,我不是固步自封,我能感受到来自其他城市、不同国家的新鲜气息。在与同行朋友交流的过程中,你会发现这个行业的立体面,每个人不同的想法都非常有意思。当然,冲撞必不可少,成长的代价和痛苦也一定会非常给力地到位。迷茫和挣扎也一定会伴其左右,因为这就是一场典型的英雄之旅。但我在这个领域除了收获技能和知识,还收获了良师新友和分享的机会和能力。我并不想带来太多负面情绪给大家,我希望阅读文章的朋友能获得新的想法和力量。 过去一年半走过了深圳、上海、北京、南京,参加过中国系统架构师大会、COSCON中国开源年会、OSCHINA中国开源峰会、PstgreSQL中国技术大会、全国互联网架构峰会、ITPUB在线分享等各种会议。在机场写代码,在会场交流,在深夜写PPT。而这一系列的锻炼也磨练出了我的耐心、清晰的逻辑能力、良好的口语表达、自我的认识。我记得最开始,我编写PPT和准备分享都要提前2~3周准备,每天都抽空看看,现在已经变成提前1周准备,随后临场发挥。最近几期的音频和视频分享可以在公众号ALC Beijing 和B站收听和收看,欢迎关注。^_^ 后序篇幅所限,未能展开叙述。在文章最后,还是有些”鸡汤”想和大家分享。例如:与其病态地熬时间地做事情,不如多费心在身体锻炼和思考如何高效集中工作。作为过来人,觉得非常重要。此外,在你的英雄之旅中,找寻本我,释放压抑的情绪,活出自己。每一字一句,是我斟酌良久,给出的建议。因为在高节奏的生活和工作中,我逐渐发现自己人云亦云,被快餐手机消费,压抑自己的情绪,活着像个机器人,追寻所谓的“你好,我好,大家好”。 而现在,愿我,与你都能拥有力量,在不伤害他人的前提下,释放自己的能量,成为我自己,勇敢而执着地踏上英雄之旅!

June 16, 2020 · 1 min · jiezi

英雄之旅行走在开源领域的一个自叙故事

作者介绍潘娟,京东数科高级DBA&Apache ShardingSphere PMC,主要负责京东数科分布式数据库开发、数据库运维自动化平台开发等工作。曾负责京东数科数据库自动化平台设计与开发,现专注于Apache ShardingSphere分布式数据库中间件平台的开发。主要在分布式数据库、开源、分布式架构等相关领域进行探索。多次受邀参加数据库&架构领域的相关会议并进行分享交流。 前序《英雄之旅》是由美国神话学家约瑟夫·坎贝尔提出的。好莱坞很多经典IP巨作即是把《英雄之旅》的套路搬上了巨幕。 最近在读一些心理学书籍,随即发现我们每个人的一生都是一场英雄之旅。从被使命召唤、踏上艰辛的考验之旅、接收他人恩赐、发现自我、到达胜利顶点、回归自我。然而,很多人可能一生都行走在发现自我的路上,若是有幸,则能到达顶点,回归自我。 这篇分享以”英雄之旅“为开始,是希望把自上篇文章-程序媛成长纪:从DBA到研发工程师以来到现在的经历,提取出经验点,与所有行走在英雄之旅的朋友交流,碰撞火花。毕竟我们每个人面对工作、生活、家庭、自我都有着太多冲撞和思考,需要被他人理解和照亮。:-) 由点及面上篇的自叙文谈到我从运维DBA转成了Java开发工程师。准确的说,应该是开源分布式数据库开发工程师。因为在过去一年半多的时间里,我更多地是在分布式数据库中间件平台、分布式事务框架、分布式治理等领域做相关的研发工作。主要从事的项目是Apache分布式数据库中间件平台Apache ShardingSphere, 和京东数科主导的分布式事务平台JDTX(未开源)。 之所以说”由点及面”,是因为工作的内容开始从最初新手关注的一个点,开始渗透到整个项目的各个领域。从最初做的metaData初始化加载模块到现在Apache ShardingSphere的11个一级模块中,有8个模块都是深耕接触过,此外还有公司内部的分布式事务平台JDTX。从这里可以看出一个新手开始逐渐变成了”老司机“。这让我突然想到之前很多人问过的一个问题— 类似像ShardingSphere这样的大型项目,参与的正确姿势是什么?有人选择通读文档,有人选择把模块间架构关系梳理清楚……不过在我看来,从一个点入手,由浅入深,由点及面是参与大型开源项目相对容易的方式。因为一上来就啃一本厚书,很容易厌倦和恐惧,停留在表面,最后兴趣被各种事情冲淡,最后不了了之。而从社区一个很小的任务入手,不仅可以渐渐熟悉项目、获得成就感,更能够积累社区的信赖感,树立自我品牌,最终一览众山小。而在这个过程中,你也会发现自己的知识、技能、人际关系都在崎岖的路上不断上升! 由深入广这个小节用来讨论大家争执已久的一个问题:究竟是先进行深度学习,还是先进行广度学习。标准答案似乎是:同时进行。但是从实践的角度讲,我们的精力、学习阶段、难易程度等因素都会让这个做法变得不太容易进展、很耗时、短期内没有产出。每个人从事的工作阶段、内容都有所不同,自然看法各异。从我个人角度来看,我更倾向于先深度后广度,同时尽可能兼顾彼此。 先深度,是为了帮助我们打下坚实的基础。试想一座空中楼阁,每次风吹雨打都会引发你的惊慌,又如何风轻云淡地向四周望去?正是由于有了扎实的地基,才让你觉得能够更容易理解别的高楼的建筑风格和特色,即在同一个交流层次能帮助我们快速了解他人的核心要点和提出自己的见解。在我刚入行的时候,我很不理解为什么要去听其他人的分享?我自己的一亩三分地还没有耕作完,日日新增的工作还不够我操心,我真的有时间再关注他人吗?当时,我的思想和视野都是狭窄的,我的更多精力是聚焦在了基石的打造,从GitHub提交记录可以看到我疯狂地学习和贡献,此时我没有多余的精力,也没有欲望去关注别人,于是我的视野也很有限。而当我不断熟悉了这个项目的架构与细节,这个领域的知识体系之后,突然发现我可以抽出一部分时间和精力去了解整个行业,而不仅仅是整个项目。而且此刻,我特别有欲望想看看别的楼阁是怎么搭建的、别人的项目是什么情况,我们和别人的异同在哪里。这增大我的视野,也意味着我开始转向对广度的寻求。 其实最好的做法,还是在深耕的同时,关注广度,了解别人在做什么。这样可以有效帮助我们在世界的大地图里,知道自己在哪个位置、自己的水准如何。不过,建议是建议,实践是实践。最好的方式还是问问自己,想不想,要做不做。因为,我选择,我自由,我存在。 由内向外作为开源分布式数据库开发工程师,开源是赋予这个工作岗位的神奇力量。因为在开源的世界里,有开放、自由、平等、自我提升、品牌打造,也有竞争和资本的力量。这让我发现,我不是固步自封,我能感受到来自其他城市、不同国家的新鲜气息。在与同行朋友交流的过程中,你会发现这个行业的立体面,每个人不同的想法都非常有意思。当然,冲撞必不可少,成长的代价和痛苦也一定会非常给力地到位。迷茫和挣扎也一定会伴其左右,因为这就是一场典型的英雄之旅。但我在这个领域除了收获技能和知识,还收获了良师新友和分享的机会和能力。我并不想带来太多负面情绪给大家,我希望阅读文章的朋友能获得新的想法和力量。 过去一年半走过了深圳、上海、北京、南京,参加过中国系统架构师大会、COSCON中国开源年会、OSCHINA中国开源峰会、PstgreSQL中国技术大会、全国互联网架构峰会、ITPUB在线分享等各种会议。在机场写代码,在会场交流,在深夜写PPT。而这一系列的锻炼也磨练出了我的耐心、清晰的逻辑能力、良好的口语表达、自我的认识。我记得最开始,我编写PPT和准备分享都要提前2~3周准备,每天都抽空看看,现在已经变成提前1周准备,随后临场发挥。最近几期的音频和视频分享可以在公众号ALC Beijing 和B站收听和收看,欢迎关注。^_^ 后序篇幅所限,未能展开叙述。在文章最后,还是有些”鸡汤”想和大家分享。例如:与其病态地熬时间地做事情,不如多费心在身体锻炼和思考如何高效集中工作。作为过来人,觉得非常重要。此外,在你的英雄之旅中,找寻本我,释放压抑的情绪,活出自己。每一字一句,是我斟酌良久,给出的建议。因为在高节奏的生活和工作中,我逐渐发现自己人云亦云,被快餐手机消费,压抑自己的情绪,活着像个机器人,追寻所谓的“你好,我好,大家好”。 而现在,愿我,与你都能拥有力量,在不伤害他人的前提下,释放自己的能量,成为我自己,勇敢而执着地踏上英雄之旅!

June 16, 2020 · 1 min · jiezi

Redis-set类型hash类型Zset有序集合常用命令

setsadd myset "str"--插入数据smenbers myset--查看所有数据sismenber myset "str"--查看str是否属于myset,是返回1,否返回0scard myset--返回myset中的元素个数srandmenber myset--随机返回myset中的一个元素srandmenber myset n--随机返回n个元素spop随机删除myset中的一个元素smove myset myset "str"--将myset中的str元素移动到myset2中sdiff myset myset2 --返回两个集合中不同的元素sinter myset myste2 --返回两个集合中相同的元素sunion myset myset2 --返回两个集合合并后的总元素Hash格式:key-field-value hset myhash field value1 --添加一个数据hget myhash field --获取该字段的值hmset myhash field1 value1 field2 value2 --批量插入数据hmget mthash field1 field2 --批量获取数据hgetall myhash --获取所有field1和valuehdel myhash field1 --删除指定hash的字段hlen myhash -返回hash中有多少个值hexists myhash field1 --判断myhash中的field1是否存在hkeys myhash --返回所有的fieldhvals myhash --返回所有的valuehincrby myshash field1 1--field1自增1hdecrby myhash field1 1 --field1 自减1hsetnx myshash field1 va1 --如果field1不存在则添加,存在则无法添加 ...

June 16, 2020 · 1 min · jiezi

Go语言之-Redis

Redis驱动 推荐包https://github.com/astaxie/go...安装go get -u github.com/astaxie/goredis 上述驱动的源码地址,源码是最好的文档! 很香~~~https://github.com/astaxie/goredis/blob/master/redis.go实例代码 package main import ( "fmt" "github.com/astaxie/goredis")func main () { var client goredis.Client client.Addr="127.0.0.1:6379" err:=client.Set("test",[]byte("hello world")) if err!=nil { panic(err) }else { fmt.Println("设置成功") } res,err:=client.Get("test") if err!=nil { panic(err) }else { fmt.Println("%T",res) fmt.Println(string(res)) } ts,err:=client.Setnx("test01",[]byte("lamp")) if err !=nil { panic(err) }else { fmt.Println(ts) } f:=make(map[string]interface{}) f["name"]="zhangsan" f["age"]=23 f["sex"]="nam" err=client.Hmset("test_hash",f) if err!=nil { panic(err) }else { fmt.Println("hash数据设置成功") } name,err:=client.Hget("test_hash","name") if err!=nil { panic(err) }else { fmt.Printf("%T\n",name) // []uint8 字节型(字符类型):byte(uint8别名) fmt.Println("hash_name:",string(name)) }}

June 10, 2020 · 1 min · jiezi

Redis单线程为什么快

为啥这么快:1.redis是基于内存的,内存的读写速度非常快; 2.redis是单线程的,省去了很多上下文切换线程的时间; 3.redis使用多路复用技术,可以处理并发的连接; 简单解释下第二条:上下文切换就是cpu在多线程之间进行轮流执行(抢占cpu资源),而redis单线程的,因此避免了繁琐的多线程上下文切换。 重点解释下多路复用: 多路-指的是多个socket连接,复用-指的是复用一个线程。 目前,多路复用主要有三种技术:select,poll,epoll。它们出现的顺序是按时间先后的,越排后的技术改正了之前技术的缺点。epoll是最新的也是目前最好的多路复用技术。 举个例子:一个酒吧服务员,前面有很多醉汉,epoll这种方式相当于一个醉汉吼了一声要酒,服务员听见之后就去给他倒酒,而在这些醉汉没有要求的时候服务员可以玩玩手机干点别的。但是select和poll技术是这样的场景:服务员轮流着问各个醉汉要不要倒酒,没有空闲的时间。io多路复用的意思就是多个醉汉共用一个服务员。 select: 1.会修改传入的参数,对于多个调用的函数来说非常不友好; 2.要是sock(io流出现了数据),select只能轮询这去找数据,对于大量的sock来说开销很大; 3.不是线程安全的,很恐怖; 4.只能监视1024个连接;poll: 1.还不是线程安全的... 2.去掉了1024个连接的限制; 3.不修改传入的参数了;epoll: 1.线程安全了; 2.epoll不仅能告诉你sock有数据,还能告诉你哪个sock有数据,不用轮询了; 3.however,只支持linux系统;

June 9, 2020 · 1 min · jiezi

Redis实战-读书分享2

第二章 使用Redis构建web应用(案例分析)2.1 登录和cookie缓存业务分析登录时验证token令牌用户每次浏览页面,保存用户浏览时间用户浏览商品页,将商品保存到最近浏览,并限额25个定期清理token令牌,最大保存1000万个(守护进程触发,超过能移除最多100个最旧令牌,并从散列中删除令牌对应的用户信息,最近浏览商品也进行存储,未超过休眠1s)数据构建设计hash 登录token(token、userID)zset 最近登录用户(token、浏览页面的时间戳)zset 最近浏览商品(商品ID、浏览商品的时间戳) 代码实现 python# 尝试获取并返回令牌对应的用户,这里有漏洞,应该判断token是否合法,并不能仅仅判断Redis中是否有无def check_token(conn,token): return conn.hget('login:',token)# 更新token,并保持最近浏览时间,和最近浏览商品 def update_token(conn,token,user,item=None): timestamp = time.time() conn.hset('login:',token,user) conn.zadd('recent:',token,timestamp) if item : conn.zadd('viewed:' + token,item,timestamp) conn.zremrangebyrank('viewed:' + token,0,-26)# 循环清理超数量令牌QUIT = FalseLIMIT = 10000000def clean_sessions(conn): while not QUIT: size = conn.zcard('recent:') if size < LIMIT time.sleep(1) continue # 获取删除的个数,没到100,就选size-LIMIT,超过100就选100 end_index = min(size - LIMIT,100) tokens = conn.zrange('recent:',0,end_index-1) session_keys = [] for token in tokens: session_keys.append('viewed:' + 'token') # 删除最近浏览商品 conn.delete(*session_keys) # 删除登录token conn.hdel('login:',*token) # 删除最近浏览页面记录 conn.zrem('recent:',*token) 2.2 网页缓存(静态化)数据结构设计 : 存储在string中代码实现 Python# callback-对于不能被缓存的数据,可直接调用回调函数def cache_request(conn,request,callback): if not can_cache(conn,request): return callback(request) # 将request请求转换成字符串,方便查找 page_key = 'cache:' + hash_request(request) # 尝试查找被缓存页面, content = conn.get(page_key) if not content : content = callback(request) # 将新生成页面放在缓存中,并设置5分钟过期 conn.setx(page_key,content,300) return content

June 9, 2020 · 1 min · jiezi

Redis实战读书分享1

目前啊,公司有些动荡,程序员??真是不容易呀。自己趁着空余的时间,将原来看过的书和知识点再捋一遍。ps: 纯手打,希望大家多多关注和点赞,后续陆续更新,希望大家一起进步! 图片使用的是processOn画的,有兴趣的小伙伴可以去试一下 如果同学需要电子书,可以评论区留言 吐槽processon,非会员文件最多9个,坑! 第一部分 入门第一章 初识 redis1.1 Redis简介Redis 概念Redis是一个速度很快的非关系型数据库,可以存储key-value之间的映射,可以将存储在内存中的键值对数据持久化到硬盘,可以使用复制特性来扩展读性能,可以使用客户端分片来扩展写性能Redis与其他数据库对比数据库名称 类型 数据存储 查询类型 附件功能 Redis 使用内存存储(in-memory)非关系型数据库 字符串、列表、集合、散列、有序集合 每种数据类型都有自己的专属命令,另外还有批量操作和不完成的事务支持 发布订阅、主从复制、持久化、脚本 memcached 使用内存存储的键值缓存 键值对之间的映射 创建、读取、更新、删除等命令 为提升性能而设的多线程服务器 MySQL 关系型数据库 数据库-表-行 结构 CURD相关函数 支持ACID特性、主从复制、主主复制 postgresql 关系型数据库 同上 同上 同上 MongoDB 使用硬盘存储(on-disk)的非关系文档存储 数据库-表-BSON文档 结构 创建、读取、更新、删除等命令 支持map-reduce操作、主从复制、分片、空间索引 附加特性RDB 和AOF 两种持久化方式支持主从复制①执行复制的从服务器1.2 Redis数据结构 字符串 stringset hello world get hello del hello列表 listrpush list-key item lpush list-key item2 lrange list-key 0 -1 lindex list-key 1 lpop list-key rpop list-key集合 set列表可以存储多个相同字符串,集合存储的字符串各不相同 sadd set-key item 添加成功返回1,已经存在返回0 smembers set-key sismember set-key item 已经存在返回1,不存在返回0 srem set-key item散列 hash可以将hash看做关系型数据库的行 hset hash-key sub-key1 value1 hgetall hash-key hdel hash-key sub-key1 hget hash-key sub-key1有序集合 zset有序集合和散列,都可以存储键值对有序集合中,键:成员,值:分值,分值必须为浮点数Redis中唯一一个,既可以根据成员访问元素,又可以根据分值以及分值的排列顺序访问元素的结构 zadd zset-key 123 member1 zrange zset-key 0 -1 withscores zrangeby zset-key 0 800 withscores 根据分值的范围查找 zrem zset-key member11.3 Redis案列分析(类stackoverflow)1.3.1 对文章进行投票业务分析可以对文章进行点赞文章评分:得票数 * 常量 + 文章发布时间每人每篇只能投一次,只能投一周数据构建设计hash 文章信息(标题、网址、发布用户、发布时间、投票数)zset 文章发布时间(文章ID、发布时间)-可以通过发布时间排序zset 文章评分(文章ID、评分)-可以通过评分排序set 文章已投用户列表(key-文章ID、value-已投用户ID)-一周后就可删除 实现逻辑当用户尝试对一篇文章投票,先去文章发布时间的zset中(zscore命令)检查文章是否超过一周如果文章仍然可以投票,zadd将用户添加到文章已投用户列表set中如果添加成功,则表示该用户第一次对该文章投票,并(zincrby命令)为文章评分zset添加400(设置的常量),并(hincrby命令)为文章信息hash中投票数+1代码展示 python’‘’ 这里应该使用Redis事务,后续章节再添加进来‘’‘ONE_WEEK_IN_SECONDS = 7 * 86400VOTE_SCORE = 400def article_vote(conn,uer,article): cutoff = time.time() - ONE_WEEK_IN_SECONDS if conn.zscore('time:'+ article)< cutoff return # -1的好处是无论前面添加了栏目,ID的索引-1 article_id = article.partition(':')[-1] if conn.sadd('voted:' + article_id,user) conn.zincrby('score:',article,VOTE_SCORE) conn.hincrby(article,'votes',1)1.3.2 发布文章实现逻辑首先创建一个文章ID,可以通过计数器(INCR命令)将文章作者ID添加(SADD命令)到文章已投用户列表,本人默认已经投了自己的文章为文章已投用户列表的set设置(EXPIRE命令)过期时间,1周后自动删除存储(HMSET)文章相关信息将初始评分和发布时间分别添加(ZADD)到对应的有序集合中代码展示 Pythondef post_article(conn,user,title,link): article_id = str(conn.incr('article:')) voted = 'voted:' + article_id conn.sadd(voted,user) conn.expire(voted,ONE_WEEK_IN_SECONDS) now = time.time() article = 'article:' + article_id conn.hmset(article,{ 'title':title, 'link':link, 'poster':user, 'time':now, 'votes':1 }) conn.zadd('time:',article,now) conn.zadd('score:',article,now + VOTE_SCORE)1.3.3 获取文章(评分最高的文章、最新发布的文章)实现逻辑先使用ZREVRANGE(倒序范围取值)在zset中取出多个文章ID然后在hash中对每篇文章ID执行HGETALL取出文章详细信息代码实现 PythonARTICLES_PER_PAGE = 25def get_articles(conn,page,order='score:'): start = (page-1) * ARTICLES_PER_PAGE end = start + ARTICLES_PER_PAGE -1 ids = conn.zrevrange(order,start,end) articles = [] for id in ids: article_data = conn.hgetall(id) # 给源数据添加ID article_data['id'] = id articles.append(article_data) return articles1.3.3 对文章进行分组(分类,标签等类似功能),并取出分组中评分最高或时间最新文章实现逻辑分组可使用集合set将分组集合set、评分有序集合zset(或者时间的有序集合zset),进行交集(ZINTERSTORE命令)计算,存储到一个有序集合然后从有序集合中获取排序的文章信息,为了减少Redis计算量,将交集结果缓存60秒 代码实现 Pythondef get_group_articles(conn,group,page,order='score:'): #交集的key key = order + group if not conn.exists(key): conn.zinterstore(key,['group:'+group,order],aggregate='max') conn.expire(key,60) #调用原有方法获取文章排序 return get_articles(conn,page,key)

June 8, 2020 · 2 min · jiezi

初识Redis一

前言今天来讲讲redis以下知识点,如有不当请多指教! Redis持久化主从复制Sentinel机制Redis ClusterRedis持久化redis是基于内存的,如果不想办法将数据保存在硬盘上,一旦redis重启(退出/故障),内存的数据将会全部丢失。 Redis提供了两种持久化方法: RDB(基于快照),将某一时刻的所有数据保存到一个RDB文件中。 AOF(append-only-file),当Redis服务器执行写命令的时候,将执行的写命令保存到AOF文件中。 RDB保存某个时间点的全量数据快照 手动触发SAVE:阻塞Redis的服务器进程,知道RDB文件被创建完毕 BGSAVE:Fork出一个子进程来创建RDB文件,不阻塞服务器进程,使用lastsave指令可以查看最近的备份时间 自动触发根据redis.conf配置里的save m n定时触发(用的是BGSAVE) 主从复制时,主节点自动触发 执行Debug Relaod 执行Shutdown且没有开启AOF持久化 注意:Redis服务器在启动的时候,如果发现有RDB文件,就会自动载入RDB文件(不需要人工干预) RDB的优缺点优点: RDB是一个紧凑压缩的二进制文件,代表Redis在某个时间点上的数据快照,适合备份,全量复制等场景。 且加载RDB恢复数据远远快于AOF的方式。 缺点: 没办法做到实时持久化/秒级持久化,因为bgsave每次运行都要执行fork操作创建子进程,属于重量级操作,频繁执行成本过高。 RDB的相关配置//在n秒内修改m条数据时创建RDB文件save 900 1save 300 10save 60 10000stop-writes-on-bgsave-error yes //bgsave出错时停止写入rdbcompression yes //压缩RDB文件rdbchecksum yes //校验文件是否损坏AOF与RDB不一样的是,AOF记录的是命令,而不是数据。 如何开启AOF? 只需在配置文件中将appendonly设置为yes即可。 AOF的工作流程:1、所有的写入命令追加到aof_buf缓冲区中。 2、AOF会根据对应的策略向磁盘做同步操作。刷盘策略由appendfsync参数决定。 3、定期对AOF文件进行重写。重写策略由auto-aof-rewrite-percentage,auto-aof-rewrite-min-size两个参数决定。 appendfsync参数有如下取值: appendfsync always # 每次有数据修改发生时都会写入AOF文件。appendfsync everysec # 每秒钟同步一次,该策略为AOF的默认策略。appendfsync no # 从不同步。高效但是数据不会被持久化。AOF重写为什么要重写? 重写后可以加快节点启动时的加载时间 重写后的文件为什么可以变小? 进程内超时的数据不用再写入到AOF文件中 多条写命令可以合并为一个 重写条件1、手动触发      直接调用bgrewriteaof命令 2、自动触发 先来看看有关参数: auto-aof-rewrite-min-size:执行AOF重写时,文件的最小体积,默认值为64MB。 auto-aof-rewrite-percentage:执行AOF重写时,当前AOF大小(即aof_current_size)和上一次重写时AOF大小(aof_base_size)的比值。 只有当auto-aof-rewrite-min-size和auto-aof-rewrite-percentage两个参数同时满足时,才会自动触发AOF重写。 后台重写Redis将AOF重写程序放到子进程里执行(BGREWRITEAOF命令),像BGSAVE命令一样fork出一个子进程来完成重写AOF的操作,从而不会影响到主进程。 AOF后台重写是不会阻塞主进程接收请求的,新的写命令请求可能会导致当前数据库和重写后的AOF文件的数据不一致! 为了解决数据不一致的问题,Redis服务器设置了一个AOF重写缓冲区,当子进程完成重写后会发送信号让父进程将AOF重写缓冲区的数据写到新的AOF文件。 ...

June 8, 2020 · 1 min · jiezi

别让HR再质问我我费劲招的人你用缓存问废了不能简单点

概念缓存穿透 在高并发下,查询一个不存在的值时,缓存不会被命中,导致大量请求直接落到数据库上,如活动系统里面查询一个不存在的活动。 缓存击穿 在高并发下,对一个特定的值进行查询,但是这个时候缓存正好过期了,缓存没有命中,导致大量请求直接落到数据库上,如活动系统里面查询活动信息,但是在活动进行过程中活动缓存突然过期了。 缓存雪崩 在高并发下,大量的缓存key在同一时间失效,导致大量的请求落到数据库上,如活动系统里面同时进行着非常多的活动,但是在某个时间点所有的活动缓存全部过期。 常见解决方案直接缓存NULL值限流缓存预热分级缓存缓存永远不过期layering-cache实践在layering-cache里面结合了缓存NULL值,缓存预热,限流、分级缓存和间接的实现"永不过期"等几种方案来应对缓存穿透、击穿和雪崩问题。 直接缓存NULL值 应对缓存穿透最有效的方法是直接缓存NULL值,但是缓存NULL的时间不能太长,否则NULL数据长时间得不到更新,也不能太短,否则达不到防止缓存击穿的效果。 我在layering-cache对NULL值进行了特殊处理,一级缓存不允许存NULL值,二级缓存可以配置缓存是否允许存NULL值,如果配置可以允许存NULL值,框架还支持配置缓存非空值和NULL值之间的过期时间倍率,这使得我们能精准的控制每一个缓存的NULL值过期时间,控制粒度非常细。当NULL缓存过期我还可以使用限流,缓存预热等手段来防止穿透。 示例: @Cacheable(value = "people", key = "#person.id", depict = "用户信息缓存", firstCache = @FirstCache(expireTime = 10, timeUnit = TimeUnit.MINUTES), secondaryCache = @SecondaryCache(expireTime = 10, timeUnit = TimeUnit.HOURS, isAllowNullValue = true, magnification = 10))public Person findOne(Person person) { Person p = personRepository.findOne(Example.of(person)); logger.info("为id、key为:" + p.getId() + "数据做了缓存"); return p;}在这个例子里面isAllowNullValue = true表示允许缓存NULL值,magnification = 10表示NULL值和非NULL值之间的时间倍率是10,也就是说当缓存值为NULL时,二级缓存的有效时间将是1个小时。 限流 应对缓存穿透的常用方法之一是限流,常见的限流算法有滑动窗口,令牌桶算法和漏桶算法,或者直接使用队列、加锁等,在layering-cache里面我主要使用分布式锁来做限流。 layering-cache数据读取流程: 下面是读取数据的核心代码: ...

June 5, 2020 · 2 min · jiezi

自定义RedisCacheManager

自定义RedisCacheManager 在项目的Redis配置类RedisConfig中,按照上一步分析的定制方法自定义名为cacheManager的Bean组件 java @Bean public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { // 分别创建String和JSON格式序列化对象,对缓存数据key和value进行转换 RedisSerializer<String> strSerializer = new StringRedisSerializer(); Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class); // 解决查询缓存转换异常的问题 ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jacksonSeial.setObjectMapper(om); // 定制缓存数据序列化方式及时效 RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofDays(1)) .serializeKeysWith(RedisSerializationContext.SerializationPair .fromSerializer(strSerializer)) .serializeValuesWith(RedisSerializationContext.SerializationPair .fromSerializer(jacksonSeial)) .disableCachingNullValues(); RedisCacheManager cacheManager = RedisCacheManager .builder(redisConnectionFactory).cacheDefaults(config).build(); return cacheManager; } 上述代码中,在RedisConfig配置类中使用@Bean注解注入了一个默认名称为方法名的cacheManager组件。在定义的Bean组件中,通过RedisCacheConfiguration对缓存数据的key和value分别进行了序列化方式的定制,其中缓存数据的key定制为StringRedisSerializer(即String格式),而value定制为了Jackson2JsonRedisSerializer(即JSON格式),同时还使用entryTtl(Duration.ofDays(1))方法将缓存数据有效期设置为1天 完成基于注解的Redis缓存管理器RedisCacheManager定制后,可以对该缓存管理器的效果进行测试(使用自定义序列化机制的RedisCacheManager测试时,实体类可以不用实现序列化接口) 学习让人快乐,学习更让人觉得无知!学了1个多月的《Java工程师高薪训练营》,才发现自己对每个技术点的认知都很肤浅,根本深不下去,立个Flag:每天坚持学习一小时,一周回答网上3个技术问题,把自己知道都分享出来。

June 5, 2020 · 1 min · jiezi

Redis注解默认序列化机制

Redis注解默认序列化机制 打开Spring Boot整合Redis组件提供的缓存自动配置类RedisCacheConfiguration(org.springframework.boot.autoconfigure.cache包下的),查看该类的源码信息,其核心代码如下 java @Configuration class RedisCacheConfiguration { @Bean public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory,ResourceLoader resourceLoader) { RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory) .cacheDefaults(this.determineConfiguration(resourceLoader.getClassLoader())); List<String> cacheNames = this.cacheProperties.getCacheNames(); if(!cacheNames.isEmpty()) { builder.initialCacheNames(new LinkedHashSet(cacheNames)); } return (RedisCacheManager)this.customizerInvoker.customize(builder.build()); } private org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration(ClassLoader classLoader){ if(this.redisCacheConfiguration != null) { return this.redisCacheConfiguration; } else { Redis redisProperties = this.cacheProperties.getRedis(); org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration.defaultCacheConfig(); config = config.serializeValuesWith(SerializationPair.fromSerializer( new JdkSerializationRedisSerializer(classLoader))); ... return config; } } } 从上述核心源码中可以看出,同RedisTemplate核心源码类似,RedisCacheConfiguration内部同样通过Redis连接工厂RedisConnectionFactory定义了一个缓存管理器RedisCacheManager;同时定制RedisCacheManager时,也默认使用了JdkSerializationRedisSerializer序列化方式。 如果想要使用自定义序列化方式的RedisCacheManager进行数据缓存操作,可以参考上述核心代码创建一个名为cacheManager的Bean组件,并在该组件中设置对应的序列化方式即可 * 注意,在Spring Boot 2.X版本中,RedisCacheManager是单独进行构建的。因此,在Spring Boot 2.X版本中,对RedisTemplate进行自定义序列化机制构建后,仍然无法对RedisCacheManager内部默认序列化机制进行覆盖(这也就解释了基 于注解的Redis缓存实现仍然会使用JDK默认序列化机制的原因),想要基于注解的Redis缓存实现也使用自定义序列化机制,需要自定义RedisCacheManager. ...

June 5, 2020 · 1 min · jiezi

redis-实现搜索热词统计

核心需求一个项目中,遇到了搜索热词统计的需求,我使用了 Redis 的五大数据类型之一 Sorted Set 实现。目前有两项数据需要统计:“当日搜索热词 top10”和“当周搜索热词 top10”。 关于这两项数据的统计方法,目前想到了两种实现方法: 两个 Redis 的 Sorted Set 实现,一个 Sorted Set A 统计当天,0 点 top10 记录进 MySQL,Sorted Set 清零。一个 Sorted Set B 统计当周,每周日 top10 记录进 MySQL,Sorted Set B 清零。只使用用一个 Sorted Set 记录当天搜索热词,0 点 top10 记录进 MySQL,Sorted Set 清零。到周日时,会有 7 10 行记录。把这 7 10 行遍历,每次便利都记录进 Sorted Set,全部遍历结束后,再从 Sorted Set 中取出 top10 记录进 MySQL 的周热词统计表中。Sorted Set 是 Redis 的数据结构,方法 1 会占用两份内存,一份当天的,一份当周的。方法 2 会提高系统的复杂度,并且在统计周表时,可能会出现短时间内大量的计算(当然可以使用定时任务,把周表的统计放到凌晨进行)。 ...

June 3, 2020 · 2 min · jiezi

redis-哨兵模式集群配置

都是在本地虚拟机作为例子如果是生产环境修改对应的ip即可1、master 配置 /home/redis/redis-5.0.8/redis.conf bind 127.0.0.1port 6379tcp-backlog 511timeout 0tcp-keepalive 300daemonize nosupervised nopidfile /var/run/redis_6379.pidloglevel noticelogfile "6379.log"databases 16always-show-logo yessave 900 1save 300 10save 60 10000rdbcompression yesdbfilename dump.rdbdir /usr/local/redis/redis-6379/replica-serve-stale-data yesreplica-read-only yesrepl-diskless-sync norepl-diskless-sync-delay 5lazyfree-lazy-eviction nolazyfree-lazy-expire nolazyfree-lazy-server-del noreplica-lazy-flush noappendonly nono-appendfsync-on-rewrite noaof-load-truncated yesaof-use-rdb-preamble yeslua-time-limit 5000slowlog-max-len 128latency-monitor-threshold 0list-max-ziplist-size -2list-compress-depth 0zset-max-ziplist-entries 128zset-max-ziplist-value 64hll-sparse-max-bytes 3000stream-node-max-bytes 4096stream-node-max-entries 100activerehashing yesclient-output-buffer-limit normal 0 0 0client-output-buffer-limit replica 256mb 64mb 60client-output-buffer-limit pubsub 32mb 8mb 60hz 10dynamic-hz yesaof-rewrite-incremental-fsync yesrdb-save-incremental-fsync yesslave-6380 bind 127.0.0.1port 6380tcp-backlog 511timeout 0tcp-keepalive 300daemonize nosupervised nopidfile /var/run/redis_6380.pidloglevel noticelogfile "6380.log"databases 16always-show-logo yessave 900 1save 300 10save 60 10000rdbcompression yesdbfilename dump.rdbdir /usr/local/redis/redis-6380/replica-serve-stale-data yesreplica-read-only yesrepl-diskless-sync norepl-diskless-sync-delay 5lazyfree-lazy-eviction nolazyfree-lazy-expire nolazyfree-lazy-server-del noreplica-lazy-flush noappendonly nono-appendfsync-on-rewrite noaof-load-truncated yesaof-use-rdb-preamble yeslua-time-limit 5000slowlog-max-len 128latency-monitor-threshold 0list-max-ziplist-size -2list-compress-depth 0zset-max-ziplist-entries 128zset-max-ziplist-value 64hll-sparse-max-bytes 3000stream-node-max-bytes 4096stream-node-max-entries 100activerehashing yesclient-output-buffer-limit normal 0 0 0client-output-buffer-limit replica 256mb 64mb 60client-output-buffer-limit pubsub 32mb 8mb 60hz 10dynamic-hz yesaof-rewrite-incremental-fsync yesrdb-save-incremental-fsync yes#replicaof 127.0.0.1 6379slave-6381 ...

May 31, 2020 · 2 min · jiezi

redis-安装

1.新建redis文件夹mkdir /home/redis2.进入redis目录cd /home/redis3.下载 redis-5.0.8 wget http://download.redis.io/rele...4.解压redistar zxvf redis-5.0.8.tar.gz5.进入 redis-5.0.8cd /home/reids/redis-5.0.8/src6.编译(编译前确定依据安装了gcc,如果没有安装则yum install gcc)make7.配置redis8.设置redis为开机启动 新建开机启动脚本 vi /etc/init.d/redis 输入开机脚本: #chkconfig: 2345 10 90# description: Start and Stop redis# Simple Redis init.d script conceived to work on Linux systems# as it does use of the /proc filesystem.REDISPORT=6379EXEC=/home/redis/redis-5.0.8/src/redis-serverCLIEXEC=/home/redis/redis-5.0.8/src/redis-cliPIDFILE=/var/lib/redis/redis_${REDISPORT}.pid#CONF="/etc/redis/${REDISPORT}.conf"CONF="/home/redis/redis-5.0.8/redis.conf"case "$1" in start) if [ -f $PIDFILE ] then echo "$PIDFILE exists, process is already running or crashed" else echo "Starting Redis server..." $EXEC $CONF & fi ;; stop) if [ ! -f $PIDFILE ] then echo "$PIDFILE does not exist, process is not running" else PID=$(cat $PIDFILE) echo "Stopping ..." $CLIEXEC -p $REDISPORT shutdown while [ -x /proc/${PID} ] do echo "Waiting for Redis to shutdown ..." sleep 1 done echo "Redis stopped" fi ;; restart) "$0" stop sleep 3 "$0" start ;; *) echo "Please use start or stop or restart as first argument" ;;esac9.添加redis服务chkconfig --add redis10.设置为开机启动 chkconfig redis on 11.启动服务 nohup service redis start ...

May 31, 2020 · 1 min · jiezi

Redis操作配置信息详细

CMD操作CONFIG 命令查看或设置配置项。CONFIG get * 查看所有配置CONFIG get XXXCONFIG set XXX YYY (设置XXX = YYY)所有配置信息**提示:第一行为key 第二行为value**1. "dbfilename" //指定本地数据库文件名,默认值为dump.rdb 2. "dump.rdb" 3. "requirepass" 4. //设置Redis连接密码,如果配置了连接密码,客户端在连接Redis时需要通过AUTH <password>命令提供密码,默认关闭 5. //在登录的时候的时候输入密码: 6. // redis-cli -p 6379 -a 密码(a =auth) 7. 4) "" 8. 5) "masterauth" 9. // 当master服务设置了密码保护时,slav服务连接master的密码 这个就是权限验证 10. 6) "" 11. 7) "unixsocket" 12. 8) "" 13. 9) "logfile" 14. //日志记录方式,默认为标准输出,如果配置Redis为守护进程方式运行,而这里又配置为日志记录方式为标准输出 15. 10) "/var/log/redis/redis-server.log" 16. 11) "pidfile" 17. 12) "/var/run/redis/redis-server.pid" 18. 13) "maxmemory" 19. // 指定Redis最大内存限制,Redis在启动时会把数据加载到内存中, 20. //达到最大内存后,Redis会先尝试清除已到期或即将到期的Key,当此方法处理 后, 21. //仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。 22. //Redis新的vm机制,会把Key存放内存,Value会存放在swap区 23. 14) "0" 24. 15) "maxmemory-samples" 25. 16) "3" 26. 17) "timeout" 27. //当 客户端闲置多长时间后关闭连接,如果指定为0,表示关闭该功能 28. 18) "0" 29. 19) "tcp-keepalive" 30. 20) "0" 31. 21) "auto-aof-rewrite-percentage" 32. 22) "100" 33. 23) "auto-aof-rewrite-min-size" 34. 24) "67108864" 35. 25) "hash-max-ziplist-entries" 36. 26) "512" 37. 27) "hash-max-ziplist-value" 38. 28) "64" 39. 29) "list-max-ziplist-entries" 40. 30) "512" 41. 31) "list-max-ziplist-value" 42. 32) "64" 43. 33) "set-max-intset-entries" 44. 34) "512" 45. 35) "zset-max-ziplist-entries" 46. 36) "128" 47. 37) "zset-max-ziplist-value" 48. 38) "64" 49. 39) "lua-time-limit" 50. 40) "5000" 51. 41) "slowlog-log-slower-than" 52. 42) "10000" 53. 43) "slowlog-max-len" 54. 44) "128" 55. 45) "port" //端口号 56. 46) "6379" 57. 47) "databases" //设置数据库的数量,默认数据库为0 58. 48) "16" 59. 49) "repl-ping-slave-period" 60. 50) "10" 61. 51) "repl-timeout" 62. 52) "60" 63. 53) "repl-backlog-size" 64. 54) "1048576" 65. 55) "repl-backlog-ttl" 66. 56) "3600" 67. 57) "maxclients" 68. //设置同一时间最大客户端连接数,默认无限制,Redis可以同时打开的客户端连接数为 69. //Redis进程可以打开的最大文件描述符数,如果设 置 maxclients 0,表示不作限制。 70. //当客户端连接数到达限制时, 71. //Redis会关闭新的连接并向客户端返回max number of clients reached错误信息 72. 58) "3984" 73. 59) "watchdog-period" 74. 60) "0" 75. 61) "slave-priority" 76. 62) "100" 77. 63) "min-slaves-to-write" 78. 64) "0" 79. 65) "min-slaves-max-lag" 80. 66) "10" 81. 67) "hz" 82. 68) "10" 83. 69) "no-appendfsync-on-rewrite" 84. 70) "no" 85. 71) "slave-serve-stale-data" 86. 72) "yes" 87. 73) "slave-read-only" 88. 74) "yes" 89. 75) "stop-writes-on-bgsave-error" 90. 76) "yes" 91. 77) "daemonize" //以守护进程的方式运行 92. 78) "yes" 93. 79) "rdbcompression" 94. //指定存储至本地数据库时是否压缩数据,默认为yes,Redis采用LZF压缩, 95. //如果为了节省CPU时间,可以关闭该选项,但会导致数据库文件变的巨大 96. 80) "yes" 97. 81) "rdbchecksum" 98. 82) "yes" 100. 83) "activerehashing" 101. // 指定是否激活重置哈希,默认为开启 102. 84) "yes" 104. 85) "repl-disable-tcp-nodelay" 105. 86) "no" 106. 87) "aof-rewrite-incremental-fsync" 107. 88) "yes" 108. 89) "appendonly" 109. //. 指定是否在每次更新操作后进行日志记录,Redis在默认情况下是异步的把数据写入磁盘, 110. //如果不开启,可能会在断电时导致一段时间内//的数据丢失。 111. //因为 redis本身同步数据文件是按上面save条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为no 112. // 这个和save 保存同步有关系 113. 90) "no" 114. 91) "dir" //指定本地数据库存放目录 115. 92) "/var/lib/redis" 117. 93) "maxmemory-policy" 118. 94) "volatile-lru" 120. 95) "appendfsync" 121. //之前的那个是否记录日志,这里表示记录日志的类型 122. // 指定更新日志条件,共有3个可选值: 123. //no:表示等操作系统进行数据缓存同步到磁盘(快) 124. //always:表示每次更新操作后手动调用fsync()将数据写到磁盘(慢,安全) 125. //everysec:表示每秒同步一次(折衷,默认值) 126. 96) "everysec" 128. 97) "save" 129. //指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合 (让数据一致性,内存的数据和磁盘的数据) 130. 98) "900 1 300 10 60 10000" 131. // 分别表示900秒(15分钟)内有1个更改,300秒(5分钟)内有10个更改以及60秒内有10000个更改。 133. 99) "loglevel" 134. //日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning 135. 100) "notice" 137. 101) "client-output-buffer-limit" 138. 102) "normal 0 0 0 slave 268435456 67108864 60 pubsub 33554432 8388608 60" 139. 103) "unixsocketperm" 140. 104) "0" 142. 105) "slaveof" 143. //slave 奴隶主要指从数据库 主从数据库就是集群环境中使用的 144. //设置当本机为slav服务时,设置master服务的IP地址及端口,在Redis启动时,它会自动从master进行数据同步 145. 106) "" 146. 107) "notify-keyspace-events" 147. 108) "" 148. 109) "bind" //绑定的主机的ip 149. 110) "0.0.0.0" `

May 31, 2020 · 3 min · jiezi

Redis-AOF一篇即懂

序章除了RDB持久化功能以外,Redis还提供了AOF(Append Only File)持久化功能。与RDB持久化通过保存数据库中的键值对来记录数据库状态不同,AOF持久化是通过保存Redis所执行的写命令来记录数据库状态的。 AOF如何打开redis默认情况是关闭AOF的,所以要使用就要先通过以下方式打开:第一种方式:打开 redis.conf  修改以下参数: appendonly  yes        (默认no,关闭)表示是否开启AOF持久化:  appendfilename “appendonly.aof”   AOF持久化配置文件的名称:** 默认情况下redis安装目录会生成 appendonly.aof文件,如果没有则执行以下方式第二种方式:在cmd通过redis-cli连接到服务器的命令界面里输入config set appendonly yes config set save “”(可选) 执行的第一条命令开启了 AOF 功能: Redis 会阻塞直到初始 AOF 文件创建完成为止, 之后 Redis 会继续处理命令请求, 并开始将写入命令追加到 AOF 文件末尾。 执行的第二条命令用于关闭 RDB 功能。 这一步是可选的, 如果你愿意的话, 也可以同时使用 RDB 和 AOF 这两种持久化功能。(如果RDB和AOF同时开启,则AOF优先加载) AOF如何实现持久化AOF持久化功能的实现可以分为命令追加、文件写入、文件同步三个步骤。 命令追加: 当AOF持久化功能打开时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾。 AOF文件的写入与同步: 每当服务器常规任务函数被执行、 或者事件处理器被执行时, aof.c/flushAppendOnlyFile 函数都会被调用, 这个函数执行以下两个工作: WRITE:根据条件,将 aof_buf 中的缓存写入到 AOF 文件。 SAVE:根据条件,调用 fsync 或 fdatasync 函数,将 AOF 文件保存到磁盘中。 两个步骤都需要根据一定的条件来执行, 而这些条件由 AOF 所使用的保存模式来决定, 以下小节就来介绍 AOF 所使用的三种保存模式, 以及在这些模式下, 步骤 WRITE 和 SAVE 的调用条件。 ...

May 31, 2020 · 2 min · jiezi

Redis协议规范RESP一篇即懂

序章Redis 即REmoteDictionaryServer (远程字典服务); 而Redis的协议规范是 Redis Serialization Protocol (Redis序列化协议) 该协议是用于与Redis服务器通信的,用的较多的是Redis-cli通过pipe与Redis服务器联系; 协议如下: 客户端以规定格式的形式发送命令给服务器; 服务器在执行最后一条命令后,返回结果。 特点1、实现简单2、快速解析3、可读性好 客户端发送命令规定格式(类型):5种类型提示:间隔符号,在Linux下是\r\n,在Windows下是\n1、简单字符串 Simple Strings, 以 "+"加号 开头 格式:+ 字符串 \r\n 字符串不能包含 CR或者 LF(不允许换行) eg: "+OK\r\n" 注意:为了发送二进制安全的字符串,一般推荐使用后面的 Bulk Strings类型 2、错误 Errors, 以"-"减号 开头 格式:- 错误前缀 错误信息 \r\n 错误信息不能包含 CR或者 LF(不允许换行),Errors与Simple Strings很相似,不同的是Erros会被当作异常来看待 eg: "-Error unknow command 'foobar'\r\n" 3、整数型 Integer, 以 ":" 冒号开头 格式:: 数字 \r\n eg: ":1000\r\n" 4、大字符串类型 Bulk Strings, 以 "$"美元符号开头,长度限制512M 格式:$ 字符串的长度 \r\n 字符串 \r\n ...

May 31, 2020 · 1 min · jiezi

Spring-Boot-2X六Spring-Boot-集成-Redis

Redis 简介什么是 RedisRedis 是目前使用的非常广泛的免费开源内存数据库,是一个高性能的 key-value 数据库。 Redis 与其他 key-value 缓存(如 Memcached )相比有以下三个特点: 1.Redis 支持数据的持久化,它可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。2.Redis 不仅仅支持简单的 key-value 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。3.Redis 支持数据的备份,即 master-slave 模式的数据备份。Redis 优势如下: 1.性能极高。Redis 能读的速度是 110000 次/s,写的速度是 81000 次/s。2.丰富的数据类型。Redis 支持二进制案例的 Strings,Lists,Sets 及 Ordered Sets 数据类型操作。3.原子性。Redis 所有的操作都是原子性的,意思是要么成功执行要么失败完全不执行。单个操作是原子性的,多个操作也是,通过 MULTI 和 EXEC 指令抱起来。4.丰富的特性。Redis 还支持 publish/subscribe,通知,key 过期等特性。Spring Boot 集成 Redis1.在项目中添加依赖<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.9.RELEASE</version> <relativePath /> <!-- lookup parent from repository --> </parent> <groupId>cn.zwqh</groupId> <artifactId>spring-boot-redis</artifactId> <version>0.0.1-SNAPSHOT</version> <name>spring-boot-redis</name> <description>spring-boot-redis</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>查看 jar 包时发现,Spring Data Redis 下 org.springframework.data.redis.connection 包路径下面默认有两个包 jedis 和 lettuce,这说明 Spring Boot 已经默认包装适配了这两个 Redis 客户端。 ...

November 5, 2019 · 3 min · jiezi

吊打面试官系列缓存雪崩击穿穿透

你知道的越多,你不知道的越多点赞再看,养成习惯 前言Redis在互联网技术存储方面使用如此广泛,几乎所有的后端技术面试官都要在Redis的使用和原理方面对小伙伴们进行360°的刁难。作为一个在互联网公司面一次拿一次offer的面霸(请允许我使用一下夸张的修辞手法),打败了无数竞争对手,每次都只能看到无数落寞的身影失望的离开,略感愧疚,在一个寂寞难耐的夜晚,我痛定思痛,决定开始写《吊打面试官》系列,希望能帮助各位读者以后面试势如破竹,对面试官进行360°的反击,吊打问你的面试官,让一同面试的同僚瞠目结舌,疯狂收割大厂offer! 一点感慨本来都把稿子放到公众号保存了,洗澡的时候想了一下晚上的比赛,觉得还是打开电脑写点东西,跟文章内容没关系,只是一点个人的感慨,不知道多少小伙伴看了昨天SKT VS G2的比赛,又不知道多少小伙伴还记得Faker手抖的那一幕。 不知道你们看了是什么感受,我看到他手抖的时候我内心也抖了,世界赛我支持的都是LPL的队伍,但是我喜欢李哥这个人,那种对胜利的执著,这么多年了那种坚持自己的坚持,这么多利益诱惑在面前却只想要胜利,这样的人我好喜欢啊,我想很多人也喜欢。 可能就像很多网友说的那样,英雄迟暮,但是我觉得他还是有点东西,就像很多人说我们程序员只能吃年轻饭一样,但是如果你坚持自己的坚持,做个腹有诗书气自华的仔,我想最后肯定会得到自己的得到。 好了我也不煽情了,我们开始讲技术吧。 正文上一期吊打系列我们提到了Redis的基础知识,还没看的小伙伴可以回顾一下 《吊打面试官》系列-Redis基础 那提到Redis我相信各位在面试,或者实际开发过程中对缓存雪崩,穿透,击穿也不陌生吧,就算没遇到过但是你肯定听过,那三者到底有什么区别,我们又应该怎么去防止这样的情况发生呢,我们有请下一位受害者。 面试开始一个大腹便便,穿着格子衬衣的中年男子,拿着一个满是划痕的mac向你走来,看着快秃顶的头发,心想着肯定是尼玛顶级架构师吧!但是我们腹有诗书气自华,虚都不虚。 小伙子我看你的简历上写到了Redis,那么我们直接开门见山,直接怼常见的几个大问题,Redis雪崩了解么?帅气迷人的面试官您好,我了解的,目前电商首页以及热点数据都会去做缓存 ,一般缓存都是定时任务去刷新,或者是查不到之后去更新的,定时任务刷新就有一个问题。 举个简单的例子:如果所有首页的Key失效时间都是12小时,中午12点刷新的,我零点有个秒杀活动大量用户涌入,假设当时每秒 6000 个请求,本来缓存在可以扛住每秒 5000 个请求,但是缓存当时所有的Key都失效了。此时 1 秒 6000 个请求全部落数据库,数据库必然扛不住,它会报一下警,真实情况可能DBA都没反应过来就直接挂了。此时,如果没用什么特别的方案来处理这个故障,DBA 很着急,重启数据库,但是数据库立马又被新的流量给打死了。这就是我理解的缓存雪崩。 我刻意看了下我做过的项目感觉再吊的都不允许这么大的QPS直接打DB去,不过没慢SQL加上分库,大表分表可能还还算能顶,但是跟用了Redis的差距还是很大 同一时间大面积失效,那一瞬间Redis跟没有一样,那这个数量级别的请求直接打到数据库几乎是灾难性的,你想想如果打挂的是一个用户服务的库,那其他依赖他的库所有的接口几乎都会报错,如果没做熔断等策略基本上就是瞬间挂一片的节奏,你怎么重启用户都会把你打挂,等你能重启的时候,用户早就睡觉去了,并且对你的产品失去了信心,什么垃圾产品。 面试官摸了摸自己的头发,嗯还不错,那这种情况咋整?你都是怎么去应对的?处理缓存雪崩简单,在批量往Redis存数据的时候,把每个Key的失效时间都加个随机值就好了,这样可以保证数据不会在同一时间大面积失效,我相信,Redis这点流量还是顶得住的。 setRedis(Key,value,time + Math.random() * 10000);如果Redis是集群部署,将热点数据均匀分布在不同的Redis库中也能避免全部失效的问题,不过本渣我在生产环境中操作集群的时候,单个服务都是对应的单个Redis分片,是为了方便数据的管理,但是也同样有了可能会失效这样的弊端,失效时间随机是个好策略。 或者设置热点数据永远不过期,有更新操作就更新缓存就好了(比如运维更新了首页商品,那你刷下缓存就完事了,不要设置过期时间),电商首页的数据也可以用这个操作,保险。 那你了解缓存穿透和击穿么,可以说说他们跟雪崩的区别么?嗯,了解,我先说一下缓存穿透吧,缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,我们数据库的 id 都是1开始自增上去的,如发起为id值为 -1 的数据或 id 为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大,严重会击垮数据库。 小点的单机系统,基本上用postman就能搞死,比如我自己买的阿里云服务 像这种你如果不对参数做校验,数据库id都是大于0的,我一直用小于0的参数去请求你,每次都能绕开Redis直接打到数据库,数据库也查不到,每次都这样,并发高点就容易崩掉了。 至于缓存击穿嘛,这个跟缓存雪崩有点像,但是又有一点不一样,缓存雪崩是因为大面积的缓存失效,打崩了DB,而缓存击穿不同的是缓存击穿是指一个Key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个完好无损的桶上凿开了一个洞。 面试官露出欣慰的眼光,那他们分别怎么解决缓存穿透我会在接口层增加校验,比如用户鉴权校验,参数做校验,不合法的参数直接代码Return,比如:id 做基础校验,id <=0的直接拦截等。 这里我想提的一点就是,我们在开发程序的时候都要有一颗“不信任”的心,就是不要相信任何调用方,比如你提供了API接口出去,你有这几个参数,那我觉得作为被调用方,任何可能的参数情况都应该被考虑到,做校验,因为你不相信调用你的人,你不知道他会传什么参数给你。 举个简单的例子,你这个接口是分页查询的,但是你没对分页参数的大小做限制,调用的人万一一口气查 Integer.MAX_VALUE 一次请求就要你几秒,多几个并发你不就挂了么?是公司同事调用还好大不了发现了改掉,但是如果是黑客或者竞争对手呢?在你双十一当天就调你这个接口会发生什么,就不用我说了吧。这是之前的Leader跟我说的,我觉得大家也都应该了解下。 从缓存取不到的数据,在数据库中也没有取到,这时也可以将对应Key的Value对写为null、位置错误、稍后重试这样的值具体取啥问产品,或者看具体的场景,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。 这样可以防止攻击用户反复用同一个id暴力攻击,但是我们要知道正常用户是不会在单秒内发起这么多次请求的,那网关层Nginx本渣我也记得有配置项,可以让运维大大对单个IP每秒访问次数超出阈值的IP都拉黑。 那你还有别的办法么?还有我记得Redis还有一个高级用法布隆过滤器(Bloom Filter)这个也能很好的防止缓存穿透的发生,他的原理也很简单就是利用高效的数据结构和算法快速判断出你这个Key是否在数据库中存在,不存在你return就好了,存在你就去查了DB刷新KV再return。 那又有小伙伴说了如果黑客有很多个IP同时发起攻击呢?这点我一直也不是很想得通,但是一般级别的黑客没这么多肉鸡,再者正常级别的Redis集群都能抗住这种级别的访问的,小公司我想他们不会感兴趣的。把系统的高可用做好了,集群还是很能顶的。 缓存击穿的话,设置热点数据永远不过期。或者加上互斥锁就能搞定了 作为暖男,代码我肯定帮你们准备好了 面试结束嗯嗯还不错,三个点都回答得很好,今天也不早了,面试就先到这里,明天你再过来二面我继续问一下你关于Redis集群高可用,主从同步,哨兵等知识点的问题。晕居然还有下一轮面试!(强行下一期的伏笔哈哈)但是为了offer还是得舔,嗯嗯,好的帅气面试官。 能回答得这么全面这么细节还是忍不住点赞 (暗示点赞,每次都看了不点赞,你们想白嫖我么?你们好坏喲,不过我喜欢) ...

November 5, 2019 · 1 min · jiezi

springboot整合redistoken验证登录

写在前面redis是一种可基于内存也可基于持久话的日志型、key-value数据库。因为性能高,存储数据类型丰富等优势常被用作数据缓存。本文介绍了springboot2.2.0整合redis的常规步骤。阅读本文,你大概需要5分钟左右的时间整合redis一. 安装redis根据你的操作系统选择redis版本下载并安装redis点击下载下载文件重命名为redis->打开该文件->cmd到当前文件夹下->redis-server.exe redis.windows.conf 即可启动redis可以为redis设置环境变量,这个大家都懂的。注意:防火墙应该为redis打开!二.引入redis依赖在pom.xml文件下引入redis相关依赖 <!-- redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <!-- 1.5的版本默认采用的连接池技术是jedis 2.0以上版本默认连接池是lettuce, 在这里采用jedis,所以需要排除lettuce的jar --> <exclusions> <exclusion> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </exclusion> <exclusion> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> </exclusion> </exclusions> </dependency> <!-- 添加jedis客户端 --> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency> <!--spring2.0集成redis所需common-pool2--> <!-- 必须加上,jedis依赖此 --> <!-- spring boot 2.0 的操作手册有标注 大家可以去看看 地址是:https://docs.spring.io/spring-boot/docs/2.0.3.RELEASE/reference/htmlsingle/--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.5.0</version> </dependency>3. 全局配置单服务器redis在properties文件中,加入以下基本配置 spring.redis.port=6379spring.redis.host=127.0.0.1#我的redis连接不需要密码#spring.redis.password=123spring.redis.jedis.pool.max-active=100spring.redis.jedis.pool.max-idle=5spring.redis.jedis.pool.max-wait=60000spring.redis.database=0spring.redis.timeout=10000#若开启redis方式的session存储 type值应为redisspring.session.store-type=redisspring.session.timeout=10server.servlet.session.timeout=104.新建RedisServer.java。创建一个对redis的基本操作的类package com.dbc.usermanager.service;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.data.redis.core.ValueOperations;import org.springframework.stereotype.Service;import java.util.concurrent.TimeUnit;@Servicepublic class RedisService { @Autowired protected StringRedisTemplate redisTemplate; /** * 写入redis缓存(不设置expire存活时间) * @param key * @param value * @return */ public boolean set(final String key, String value){ boolean result = false; try { ValueOperations operations = redisTemplate.opsForValue(); operations.set(key, value); result = true; } catch (Exception e) { System.out.println("写入redis缓存失败!错误信息为:" + e.getMessage()); } return result; } /** * 写入redis缓存(设置expire存活时间) * @param key * @param value * @param expire * @return */ public boolean set(final String key, String value, Long expire){ boolean result = false; try { ValueOperations operations = redisTemplate.opsForValue(); operations.set(key, value); redisTemplate.expire(key, expire, TimeUnit.SECONDS); result = true; } catch (Exception e) { System.out.println("写入redis缓存(设置expire存活时间)失败!错误信息为:" + e.getMessage()); } return result; } /** * 读取redis缓存 * @param key * @return */ public Object get(final String key){ Object result = null; try { ValueOperations operations = redisTemplate.opsForValue(); result = operations.get(key); } catch (Exception e) { System.out.println("读取redis缓存失败!错误信息为:" + e.getMessage()); } return result; } /** * 判断redis缓存中是否有对应的key * @param key * @return */ public boolean exists(final String key){ boolean result = false; try { result = redisTemplate.hasKey(key); } catch (Exception e) { System.out.println("判断redis缓存中是否有对应的key失败!错误信息为:" + e.getMessage()); } return result; } /** * redis根据key删除对应的value * @param key * @return */ public boolean remove(final String key){ boolean result = false; try { if(exists(key)){ redisTemplate.delete(key); } result = true; } catch (Exception e) { System.out.println("redis根据key删除对应的value失败!错误信息为:" + e.getMessage()); } return result; } /** * redis根据keys批量删除对应的value * @param keys * @return */ public void remove(final String... keys){ for(String key : keys){ remove(key); } }}

November 5, 2019 · 2 min · jiezi

springbootplus是易于使用快速高效功能丰富开源的spring-boot-脚手架

spring-boot-plus是一套集成spring boot常用开发组件的后台快速开发框架Spring-Boot-Plus是易于使用,快速,高效,功能丰富,开源的spring boot 脚手架.前后端分离,专注于后端服务 目标每个人都可以独立、快速、高效地开发项目!版本库GITHUB | GITEE官网springboot.plus主要特性集成spring boot 常用开发组件集、公共配置、AOP日志等集成mybatis plus快速dao操作快速生成后台代码: entity/param/vo/controller/service/mapper/xml集成swagger2,可自动生成api文档集成jwt、shiro/spring security权限控制集成redis、spring cache、ehcache缓存集成rabbit/rocket/kafka mq消息队列集成druid连接池,JDBC性能和慢查询检测集成spring boot admin,实时检测项目运行情况使用assembly maven插件进行不同环境打包部署,包含启动、重启命令,配置文件提取到外部config目录项目架构 项目环境中间件版本备注JDK1.8+JDK1.8及以上 MySQL5.7+5.7及以上 Redis3.2+ 技术选型技术版本备注Spring Boot2.2.0.RELEASE最新发布稳定版 Spring Framework5.2.0.RELEASE最新发布稳定版 Mybatis3.5.2持久层框架 Mybatis Plus3.2.0mybatis增强框架 Alibaba Druid1.1.20数据源 Fastjson1.2.62JSON处理工具集 swagger22.6.1api文档生成工具 commons-lang33.9常用工具包 commons-io2.6IO工具包 commons-codec1.13加密解密等工具包 commons-collections44.4集合工具包 reflections0.9.11反射工具包 hibernate-validator6.0.17.Final后台参数校验注解 Shiro1.4.1权限控制 JWT3.8.3JSON WEB TOKEN hutool-all5.0.3常用工具集 lombok1.18.10注解生成Java Bean等工具 mapstruct1.3.1.Final对象属性复制工具 CHANGELOGCHANGELOG.mdJava DocsJava Api Docs使用克隆 spring-boot-plusgit clone https://github.com/geekidea/spring-boot-plus.gitcd spring-boot-plusMaven 构建默认使用local环境,对应配置文件:application-local.ymlmvn clean package -Plocal5分钟完成增删改查1. 创建数据库表-- ------------------------------ Table structure for foo_bar-- ----------------------------DROP TABLE IF EXISTS `foo_bar`;CREATE TABLE `foo_bar`( `id` bigint(20) NOT NULL COMMENT '主键', `name` varchar(20) NOT NULL COMMENT '名称', `foo` varchar(20) DEFAULT NULL COMMENT 'Foo', `bar` varchar(20) NOT NULL COMMENT 'Bar', `remark` varchar(200) DEFAULT NULL COMMENT '备注', `state` int(11) NOT NULL DEFAULT '1' COMMENT '状态,0:禁用,1:启用', `version` int(11) NOT NULL DEFAULT '0' COMMENT '版本', `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` timestamp NULL DEFAULT NULL COMMENT '修改时间', PRIMARY KEY (`id`)) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT ='FooBar';-- ------------------------------ Records of foo_bar-- ----------------------------INSERT INTO foo_bar (id, name, foo, bar, remark, state, version, create_time, update_time) VALUES (1, 'FooBar', 'foo', 'bar', 'remark...', 1, 0, '2019-11-01 14:05:14', null);INSERT INTO foo_bar (id, name, foo, bar, remark, state, version, create_time, update_time) VALUES (2, 'HelloWorld', 'hello', 'world', null, 1, 0, '2019-11-01 14:05:14', null);2.使用代码生成器生成增删改查代码修改数据库信息修改组件名称/作者/数据库表名称/主键id ...

November 3, 2019 · 3 min · jiezi

如果你是小白linux快速入门技术值得你收藏

Linux是一个操作系统软件。 与Windows不同的是,Linux是一套开放源代码程序的、并可以自由传播的类Unix操作系统,它是一个支持多用户、多任务、多线程和多CPU的操作系统。它能运行主要的UNIX工具软件、应用程序和网络协议。它支持32位和64位硬件。 Linux继承了Unix以网络为核心的设计思想,是一个性能稳定的多用户网络操作系统。 如何快速入门,需要哪些知识点?1:linux环境专栏 linux系统安装 linux开发环境ssh与Samba配置 linux开发环境Gcc配置 linux的命令操作8条目录操作与5条文件操作 2:shell 脚本编程案例 3:统计文件单词数量(文本操作) 状态机实现文件单词统计 4:实现通讯录(结构体) 通讯录实现的架构设计与需求分析 链表的实现与数据结构的定义 架构接口层的实现 业务逻辑的分析与实现 通讯录人员操作代码的调试 通讯录人员操作代码调试与运行 通讯录删除人员操作的调试与BUG解决 文件保存于加载的接口层实现 文件保存业务实现 通讯录调试与运行 5:并发下的技术方案(锁) 多线程并发锁的项目介绍 多线程并发锁的方案一互斥锁 多线程并发锁的方案一自旋锁 多线程并发锁的方案一原子操作 总结如果你以前从未接触过Linux,上面这五个知识点其实是最简单的入门技术知识,你只需多学多操作即可 linux入门教程 希望刚 开始接触linux的 你少走弯路 祝 学习愉快 ~

November 2, 2019 · 1 min · jiezi

阿里云部署-3redis

redis下载和编译wget http://download.redis.io/releases/redis-4.0.2.tar.gztar xzf redis-4.0.2.tar.gzcd redis-4.0.2make启动服务后台启动redis cd redis-4.0.2/src/redis-server &查询redis进程 ps -ef | grep redis可以看到redis已经启动了 root 19141 19065 0 12:50 pts/1 00:00:03 ./src/redis-server 0.0.0.0:6379root 19238 19196 0 14:00 pts/0 00:00:00 grep --color=auto redis结束进程 kill -9 pid初步测试启动redis客户端 cd redis-4.0.2/src/redis-cli127.0.0.1:6379> set test 1OK127.0.0.1:6379> get test"1"redis安装成功了。 配置服务器远程连接默认配置只能是本地访问,我们修改redis-4.0.2/redis.conf配置文件将 bind 127.0.0.1修改为 bind 0.0.0.0 防火墙你需要添加安全组规则,打开服务器防火墙上的6379端口。 设置远程连接密码默认配置开启了保护模式 protected-mode yes这时你需要设置密码才可以远程连接上redis,密码设置非常简单,只需要在requirepass字段上填写你的密码即可 requirepass 你的密码配置完毕,后台启动你的redis可以了。 ./redis-server /etc/redis/redis.confnode客户端连接我用的是npm上的redis包,此时根据前面你的配置就可以远程连接服务器上的redis了。结合开发文档,就可以进行实际开发了。 const redis = require('redis');const config = require('../config');const logger = require('log4js').getLogger('app');class RedisClient { constructor() { if (!RedisClient.instance) { this.client = redis.createClient({ host: config.redis.host, port: config.redis.port, password: config.redis.password, }); const client = this.client; RedisClient.instance = client; client.on("error", (err) => { logger.error('redis connect err: %s', err.toString()); }); client.on("connect", () => { logger.info('redis connect success'); }); } }}module.exports = new RedisClient().client; ...

October 23, 2019 · 1 min · jiezi

深入学习Redis四基本类型List剖析

更多精彩文章,关注公众号【ToBeTopJavaer】,更有数万元精品vip资源免费等你来拿!!!接下来我们要剖析的基本类型是List,相信大家对List都不会陌生吧,下面我们将深入源码剖析Redis中List的实现。 存储类型 存储有序的字符串(从左到右),元素可以重复。可以充当队列和栈的角色。 操作命令 元素增减 lpush queue alpush queue b crpush queue d elpop queuerpop queueblpop queuebrpop queue取值 lindex queue 0lrange queue 0 -1 存储( 实现) 原理 在早期的版本中,数据量较小时用 ziplist 存储,达到临界值时转换为linkedlist 进行存储,分别对应 OBJ_ENCODING_ZIPLIST 和OBJ_ENCODING_LINKEDLIST 。 3.2 版本之后,统一用 quicklist 来存储。quicklist 存储了一个双向链表,每个节点都是一个 ziplist。 127.0.0.1:6379> object encoding queue"quicklist"什么是quicklist?quicklist(快速列表)是 ziplist 和 linkedlist 的结合体。 quicklist.h源码如下,head 和 tail 指向双向列表的表头和表尾 typedef struct quicklist { quicklistNode \*head; /\* 指向双向列表的表头 \*/ quicklistNode \*tail; /\* 指向双向列表的表尾 \*/ unsigned long count; /\* 所有的 ziplist 中一共存了多少个元素 \*/ unsigned long len; /\* 双向链表的长度, node 的数量 \*/ int fill : 16; /\* fill factor for individual nodes \*/ unsigned int compress : 16; /\* 压缩深度, 0: 不压缩; \*/} quicklist;redis.conf 相关参数: ...

October 22, 2019 · 2 min · jiezi

深入学习Redis三基本类型Hash剖析

更多精彩文章,关注公众号【ToBeTopJavaer】,更有数万元精品vip资源免费等你来拿!!!接下来我们要剖析的基本类型是Hash,相信大家对Hash都不会陌生吧,下面我们将深入源码剖析Redis中Hash的实现。 首先我们看一张图: 存储类型 包含键值对的无序散列表。value 只能是字符串,不能嵌套其他类型。 同样是存储字符串,Hash 与 String 的主要区别? 1、把所有相关的值聚集到一个 key 中,节省内存空间 2、只使用一个 key,减少 key 冲突 3、当需要批量获取值的时候,只需要使用一个命令,减少内存/IO/CPU 的消耗 Hash 不适合的场景: 1、Field 不能单独设置过期时间 2、没有 bit 操作 3、需要考虑数据量分布的问题(value 值非常大的时候,无法分布到多个节点) 操作命令 存储(实现)原理 Redis 的 Hash 本身也是一个 KV 的结构,类似于 Java 中的 HashMap。 外层的哈希(Redis KV 的实现)只用到了 hashtable。当存储 hash 数据类型时, 我们把它叫做内层的哈希。内层的哈希底层可以使用两种数据结构实现: ziplist:OBJ_ENCODING_ZIPLIST(压缩列表) hashtable:OBJ_ENCODING_HT(哈希表) 如下图所示: 问题一、那么在什么时候会用到ziplist,什么时候用到hashtable呢? 在redis.conf我们可以看到: 在源码中: /* 源码位置: t_hash.c , 当达字段个数超过阈值, 使用 HT 作为编码 */if (hashTypeLength(o) > server.hash_max_ziplist_entries)hashTypeConvert(o, OBJ_ENCODING_HT);/*源码位置: t_hash.c, 当字段值长度过大, 转为 HT */for (i = start; i <= end; i++) {if (sdsEncodedObject(argv[i]) &&sdslen(argv[i]->ptr) > server.hash_max_ziplist_value){hashTypeConvert(o, OBJ_ENCODING_HT);break;}}复制代码从而我们可以得知,当 hash 对象同时满足以下两个条件的时候,使用 ziplist 编码: ...

October 22, 2019 · 3 min · jiezi

Redis进阶

前提Redis持久化redis是基于内存的,如果不想办法将数据保存在硬盘上,一旦redis重启(退出/故障),内存的数据将会全部丢失。 Redis提供了两种持久化方法: RDB(基于快照),将某一时刻的所有数据保存到一个RDB文件中。AOF(append-only-file),当Redis服务器执行写命令的时候,将执行的写命令保存到AOF文件中。 RDB保存某个时间点的全量数据快照 手动触发 SAVE:阻塞Redis的服务器进程,知道RDB文件被创建完毕 BGSAVE:Fork出一个子进程来创建RDB文件,不阻塞服务器进程,使用lastsave指令可以查看最近的备份时间 自动触发根据redis.conf配置里的save m n定时触发(用的是BGSAVE) 主从复制时,主节点自动触发执行Debug Relaod执行Shutdown且没有开启AOF持久化 RDB的优缺点:优点:RDB是一个紧凑压缩的二进制文件,代表Redis在某个时间点上的数据快照,适合备份,全量复制等场景。且加载RDB恢复数据远远快于AOF的方式。 缺点:没办法做到实时持久化/秒级持久化,因为bgsave每次运行都要执行fork操作创建子进程,属于重量级操作,频繁执行成本过高。 RDB的相关配置//在n秒内修改m条数据时创建RDB文件save 900 1save 300 10save 60 10000stop-writes-on-bgsave-error yes //bgsave出错时停止写入rdbcompression yes //压缩RDB文件rdbchecksum yes //校验文件是否损坏

October 17, 2019 · 1 min · jiezi

Redis-Streams与Spark的完美结合

来源:Redislabs作者:Roshan Kumar翻译:Kevin  (公众号:中间件小哥) 最近,我有幸在 Spark +AI 峰会上发表了题目为“Redis + Structured Streaming:扩展您的持续应用的完美组合”的演讲。 我对这个主题的兴趣是由 Apache Spark 和 Redis 在过去几个月中引入的新功能引起的。根据我之前使用 Apache Spark 的经验,我很欣赏它在运行批处理时的优雅,并且它在 2.0 版本中引入 Structured Streaming 是在这个方向上的进一步发展。 与此同时,Redis 最近宣布了用于管理流数据的新数据结构,称为“Streams”。Redis Streams 提供了生产者和消费者之间的异步通信功能以及持久性、回顾性查询功能和类似于 Apache Kafka 的横向扩展选项。从本质上讲,Redis 通过Streams 提供了一个轻便、快速、易于管理的流数据库,使数据工程师们受益良多。 此外,开发 Spark-Redis 库是为了使 Redis 可以作为弹性分布式数据集(RDD)使用。因为现在有了 Structured Streaming 和 Redis Streams,我们决定扩展 Spark-Redis 库将 Redis Streams 集成为 Apache Spark Structured Streaming 的数据源。 在上个月的演讲中,我演示了如何在 Redis Streams 中收集用户活动数据并将其下载到 Apache Spark 进行实时数据分析。我开发了一个小型的适合移动设备的 Node.js 应用程序,在这个程序中人们可以点击投票给他们最喜欢的狗来进行有趣的比赛。 这是一场艰苦的战斗,有几个观众甚至是黑客很有创意地攻击了我的应用程序。他们使用“页面检查”选项更改了 HTML 按钮名称试图弄乱应用的显示。但最终他们失败了,因为 Redis Streams,Apache Spark,Spark-Redis 库和我的代码都足够的强大,可以有效地应对这些攻击。 ...

October 17, 2019 · 1 min · jiezi

缓存常见问题

文章首发于公众号松花皮蛋的黑板报 作者就职于京东,在稳定性保障、敏捷开发、高级JAVA、微服务架构有深入的理解 缓存穿透:缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且处于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。 解决办法:有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。 缓存雪崩缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过大雪崩。 解决方案:缓存失效时的雪崩效应对底层系统的冲击非常可怕。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线 程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。这里分享一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。 缓存击穿某个极度“热点”数据在某个时间点过期时,导致后端DB压力急剧上升。 对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。 缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。 解决办法使用互斥锁:感知到缓存失效,去查询DB时,使用分布式锁,使得只有一个线程去数据库加载数据,加锁失败的线程,等待即可。手动过期:redis上从不设置过期时间,功能上将过期时间存在key对应的value里,如果发现要过期,通过一个后台的异步线程进行缓存的构建,也就是“手动”过期。 文章来源:www.liangsonghua.me 作者介绍:京东资深工程师-梁松华,在稳定性保障、敏捷开发、JAVA高级、微服务架构方面有深入的理解

October 17, 2019 · 1 min · jiezi

如何将-Redis-用于微服务通信的事件存储

来源:Redislabs 作者:Martin Forstner 翻译:Kevin (公众号:中间件小哥) 以我的经验,将某些应用拆分成更小的、松耦合的、可协同工作的独立逻辑业务服务会更易于构建和维护。这些服务(也被称为微服务)各自管理自己的技术栈,因此很容易独立于其他服务进行开发和部署。前人已经总结了很多关于使用这种架构设计的好处,在此我就不再赘述了。关于这种设计,有一个方面我一直在重点关注,因为如果没有它,将会导致一些有趣的挑战。虽然构建松耦合的微服务是一个非常轻量级和快速的开发过程,但是这些服务之间共享状态、事件以及数据的通信模型却不那么简单。我使用过的最简单的通信模型就是服务间直接通信,但是这种模型被 Fernando Dogio 明确地证明一旦服务规模扩大就会失效,会导致服务崩溃、重载逻辑以及负载增加等问题,从而可能引起的巨大麻烦,因此应该尽量避免使用这种模型。还有一些其他通信模型,比如通用的发布/订阅模型、复杂的 kafka 事件流模型等,但是最近我在使用 Redis 构建微服务间的通信模型。 拯救者 Redis! 微服务通过网络边界发布状态,为了跟踪这种状态,事件通常需要被保存在事件存储中。由于事件通常是一种异步写入操作的不可变流的记录(又被称为事务日志),因此适用于以下场景: 1. 顺序很重要(时间序列数据) 2. 丢失一个事件会导致错误状态 3. 回放状态在任何给定时间点都是已知的 4. 写操作简单且快捷 5. 读操作需要更多的时间,以至于需要缓存 6. 需要高可扩展性,服务之间都是解耦的,没有关联 使用 Redis,我始终可以轻松实现发布-订阅模式。但现在,Redis 5.0 提供了新的Streams 数据类型,我们可以以一种更加抽象的方式对日志数据结构进行建模-使之成为时间序列数据的理想用例(例如最多一次或最少一次传递语义的事务日志)。基于双主功能,轻松简单的部署以及内存中的超快速处理能力,Redis 流成为一种管理大规模微服务通信的必备工具。基本的模型被称为命令查询职责分离(CQRS),它将命令和查询分开执行,命令使用 HTTP 协议,而查询采用 RESP(Redis 序列化协议)。让我们使用一个例子来说明如何使用 Redis 作为事件存储。 OrderShop简单应用概述 我创建了一个简单但是通用的电子商务应用作为例子。当创建/删除客户、库存物品或订单时,使用 RESP 将事件异步传递到 CRM 服务,以管理 OrderShop 与当前和潜在客户的互动。像许多常见应用程序的需求一样,CRM 服务可以在运行时启动和停止,而不会影响其他微服务。这需要捕获在其停机期间发送给它的所有消息以进行后续处理。 下图展示了 9 个解耦的微服务的互连性,这些微服务使用由 Redis 流构建的事件存储进行服务间通信。他们通过侦听事件存储(即 Redis 实例)中特定事件流上的任何新创建的事件来执行此操作。 图1. OrderShop 架构 我们的 OrderShop 应用程序的域模型由以下 5 个实体组成: 顾客产品库存订单账单通过侦听域事件并保持实体缓存为最新状态,事件存储的聚合功能仅需调用一次或在响应时调用。 图2. OrderShop 域模型 安装并运行OrderShop 按照如下步骤安装并运行 OrderShop 应用: ...

October 16, 2019 · 1 min · jiezi

一文了解-Redis-内存监控和内存消耗

Redis 是一种内存数据库,将数据保存在内存中,读写效率要比传统的将数据保存在磁盘上的数据库要快很多。所以,监控 Redis 的内存消耗并了解 Redis 内存模型对高效并长期稳定使用 Redis 至关重要。 内存使用统计通过 info memory 命令可以获得 Redis 内存相关的指标。较为重要的指标和解释如下所示: 属性名属性说明used_memoryRedis 分配器分配的内存总量,也就是内部存储的所有数据内存占用量used_memory_human以可读的格式返回 used_memoryused_memory_rss从操作系统的角度显示 Redis 进程占用的物理内存总量used_memory_rss_humanused_memory_rss 的用户宜读格式的显示used_memory_peak内存使用的最大值,表示 used_memory 的峰值used_memory_peak_human以可读的格式返回 used_memory_peak的值used_memory_luaLua 引擎所消耗的内存大小。mem_fragmentation_ratioused_memory_rss / used_memory 的比值,可以代表内存碎片率maxmemoryRedis 能够使用的最大内存上限,0表示没有限制,以字节为单位。maxmemory_policyRedis 使用的内存回收策略,可以是 noeviction、allkeys-lru、volatile-lru、allkeys-random、volatile-random 或者 volatile-ttl。默认是noeviction,也就是不会回收。 当 mem_fragmentation_ratio > 1 时,说明有部分内存并没有用于数据存储,而是被内存碎片所消耗,如果该值很大,说明碎片率严重。当 mem_fragmentation_ratio < 1 时,这种情况一般出现在操作系统把 Redis 内存交换 (swap) 到硬盘导致,出现这种情况要格外关注,由于硬盘速度远远慢于内存,Redis 性能会变得很差,甚至僵死。 当 Redis 内存超出可以获得内存时,操作系统会进行 swap,将旧的页写入硬盘。从硬盘读写大概比从内存读写要慢5个数量级。used_memory 指标可以帮助判断 Redis 是否有被swap的风险或者它已经被swap。 在 Redis Administration 一文 (链接在文末) 建议要设置和内存一样大小的交换区,如果没有交换区,一旦 Redis 突然需要的内存大于当前操作系统可用内存时,Redis 会因为 out of memory 而被 Linix Kernel 的 OOM Killer 直接杀死。虽然当 Redis 的数据被换出 (swap out) 时,Redis的性能会变差,但是总比直接被杀死的好。 ...

October 16, 2019 · 2 min · jiezi

springdatarediscache-使用及源码走读

预期读者准备使用 spring 的 data-redis-cache 的同学了解 @CacheConfig,@Cacheable,@CachePut,@CacheEvict,@Caching 的使用深入理解 data-redis-cache 的实现原理文章内容说明如何使用 redis-cache自定义 keyGenerator 和过期时间源码解读自带缓存机制的不足快速入门maven 加入 jar 包 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency>配置 redis spring.redis.host=127.0.0.1开启 redis-cache @EnableCaching@CacheConfig,@Cacheable,@CachePut,@CacheEvict,@Caching 的功能 @Cacheable 会查询缓存中是否有数据,如果有数据则返回,否则执行方法@CachePut 每次都执行方法,并把结果进行缓存@CacheEvict 会删除缓存中的内容@Caching 相当于上面三者的综合,用于配置三者的行为@CacheConfig 配置在类上,用于配置当前类的全局缓存配置详细配置经过上面的配置,就已经可以使用 redis-cache 了,但是还是有些问题需要问自己一下,比如 存储在 redis 的 key 是什么样子的,我可以自定义 key 吗存储到 redis 的 value 是怎么序列化的存储的缓存是多久过期并发访问时,会不会直接穿透从而不断的修改缓存内容过期时间,序列化方式由此类决定 RedisCacheConfiguration,可以覆盖此类达到自定义配置。默认配置为RedisCacheConfiguration.defaultCacheConfig() ,它配置为永不过期,key 为 String 序列化,并加上了一个前缀做为命名空间,value 为 Jdk 序列化,所以你要存储的类必须要实现 java.io.Serializable 。 存储的 key 值的生成由 KeyGenerator 决定,可以在各缓存注解上进行配置,默认使用的是 SimpleKeyGenerator 其存储的 key 方式为 SimpleKey [参数名1,参数名2],如果在同一个命名空间下,有两个同参数名的方法就公出现冲突导致反序列化失败。 并发访问时,确实存在多次访问数据库而没有使用缓存的情况 https://blog.csdn.net/clementad/article/details/52452119 Srping 4.3提供了一个sync参数。是当缓存失效后,为了避免多个请求打到数据库,系统做了一个并发控制优化,同时只有一个线程会去数据库取数据其它线程会被阻塞。自定义存储 key根据上面的说明 ,很有可能会存在存储的 key 一致而导致反序列化失败,所以需要自定义存储 key ,有两种实现办法 ,一种是使用元数据配置 key(简单但难维护),一种是全局设置 keyGenerator ...

October 13, 2019 · 3 min · jiezi

Redis5源码学习浅析redis命令之restore篇

Grape 命令语法命令含义:反序列化给定的序列化值,并将它和给定的 key 关联。 命令格式:RESTORE key ttl serialized-value [REPLACE] [ABSTTL] [IDLETIME seconds] [FREQ frequency]命令实战redis> DEL mykey0redis> RESTORE mykey 0 "\n\x17\x17\x00\x00\x00\x12\x00\x00\x00\x03\x00\ x00\xc0\x01\x00\x04\xc0\x02\x00\x04\xc0\x03\x00\ xff\x04\x00u#<\xc0;.\xe9\xdd"OKredis> TYPE mykeylistredis> LRANGE mykey 0 -11) "1"2) "2"3) "3"返回值如果反序列化成功那么返回 OK ,否则返回一个错误。 源码分析源码分析部分我们分为几个部分来讲解。 参数处理void restoreCommand(client *c) { long long ttl, lfu_freq = -1, lru_idle = -1, lru_clock = -1; rio payload; int j, type, replace = 0, absttl = 0; robj *obj; /* 解析参数 */ for (j = 4; j < c->argc; j++) { int additional = c->argc-j-1; if (!strcasecmp(c->argv[j]->ptr,"replace")) { replace = 1; } else if (!strcasecmp(c->argv[j]->ptr,"absttl")) { absttl = 1; } else if (!strcasecmp(c->argv[j]->ptr,"idletime") && additional >= 1 && lfu_freq == -1) { if (getLongLongFromObjectOrReply(c,c->argv[j+1],&lru_idle,NULL) != C_OK) return; if (lru_idle < 0) { addReplyError(c,"Invalid IDLETIME value, must be >= 0"); return; } lru_clock = LRU_CLOCK(); j++; /* Consume additional arg. */ } else if (!strcasecmp(c->argv[j]->ptr,"freq") && additional >= 1 && lru_idle == -1) { if (getLongLongFromObjectOrReply(c,c->argv[j+1],&lfu_freq,NULL) != C_OK) return; if (lfu_freq < 0 || lfu_freq > 255) { addReplyError(c,"Invalid FREQ value, must be >= 0 and <= 255"); return; } j++; /* Consume additional arg. */ } else { addReply(c,shared.syntaxerr); return; } }在上边我们提到了restore命令格式,我们可以看到,在第四个参数开始都是可选参数,所以解析参数中是从j=4开始遍历的,在遍历的过程中会根据不同的参数做不同的操作。我们依次来看下这四个命令: ...

October 8, 2019 · 2 min · jiezi

Redis的主从复制MasterSlave

一、是什么主从复制,主机数据更新后根据配置和策略,自动同步到备机的master/slave机制,Master以写为主,Slave以读为主 二、能干啥读写分离 容灾备份 三、怎么配配从(库)不配主(库)从库配置:slaveof 主库IP 主库端口每次与master断开后,都需要重新连接,除非你配置进redis.conf文件info replication修改配置文件细节操作拷贝多个redis.conf文件开启daemonize yesPid文件名字指定端口log文件名字Dump.rdb名字四、 一主二从模式 slaveof 127.0.0.1 6379 // 通过此命令,使6380,6381服务作为6379的从机五、 薪火相传模式slaveof 127.0.0.1 6379 // 通过此命令,使6380作为6379的从机slaveof 127.0.0.1 6380 // 通过此命令,使6381作为6380的从机六、 反客为主模式基于一主二从模式,主机挂了,两个从机中选一个作为新主机 slaveof no one // 选为主机七、复制原理Slave启动成功连接到master后会发送一个sync命令Master接到命令启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕后,master将传送整个数据文件到slave,以完成一次完全同步全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。增量复制:Master继续将新的所有收集到的修改命令一次传给slave, 完成同步但只有是重新连接master,一次完全同步(全量复制)将会被自动执行 八、哨兵模式是什么反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库怎么配基于一主二从,6379带着6380,6381在redis配置文件目录新建sentinel.conf 文件sentinel monitor 被监控数据库名字(自己起名字)127.0.0.1 6379 1最后一个1,表示主机挂了后,slave投票看谁接替成为主机,得票数多少后成为主机启动哨兵,redis-sentinel /etc/redis/sentinel.conf一组sentinel能同时监控多个Master

October 8, 2019 · 1 min · jiezi

Redis的发布订阅

一、是什么进程间的一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息 二、命令subscribe c1 c2 c3 // 可以一次性订阅多个publish c2 hellow // 给c2发布xiaoxipsubscribe new* // 订阅多个,通配符*三、案例 在另一个会话窗口发送消息 一次订阅多个 发布消息

October 7, 2019 · 1 min · jiezi

Redis的事务

一、是什么可以一次执行多个命令, 本质是一组命令的集合,一个事务中的所有命令都会序列化,按顺序地串行化执行而不会被其他命令插入,不许加塞 二、能干啥一个队列中,一次性,顺序性的执行一系列命令 三、示例演示正常执行 127.0.0.1:6379> MULTI // 开启事务OK127.0.0.1:6379> set k1 v1 //入队QUEUED127.0.0.1:6379> set k2 v2QUEUED127.0.0.1:6379> get k2QUEUED127.0.0.1:6379> set k3 v3QUEUED127.0.0.1:6379> EXEC //执行事务1) OK // 执行结果2) OK3) "v2"4) OK放弃事务 127.0.0.1:6379> MULTIOK127.0.0.1:6379> set k1 v1QUEUED127.0.0.1:6379> set k2 22QUEUED127.0.0.1:6379> set k3 33QUEUED127.0.0.1:6379> DISCARD //放弃事务OK127.0.0.1:6379> get k2 // 获取到的还是先前设置的v2"v2"一损俱损 127.0.0.1:6379> MULTIOK127.0.0.1:6379> set k4 v4QUEUED127.0.0.1:6379> setget k5 55 //命令错误,全都执行不成功(error) ERR unknown command 'setget'127.0.0.1:6379> set k6 v6QUEUED127.0.0.1:6379> EXEC(error) EXECABORT Transaction discarded because of previous errors.127.0.0.1:6379> get k4 // 获取的为nil(nil)冤头债主 ...

October 7, 2019 · 1 min · jiezi

Redis持久化之AOF

一、是什么以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就会根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。 二、appendonly.aof 文件配置默认关闭,开启appendonly yes优先级同时开启了rdb和aof两种模式,aof优先三、AOF启动/修复/恢复正常恢复启动:设置Yes将数据的aof文件复制一份保存到对应的目录恢复:重启redis然后重新加载异常恢复启动:设置Yes备份被写坏的AOF文件修复:Redis-check-aof --fix 进行修复恢复:重启redis然后重新加载四、Rewrite是什么AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制,当AOF文件的大小超过所设定的阀值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集,可以使用命令bgrewriteaof#appendfsync always // 每次数据变更会被立即记录到磁盘,性能差数据保存完整appendfsync everysec // 出厂默认设置,异步操作,每秒记录,一秒宕机会丢失数据#appendfsync no 重写原理 AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename),遍历新进程的内存中数据,每条记录有一条Set语句。重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据内容用命令的方式重写了一个新的aof文件,这点和快照有点类似触发机制 Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍,且文件大于64M时触发auto-aof-rewrite-percentage 100 // 设置重写的基准值auto-aof-rewrite-min-size 64mb // 设置重写的基准值

October 6, 2019 · 1 min · jiezi

Redis持久化之RDB

一、是什么在指定的时间内将内存中的数据集快照写入磁盘,也就是Snapshot快照,它恢复时是将快照文件直接读到内存中。Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束,再用这个临时文件替换上次持久化的文件,整个过程中,主进程时不进行任何IO操作的,这就是确保了极高的性能,如果需要进行大规模数据的恢复,且对于数据的完整性不是非常敏感,那RDB方式比AOF方式更加高效,RDB的缺点是最后一次持久化的数据可能丢失。 二、ForkFork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等)数值都与原进程一致,但是是一个全新的进程,并作为原进程的子进程。 三、dump.rdb文件配置这是我的配置路径如何触发RDB快照 save 900 1 // 900秒内有1次改动,则持久化一次save 300 10 // 300秒内有10次改动,则持久化一次save 60 10000 // 60秒内有10000次改动,则持久化一次以上三个任意满足一个条件即可 save 或bgsave命令执行flushall命令也会产生rdb文件,不过是空的,无意义 四、优势与劣势优势适合大规模的数据恢复对数据完整性和一致性要求不高劣势在一定间隔时间做一次备份,所以如果redis意外down的话,就会丢失最后一次快照后的所有修改。Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑。

October 6, 2019 · 1 min · jiezi

Dubbo注册中心

1.注册中心的作用利用注册中心,服务提供者可以动态添加删除服务,服务消费者在收到更新通知后,可以拉取最新的服务从而实现同步。可以在注册中心实现统一配置,参数的动态调整可以自动通知到所有服务节点。 2.Dubbo四种注册中心实现Dubbo注册中心的实现在dubbo-registry模块。 2.1 ZooKeeper基于Zookeeper。ZooKeeper学习 2.1.1 Zookeeper注册中心数据结构 2.1.2 Zookeeper注册中心实现原理Zookeeper注册中心采用"事件通知" + “客户端拉取”的实现方式: 客户端在第一次连接注册中心的时候,会获取对应目录下的全量数据。客户端会在订阅的节点上注册一个watch,客户端与注册中心之间保持TCP长连接。当节点发生事务操作,节点的版本号发生改变,就会触发watch事件,推送数据给订阅方,订阅方收到通知之后,就会拉取对应目录下的数据。服务治理中心会订阅所有service层的数据,service被设置成"*",表示订阅全部。2.1.3 Zookeeper注册和取消注册//注册protected void doRegister(URL url) { try { //创建目录 zkClient.create(toUrlPath(url), url.getParameter(Constants.DYNAMIC_KEY, true)); } catch (Throwable e) { throw new RpcException("Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e); }}//取消注册protected void doUnregister(URL url) { try { //删除目录 zkClient.delete(toUrlPath(url)); } catch (Throwable e) { throw new RpcException("Failed to unregister " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e); }}private String toUrlPath(URL url) { return toCategoryPath(url) + Constants.PATH_SEPARATOR + URL.encode(url.toFullString());}//返回"/dubbo/接口名称/providers"private String toCategoryPath(URL url) { return toServicePath(url) + Constants.PATH_SEPARATOR + url.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY);}//返回"/dubbo/接口名称"private String toServicePath(URL url) { String name = url.getServiceInterface(); if (Constants.ANY_VALUE.equals(name)) { return toRootPath(); } return toRootDir() + URL.encode(name);}//返回"/"或者"/dubbo/"private String toRootDir() { if (root.equals(Constants.PATH_SEPARATOR)) { return root; } return root + Constants.PATH_SEPARATOR;}private String toRootPath() { return root;}private final static String DEFAULT_ROOT = "dubbo";private final ZookeeperClient zkClient;public ZookeeperRegistry(URL url, ZookeeperTransporter zookeeperTransporter) { //省略n行代码 String group = url.getParameter(Constants.GROUP_KEY, DEFAULT_ROOT); //添加前缀"/" if (!group.startsWith(Constants.PATH_SEPARATOR)) { group = Constants.PATH_SEPARATOR + group; } this.root = group; zkClient = zookeeperTransporter.connect(url); //省略n行代码}ZookeeperClient和ZookeeperTransporter都是接口,在Dubbo中有两种实现,一种是基于curator客户端的CuratorZookeeperClient和CuratorZookeeperTransporter;另一种是基于zkclient客户端的ZkclientZookeeperClient和ZkclientZookeeperTransporter。默认实现是curator。 ...

October 5, 2019 · 6 min · jiezi

redis-跳表skip-list-与-mysql-索引-Btree

跳表(skip list) 查询数据的时间复杂度是 O(n)插入操作的时间复杂度是 O(n)删除操作的时间复杂度是 O(n)索引动态更新的时间复杂度 O(1)B+ 树(B+tree)

October 5, 2019 · 1 min · jiezi

Redis5源码学习浅析redis命令之rename篇

baiyan 命令语法命令含义:将 key改名为newkey命令格式: RENAME key newkey命令实战: 127.0.0.1:6379> keys *1) "kkk"2) "key1"127.0.0.1:6379> rename kkk key1OK127.0.0.1:6379> keys *1) "key1"127.0.0.1:6379> rename kkk kkk(error) ERR no such key返回值: 改名成功时提示 OK ,失败时候返回一个错误 源码分析主要流程rename命令的处理函数是renameCommand(): void renameCommand(client *c) { renameGenericCommand(c,0);}renameCommand()函数调用了底层通用重命名函数: void renameGenericCommand(client *c, int nx) { robj *o; long long expire; int samekey = 0; // 重命名之前和之后的键名相同,置samekey标志为1,不做处理 if (sdscmp(c->argv[1]->ptr,c->argv[2]->ptr) == 0) samekey = 1; // 如果重命名之前的键不存在,直接返回 if ((o = lookupKeyWriteOrReply(c,c->argv[1],shared.nokeyerr)) == NULL) return; // 如果置了samekey标志为1,代表重命名前后的键名相同,那么什么都不做,直接返回OK if (samekey) { addReply(c,nx ? shared.czero : shared.ok); return; } incrRefCount(o); // 由于查找到了o,引用计数++ expire = getExpire(c->db,c->argv[1]); // 获取重命名之前键的过期时间 if (lookupKeyWrite(c->db,c->argv[2]) != NULL) { // 如果重命名之后的键已经存在 if (nx) { // 是否是执行的renamenx命令 decrRefCount(o); addReply(c,shared.czero); return; } /* 重命名之后的键已经存在,需要删除这个已存在的键 */ dbDelete(c->db,c->argv[2]); } dbAdd(c->db,c->argv[2],o); // 到这里重命名之后的键一定不存在了,可以添加这个键 if (expire != -1) setExpire(c,c->db,c->argv[2],expire); // 如果之前设置了过期时间,同样给新键设置过期时间 dbDelete(c->db,c->argv[1]); // 新键创建完毕,需删除之前的键 signalModifiedKey(c->db,c->argv[1]); // 发出修改键信号 signalModifiedKey(c->db,c->argv[2]); // 发出修改键信号 notifyKeyspaceEvent(NOTIFY_GENERIC,"rename_from", c->argv[1],c->db->id); // 键空间事件触发 notifyKeyspaceEvent(NOTIFY_GENERIC,"rename_to", c->argv[2],c->db->id); // 键空间事件触发 server.dirty++; addReply(c,nx ? shared.cone : shared.ok);}我们首先整理一下这个命令的思路,如果让我们自己去实现重命名一个键这个命令,应该怎么做呢?首先我们会判断一些边界条件。我们知道,键是放在键空间字典里的。假设我们现在有key1-value1和key2-value2。现在需要把key1重命名为key2,即重命名之后生成值为key2-value1的键值对。考虑以下边界情况: ...

October 4, 2019 · 1 min · jiezi

使用redis位图bitMap-实现签到功能PHP版本

需要优化的地方,请各位看官在评论帮忙指出一、需求记录用户签到,查询用户签到二、技术方案1、使用mysql(max_time字段为连续签到天数) 思路: (1)用户签到,插入一条记录,根据create_time查询昨日是否签到,有签到则max_time在原基础+1,否则,max_time=0 (2)检测签到,根据user_id、create_time查询记录是否存在,不存在则表示未签到 2、使用redis位图功能思路:(1)每个用户每个月单独一条redis记录,如00101010101010,从左往右代表01-31天(每月有几天,就到几天)(2)每月8号凌晨,统一将redis的记录,搬至mysql,记录如图 (3)查询当月,从redis查,上月则从mysql获取3、方案对比举例:一万个用户签到365天方案1、mysql 插入365万条记录· 频繁请求数据库做一些日志记录浪费服务器开销。· 随着时间推移数据急剧增大· 海量数据检索效率也不高,同时只能用时间create_time作为区间查询条件,数据量大肯定慢方案2、mysql 插入12w条记录· 节省空间,每个用户每天只占用1bit空间 1w个用户每天产生10000bit=1050byte 大概为1kb的数据· 内存操作存度快三、实现(方案2)1、key结构前缀_年份_月份:用户id -- sign_2019_10:01查询:单个:keys sign_2019_10_01全部:keys sign_*月份:keys sign_2019_10:*2、mysql表结构 3、代码(列出1个调用方法,与三个类)(1)签到方法 public static function userSignIn($userId) { $time = Time(); $today = date('d', $time); $year = date('Y', $time); $month = date('m', $time); $signModel = new Sign($userId,$year,$month); //1、查询用户今日签到信息 $todaySign = $signModel->getSignLog($today); if ($todaySign) { return self::jsonArr(-1, '您已经签到过了', []); } try { Db::startTrans(); $signModel->setSignLog($today); //4、赠送积分 if (self::SING_IN_SCORE > 0) { $dataScore['order_id'] = $userId.'_'.$today; $dataScore['type'] = 2;//2、签到 $dataScore['remark'] = '签到获得积分'; Finance::updateUserScore(Finance::OPT_ADD, $userId, self::SING_IN_SCORE, $dataScore); } $code = '0'; $msg = '签到成功'; $score = self::SING_IN_SCORE; Db::commit(); } catch (\Exception $e) { Db::rollback(); $code = '-2'; $msg = '签到失败'; $score = 0; } return self::jsonArr($code, $msg, ['score' => $score]); }(2)redis基类 ...

October 4, 2019 · 4 min · jiezi

Redis从认识安装到实现CURD

Redis从一无所知,到知道一点点 Redis是一个使用ANSI C编写的开源、支持网络、基于内存、可选持久性的键值对存储数据库 ——维基百科可以简单的说,Redis就是一款高性能的NoSQL数据库 什么是NoSQL?我们前面所学习的MySQL数据库是典型的的SQL数据库也就是传统的关系型数据库,而我们今天学习的Redis数据库则是一款NoSQL数据库,也叫作非关系型数据库,它与我们熟悉的MySQL等的概念完全是不一样的,它是一项全新的数据库理念,我们帖一组百度百科的解释 NoSQL,泛指非关系型的数据库。随着互联网web2.0网站的兴起,传统的关系数据库在处理web2.0网站,特别是超大规模和高并发的SNS类型的web2.0纯动态网站已经显得力不从心,出现了很多难以克服的问题,而非关系型的数据库则由于其本身的特点得到了非常迅速的发展。NoSQL数据库的产生就是为了解决大规模数据集合多重数据种类带来的挑战,尤其是大数据应用难题 ——百度百科说明:我们现在所看到的的博客,RSS,P2P,微博,抖音等均属于 Web2.0的产物,Web2.0相比较过去的Web1.0更加注重于用户的交互,用户不仅可以浏览,还可以上传一些资源到网站上,例如图片文字或者说短视频等,使得用户也参与到了网站内容的制造中去了 为什么使用NoSQL?部署成本低:部署操作简单,以开源软件为主存储格式丰富:支持 key-value形式、文档、图片等众多形式,包括对象或者集合等格式速度快:数据存储在缓存中,而不是硬盘中,而且例如Redis基于键值对,同时不需要经过SQL层解析,性能非常高无耦合性,易扩展 在SQL中,一个正在使用的数据是不允许删除的,但NoSQL却可以操作NoSQL可以替代SQL吗?有人会说,NoSQL = Not SQL ,但是我更倾向这样理解 NoSQL = Not only SQL ,我们不能以一个绝对的结论来判定两项技术的好坏,每一项技术的产生都有其特定的原因,在我看来,NoSQL更适合作为SQL数据库的补充,由于海量数据的出现,性能的要求高了起来,而NoSQL这种产物,对于结构简单但是数据量大的数据处理起来要比传统的SQL快很多,但是同样的,其逻辑运算就必须很简单,否则它也是力不从心的 在我看来,可以简单的说,NoSQL就是以功能换取性能,但是需要处理复杂的业务逻辑还需要使用关系型数据库,所以说想要在模型中完全用NoSQL替代SQL是不现实的,两者更像是互补的关系 SQL的好处: 支持在一个表以及多表之前进行复杂的查询操作支持对事物的处理,能保证数据的安全要求学习成本低,资料较多市面上的NoSQL产品非常多,我们今天所要介绍的就是其中一款基于键值存储的数据库——Redis 初识Redis我们在一开始提到了,Redis就是一款高性能的NoSQL数据库,那么它的应用场景是什么呢? 用于用户内容缓存,可以处理大量数据的高访问负载,例如:数据查询,新闻,商品内容任务队列,例如:秒杀,12306在线好友列表应用、网站访问统计排行由于其基于键值存储,那么可以支持的存储的类型有什么呢? 字符串类型 - String列表 - list:linkedlist集合 - set有序集合 - sortedset哈希 - hash:map下载安装linux官网:https://redis.io 由于官网访问速度过慢,我们可以访问对应的中文网:http://www.redis.net.cn 下载,解压,编译: $ wget http://download.redis.io/releases/redis-5.0.4.tar.gz$ tar xzf redis-5.0.4.tar.gz$ cd redis-5.0.4$ make二进制文件是编译完成后在src目录下. 运行如下: $ src/redis-server你能使用Redis的内置客户端进行进行redis代码的编写,例如我们存入一个键值 name-zhangsan $ src/redis-cliredis> set name zhangsanOKredis> get name"zhangsan"windows我们可以去github中寻找windows版本,不过版本会有所滞后 https://github.com/microsofta... 解压即可用 redis-server.exe:redis服务器端redis-cli.exe:redis的客户端redis.windows.conf:配置文件常见支持类型—存取删除命令操作(一) 字符串类型 - String(1) 存储set key value- 127.0.0.1:6379> set address beijing- OK(2) 获取get key- 127.0.0.1:6379> get address- “beijing”(2) 删除del key- 127.0.0.1:6379> del address- (integer) 1(二) 列表类型 - list添加一个元素到列表的头部(左边)或者尾部(右边) ...

October 4, 2019 · 3 min · jiezi

Redis5源码学习浅析redis命令之persist篇

Grape 命令语法命令含义:移除给定key的生存时间,将这个 key 从『易失的』(带生存时间 key )转换成『持久的』(一个不带生存时间、永不过期的 key )。命令格式: PERSIST key命令实战: redis> SET mykey "Hello"OKredis> EXPIRE mykey 10(integer) 1redis> TTL mykey(integer) 10redis> PERSIST mykey(integer) 1redis> TTL mykey(integer) -1redis> 返回值: 当生存时间移除成功时,返回 1 . 如果 key 不存在或 key 没有设置生存时间,返回 0 . 源码分析命令入口:/* PERSIST key */void persistCommand(client *c) { //查找这个key,调用lookupKeyWrite函数 if (lookupKeyWrite(c->db,c->argv[1])) { //如果这个key存在,调用删除函数,调用dictGenericDelete函数 if (removeExpire(c->db,c->argv[1])) { addReply(c,shared.cone); server.dirty++; } else { addReply(c,shared.czero); } } else { addReply(c,shared.czero); }}此处是persist命令的入口,我们可以看到这个命令在执行的过程中有一下几个阶段:1.查找你要执行persist命令的key,看是否存在,如果不存在则直接返回客户端信息。如果存在,这调用删除函数移除过期时间,删除成功返回给客户端成功信息,aof标志加1。 判断key的可用性/* Lookup a key for write operations, and as a side effect, if needed, expires * the key if its TTL is reached. * * Returns the linked value object if the key exists or NULL if the key * does not exist in the specified DB. *//* 查找这个key*/robj *lookupKeyWrite(redisDb *db, robj *key) { //首先查找这个key是否过期,过期则删除,此函数在之前的几篇文章中已经介绍过,此处不在赘述。 expireIfNeeded(db,key); 在上一个步骤的基础上,如果此键被删除则返回null,否则返回这个键的值 return lookupKey(db,key,LOOKUP_NONE);}int removeExpire(redisDb *db, robj *key) { /* An expire may only be removed if there is a corresponding entry in the * main dict. Otherwise, the key will never be freed. */ serverAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL); return dictDelete(db->expires,key->ptr) == DICT_OK;}这个过程是在判断这个key是否存在,它分为两个部分,首先他会查找这个key是否过期,如果过期则删除,然后去查询这个key,反之直接查找这个key,此处又一个疑问,为什么不是先查找这个key是否存在再去判断是否过期?我的想法:redis中存储过期时间和key-value是两个字典,在expire字典中存在的值一定在key-value字典中存在,那么在expire过期的时候就涉及reids的惰性删除机制,为了满足这个机制,reids在设计的时候会先对key的过期时间做判断,然后再去判断这个key是否存在,此时如果对redis的删除机制感兴趣的话,请查阅关于Redis数据过期策略。 ...

October 3, 2019 · 2 min · jiezi

Redis5源码学习浅析redis命令之randomkey篇

baiyan 命令语法命令含义:从当前选定数据库随机返回一个key命令格式: RANDOMKEY命令实战: 127.0.0.1:6379> keys *1) "kkk"2) "key1"127.0.0.1:6379> randomkey"key1"127.0.0.1:6379> randomkey"kkk"返回值: 随机的键;如果数据库为空则返回nil 源码分析主体流程keys命令对应的处理函数是randomKeyCommand(): void randomkeyCommand(client *c) { robj *key; // 存储获取到的key if ((key = dbRandomKey(c->db)) == NULL) { // 调用核心函数dbRandomKey() addReply(c,shared.nullbulk); // 返回nil return; } addReplyBulk(c,key); // 返回key decrRefCount(key); // 减少引用计数}随机键生成以及过期判断randomKeyCommand()调用了dbRandomKey()函数来真正生成一个随机键: robj *dbRandomKey(redisDb *db) { dictEntry *de; int maxtries = 100; int allvolatile = dictSize(db->dict) == dictSize(db->expires); while(1) { sds key; robj *keyobj; de = dictGetRandomKey(db->dict); // 获取随机的一个dictEntry if (de == NULL) return NULL; // 获取失败返回NULL key = dictGetKey(de); // 获取dictEntry中的key keyobj = createStringObject(key,sdslen(key)); // 根据key字符串生成robj if (dictFind(db->expires,key)) { // 去过期字典里查找这个键 ... if (expireIfNeeded(db,keyobj)) { // 判断键是否过期 decrRefCount(keyobj); // 如果过期了,删掉这个键并减少引用计数 continue; // 当前键过期了不能返回,只返回不过期的键,进行下一次随机生成 } } return keyobj; }}那么这一层的主逻辑又调用了dictGetRandomKey(),获取随机的一个dictEntry。假设我们已经获取到了随机生成的dictEntry,我们随后取出key。由于不能返回过期的key,所以我们需要先判断键是否过期,如果过期了就不能返回了,直接continue;如果不过期就可以返回。 ...

October 1, 2019 · 2 min · jiezi

goredisparser高效的Redis解析工具支持查找大-key

go-redis-parser,项目地址:https://github.com/8090Lamber... 一个简单、安全的Redis 解析器。了解到目前存在的 parser,大部分都是单进程解析完成后,再统一输出,拉长了整体的执行时间,决定自己重写一个。它的特点是:离线即用,不必连接线上服务,并且利用 golang 语言本身的协程,实现边解析边写文件内容,非常高效。简单介绍下这个工具: Feature支持Redis从2.8版本至5.0版本的,除了 module外的所有数据类型。包括: StringHashListSetSortedSedStream(敲黑板,重点重点重点,重要的事情说三遍)Other导出 server 所有 key/values,而且会找出现有各类型的 bigkey(类似redis-cli的 --bigkeys 参数输出) Installationgo-redis-parser 是可执行的二进制文件(binary file),可以使用 git 或者 go get 来下载安装 via git$ git clone https://github.com/8090Lambert/go-redis-parser.git && cd go-redis-parser$ go installvia go$ go get github.com/8090Lambert/go-redis-parserUsing在使用之前,你需要先设置 export PATH=$PATH:$GOPATH/bin $ go-redis-parser -rdb <dump.rdb> -o <gen-file folder> -type <gen-file type, json or csv, default csv>Generate file目前可导出两种类型的文件:json、csv。这里看下 csv 文件的示例 BigKeys outputsBigKeys的输出示例: # Scanning the rdb file to find biggest keys-------- summary -------Sampled 6 keys in the keyspace!Total key length in bytes is 17Biggest string found 's' has 1 bytesBiggest hash found 'h' has 1 fieldsBiggest list found 'li' has 2 itemsBiggest sortedset found 'zset' has 2 membersBiggest set found 'set' has 2 membersBiggest stream found 'stream' has 3 entries1 string with 1 bytes1 hash with 1 fields1 list with 2 items1 sortedset with 2 members1 set with 2 members1 stream with 3 entries

September 30, 2019 · 1 min · jiezi

Go实现双向链表

本文介绍什么是链表,常见的链表有哪些,然后介绍链表这种数据结构会在哪些地方可以用到,以及 Redis 队列是底层的实现,通过一个小实例来演示 Redis 队列有哪些功能,最后通过 Go 实现一个双向链表。 目录1、链表 1.1 说明1.2 单向链表1.3 循环链表1.4 双向链表2、redis队列 2.1 说明2.2 应用场景2.3 演示3、Go双向链表 3.1 说明3.2 实现4、总结5、参考文献1、链表1.1 说明 链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而顺序表相应的时间复杂度分别是O(logn)和O(1)。 链表有很多种不同的类型:单向链表,双向链表以及循环链表。 优势:可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。链表允许插入和移除表上任意位置上的节点。 劣势:由于链表增加了节点指针,空间开销比较大。链表一般查找数据的时候需要从第一个节点开始每次访问下一个节点,直到访问到需要的位置,查找数据比较慢。 用途:常用于组织检索较少,而删除、添加、遍历较多的数据。 如:文件系统、LRU cache、Redis 列表、内存管理等。 1.2 单向链表链表中最简单的一种是单向链表, 一个单向链表的节点被分成两个部分。它包含两个域,一个信息域和一个指针域。第一个部分保存或者显示关于节点的信息,第二个部分存储下一个节点的地址,而最后一个节点则指向一个空值。单向链表只可向一个方向遍历。 单链表有一个头节点head,指向链表在内存的首地址。链表中的每一个节点的数据类型为结构体类型,节点有两个成员:整型成员(实际需要保存的数据)和指向下一个结构体类型节点的指针即下一个节点的地址(事实上,此单链表是用于存放整型数据的动态数组)。链表按此结构对各节点的访问需从链表的头找起,后续节点的地址由当前节点给出。无论在表中访问哪个节点,都需要从链表的头开始,顺序向后查找。链表的尾节点由于无后续节点,其指针域为空,写作为NULL。 1.3 循环链表循环链表是与单向链表一样,是一种链式的存储结构,所不同的是,循环链表的最后一个结点的指针是指向该循环链表的第一个结点或者表头结点,从而构成一个环形的链。 循环链表的运算与单链表的运算基本一致。所不同的有以下几点: 1、在建立一个循环链表时,必须使其最后一个结点的指针指向表头结点,而不是像单链表那样置为NULL。 2、在判断是否到表尾时,是判断该结点链域的值是否是表头结点,当链域的值等于表头指针时,说明已到表尾。而非象单链表那样判断链域的值是否为NULL。 1.4 双向链表 双向链表其实是单链表的改进,当我们对单链表进行操作时,有时你要对某个结点的直接前驱进行操作时,又必须从表头开始查找。这是由单链表结点的结构所限制的。因为单链表每个结点只有一个存储直接后继结点地址的链域,那么能不能定义一个既有存储直接后继结点地址的链域,又有存储直接前驱结点地址的链域的这样一个双链域结点结构呢?这就是双向链表。 在双向链表中,结点除含有数据域外,还有两个链域,一个存储直接后继结点地址,一般称之为右链域(当此“连接”为最后一个“连接”时,指向空值或者空列表);一个存储直接前驱结点地址,一般称之为左链域(当此“连接”为第一个“连接”时,指向空值或者空列表)。 2、redis队列2.1 说明Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边) Redis 列表使用两种数据结构作为底层实现:双端列表(linkedlist)、压缩列表(ziplist) 通过配置文件中(list-max-ziplist-entries、list-max-ziplist-value)来选择是哪种实现方式 在数据量比较少的时候,使用双端链表和压缩列表性能差异不大,但是使用压缩列表更能节约内存空间 redis 链表的实现源码 redis src/adlist.h 2.2 应用场景消息队列,秒杀项目 秒杀项目: 提前将需要的商品码信息存入 Redis 队列,在抢购的时候每个用户都从 Redis 队列中取商品码,由于 Redis 是单线程的,同时只能有一个商品码被取出,取到商品码的用户为购买成功,而且 Redis 性能比较高,能抗住较大的用户压力。 ...

September 20, 2019 · 3 min · jiezi

redis5源码分析浅析redis命令之del篇

baiyan 命令语法命令格式: DEL [key1 key2 …]命令实战: 127.0.0.1:6379> del key1(integer) 1返回值:被删除 key 的数量 源码分析首先我们开启一个redis客户端,使用gdb -p redis-server的端口。由于del命令对应的处理函数是delCommand(),所以在delCommand处打一个断点,然后在redis客户端中执行以下几个命令: 127.0.0.1:6379> set key1 value1 EX 100OK127.0.0.1:6379> get key1"value1"127.0.0.1:6379> del key1首先设置一个键值对为key1-value1、过期时间为100秒的键值对,然后在100秒之内对其进行删除,执行del key1,,删除这个还没有过期的键。我们看看redis服务端是如何执行的: Breakpoint 1, delCommand (c=0x7fb46230dec0) at db.c:487487 delGenericCommand(c,0);(gdb) sdelGenericCommand (c=0x7fb46230dec0, lazy=0) at db.c:468468 void delGenericCommand(client *c, int lazy) {(gdb) n471 for (j = 1; j < c->argc; j++) {(gdb) p c->argc $1 = 2(gdb) p c->argv[0]$2 = (robj *) 0x7fb462229800(gdb) p *$2$3 = {type = 0, encoding = 8, lru = 8575647, refcount = 1, ptr = 0x7fb462229813}我们看到,delCommand()直接调用了delGenericCommand(c,0),貌似是一个封装好的通用删除函数,我们s进去,看下内部的执行流程。 ...

September 20, 2019 · 2 min · jiezi

Redis压缩包没有redisconf快速启动之记录一

转载请标明出处: http://dujinyang.blog.csdn.net/本文出自:【奥特曼超人的博客】Redis压缩包配置环境变量,直接CMD中启动,默认是打开redis.conf,当然,压缩包是没有的,这里是自行创建的 redis.windows-service.conf 和 redis.windows.conf 对应服务端和客户端的使用。 直接启动应该都会遇到这个提示警告warning,这里不是win7,懒得去找etc下的文件,也没找到…… Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf所以这里自己写了redis-ser.exe 和redis-ser.bat 作为调用,在命令行CMD输入就可以调起。 加入启动服务: redis-server.exe --service-install redis.windows.conf命令: redis-server redis.windows.confredis-cli –h 127.0.0.1 –p 8088<port> -a dujinyang<pwd> 快速启动: redis-ser 或 redis-cli2redis.windows.conf日志记录级别:loglevel notice (Redis支持四个级别:debug、verbose、notice、warning)日志记录方式:logfile ""数据库的数量:databases 16 (可以使用SELECT<dbid>命令在连接上指定数据库id)指定在多长时间内,有多少次更新操作,将数据同步到数据文件: save <seconds> <changes>save 900 1 //900秒至少有1次更新同步到数据文件 save 300 10 //300秒至少有10次更新同步到数据文件 save 60 10000 //60秒如果有10000次更新同步到数据文件指定存储至本地数据库时是否压缩数据: rdbcompression yes.默认为yes,Redis采用LZF压缩,如果为了节省CPU时间,可以关闭该选项,但会导致数据库文件变的巨大。指定本地数据库文件名: dbfilename dump.rdb指定本地数据库存放目录: dir ./设置当本机为slav服务时,设置master服务的IP地址及端口,在Redis启动时,它会自动从master进行数据同步: slaveof <masterip> <masterport>当master服务设置了密码保护时,slav服务连接master的密码: masterauth <master-password>设置Redis连接密码,如果配置了连接密码,客户端在连接Redis时需要通过AUTH<password>命令提供密码: requirepass foobared(默认关闭)设置同一时间最大客户端连接数,默认无限制,Redis可以同时打开的客户端连接数为Redis进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。 当客户端连接数到达限制时,Redis会关闭新的连接并向客户端返回max number of clients reached错误信息: maxclients 10000指定Redis最大内存限制,Redis在启动时会把数据加载到内存中,达到最大内存后,Redis会先尝试清除已到期或即将到期的Key,当此方法处理 后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis新的vm机制,会把Key存放内存,Value会存放在swap区: maxmemory <bytes>指定是否在每次更新操作后进行日志记录,Redis在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为 redis本身同步数据文件是按上面save条件来同步的,所以有的数据会在一段时间内只存在于内存中。appendonly no (默认为no)指定更新日志文件名。appendfilename "appendonly.aof"指定更新日志条件。appendfsync everysecappendfsync always //表示每次更新操作后手动调用fsync()将数据写到磁盘(慢,安全) appendfsync everysec //表示每秒同步一次(折衷,默认值) appendfsync no //表示等操作系统进行数据缓存同步到磁盘(快)指定包含其它的配置文件,可以在同一主机上多个Redis实例之间使用同一份配置文件,而同时各个实例又拥有自己的特定配置文件。include /path/to/local.conf ...

September 19, 2019 · 1 min · jiezi

Redis-的-4-大法宝2019-必学中间件

Redis 的 4 大法宝,2019 必学中间件 Redis是什么? 全称:REmote DIctionary Server Redis是一种key-value形式的NoSQL内存数据库,由ANSI C编写,遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。 Redis最大的特性是它会将所有数据都放在内存中,所以读写速度性能非常好。当然,它也支持将内存中的数据以快照和日志的形式持久化到硬盘,这样即使在断电、机器故障等异常情况发生时数据也不会丢失,Redis能从硬盘中恢复快照数据到内存中。 官网:https://redis.io/ 中文:http://www.redis.cn/ Github:https://github.com/antirez/redis Redis有什么优势? 1、性能高,速度快 Redis命令执行速度非常快,官方给出的读写性能可以达到10W/秒。为什么会如此之快呢?有以下几个因素: 数据存储在内存中,直接与内存连接。由相对底层的C语言实现,离操作系统更近。实现源码很精湛,仅仅几万行代码,简单稳定。使用了单线程模型,无多线程竞争、锁等问题。2、丰富的数据结构 Redis与其他的内存数据库不同的是,Redis拥有丰富的数据类型,如字符串、哈希、列表、集合、有序集合等。正是因为Redis丰富的数据类型,所有它能应用的场景非常多。 3、丰富的特性 除了支持丰富的数据结构外,还支持以下高级功能。 支持键过期功能,可以用来实现定时缓存。支持发布/订阅功能,可以有来实现消息队列。支持事务功能,可以保证多条命令的事务性。支持供管道功能,能够批量处理命令。支持Lua脚本功能。支持集群分片和数据复制功能。支持内存数据持久化硬盘功能。4、丰富的客户端 官网索引:http://www.redis.cn/clients.html 从官网给出的客户端列表可以看出,各种各种的语言都能接入到Redis,接入包括了所有的主流开发语言。 目前使用Redis的公司非常多,国内外都有很多重量级的公司在用。所以,现在学习Redis是大势所趋,学好Redis能为自己在日后的工作谋生中增加一个强有利的竞争手段。 欢迎工作一到五年的Java工程师朋友们加入JavaQQ群:219571750,群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!

September 10, 2019 · 1 min · jiezi

Centos7-编译安装-LNMP-可能是全网最全

声明本文档请用于学习,生产环境谨慎使用 前言网络上有很多关于Windows或者Linux下面安装LNMP的教程,非常多。但是我还是选择自己去一步一步实现,即使我已经按照网上的教程在不同系统中装了N多次。为什么?因为不懂。不懂为什么需要那些系统软件,不懂那些编译项为什么要加?有何作用?只会复制粘贴。 几点建议: 编译安装只是环境搭建的开始,别想着把所有问题都在此阶段解决坚持按需编译,最小化安装,把问题暴露出来,去解决它对使用的每一个编译参数负责远离复制粘贴,你可能还有救完全不了解的软件尽可能避免编译安装目标我们把Laravel跑起来! 基本情况软件文档列表此处文档是基础软件文档,不是很全,一部分软件文档在内容中。 NginxMysqlRedisPhpSwoole系统要求# 查看CentOS版本[root@bogon source]# cat /etc/redhat-release CentOS Linux release 7.6.1810 (Core) [root@bogon source]# cat /proc/versionLinux version 3.10.0-957.el7.x86_64 (mockbuild@kbuilder.bsys.centos.org) (gcc version 4.8.5 20150623 (Red Hat 4.8.5-36) (GCC) ) #1 SMP Thu Nov 8 23:39:32 UTC 2018软件列表系统内置 CURL 命令,所以我们选择CURL下载软件curl -O xxx,如果软件包出现解压问题,不是因为tar -zxf 解压不了这个包。而是说明这个包下载的不完整,或者说因为网络原因只是一个空包,解决办法是删除后重新下载。另外防止网站迁移,请使用curl -OL参数.-L:跟踪重定向国外资源下载太慢,请使用迅雷下载,然后通过scp上传到服务器,常用参数: -r 下载整个目录-p 指定端口,默认 22使用场景: scp -p <server port> <server user>@<server ip>:<server path>/<server file> <local path> 这是下载服务器资源,资源将被保存到 <local path>。scp -p <server port> <local path>/<local file> <server user>@<server ip>:<server path> 这是上传本地文件到服务器指定路径mysql-8.0.17.tar.gznginx-1.16.1.tar.gzphp-7.3.9.tar.gzredis-5.0.5.tar.gzswoole-4.4.4.tgzSoftware Collections为什么先说这个,为了给大家提供一个不需要关注 lnmp 系统依赖,而只是编译安装 lnmp 的方法。 ...

September 9, 2019 · 16 min · jiezi

docker常用指令详解

指令详解从远程仓库拉取镜像//docker image pull library/hello-world默认从library拉取可以省略docker image pull hello-world查看镜像列表docker image ls运行一个docker的镜像,产生一个容器实例//运行docker run hello-world//echo会在shell打印一段文字,起提示作用Hello world检查本地是否存在指定的镜像,不存在就从公有仓库下载利用镜像创建并启动一个容器分配一个文件系统,并在只读的镜像层外面挂载一层可读写层从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去从地址池配置一个 ip 地址给容器执行用户指定的应用程序执行完毕后容器被终止查看容器// 列出本机正在运行的容器docker container ls// 列出本机所有容器,包括终止运行的容器docker container ls --all启动一个可交互的容器docker run -t -i ubuntu:14.04 /bin/bashdocker run 命令,并启动名称为ubuntu:14.04的容器 。-t 表示在新容器内指定一个伪终端或终端-i表示允许我们对容器内的 (STDIN) 进行交互。可以将-t -i缩减为-it好记我们在容器内还指定了一个新的命令: /bin/bash 。这将在容器内启动 bash shell,这是一个交互程序注意":"后面为TAG,如果没有指定默认就为latest所以当容器(container)启动之后,我们会获取到一个命令提示符: root@af8bae53bdd3:/#这代表我们已经进入了容器内部 启动守护进程,后台一直运行容器docker run -d ubuntu:14.04//注意-d和-it同时使用,-it会无效,-d需要返回一个容器id端口映射,挂载数据卷,命名容器docker run -d --name=ant_web_pro -v /宿主机绝对路径:/容器内目录 -p docker外部端口:docker内部端口 nginx-P: 随机端口映射,容器内部端口随机映射到主机的高端口-p: 指定端口映射,格式为:主机(宿主)端口:容器端口--name="nginx-lb": 为容器指定一个名称;--volume , -v: 绑定一个卷如果是容器里的目录不存在,两者都会自动创建-v $PWD/目录:/容器内目录 $PWD表示当前文件夹当你在容器容器内部改变数据卷是其实软连接到了外部,数据直接在宿主机改变查看数据卷docker volume ls启动已终止的容器容器可以通过run新建一个来运行,也可以重新start已经停止的container,但start不能够再指定容器启动时运行的指令,因为docker只能有一个前台进程。容器stop(或Ctrl+D)时,会在保存当前容器的状态之后退出,下次start时保有上次关闭时更改。而且每次进入attach进去的界面是一样的,与第一次run启动或commit提交的时刻相同。 docker start $CONTAINER_IDdocker stop $CONTAINER_IDdocker restart $CONTAINER_ID退出容器exit进入容器docker exec -it ant-design-pro_web /bin/bash//docker exec -it 64b9ded82141 bash (可以是id) 删除一个或多个container、image(rm、rmi)// 删除容器docker rm <container_id/contaner_name>// 删除所有停止的容器docker rm $(docker ps -a -q)// 删除镜像docker rmi <image_id/image_name ...>commit 容器,创建新镜像我们期望能定制自己的镜像,在里面安装一些基础环境(比如上文中的 node),然后制作出自己要的基础镜像。 ...

September 7, 2019 · 1 min · jiezi

linux下安装redis

linux下安装redis1.下载压缩包 redis官网下载链接:https://redis.io/download 如果是新机器的话,安装redis的过程中需要依赖tcl,所以一并将压缩包下载下来,tcl下载地址:https://sourceforge.net/proje...,选择相应的版本后下载相应的gz包 将下载的.tar.gz 文件转移到自己设定的位置(/usr/local/src),并解压 2. 安装tcl(不进行make test可以不装) 进入到tcl解压后的unix目录,运行./configure命令 # 进入unix文件夹cd /usr/local/src/tcl8.6.8/unix/# prefix 作用是修改tcl编译安装后生成文件的位置,如果不需要改变的话,直接运行./configure# ./configure --prefix=/usr/local/tcl8/ --exec_prefix=/usr/local/tcl8/ --enable-shared./configure# 编译并安装make && make install 命令运行完没有报错,说明安装完成,/usr/local/目录下会有tcl8文件夹,进入tcl8/bin目录下,会有tclsh8.6文件,为该文件建立硬链接 # 建立硬链接ln tclsh8.6 tclsh# 运行tcl ./tclsh8.6也可以,输入tclsh会出现%信息./tclsh#简单测试,会输出:3%exp 1 + 2 修改环境变量和链接库,完成后就可以在别处直接使用./tclsh vi ~/.bashrcexport PATH=$PATH:/usr/local/tcl8export DB_LIBRARY_PATH=$DB_LIBRARY_PATH:/usr/local/tcl8/libsource ~/.bashrc3. 安装redis# 进入解压目录cd /usr/local/src/redis# 编辑make# 测试(需要依赖tcl)make test# PREFIX安装指定目录 否则安装到/usr/local/bin里面了make install PREFIX=/usr/local/redis4. redis的生产环境启动方案 1. redis解压文件夹utils目录下,有个redis_init_script脚本,将redis_init_script脚本拷贝到linux的/etc/init.d目录中,,将redis_init_script重命名为redis_6379,6379是我们希望这个redis实例监听的端口号 cp redis_init_script /etc/init.d/redis_6379 2. 修改redis_6379脚本的第6行的REDISPORT,设置为相同的端口号(默认就是6379) 3. 创建两个目录:/etc/redis(存放redis的配置文件),/var/redis/6379(存放redis的持久化文件) mkdir /etc/redismkdir -p /var/redis/6379 4. 修改redis配置文件(默认在根目录下,redis.conf),拷贝到/etc/redis目录中,修改名称为6379.conf cp redis.conf /etc/redis/6379.conf 5. 修改6379.conf中的部分配置为生产环境 ...

September 6, 2019 · 1 min · jiezi

SpringBoot-2x-整合redis

看了好多篇文章,再根据自己的理解进行的配置。 一、Mevan相关配置1.我的SpringBoot版本 <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.7.RELEASE</version> <relativePath/> <!-- lookup parent from repository --></parent>2.引入spring-redis的依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency>二、application.yml中加入redis依赖spring: datasource: url: jdbc:mysql://localhost:3306/spring_cache?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 123456 redis: host: 39.107.250.174 port: 6379 database: 0 timeout: 1000s # 数据库连接超时时间,2.0 中该参数的类型为Duration,这里在配置的时候需要指明单位 # 连接池配置,2.0中直接使用jedis或者lettuce配置连接池 jedis: pool: # 最大空闲连接数 max-idle: 500 # 最小空闲连接数 min-idle: 50 # 等待可用连接的最大时间,负数为不限制 max-wait: -1 # 最大活跃连接数,负数为不限制 max-active: -1 cache: redis: time-to-live: -1 #毫秒 #以下可忽略mybatis: configuration: #开启驼峰命名 map-underscore-to-camel-case: truelogging: level: com.scitc.cache.mapper : debug三、配置编写redis配置类package com.scitc.cache.config;import com.fasterxml.jackson.annotation.JsonAutoDetect;import com.fasterxml.jackson.annotation.PropertyAccessor;import com.fasterxml.jackson.databind.ObjectMapper;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Value;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.cache.CacheManager;import org.springframework.cache.annotation.CachingConfigurerSupport;import org.springframework.cache.annotation.EnableCaching;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.cache.RedisCacheConfiguration;import org.springframework.data.redis.cache.RedisCacheManager;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;import org.springframework.data.redis.serializer.RedisSerializationContext;import org.springframework.data.redis.serializer.RedisSerializer;import org.springframework.data.redis.serializer.StringRedisSerializer;import java.time.Duration;@Slf4j@Configuration@EnableCaching//启用缓存,这个注解很重要;//继承CachingConfigurerSupport,为了自定义生成KEY的策略。可以不继承。public class RedisConfig extends CachingConfigurerSupport { @Value("${spring.cache.redis.time-to-live}") private Duration timeToLive = Duration.ZERO; @Bean(name = "redisTemplate") public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值 Jackson2JsonRedisSerializer<Object> redisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper mapper = new ObjectMapper(); mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); redisSerializer.setObjectMapper(mapper); template.setValueSerializer(redisSerializer); //使用StringRedisSerializer来序列化和反序列化redis的key值 template.setKeySerializer(new StringRedisSerializer()); template.afterPropertiesSet(); return template; } @Bean public CacheManager cacheManager(RedisConnectionFactory factory) { RedisSerializer<String> redisSerializer = new StringRedisSerializer(); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); //解决查询缓存转换异常的问题 ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); // 配置序列化(解决乱码的问题) RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(timeToLive) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer)) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)) .disableCachingNullValues(); RedisCacheManager cacheManager = RedisCacheManager.builder(factory) .cacheDefaults(config) .build(); return cacheManager; }}然后我发现有好几篇文章配置 CacheManager 采用的是如下配置: ...

September 6, 2019 · 8 min · jiezi

Redis的应用场景汇总

Redis应用场景Redis作为一个非关系型数据库,除了在访问速度上拥有显著优势外,其本身支持的多种数据类型也非常有用,能覆盖系统开发中的很多应用场景。下面列举的场景有的是从网上其他人的博客里看到的,有的自己开发时尝试过的一些解决方案后记录下来的,希望能给以后的开发带来启发。 在说应用场景前先说一些是否觉得使用Redis的建议 使用建议Redis 速度快是建立在内存数据库基础上的,但是一台服务器的内存要比磁盘金贵许多,所以在项目初期不要想什么都往 Redis 里放,这样当数据量上来后很快内存就会不够用,反而得不偿失。合理的利用有限的内存,将读(写)频繁的热数据放在 Redis 中才能更好感受到它带来的性能提升。Redis 虽然提供了RDB和AOF两种持久化方式,但是普遍还是认为 Redis 的持久化并不是很靠谱。非常重要的数据不要依赖Redis来开发,或者最起码不要只在Redis中持久化MySQL经过不断优化性能已经非常好,所以MySQL提供的数据结构和访问效率能满足的需求的情况下不要引入Redis,多引入一个组件就多一个可能的故障节点,尤其在保持数据一致性的场景中数据(比如用户余额)应该只放在数据库中,除非你知道怎么解决考系统的分布式事务。缓存作为Key-Value形态的内存数据库,Redis 最先会被想到的应用场景便是作为数据缓存。而使用 Redis 缓存数据非常简单,只需要通过string类型将序列化后的对象存起来即可,不过也有一些需要注意的地方: 必须保证不同对象的 key 不会重复,并且使 key 尽量短,一般使用类名(表名)加主键拼接而成。选择一个优秀的序列化方式也很重要,目的是提高序列化的效率和减少内存占用。缓存内容与数据库的一致性,这里一般有两种做法: 只在数据库查询后将对象放入缓存,如果对象发生了修改或删除操作,直接清除对应缓存(或设为过期)。在数据库新增和查询后将对象放入缓存,修改后更新缓存,删除后清除对应缓存(或设为过期)。消息队列Redis 中list的数据结构实现是双向链表,所以可以非常便捷的应用于消息队列(生产者 / 消费者模型)。消息的生产者只需要通过lpush将消息放入 list,消费者便可以通过rpop取出该消息,并且可以保证消息的有序性。如果需要实现带有优先级的消息队列也可以选择sorted set。而pub/sub功能也可以用作发布者 / 订阅者模型的消息。无论使用何种方式,由于 Redis 拥有持久化功能,也不需要担心由于服务器故障导致消息丢失的情况。 时间轴(Timeline)list作为双向链表,不光可以作为队列使用。如果将它用作栈便可以成为一个公用的时间轴。当用户发完微博后,都通过lpush将它存放在一个 key 为LATEST_WEIBO的list中,之后便可以通过lrange取出当前最新的微博。 循环链表list 还可以作为循环链表使用 RPOPLPUSH source destination 命令 RPOPLPUSH 在一个原子时间内,执行以下两个动作: 将列表 source 中的最后一个元素(尾元素)弹出,并返回给客户端。将 source 弹出的元素插入到列表 destination ,作为 destination 列表的的头元素。如果 source 和 destination 相同,则列表中的表尾元素被移动到表头,并返回该元素,可以把这种特殊情况视作列表的旋转(rotation)操作。 比如有个进程来完成派单任务,需要将用户发送过来的申请依次派发给工作人员,那么就可以把工作人员的身份标示维护在循环列表中,从列表尾部读取每次读取身份标示后相应的标示都会被放到列表头如此循环往复。 排行榜使用sorted set和一个计算热度的算法便可以轻松打造一个热度排行榜,zrevrangebyscore可以得到以分数倒序排列的序列,zrank可以得到一个成员在该排行榜的位置(是分数正序排列时的位置,如果要获取倒序排列时的位置需要用zcard-zrank)。 计数器计数功能应该是最适合 Redis 的使用场景之一了,因为它高频率读写的特征可以完全发挥 Redis 作为内存数据库的高效。在 Redis 的数据结构中,string、hash和sorted set都提供了incr方法用于原子性的自增操作,下面举例说明一下它们各自的使用场景: 如果应用需要显示每天的注册用户数,便可以使用string作为计数器,设定一个名为REGISTERED_COUNT_TODAY的 key,并在初始化时给它设置一个到凌晨 0 点的过期时间,每当用户注册成功后便使用incr命令使该 key 增长 1,同时当每天凌晨 0 点后,这个计数器都会因为 key 过期使值清零。每条微博都有点赞数、评论数、转发数和浏览数四条属性,这时用hash进行计数会更好,将该计数器的 key 设为weibo:weibo_id,hash的 field 为like_number、comment_number、forward_number和view_number,在对应操作后通过hincrby使hash 中的 field 自增。如果应用有一个发帖排行榜的功能,便选择sorted set吧,将集合的 key 设为POST_RANK。当用户发帖后,使用zincrby将该用户 id 的 score 增长 1。sorted set会重新进行排序,用户所在排行榜的位置也就会得到实时的更新。好友关系这个场景最开始是是一篇介绍微博 Redis 应用的 PPT 中看到的,其中提到微博的 Redis 主要是用在在计数和好友关系两方面上,当时对好友关系方面的用法不太了解,后来看到《Redis 设计与实现》中介绍到作者最开始去使用 Redis 便是希望能通过set解决传统数据库无法快速计算集合中交集这个功能。后来联想到微博当前的业务场景,确实能够以这种方式实现,所以姑且猜测一下: ...

August 28, 2019 · 1 min · jiezi

一为什么要使用Redis

最近看过不少学习技术的文章,这些文章给了我不少的感触。我认为学习技术的时候不应该买一本很厚书就一头专进去。而应该按照以下方式来进行学习: 1. 先了解这些技术解决了什么样的问题,为什么这些要使用这些技术作为起始点;2. 了解了技术解决了什么问题之后?我们就可以继续深入下去了。如果我们对其中一个技术点感兴趣或者面试需要,那么我们可以查找相关的书籍或者博客了解这个技术点是怎么实现的。3. 如果你还想继续深入学习,那么最后应该做的就是“对比性学习”了,不知道大家有没有感触:在大四的时候需要学论文,你会参考多篇文章,这样进行对比,你对所需要学习的知识就会更加深入的理解。这篇文章我将陈述Redis解决了什么问题。大部分内容都在网络上已经出现了,我只是进行简单的整理,文章的最后我会将参考文章的链接给出。 Redis解决了什么问题? 大规模读写数据与数据库读写能力之间的矛盾 简单回顾一下CPU高速缓存的发展历程,为了解决CPU的计算速度与内存的读取速度之间的巨大差异,CPU使用高速缓存来存放指令和数据。高速缓存从最初的主板缓存到现在的3级缓存,缓存大小也不断变大。来自网络的数据表明:CPU高速缓存的命中率大约为80%。 类比电脑发展过程中CPU与内存的矛盾,可以察觉到大型网站中大规模读写数据与数据库读写能力之间的矛盾与此矛盾类似。我们也可以在数据库与应用之间构建一块比数据库速度更快存储区域——缓存。大家最熟悉的也莫过于Redis用作缓存,我们知道Redis的作者设计Redis的初衷是因为他使用关系型数据库时,无论如何优化,性能都不能达到自己的期望,于是便自己手写了一个内存数据库。 在做为缓存的情况下,我们有一下应用场景: 1. 热点数据 例如我们可以将SQL查询结果保存在内存中,也可以将用户经常查看的图片保存在内存中。 2. 排行榜 基于Redis提供的zset这种数据结构我们可以更加便捷的实现排行榜。实现排行榜的相关内容可以参考排行榜算法设计实现比较。在小规模数据的情况下,使用Mysql实现排行榜没有多少问题,但是一旦数据量上去了,那么持续的进行Mysql读写将会成为瓶颈。 3. 计数器/限速器 计数器的应用场景之一是统计用户的点赞数,限速器的应用场景之一是限制用户ip的访问次数。之所以Redis能用于计数器是因为Redis是单线程的,每次都必须前一个指令执行完,再执行下一个指令。这样就保证不会同时执行多条指令;也即不会出现并发问题。限速器的原理类似。 4. 共同好友 利用Redis提供的Set数据结构的求交集操作sinter可以更加便捷地求两个Set集合的交集;而使用数据库的连表查询将造成性能的开销很多,因为大型网站的用户数量巨大。 5. 简单消息队列 Redis的提供的发布/订阅是一个极其简单的消息系统。它不像Kafka那样提供了分成不同的topic并且分成不同的分区并且提供持久化的功能。Redis的消息队列用在不需要高可靠的场景。 6. session共享 Session是用来记录是用户是谁。当在应用使用集群方式部署的时候,我们需要一个统一管理session的地方,可以使用数据库来记录session,但是这时对数据库的性能要求较高,此外session通常是具有时效性的,这段逻辑我们需要在代码中实现,但是如果使用Redis来共享session,那么不会出现这样的问题。 在学习redis的过程中,一直有一个疑问:当我们开发数据库应用程序时,可以使用MyBatis的一二级缓存,Guava中的缓存模块,甚至是Java自带的Map数据结构来达到缓存数据的目的,为什么还要使用Redis作为缓存呢?参考一篇博客总结如下:1. 无论MyBatis的缓存,还是Guava,Map数据结构作为缓存,它们的生命周期都是随着JVM的销毁而结束。在微服务或者应用程序挂掉的时候,我们并不希望缓存也消失。2. 在多个实例的情况下,存在数据不一致。如一个微服务启动多个实例,或者多个节点上部署同一个微服务,这时不同实例内缓存的数据不一致。参考:CPU缓存读完这篇文章,你就知道为什么生产环境大多数要用redis数据库!

August 18, 2019 · 1 min · jiezi

Redis-Module原理一

Redis自4.0版本之后便支持了模块扩展功能, 在使用的过程中发现了一些Redis实现的疑问, Redis推荐开发人员通过引入redismodule.h, 来调用指定接口来支持扩展, 其中要求实现程序必须实现RedisModule_OnLoad方法, 该方法主要加载模块, 注册相应的api, 对context上下文注入, 那么它是什么时候被调用的呢? 又比如Redis为开发人员提供几组API, 比如RedisModule_StringPtrlen用于返回抽象类型字符串的长度, 但是翻遍所有的代码也没有该函数声明的具体实现, 那么它是什么时候在哪里被实现的呢? 带着这些问题这篇文章会做出解答 (一) 使用 首先根据 官方文档 , 用户扩展模块需要通过配置文件指定需要加载的链接库 *通过配置loadmodule指定用户扩展的链接库* 当然动态库需要用户自己去编译生成, 在编译之前需要将指定的module引入redismodule.h头文件, 最简单的方式是直接将源文件拉入modules下, 修改MakeFile文件并执行make编译 *修改MakeFile* 那么redis是何时开始加载的呢? (二)链接库的加载 redis服务端的入口在server.c源文件中, 其主要任务为初始化数据结构, 初始化设置, 启动eventloop事件加载器等等, 其中在启动事件加载器之前, redis会先加载指定的链接库(redis也支持通过命令行加载库) server.sentinel_mode = checkForSentinelMode(argc,argv); initServerConfig(); ACLInit(); /* The ACL subsystem must be initialized ASAP because the basic networking code and client creation depends on it. */ //初始化模块 加载动态链接库 moduleInitModulesSystem(); moduleInitModulesSystem函数负责加载模块 ...

August 17, 2019 · 2 min · jiezi

redis之啥也没写

3 Redis基本内容3.1 Redis的数据类型字符串列表集合散列表有序集合 3.2 Redis集群的数据存储方式3.2.1 单节点3.2.2 主从模式(master/slaver)a) 一个Master可以有多个Slavesb) 默认配置下,master节点可以进行读和写,slave节点只能进行读操作,写操作被禁止c) 不要修改配置让slave节点支持写操作,没有意义,原因一,写入的数据不会被同步到其他节点;原因二,当master节点修改同一条数据后,slave节点的数据会被覆盖掉d) slave节点挂了不影响其他slave节点的读和master节点的读和写,重新启动后会将数据从master节点同步过来e) master节点挂了以后,不影响slave节点的读,Redis将不再提供写服务,master节点启动后Redis将重新对外提供写服务。f) master节点挂了以后,不会slave节点重新选一个master对有密码的情况说明一下,当master节点设置密码时:客户端访问master需要密码;启动slave需要密码,在配置中进行配置即可;客户端访问slave不需要密码3.2.3 Sentinel模式sentinel模式:a) sentinel模式是建立在主从模式的基础上,如果只有一个Redis节点,sentinel就没有任何意义;b) 当master节点挂了以后,sentinel会在slave中选择一个做为master,并修改它们的配置文件,其他slave的配置文件也会被修改,比如slaveof属性会指向新的master;c) 当master节点重新启动后,它将不再是master而是做为slave接收新的master节点的同步数据;d) sentinel因为也是一个进程有挂掉的可能,所以sentinel也会启动多个形成一个sentinel集群;e) 当主从模式配置密码时,sentinel也会同步将配置信息修改到配置文件中,不许要担心;f) 一个sentinel或sentinel集群可以管理多个主从Redis;g) sentinel最好不要和Redis部署在同一台机器,不然Redis的服务器挂了以后,sentinel也挂了;h) sentinel监控的Redis集群都会定义一个master名字,这个名字代表Redis集群的master Redis;当使用sentinel模式的时候,客户端就不要直接连接Redis,而是连接sentinel的ip和port,由sentinel来提供具体的可提供服务的Redis实现,这样当master节点挂掉以后,sentinel就会感知并将新的master节点提供给使用者。sentinel模式基本可以满足一般生产的需求,具备高可用性。但是当数据量过大到一台服务器存放不下的情况时,主从模式或sentinel模式就不能满足需求了,这个时候需要对存储的数据进行分片,将数据存储到多个Redis实例中。3.2.4 Cluster模式cluster的出现是为了解决单机Redis容量有限的问题,将Redis的数据根据一定的规则分配到多台机器。cluster:cluster可以说是sentinel和主从模式的结合体,通过cluster可以实现主从和master重选功能,所以如果配置两个副本三个分片的话,就需要六个Redis实例。因为Redis的数据是根据一定规则分配到cluster的不同机器的,当数据量过大时,可以新增机器进行扩容这种模式适合数据量巨大的缓存要求,当数据量不是很大使用sentinel即可。

August 17, 2019 · 1 min · jiezi

thinkqueue-解析上

前言分析之前请大家务必了解消息队列的实现如果不了解请先阅读下:有赞消息队列设计去哪儿网消息队列设计 tp5的消息队列是基于database redis 和tp官方自己实现的 Topthink本章是围绕redis来做分析 存储key:key类型描述queues:queueNamelist要执行的任务think:queue:restartstring重启队列时间戳queues:queueName:delayedzSet延迟任务queues:queueName:reservedzSet执行失败,等待重新执行执行命令work和listen的区别在下面会解释| 命令 | 描述 |php think queue:work监听队列php think queue:listen监听队列php think queue:restart重启队列php think queue:subscribe暂无,可能是保留的 官方有什么其他想法但是还没实现行为标签标签描述worker_daemon_start守护进程开启worker_memory_exceeded内存超出worker_queue_restart重启守护进程worker_before_process任务开始执行之前worker_before_sleep任务延迟执行queue_failed任务执行失败命令参数参数默认值可以使用的模式描述queuenullwork,listen要执行的任务名称daemonnullwork以守护进程执行任务delay0work,listen失败后重新执行的时间forcenullwork失败后重新执行的时间memory128Mwork,listen限制最大内存sleep3work,listen没有任务的时候等待的时间tries0work,listen任务失败后最大尝试次数模式区别1: 执行原理不同work: 单进程的处理模式;无 daemon 参数 work进程在处理完下一个消息后直接结束当前进程。当不存在新消息时,会sleep一段时间然后退出;有 daemon 参数 work进程会循环地处理队列中的消息,直到内存超出参数配置才结束进程。当不存在新消息时,会在每次循环中sleep一段时间; listen: 父进程 + 子进程 的处理模式;会在所在的父进程会创建一个单次执行模式的work子进程,并通过该work子进程来处理队列中的下一个消息,当这个work子进程退出之后;所在的父进程会监听到该子进程的退出信号,并重新创建一个新的单次执行的work子进程; 2: 退出时机不同work: 看上面listen: 所在的父进程正常情况会一直运行,除非遇到下面两种情况01: 创建的某个work子进程的执行时间超过了 listen命令行中的--timeout 参数配置;此时work子进程会被强制结束,listen所在的父进程也会抛出一个 ProcessTimeoutException 异常并退出; 开发者可以选择捕获该异常,让父进程继续执行;02: 所在的父进程因某种原因存在内存泄露,则当父进程本身占用的内存超过了命令行中的 --memory 参数配置时,父子进程均会退出。正常情况下,listen进程本身占用的内存是稳定不变的。 3: 性能不同work: 是在脚本内部做循环,框架脚本在命令执行的初期就已加载完毕; listen: 是处理完一个任务之后新开一个work进程,此时会重新加载框架脚本; 因此 work 模式的性能会比listen模式高。注意: 当代码有更新时,work 模式下需要手动去执行 php think queue:restart 命令重启队列来使改动生效;而listen 模式会自动生效,无需其他操作。 4: 超时控制能力work: 本质上既不能控制进程自身的运行时间,也无法限制执行中的任务的执行时间;listen: 可以限制其创建的work子进程的超时时间; 可通过 timeout 参数限制work子进程允许运行的最长时间,超过该时间限制仍未结束的子进程会被强制结束;expire 和time的区别 ...

August 8, 2019 · 2 min · jiezi

Redis-字符串对象

Redis 字符串对象字符串对象的编码可以是 int 、 raw 或者 embstr. 如果一个字符串对象保存的是整数值, 并且这个整数值可以用 long 类型来表示, 那么字符串对象会将整数值保存在字符串对象结构的 ptr 属性里面 (将 void* 转换成 long), 并将字符串对象的编码设置为 int. int举个例子, 如果我们执行以下 SET 命令, 那么服务器将创建一个如图所示的 int 编码的字符串对象作为 number 键的值: redis> SET number 10086OKredis> OBJECT ENCODING number"int" 值得注意的是:long 类型表示的是 C 语言中的 8 个字节的长整型.编码 int 则指的是 Redis 中的 REDIS_ENCODING_INT 编码.在 redis redisObject 数据结构 中有说过, ptr 是一个指针, 指向实际保存值的数据结构, 这个数据结构由 type 属性和 encoding 属性决定的. 我的系统是 Ubuntu 16.04 64位, 用的是 Redis 3.0.7 版本, 可以存下 -9223372036854775808~9223372036854775807 范围的值. 也就是说, 这个范围内的值都会被编码为 int. ...

August 8, 2019 · 3 min · jiezi

Redis进阶应用RedisLua脚本实现复合操作

一、引言Redis是高性能的key-value数据库,在很大程度克服了memcached这类key/value存储的不足,在部分场景下,是对关系数据库的良好补充。得益于超高性能和丰富的数据结构,Redis已成为当前架构设计中的首选key-value存储系统。 虽然Redis官网上提供了200多个命令,但做程序设计时还是避免不了为了实现一小步业务逻辑而多次调用Redis的情况。 以compare and set场景为例。如果使用Redis原生命令,需要从Redis中获取这个key,然后提取其中的值进行比对:如果相等就不做处理;如果不相等或者key不存在则将key设置成目标值。仅仅一个单点的compare and set操作就需要与Redis通讯两次。 此外,这种分散操作无法利用Redis的原子特性,占用多次网络IO。 今天我们就来探讨一下如何优雅地应对上述场景。 二、Redis与Lua在介绍Lua之前,我们需要先对这个语言有个初步了解。Lua 是一个小巧的脚本语言,几乎可以运行在所有操作系统和平台上。我们一般不会用Lua处理特别复杂的事务,因此只需了解一些lua的基本语法即可。 Redis问世之后,其开发者也意识到了开篇提到的问题,因此Redis从2.6版本开始支持Lua脚本。新版本的Redis还支持Lua Script debug,感兴趣的小伙伴可以去官网的Documentation中找到对应介绍和QuickStart。 有了Lua脚本之后,使用Redis程序时便能够在以下方面实现显著提升: 减少网络开销:本来N次网络请求的操作,可以用一个请求完成。原先N次请求的逻辑放在Redis服务器上完成,减少了网络往返时延;原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。这是一个重要特性,一定要拿小本本记好。至于为什么是一个原子操作,我们以后再分析;复用:客户端发送的脚本会永久存储在Redis中。这样其他客户端就可以复用这一脚本,而不需要使用代码完成同样的逻辑。所以现在流传一句话:要想学好Redis,必会Lua Script。 三、通过Lua脚本实现compare and set接下来我们就实现一个简单的compare and set,并通过这个例子感受一下Lua脚本给Redis使用带来的全新体验。 首先看一下如何让Redis执行Lua脚本。 3.1 Redis的EVALRedis 127.0.0.1:6379> EVAL script numkeys key [key ...] arg [arg ...]script: 参数是一段 Lua 5.1 脚本程序。脚本不必(也不应该)定义为一个Lua函数。numkeys: 用于指定键名参数的个数。key [key ...]: 从 EVAL 的第三个参数开始算起,表示在脚本中所用到的Redis键(key)。在Lua中,这些键名参数可以通过全局变量 KEYS 数组,用1为基址的形式访问( KEYS[1] ,KEYS[2],依次类推)。arg [arg ...]: 附加参数,在Lua中通过全局变量ARGV数组访问,访问的形式和KEYS变量类似( ARGV[1] 、 ARGV[2] ,诸如此类)。这里借用一下官网的例子。 上述脚本直接返回了入参。 eval为Redis关键字;第一个引号中的内容就是Lua脚本;2为参数个数;key1和key2是KEYS[1]、KEYS[2]的入参;first和second是ARGV[1],ARGV[2]的入参。大家可以简单地将KEYS[1],KEYS[2], ARGV[1],ARGV[2]理解为占位符。 3.2 执行脚本文件和缓存脚本如果只能在命令行中写脚本执行,遇到复杂的脚本程序岂不是会抓狂? 下面我们来看一下,如何让Redis执行Lua脚本文件,同时也验证一下lua脚本的复用特性(以后我们再也不需要定期批量删除某些符合特定规则的key了)。 Redis 127.0.0.1:6379> SCRIPT LOAD scriptRedis 127.0.0.1:6379> EVALSHA sha1 numkeys key [key ...] arg [arg ...]Redis提供了一个SCRIPTLOAD命令,命令后面的script即为Lua脚本。命令将脚本script添加到脚本缓存中,但并不立即执行这个脚本。执行命令后,Redis会返回一个SHA1串,第二个EVALSHA命令即可执行。 ...

August 7, 2019 · 1 min · jiezi

跟着大彬读源码-Redis-9-对象编码之-三种list

Redis 底层使用了 ziplist、skiplist 和 quicklist 三种 list 结构来实现相关对象。顾名思义,ziplist 更节省空间、skiplist 则注重查找效率,quicklist 则对空间和时间进行折中。 在典型的双向链表中,我们有称为节点的结构,它表示列表中的每个值。每个节点都有三个属性:指向列表中的前一个和下一个节点的指针,以及指向节点中字符串的指针。而每个值字符串值实际上存储为三个部分:一个表示长度的整数、一个表示剩余空闲字节数的整数以及字符串本身后跟一个空字符。 可以看到,链表中的每一项都占用独立的一块内存,各项之间用地址指针(或引用)连接起来。这种方式会带来大量的内存碎片,而且地址指针也会占用额外的内存。这就是普通链表的内存浪费问题。 此外,在普通链表中执行随机查找操作时,它的时间复杂度为 O(n),这对于注重效率的 Redis 而言也是不可接受的。这是普通链表的查找效率太低问题。 针对上述两个问题,Redis 设计了ziplist(压缩列表)、skiplist(跳跃表)和快速链表进行相关优化。 1 ziplist对于 ziplist,它要解决的就是内存浪费的问题。也就是说,它的设计目标就是是为了节省空间,提高存储效率。 基于此,Redis 对其进行了特殊设计,使其成为一个经过特殊编码的双向链表。将表中每一项存放在前后连续的地址空间内,一个 ziplist 整体占用一大块内存。它是一个表(list),但其实不是一个链表(linked list)。 除此之前,ziplist 为了在细节上节省内存,对于值的存储采用了变长的编码方式,大概意思是说,对于大的整数,就多用一些字节来存储,而对于小的整数,就少用一些字节来存储。 也正是为了这种高效率的存储,ziplist 有很多 bit 级别的操作,使得代码变得较为晦涩难懂。不过不要紧,我们本节的目标之一是为了了解 ziplist 对比普通链表,做了哪些优化,可以更好的节省空间。 接下来我们来正式认识下压缩列表的结构。 1.1 压缩列表的结构一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。 图 1-1 展示了压缩列表的各个组成部分: 相关字段说明如下: zlbytes:4 字节,表示 ziplist 占用的字节总数(包括 zlbytes 本身占用的 4 个字节)。zltail:4 字节,表示 ziplist 表中最后一项距离列表的起始地址有多少字节,也就是表尾节点的偏移量。通过此字段,程序可以快速确定表尾节点的地址。zllen:2 字节:表示 ziplist 的节点个数。要注意的是,由于此字段只有 16bit,所以可表达的最大值为 2^16-1。一旦列表节点个数超过这个值,就要遍历整个压缩列表才能获取真实的节点数量。entry:表示 ziplist 的节点。长度不定,由保存的内容决定。要注意的是,列表的节点(entry)也有自己的数据结构,后续会详细说明。zlend:ziplist 的结束标记,值固定为 255,用于标记压缩列表的末端。图 1-2 展示了一个包含五个节点的压缩列表: ...

August 7, 2019 · 4 min · jiezi

支撑马蜂窝会员体系全面升级背后的架构设计

流量红利正逐渐走向终结,这已经不再是什么秘密。后互联网时代,如何维系住用户群,提升用户在平台上的体验是整个行业都需要考虑的事情。正是出于这一原因,现在全行业都在关注会员体系的搭建,这也是马蜂窝 2019 年重点投入的方向之一。  面对这个全行业都在发力的会员市场,要对「马蜂窝特色」的会员体系进行有力的支撑,无疑对会员体系的架构设计提出更高的要求。 马蜂窝会员体系建设从 2018 年 9 月份开始启动,经过前期对会员身份和会员权益的摸索,伴随业务的快速发展,到 2019 年上半年,为了让更多用户体验到马蜂窝高质量的会员服务,公司推出了更灵活、维度更多、权益更丰富的会员模式。在这样的背景下,初期较为粗旷的底层技术也需要及时做出调整,对核心架构和服务进行升级。 一、会员身份策略改造早期的会员身份模块由会员产品、用户属性和时间属性共同构成: 可以看到早期的会员产品比较单一,因此将产品信息设计成一级结构。这种设计的好处是逻辑简单,可以快速实现,但不易扩展,一旦新增会员类别以及不同卡种之间出现复杂关系时,不论是对项目或者对代码本身而言,维护成本都将成倍增长。 从 2019 年年初开始,马蜂窝会员体系进行了全面升级,主要体现在以下几个方面: 更完善的获客渠道,增加了在小程序端的服务展示;更丰富的会员类别,新增了非常多卡种,在最初的年度金卡和体验金卡基础上,增加了季度金卡、 7 日卡、「蜂享卡」,未来还计划推出月度金卡、学生卡等;更低的获取门槛,早期的会员身份只能通过在 App 中购买获得,为了让更多用户享受到品质更高的服务,增加了通过完成用户激励任务、供应商合作、产品搭售、线下实体卡等会员获取方式。这也意味着,同一时间段内用户的会员身份将变得愈发复杂,早期单一的会员身份策略和模型设计已经不能满足需求。重新设计会员身份的时候,我们明确了未来无论业务线如何划分会员身份,底层结构都要能够较好地支持,因此决定把会员模块身份抽离出来。会员体系升级后,产品信息调整为以 SKU 作为最小粒度重新划分,同时增加了用户信息中的来源以及获取渠道信息: 二、会员中心架构设计和优化在明确了新的会员身份策略后,我们对整个会员体系进行了梳理,将现阶段的会员中心架构设计如下: 合上面的架构图来看,目前马蜂窝会员中心系统主要划分为数据存储、核心服务、接口层、应用层四大部分: 数据储存:主要基于 MySQL 和 Redis,以及马蜂窝统一日志系统 MES核心服务:这是当前马蜂窝会员体系中最重要的一层。核心服务又可以分为三大块:(1)「四驾马车」:会员身份、权益、增值服务接入、会员积分,驱动着整个会员体系的运转; (2)交易营销:辅助四驾马车快速往前跑; (3)支撑模块:与会员体系对接的公司级别支撑模块,包括风控、监控、日志、消息总线、商家结算对账等 接口层:会员体系对外暴露的接口,包括了会员身份、权益领取、蜂蜜消费等接口应用层:主要是面向 C 端的应用,包括会员频道页、蜂蜜中心、用户权益中心、任务中心等下面重点围绕「核心服务」层展开介绍。 2.1「四驾马车」2.1.1 会员身份目前,市面上很多常见的会员产品都是采用普通的续费模式,比如一些视频平台的年度会员、季度会员。这种模式的特点是只进行时间的区分,在会员身份后生效后享受的权益完全相同,通过续费使权益时效得到相应延长。 但是马蜂窝由于业务的特殊性,会员体系需要设计得更为立体。如果只采用单纯的续费模式,会影响高忠诚度用户的使用体验。 首先,在同一类别的会员身份下,时长不同的产品对应的权益也不同。以金卡会员为例,季度金卡、年度金卡这种同类别下的会员身份,可以通过续费升级,但它们彼拥有的权益不完全相同,比如年度金卡 96 折抵额上限为 500 元,季度金卡只有 100 元。另外,同一用户在同一时间内,只要满足条件,就可同时拥有不同类别的卡种,比如金卡和蜂享卡。为了满足上述需求,我们决定引入用户身份的叠加以及续费模型。通过增加会员 SKU 叠加、续费关系表,使用户在一个时间段内不仅可以同时拥有多种身份,还可以续费已有卡种。 上图是会员身份的时间轴示意。横轴代表时间,纵轴代表不同的卡种。我们通过最终 SKU 时间轴便可以确认用户当前的会员身份。 我们将用户已有的每个 SKU 时间轴拉平,当用户在某个时间点发出购买新卡种的请求时,查看当前生效的时间轴中是否已有用户正在购买的 SPU,如果没有则叠加,如已有则需要再判断 SKU 之间的配置策略,决定是叠加还是续费;然后继续计算出正在购买的 SKU 生效时间轴;接下来根据配置好的规则,对比当前购买生效时间轴和已有 SKU 时间轴的身份关系,决定用户是否可以完成此次购买,如: 前置身份:指必须已经购买某个 SKU,才可以购买当前 SKU冲突身份:指如果已经购买某个 SKU,就不可以购买当前 SKU为了满足不同的业务需求,这里的叠加、续费关系都是可以通过运营来配置的。整个流程大致示意如下: ...

July 26, 2019 · 1 min · jiezi

redis专题14持久化配置

概述Redis的持久化有2种方式:快照rdb 和 日志aof由于rdb是在某个时间点来备份的,直接备份二进制映像文件,恢复速度快。但是由于是在时间点备份的,在备份之前进程突然被杀掉就导致上个备份点到这个备份点之间的数据都丢失了;所以要结合Aof日志来一起备份,再加上一个主从配置就比较完美了; 快照配置save 900 1 #900秒内,有1条写入,则产生快照 save 300 1000 #如果300秒内有1000次写入,则产生快照save 60 10000 #如果60秒内有10000次写入,则产生快照 # 从下往上,一个梯形结构;# 这3个选项都屏蔽,则rdb禁用;stop-writes-on-bgsave-error yes #后台备份进程出错时,主进程停不停止写入?不停止写入的话,有可能数据一致性出现偏差 rdbcompression yes #导出的rdb文件是否压缩Rdbchecksum yes #导入rbd恢复时数据时,要不要检验rdb的完整性dbfilename dump.rdb #导出来的rdb文件名dir ./ #rdb的放置路径Aof的配置appendonly no # 是否打开aof日志功能appendfsync everysec # 折中方案,每秒写1次appendfsync always # 每1个命令,都立即同步到aof. 安全,速度慢appendfsync no # 写入工作交给操作系统,由操作系统判断缓冲区大小,统一写入到aof. 同步频率低,速度快;no-appendfsync-on-rewrite yes # 正在导出rdb快照的过程中,要不要停止同步aofauto-aof-rewrite-percentage 100 #aof文件大小比起上次重写时的大小,增长率100%时,重写auto-aof-rewrite-min-size 64mb #aof文件,至少超过64M时,重写重写: redis内存里的key-value逆化;比如set num 1,incr num 1....到100;把这一百个递增的结果直接逆化成set num 100;而不是再搞一百个命令;问题解答问: 在dump rdb过程中,aof如果停止同步,会不会丢失?答: 不会,所有的操作缓存在内存的队列里, dump完成后,统一操作. 问: aof重写是指什么?答: aof重写是指把内存中的数据,逆化成命令,写入到.aof日志里.以解决aof日志过大的问题. 问: 如果rdb文件,和aof文件都存在,优先用谁来恢复数据?答: aof ...

July 16, 2019 · 1 min · jiezi

php和redis设计秒杀活动

1 说明前段时间面试的时候,一直被问到如何设计一个秒杀活动,但是无奈没有此方面的实际经验,所以只好凭着自己的理解和一些资料去设计这么一个程序主要利用到了redis的string和set,string主要是利用它的k-v结构去对库存进行处理,也可以用list的数据结构来处理商品的库存,set则用来确保用户进行重复的提交其中我们最主要解决的问题是-防止并发产生超抢/超卖 2 流程设计 3 代码3.1 服务端代码class MiaoSha{ const MSG_REPEAT_USER = '请勿重复参与'; const MSG_EMPTY_STOCK = '库存不足'; const MSG_KEY_NOT_EXIST = 'key不存在'; const IP_POOL = 'ip_pool'; const USER_POOL = 'user_pool'; /** @var Redis */ public $redis; public $key; public function __construct($key = '') { $this->checkKey($key); $this->redis = new Redis(); //todo 连接池 $this->redis->connect('127.0.0.1'); } public function checkKey($key = '') { if(!$key) { throw new Exception(self::MSG_KEY_NOT_EXIST); } else { $this->key = $key; } } public function setStock($value = 0) { if($this->redis->exists($this->key) == 0) { $this->redis->set($this->key,$value); } } public function checkIp($ip = 0) { $sKey = $this->key . self::IP_POOL; if(!$ip || $this->redis->sIsMember($sKey,$ip)) { throw new Exception(self::MSG_REPEAT_USER); } } public function checkUser($user = 0) { $sKey = $this->key . self::USER_POOL; if(!$user || $this->redis->sIsMember($sKey,$user)) { throw new Exception(self::MSG_REPEAT_USER); } } public function checkStock($user = 0, $ip = 0) { $num = $this->redis->decr($this->key); if($num < 0 ) { throw new Exception(self::MSG_EMPTY_STOCK); } else { $this->redis->sAdd($this->key . self::USER_POOL, $user); $this->redis->sAdd($this->key . self::IP_POOL, $ip); //todo add to mysql echo 'success' . PHP_EOL; error_log('success' . $user . PHP_EOL,3,'/var/www/html/demo/log/debug.log'); } } /** * @note:此种做法不能防止并发 * @func checkStockFail * @param int $user * @param int $ip * @throws Exception */ public function checkStockFail($user = 0,$ip = 0) { $num = $this->redis->get($this->key); if($num > 0 ){ $this->redis->sAdd($this->key . self::USER_POOL, $user); $this->redis->sAdd($this->key . self::IP_POOL, $ip); //todo add to mysql echo 'success' . PHP_EOL; error_log('success' . $user . PHP_EOL,3,'/var/www/html/demo/log/debug.log'); $num--; $this->redis->set($this->key,$num); } else { throw new Exception(self::MSG_EMPTY_STOCK); } }}3.2 客户端测试代码function test(){ try{ $key = 'cup_'; $handler = new MiaoSha($key); $handler->setStock(10); $user = rand(1,10000); $ip = $user; $handler->checkIp($ip); $handler->checkUser($user); $handler->checkStock($user,$ip); } catch (\Exception $e) { echo $e->getMessage() . PHP_EOL; error_log('fail' . $e->getMessage() .PHP_EOL,3,'/var/www/html/demo/log/debug.log'); }}function test2(){ try{ $key = 'cup_'; $handler = new MiaoSha($key); $handler->setStock(10); $user = rand(1,10000); $ip = $user; $handler->checkIp($ip); $handler->checkUser($user); $handler->checkStockFail($user,$ip); //不能防止并发的 } catch (\Exception $e) { echo $e->getMessage() . PHP_EOL; error_log('fail' . $e->getMessage() .PHP_EOL,3,'/var/www/html/demo/log/debug.log'); }}4 测试测试环境说明 ...

July 16, 2019 · 2 min · jiezi

手撕面试官系列四-MongoDBRedis-面试专题

MongoDB(面试题+答案领取方式见个人主页) 你说的 NoSQL 数据库是什么意思?NoSQL 与 RDBMS 直接有什么区别?为什么要使用和不使用NoSQL 数据库?说一说 NoSQL 数据库的几个优点?NoSQL 数据库有哪些类型?MySQL 与 MongoDB 之间最基本的差别是什么?你怎么比较 MongoDB、CouchDB 及 CouchBase?MongoDB 成为最好 NoSQL 数据库的原因是什么?32 位系统上有什么细微差别?journal 回放在条目(entry)不完整时(比如恰巧有一个中途故障了)会遇到问题吗?分析器在 MongoDB 中的作用是什么?名字空间(namespace)是什么?如果用户移除对象的属性,该属性是否从存储层中删除?能否使用日志特征进行安全备份?允许空值 null 吗?更新操作立刻 fsync 到磁盘?如何执行事务/加锁?为什么我的数据文件如此庞大?启用备份故障恢复需要多久?什么是 master 或 primary?什么是 secondary 或 slave?我必须调用 getLastError 来确保写操作生效了么?我应该启动一个集群分片(sharded)还是一个非集群分片的 MongoDB 环境?分片(sharding)和复制(replication)是怎样工作的?数据在什么时候才会扩展到多个分片(shard)里?当我试图更新一个正在被迁移的块(chunk)上的文档时会发生什么?如果在一个分片(shard)停止或者很慢的时候,我发起一个查询会怎样?我可以把 moveChunk 目录里的旧文件删除吗?我怎么查看 Mongo 正在使用的链接?如果块移动操作(moveChunk)失败了,我需要手动清除部分转移的文档吗?如果我在使用复制技术(replication),可以一部分使用日志(journaling)而其他部分则不使用吗?当更新一个正在被迁移的块(Chunk)上的文档时会发生什么?MongoDB 在 A:{B,C}上建立索引,查询 A:{B,C}和 A:{C,B}都会使用索引吗?如果一个分片(Shard)停止或很慢的时候,发起一个查询会怎样?MongoDB 支持存储过程吗?如果支持的话,怎么用?如何理解 MongoDB 中的 GridFS 机制,MongoDB 为何使用 GridFS 来存储文件?Redis (一) redis 和 和 memcached 什么区别?为什么高并发下有时单线程的 redis 比多线程的memcached 效率要高?redis 主从复制如何实现的?redis 的集群模式如何实现?redis 的 的 key 是如何寻址的?使用 redis 如何设计分布式锁?说一下实现思路?使用 zk 可以吗?如何实现?这两种有什么区别?知道 redis 的持久化吗?底层如何实现的?有什么优点缺点?redis 过期策略都有哪些?LRU 算法知道吗?写一下 java 代码实现?缓存穿透、缓存击穿、缓存雪崩解决方案?在选择缓存时,什么时候选择 redis ,什么时候选择缓存与数据库不一致怎么办主从数据库不一致如何解决Redis 常见的性能问题和解决方案Redis 的数据淘汰策略有哪些Redis 当中有哪些数据结构假如 Redis 里面有 1 亿个 key ,其中有 10w 个 个 key 是以某个固定的已知的前缀开头的,如果将它们全部找出来?使用 Redis 做过异步队列吗,是如何实现的Redis 如何实现延时队列Redis (二) ...

July 16, 2019 · 1 min · jiezi

你可能不知道的Redis用法

0. 引言基于Redis丰富的数据结构,除了充当缓存层来提升查询效率以外,还能应用在很多常见的场景,比如:分布式锁,消息队列,限流等。看到这些场景你可能会有疑问,Redis在这些领域好像并不出名啊,比如消息队列,出名的有Rocketmq、rabbitmq等等,很少听Redis来做这个场景,是不是存在什么问题?是的,下面的文字就来总结下Redis在这些场景的常规用法以及存在的问题。 1. 分布式锁1.1 基本使用分布式应用通常会遇到并发问题,逻辑上我们可以使用setnx指令占一个“坑”,然后处理自己的业务逻辑,最后再调用del指令释放“坑”。 > setnx lock.test trueOK... do something ...> del lock.test(integer) 1以上是常规使用,会有个问题,如果逻辑执行过程出现异常,导致没有执行到del指令,最后会陷入死锁。 1.2 过期时间因此,更进一步的做法是拿到锁以后,再给锁设置一个过期时间,这样当过程出现异常,没有执行del指令,锁也会在5s后自动释放。 > setnx lock.test trueOK> expire lock.test 5... do something ...> del lock.test(integer) 11.3 原子性问题实现以上逻辑后,仍存在问题,比如在执行setnx指令之后,但在expire指令时服务器出现异常,没有给锁设置上过期时间,锁依然会陷入死锁的状态,一直不会释放。 如何解决呢?可能你会想到事务,但在这里不行,因为expire是依赖于setnx的执行结果的,如果没有抢到锁,expire不应该被执行。事务里没有if-else的逻辑,要么全部执行,要么一个都不执行。 在Redis 2.8 版本中,作者加入了set指令的扩展参数,使得setnx和expire指令可以一起执行。 > set lock.test true ex 5 nxOK... do something ...> del lock.test上面的指令就是setnx和expire组合在一起的原子指令。 1.4 超时问题Redis分布式锁并不解决锁超时的问题,所以不建议在获取分布式锁后处理耗时较长的逻辑。因为逻辑执行得太长,锁到期自动释放,就会出现问题。 有一个稍微安全点的方案:在抢锁时,set指令的value参数设置为一个随机数,释放锁时先匹配value是否一致,再进行删除key。这种方式可以确保当前连接的操作,不会被其他连接释放,除非是过期自动释放。 以上的匹配value和删除key不是原子性的,所以需要使用lua脚本,来保证连续多个指令的原子性执行。但是这也不是一个完美的方案,只是相对安全一点。它始终没能解决锁超时,其他线程“乘虚而入”的问题。 2. 消息队列2.1 基本使用基于Redis的list数据结构,利用lpush和rpop的指令组合,可以模拟队列。 使用的代码就不贴了,逻辑比较简单。下面讨论两个个问题: 队列空了怎么办?Redis主动断开空闲连接怎么处理?队列空了怎么办?在rpop返回空时,sleep(1000)。可以这么做,但是这导致消费的延迟,Redis提供了更好的方案:阻塞读(blpop/brpop),用这个指令替代逻辑里的rpop即可。 Redis主动断开空闲连接怎么处理?使用了阻塞读以后,线程会一直阻塞在那里,如果一直没有数据,这个连接就会成了闲置连接,如果时间过久,Redis会主动断开连接,从而减少闲置资源占用。此时blpop/brpop会抛出异常,所以客户端需要捕捉该异常,并重试。 2.2 延迟队列最近有个业务需求:当某个行为触发了,则在10s后执行一段逻辑。 看到「10s后执行」这种典型的场景,个人的第一反应便是延迟队列。在Redis中,可以通过(zset)有序集来实现。将消息序列化为value,将执行时间作为score,然后轮询zset获取到期的任务进行处理。 多进程同时消费的场景中,Redis的zrem方法是关键,通过zrem来决定唯一的属主,它的返回值决定了是否有抢到任务。 进一步优化使用lua脚本,将zrangebyscore和zrem操作一同发送到服务端执行,可以减少争抢任务时的浪费。 2.3 消息多播上面讨论的是Redis作为消息队列的基本使用,实际情况Redis仍有很多不足,其中一个就是它不支持多播机制。 消息多播是指生产者生产一次消息,由中间件将消息复制到多个消息队列,每个队列都有相应的消费者进行消费。 2.3.1 PubSub为了支持多播,Redis引入了新的模块去支持:PubSub,即发布者/订阅者模式。如何使用这里就不说了,文档很详细。下面总结下缺点: ...

July 16, 2019 · 1 min · jiezi

redis专题13redis运维相关命令

常用运维命令显示服务器时间 time redis 127.0.0.1:6380> time 1) "1375270361" # 时间戳(秒)2) "504511" # 微秒数查看当前数据库的key的数量 dbsize redis 127.0.0.1:6380> dbsize (integer) 2redis 127.0.0.1:6380> select 2OK后台进程重写aof bgrewriteaof 127.0.0.1:6379> bgrewriteaofBackground append only file rewriting started保存rdb快照 bgsave(后台保存) save 127.0.0.1:6379> bgsave #内存不阻塞,当前进程dumpBackground saving started上次保存的时间 lastsave 清空数据 flushdb #清空当前db flushall #清空全部db服务器关闭 Showdown [save/nosave] 查看redis服务器的信息,性能调优 Info [Replication/CPU/Memory..] 配置项管理 动态获取或设置config,config get/set 类似php中的ini_set/getConfig get 配置项 Config set 配置项 值 (特殊的选项,不允许用此命令设置,如slave-of, 需要用单独的slaveof命令来设置) 127.0.0.1:6379> config get dbfilename1) "dbfilename"2) "dump6379.rdb"127.0.0.1:6379> config get slowlog-log-slower-than1) "slowlog-log-slower-than"2) "10000" #响应速度大于10000微妙的就会给记录下来;127.0.0.1:6379> config get slowlog-max-len1) "slowlog-max-len"2) "128" #最多能记录128条慢查询记录;slowlog get N 获取慢N条慢日志 ...

July 15, 2019 · 1 min · jiezi

跟着大彬读源码-Redis-3-服务器如何响应客户端请求下

继续我们上一节的讨论。服务器启动了,客户端也发送命令了。接下来,就要到服务器“表演”的时刻了。 1 服务器处理服务器读取到命令请求后,会进行一系列的处理。 1.1 读取命令请求当客户端与服务器之间的套接字因客户端的写入变得可读时,服务器将调用命令请求处理器执行以下操作: 读取套接字中的命令请求,并将其保存到客户端状态的输入缓冲区。对输入缓冲区的命令请求进行分析,提取出命令请求中包含的命令参数及参数个数,然后分别将参数和参数个数保存到客户端状态的 argv 属性和 argc 属性里。调用命令执行器,执行客户端指定的命令。上面的 SET 命令保存到客户端状态的输入缓存区之后,客户端状态如图 4。 之后,分析程序将对输入缓冲区中的协议进行分析,并将得出的结果保存的客户端的 argv 和 argc 属性中,如图 5 所示: 之后,服务器将通过调用命令执行器来完成执行命令的余下步骤。 1.2 查找命令实现命令执行器要做的第一件事就是根据 argv[0] 参数,在命令表(commandtable)中查找参数所指定的命令,并将找到的命令保存到 cmd 属性中。 命令表是一个字典,字典的键是一个个命令名称,比如 "SET"、"GET" 等。而字典的值则是一个个 redisCommand 结构,每个 redisCommand 结构记录了 Redis 命令的实现信息。源码如下: # server.h/redisCommandstruct redisCommand { char *name; // 命令名称。如 "SET" redisCommandProc *proc; // 对应函数指针,指向命令的实现函数。比如 SET 对应的 setCommand 函数 int arity; // 命令参数的格个数。用来检查命令请求的格式是否合法。 // 要注意的命令的名称也是一个参数。像我们上面的 SET KEY VALUE 命令,实际上有三个参数。 char *sflags; // 字符串形式的标识值。记录了命令的属性。 int flags; // 对 sflags 标识分析得出的二进制标识,由程序自动生成。检查命令时,实际上使用的是此字段 redisGetKeysProc *getkeys_proc; // 指针函数,通过此方法来指定 key 的位置。 int firstkey; // 第一个 key 的位置 int lastkey; // 最后一个 key 的位置 int keystep; // key 之间的间距 long long microseconds, calls; // 命令的总调用时间及调用次数};另外,对于 sflags 属性,可使用的标识值及含义如下表: ...

July 15, 2019 · 2 min · jiezi

SpringBoot-实战-二十-整合-Redis

微信公众号:一个优秀的废人。如有问题,请后台留言,反正我也不会听。 前言两个月没更新原创了,实在惭愧。没有借口,就是因为自己懒了。最近看了「刻意学习」,这本书谈的是学习与行动的关系,书中提到了「持续行动」 这个概念,意思就是:我们要去实实在在地去做一些事情,而且是每天都做,才能称之为「持续行动」。看完这本书以后,我意识到我必须要做些什么,那就是写作。 Redis 简介Redis 是一个开源的,基于内存的键值数据存储,用作数据库,缓存和消息代理。在实现方面,Key-Value 存储代表 NoSQL 空间中最大和最老的成员之一。Redis 支持数据结构,如字符串,散列,列表,集和带范围查询的有序集。在 spring data redis 的框架,可以很容易地编写,通过提供一个抽象的数据存储使用 Redis 的键值存储的 Spring 应用程序。非关系型数据库,基于内存,存取数据的速度不是关系型数据库所能比拟的redis 是键值对 (key-value) 的数据库数据类型字符串类型 string散列类型 hash列表类型 list集合类型 set有序集合类型 zset其中,因为SpringBoot 约定大于配置的特点,只要我们加入了 spring-data-redis 依赖包并配置 Redis 数据库,SpringBoot 就会帮我们自动配置一个 RedisTemplate ,利用它我们就可以按照以下方式操作对应的数据类型,在下面实战中我将会对这五种数据进行操作。 redisTemplate.opsForValue(); //操作字符串redisTemplate.opsForHash(); //操作hashredisTemplate.opsForList(); //操作listredisTemplate.opsForSet(); //操作setredisTemplate.opsForZSet(); //操作有序set开发环境SpringBoot 2.1.6 RELEASEspring-data-redis 2.1.9 RELEASERedis 3.2IDEAJDK8mysql关于如何安装 Redis 这里不再赘述,请自行搜索引擎搜索解决。 pom 依赖<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.58</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency></dependencies>配置文件spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC&useSSL=true username: root password: 123456 jpa: hibernate: ddl-auto: update #ddl-auto:设为 create 表示每次都重新建表 show-sql: true redis: host: localhost port: 6379 # Redis数据库索引(默认为0) database: 1 jedis: pool: #连接池最大连接数 max-active: 8 #最小空闲连接 min-idle: 0 #最大阻塞等待时间,负值表示没有限制 max-wait: -1ms #最大空闲连接 max-idle: 8 #连接超时时间(毫秒) timeout: 20ms # 无密码可不写 # password:为什么乱码? /** * 添加字符串 */ @Test public void setString(){ redisTemplate.opsForValue().set(USERKEY,"nasus"); redisTemplate.opsForValue().set(AGEKEY,24); redisTemplate.opsForValue().set(CITYKEY,"清远"); }首先是添加字符串类型的数据。它的运行结果如下: ...

July 14, 2019 · 3 min · jiezi

redis专题12正确优雅的在ThinkPHP5中使用redis

TP5的redis驱动在项目中使用遇到的问题缓存的Key前缀取的是config中配置的,没有单独管理。不能使用redis一些本身高级命令,比如sadd等。一些常用的操作可以再次封装,比如分布式锁等。key的管理类key要统一管理起来,便于后续的阅读以及扩展 <?phpnamespace libs;/** * 缓存key映射类:缓存KEY要统一配置,便于后期批量更改和管理 * 注意其命名规则: 项目名:模块名:名称:类型 tkmall:mem:uid:hash * Class CacheKeyMap * @package libs */class CacheKeyMap{ public static $prefix = 'tkmall:'; /** * 基于会员uid的hash,管理会员资料 * @param $uid * @param int $prefix * @return string */ public static function memberUidHash($uid,$prefix=0) { if($prefix){ // 用于keys,scan等命令 return self::$prefix . 'mem:' . $uid .':*'; } return self::$prefix . 'mem:' . $uid .':hash'; }}libsRedis<?phpnamespace libs;use think\cache\driver\Redis as tpRedis;/** * 定制化的redis * Class Redis * @package libs */class Redis extends tpRedis{ protected static $_instance = null; /** * 获取单例redis对象,一般用此方法实例化 * @return Redis|null */ public static function getInstance() { if(!is_null(self::$_instance)){ return self::$_instance; } self::$_instance = new self(); return self::$_instance; } /** * 架构函数 * Redis constructor. */ public function __construct() { $options = config('cache.redis'); $options['prefix'] = CacheKeyMap::$prefix; parent::__construct($options); } /** * 覆写,实际的缓存标识以CacheKeyMap来管理 * @access protected * @param string $name 缓存名 * @return string */ protected function getCacheKey($name) { return $name; } /** * redis排重锁 * @param $key * @param $expires * @param int $value * @return mixed */ public function redisLock($key, $expires, $value = 1) { //在key不存在时,添加key并$expires秒过期 return $this->handler()->set($key, $value, ['nx', 'ex' => $expires]); } /** * 调用缓存类型自己的高级方法 * @param $method * @param $args * @return mixed|void * @throws \Exception */ public function __call($method,$args){ if(method_exists($this->handler, $method)){ return call_user_func_array(array($this->handler,$method), $args); }else{ exception(__CLASS__.':'.$method.'不存在'); return; } }}服务提供者配置app/provider.php ...

July 14, 2019 · 2 min · jiezi

redis专题11KEY设计原则与技巧

对比着关系型数据库,我们对redis key的设计一般有以下两种格式: 表名:主键名:主键值:列名表名:主键值:列名 在所有主键名都是id的情况下(其实我个人不喜欢这种情况,比如user表,它的主键名就应该是user_id,而不是id,这样在表与表之间关联的时候一目了然)用冒号作为分割是设计key的一种不成文的原则,遵循这种格式设计出的key在某些redis客户端下可以有效的识别; 但是,在关系型数据中,除主键外,还有可能根据其他列来查询。如上表中, username 也是极频繁查询的,往往这种列也是加了索引的。 转换到k-v数据中,则也要相应的生成一条按照该列为主的key-value。 Set user:username:lisi:uid 9 #但是要保证username是唯一的; 这样,我们可以根据username:lisi:uid ,查出userid=9, 再查user:9:password/email ...mysql与redis的数据转换实例mysql数据准备 CREATE TABLE `book` ( `book_id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL DEFAULT '' COMMENT '书名', `add_time` int(10) NOT NULL DEFAULT '0' COMMENT '添加时间', PRIMARY KEY (`book_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='书本表';INSERT INTO book VALUES (5, 'PHP圣经', UNIX_TIMESTAMP() ), (6, 'ruby实战', UNIX_TIMESTAMP() ), (7, 'mysql运维', UNIX_TIMESTAMP() ), (8, 'ruby服务端编程', UNIX_TIMESTAMP() ); CREATE TABLE `tag` ( `tag_id` int(11) NOT NULL AUTO_INCREMENT, `tag_name` char(40) NOT NULL DEFAULT '' COMMENT '标签名', PRIMARY KEY (`tag_id`)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='标签表';INSERT INTO tag VALUES (1, 'PHP'), (2, 'ruby'), (3, 'mysql'), (4, 'database');CREATE TABLE `tag_book` ( `tag_id` int(11) NOT NULL DEFAULT '0' COMMENT '标签ID', `book_id` int(11) NOT NULL DEFAULT '0' COMMENT '书ID', KEY `tag_id` (`tag_id`), KEY `book_id` (`book_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='标签与书关系表';INSERT INTO `tag_book` (`tag_id`, `book_id`) VALUES ('4', '7'),('1', '5'),('2', '6'),('2', '8');我们有以下查询需求: ...

July 12, 2019 · 2 min · jiezi

关于Redis热点key的一些思考

关于Redis热点key的一些思考昨天在和一个已经跳槽的同事聊天时,询问他这段时间面试时碰到的一些问题。自己也想积累一下这方面的知识。其中他说了在面试某赞公司时面试官问他关于热点Key的的解决方案。于是针对这次谈话以及上网查的一些资料后的思考进行一下总结。方便后续自己查阅。 什么是热点Key其实对于热点Key,网上一查一大堆,这里我就引用网上的一段话。 从基于用户消费的数据远远大于生产的数据的角度来讲,我们平常使用的知乎等软件时,大多数人平常仅仅只是浏览,并不会去提问问题、发表的文章,偶尔会发表自己的文章或者看法,这就是一个典型的读多写少的情景,当然此类情景不太容易导致热点的产生。 在日常工作生活中一些突发的的事件,诸如:“双11”期间某些热门商品的降价促销,当这其中的某一件商品被数万次点击、购买时,会形成一个较大的需求量,这种情况下就会产生一个单一的Key,这样就会引起一个热点;同理,当被大量刊发、浏览的热点新闻,热点评论等也会产生热点;另外,在服务端读数据进行访问时,往往会对数据进行分片切分,此类过程中会在某一主机Server上对相应的Key进行访问,当访问超过主机Server极限时,就会导致热点Key问题的产生。 如何解决?针对于热点Key的解决方案网上的查找出来无非就是两种 服务端缓存:即将热点数据缓存至服务端的内存中备份热点Key:即将热点Key+随机数,随机分配至Redis其他节点中。这样访问热点key的时候就不会全部命中到一台机器上了。其实这两个解决方案前提都是知道了热点Key是什么的情况,那么如何找到热点key呢? 热点检测凭借经验,进行预估:例如提前知道了某个活动的开启,那么就将此Key作为热点Key客户端收集:在操作Redis之前对数据进行统计抓包进行评估:Redis使用TCP协议与客户端进行通信,通信协议采用的是RESP,所以能进行拦截包进行解析在proxy层,对每一个 redis 请求进行收集上报Redis自带命令查询:Redis4.0.4版本提供了redis-cli –hotkeys就能找出热点Key如果要用Redis自带命令查询时,要注意需要先把内存逐出策略设置为allkeys-lfu或者volatile-lfu,否则会返回错误。进入Redis中使用config set maxmemory-policy allkeys-lfu即可。服务端缓存假设我们已经统计出了一些热点Key,将这些数据缓存到了服务端,那么还有一个问题。就是如何保证Redis和服务端热点Key的数据一致性。我这里想到的解决方案是利用Redis自带的消息通知机制,对于热点Key客户端建立一个监听,当热点Key有更新操作的时候,客户端也随之更新。 主要代码如下,监听类负责接收到Redis的事件,然后筛选出热点Key进行相应的动作 public class KeyExpiredEventMessageListener implements MessageListener { @Autowired private RedisTemplate redisTemplate; @Override public void onMessage(Message message, byte[] pattern) { String key = new String(message.getChannel()); key = key.substring(key.indexOf(":")+1); String action = new String(message.getBody()); if (HotKey.containKey(key)){ String value = redisTemplate.opsForValue().get(key)+""; switch (action){ case "set": log.info("热点Key:{} 修改",key); HotKeyAction.UPDATE.action(key,value); break; case "expired": log.info("热点Key:{} 到期删除",key); HotKeyAction.REMOVE.action(key,null); break; case "del": log.info("热点Key:{} 删除",key); HotKeyAction.REMOVE.action(key,null); break; } } }}建立一个存储热点Key的数据结构ConcurrentHashMap,并设置相应的操作方法,这里设置了假数据,在static代码块中直接设置了两个热点Key ...

July 12, 2019 · 1 min · jiezi

Redis在执行BGSAVE和BGREWRITEAOF命令时哈希表的负载因子5而未执行这两个命令时1

前言 今天在看《Redis设计与实现》,讲解字典的实现时,说道Redis在执行BGSAVE和BGREWRITEAOF命令时,哈希表的负载因子大于等于5,而未执行这两个命令时大于等于1。 而解释仅仅是提了一句原因是: 在执行BGSAVE和BGREWRITEAOF命令时,Redis需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制技术来由于子进程的使用效率,所以在子进程存在期间,服务器会提高执行扩展操作所需的负载因子,从而尽可能避免在子进程存在期间进行哈希表扩展操作,这可以避免不必要的内存写入操作。哈希表的扩展因子:哈希表已保存节点数量/哈希表大小。扩展因子决定了是否扩展哈希表。因为基础薄弱,所以对这个结论产生了很多疑问: 什么是写时复制?为什么使用写时复制技术创建子进程时进行哈希表扩展会造成不必要的内存写入操作?写时复制 要了解什么是写时复制,我们还需要知道操作系统是怎么创建子进程的。我们一linux操作系统进行讲解。 创建子进程的唯一方式是调用fork函数,这个fork函数为创建一个和父进程几乎完全相同的进程。创建子进程时,需要将父进程的除了正文段之外几乎所有的数据拷贝至子进程,如堆,栈,数据段。 创建子进程拷贝几乎所有父进程的数据会导致创建子进程的过程很慢,从而有了写时复制这种技术来提高创建子进程的过程。从下面参考的博客中的知识我认识到: 在不使用写时复制技术的情况下,我们为进程创建一个子进程时会导致子进程拷贝父进程的数据段,堆,栈,仅有正文段不会被拷贝。 而在使用写时复制技术的情况下,我们为进程创建一个子进程时不会拷贝任何数据,此时父进程和自己成共享同一份数据,仅当父进程或者自己成需要对这份数据进行写入时,才为子进程分配相应的物理空间。为什么使用写时复制技术创建子进程时进行哈希表扩展会造成不必要的内存写入操作? 了解了写时复制之后我们就能回答这个问题了。首先服务器进程在执行BGSAVE或者BGREWRITEAOF命令时,创建了新的子进程;此时如果我们扩展哈希表,那么那么相当于忘父进程写入数据,同时会导致子进程进行复制操作。参考:Linux写时拷贝技术(copy-on-write)

July 12, 2019 · 1 min · jiezi

SpringBoot20-基础案例08集成Redis数据库实现缓存管理

本文源码GitHub:知了一笑https://github.com/cicadasmile/spring-boot-base一、Redis简介Spring Boot中除了对常用的关系型数据库提供了优秀的自动化支持之外,对于很多NoSQL数据库一样提供了自动化配置的支持,包括:Redis, MongoDB, Elasticsearch。这些案例整理好后,陆续都会上传Git。SpringBoot2 版本,支持的组件越来越丰富,对Redis的支持不仅仅是扩展了API,更是替换掉底层Jedis的依赖,换成Lettuce。本案例需要本地安装一台Redis数据库。 二、Spring2.0集成Redis1、核心依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency>2、配置文件# 端口server: port: 8008spring: application: # 应用名称 name: node08-boot-redis # redis 配置 redis: host: 127.0.0.1 #超时连接 timeout: 1000ms jedis: pool: #最大连接数据库连接数,设 0 为没有限制 max-active: 8 #最大等待连接中的数量,设 0 为没有限制 max-idle: 8 #最大建立连接等待时间。如果超过此时间将接到异常。设为-1表示无限制。 max-wait: -1ms #最小等待连接中的数量,设 0 为没有限制 min-idle: 0这样Redis的环境就配置成功了,已经可以直接使用封装好的API了。 3、简单测试案例import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;import java.util.concurrent.TimeUnit;@RestControllerpublic class RedisController { @Resource private StringRedisTemplate stringRedisTemplate ; @RequestMapping("/setGet") public String setGet (){ stringRedisTemplate.opsForValue().set("cicada","smile"); return stringRedisTemplate.opsForValue().get("cicada") ; } @Resource private RedisTemplate redisTemplate ; /** * 设置 Key 的有效期 10 秒 */ @RequestMapping("/setKeyTime") public String setKeyTime (){ redisTemplate.opsForValue().set("timeKey","timeValue",10, TimeUnit.SECONDS); return "success" ; } @RequestMapping("/getTimeKey") public String getTimeKey (){ // 这里 Key 过期后,返回的是字符串 'null' return String.valueOf(redisTemplate.opsForValue().get("timeKey")) ; }}4、自定义序列化配置import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;import org.springframework.data.redis.serializer.StringRedisSerializer;import java.io.Serializable;/** * Redis 配置 */@Configurationpublic class RedisConfig { private static final Logger LOGGER = LoggerFactory.getLogger(RedisConfig.class) ; /** * 序列化配置 */ @Bean public RedisTemplate<String, Serializable> redisTemplate (LettuceConnectionFactory redisConnectionFactory) { LOGGER.info("RedisConfig == >> redisTemplate "); RedisTemplate<String, Serializable> template = new RedisTemplate<>(); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); template.setConnectionFactory(redisConnectionFactory); return template; }}5、序列化测试import com.boot.redis.entity.User;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;import java.util.ArrayList;import java.util.List;@RestControllerpublic class SerializeController { @Resource private RedisTemplate redisTemplate ; @RequestMapping("/setUser") public String setUser (){ User user = new User() ; user.setName("cicada"); user.setAge(22); List<String> list = new ArrayList<>() ; list.add("小学"); list.add("初中"); list.add("高中"); list.add("大学"); user.setEducation(list); redisTemplate.opsForValue().set("userInfo",user); return "success" ; } @RequestMapping("/getUser") public User getUser (){ return (User)redisTemplate.opsForValue().get("userInfo") ; }}三、源代码地址GitHub地址:知了一笑https://github.com/cicadasmile/spring-boot-base码云地址:知了一笑https://gitee.com/cicadasmile/spring-boot-base ...

July 12, 2019 · 2 min · jiezi

redis专题10命令语法介绍之GEO

简述移动互联网时代LBS应用越来越多,交友软件中附近的小姐姐、外卖软件中附近的美食店铺、打车软件附近的车辆等等,那这种附近各种形形色色的XX是如何实现的呢 我么你都知道地球上的地理位置是使用二维的经纬度表示,经度范围 (-180, 180),纬度范围 (-90, 90),只要我们确定一个点的经纬度就可以明确在地球上的位置。 redis3.2版本新增的一个功能就是对GEO(地理位置)的支持。 地理位置大概提供了6个命令,分别为: GEOADDGEODISTGEOHASHGEOPOSGEORADIUSGEORADIUSBYMEMBER使用添加地理位置将给定的空间元素(纬度、经度、名字)添加到指定的键里面 GEOADD key longitude latitude member [longitude latitude member …] # 如添加杭州北京上海的地理位置 127.0.0.1:6379> geoadd city 120.20000 30.26667 hangzhou 116.41667 39.91667 beijing 121.47 31.23 shanghai获取地理位置信息从键里面返回所有给定位置元素的位置(经度和纬度),可以获取集合中任意元素的经纬度坐标,可以一次获取多个 GEOPOS key member [member …] 127.0.0.1:6379> geopos city hangzhou beijing shanghai 1) 1) "120.15000075101852417" 2) "30.2800007575645509" 2) 1) "116.39999896287918091" 2) "39.90000009167092543" 3) 1) "121.47000163793563843" 2) "31.22999903975783553" 127.0.0.1:6379> geopos city hangzhou 1) 1) "120.15000075101852417" 2) "30.2800007575645509"计算距离距离单位可以是 m、km、ml、ft,分别代表米、千米、英里和尺。如果用户没有显式地指定单位参数, 那么GEODIST默认使用米作为单位。 ...

July 11, 2019 · 1 min · jiezi

Redis-Cluster配置传播及故障恢复笔记

本笔记是对Redis Cluster Spec - Configuration handling, propagation, and failovers的归纳总结。 Epoch可以把Epoch当作是一个版本号,是一个64位无符号整形每个Node自己有一份Cluster.currentEpoch、MySelf.configEpoch、其他Node.configEpoch,详见文档。每个Master有自己的ConfigEpoch且在整个Cluster中唯一Slave的ConfigEpoch随其MasterCluster.currentEpoch,该值等于所有Node中最大的ConfigEpoch的值Master的ConfigEpoch初始值是0,也就是说Cluster.CurrentEpoch的初始值也是0Node之间Gossip传输消息时,Receiver发现Sender的ConfigEpoch比自己大,那么就更新自己的Cluster.CurrentEpoch为该值,随时间收敛,所有Node的Cluster.CurrentEpoch都变成一样。Slave PromotionSlave的动作下面是总结的在发生Slave Promotion时,Slave做的事情。 Master的动作下面是总结的在发生Slave Promotion时,Master做的事情。 传播Slots的配置Slave赢得选举之后会在己侧更新Slots上的归属信息,然后在定时的PING/PONG中将这个信息传播出去。 PING/PONG总是会携带上Slots所属Master的信息(包括ConfigEpoch) PING的Reciever如果发现Sender的某个Slot上的Master.ConfigEpoch比自己这里记录的小,那么就会返回UPDATE告诉Sender更新Slots归属信息。 下面是两个规则: 如果一个Slot不属于任何Master,然后有一个Master宣称拥有它,那么就修改己侧的Slots信息把这个Slot关联到这个Master上。如果一个Slot已经归属一个Master,然后又有一个Master宣称拥有它,那么就看谁的ConfigEpoch大,大的那个赢Node复活后遇到的问题Node A有两个Slot,然后它死了,它被顶替了,等它复活时发现两个Slot一个被Node B接管,另一个被Node C接管了,那么它: 因为自己的ConfigEpoch已经很旧了,所以它复活后不负责任何Slot然后它会成为最后一个Slot的Master的SlaveSlave迁移算法Slave迁移时一个自动过程。 举个例子,现在有Master A、B,它们对应的Slave有A1、B1、B2。现在A死了,A1顶替上去,不过这个时候A1就是一个光棍Master(它没有Slave),B有富余的Slave(B1和B2),把其中一个匀给A1当Slave。 这个过程不需要共识,因为只是修改Slave的归属,也不会修改ConfigEpoch。 Slave迁移有两个规则: 当有多个Slave富余时,选择NodeID字典顺最小的那个来迁移只有当Master的Slave数量>=cluster-migration-barrier时,才会挑选它的Slave做Migration两个跳过共识修改ConfigEpoch的操作下面两个操作比较危险,最好确定一个成功后再执行另一个: CLUSTER_FAILOVER TAKEOVER(手动Failover)直接将一个Slave提升为Master,不需要大多数Master同意。Slot Migration同样不需要大多数Master同意。所以就有可能出现同一个Slot有两个相同ConfigEpoch的Master宣称由自己负责,这种冲突的解决算法是: 如果Master A发现Master B也宣称了对Slot X的主权,并且两者的ConfigEpoch一样如果Master A的NodeID的字典顺比Master B的小那么Master A就把己侧的CurrentEpoch+1,同时ConfigEpoch改成和CurrentEpoch一样Node重制略,见文档。 移除Node略,见文档。 一些自问自答Q:ConfigEpoch何时变化? A:Slave Promotion时、手动Failover时、Slot Migration时 Q:ConfigEpoch怎么变化? A:Node->ConfigEpoch = Cluster->CurrentEpoch + 1,结果也就是Cluster->CurrentEpoch加1了。源码见这里。 Q:两个Master的ConfigEpoch一样怎么办? A:这个会出现在两个Slave同时Promotion时,解决办法是NodeID字典序比较小的那个会再一次Bump ConfigEpoch,源码见这里。 Q:ConfigEpoch有什么用? A:当有两个Master宣称自己拥有同一个/批Slot时,ConfigEpoch大的那个赢,因为大的那个代表最新信息,其他Node只会采用赢的那方所宣称的信息。 Q:CurrentEpoch有什么用? A:1)用来判定Node所获得的Cluster信息的新旧。2)当Node要变更ConfigEpoch时派用处。 参考资料官方文档: Redis Cluster Spec - Configuration handling, propagation, and failovers下面是饿了么工程师写的文章,比较透彻: ...

July 11, 2019 · 1 min · jiezi

使用Docker安装Gitlab及相关配置

最近在学习自动化部署的一些内容,自动化部署,涉及到的内容有Docker、Jenkins、Gitlab等内容,今天通过docker玩了一遍gitlab,下面是一些心得 安装GitlabDocker安装服务实在是太方便,我们通过docker来安装Gitlab,运行如下命令查看Gitlab的镜像文件 搜索镜像 sudo docker search gitlab看到镜像有很多,如果OFFICIAL这一项下面是[OK] 表示为官方的镜像,我这里使用第四个,因为这是中文版的,鄙人英语不好,还是看中文版的比较舒服。其实,中文版也就是安装了一个语言包而已,有兴趣可以自己安装第一个,然后再手动配置中文包 下载镜像 sudo docker pull twang2218/gitlab-ce-zh启动服务 docker run -d -p 8443:443 -p 8090:80 -p 8022:22 --restart always --name gitlab -v /usr/local/gitlab/etc:/etc/gitlab -v /usr/local/gitlab/log:/var/log/gitlab -v /usr/local/gitlab/data:/var/opt/gitlab --privileged=true twang2218/gitlab-ce-zh查看启动情况 // 添加-a 参数,把启动的,没有启动的都列出来sudo docker ps 配置Gitlab配置的时候,我们需要进入容器当中配置,如果直接修改我们映射到容器外部的配置文件,总会出现一些奇怪的问题,为了避免出现问题,尽量按照如下操作流程进行相关的配置和测试 第一步:进入容器 sudo docker exec -it gitlab bash第二步:修改gitlab.rb文件 sudo cd /etc/gitlabsudo vim gitlab.rb第三步:修改IP和端口 该部分内容的修改是为了解决,我们再gitlab创建项目的时候,项目访问地址是容器id的问题 // 可以使用/ 来查找关键字,找到指定的内容,然后通过n来下一个查找// 在gitlab创建项目时候http地址的host(不用添加端口)external_url 'http://xx.xx.xx.xx'// 在gitlab创建项目时候ssh地址的hostgitlab_rails['gitlab_ssh_host'] = 'xx.xx.xx.xx'(不用添加端口)# docker run 的时候我们把22端口映射为外部的8022了,这里修改下gitlab_rails['gitlab_shell_ssh_port'] = 8022第四步:修改邮箱 ...

July 10, 2019 · 1 min · jiezi

跟着大彬读源码-Redis-2-服务器如何响应客户端请求上

上次我们通过问题“启动服务器,程序都干了什么?”,跟着源码,深入了解了 Redis 服务器的启动过程。 既然启动了 Redis 服务器,那我们就要连上 Redis 服务干些事情。这里我们可以通过 redis-cli 测试。 现在客户端和服务器都准备好了,那么Redis 客户端和服务器如何建立连接?服务器又是如何响应客户端的请求呢? 1 连接服务器客户端和服务器进行通讯,首先应该就是建立连接。接下来,我们来看下 redis-cli 与服务器的连接过程。 还记得我们上次使用 gdb 调试程序的步骤吗?让我们对 redis-cli 再来一次,看看源码的执行步骤。在开始之前,记得在编辑器打开 redis-cli.c,定位到 main 函数的位置,毕竟 gdb 看代码没有编辑器看着舒服。 debug 步骤如下: # bashcd /opt/redis-3.2.13// 启动 Redis 服务。Ctrl+c 可推出服务器启动页,同时保持服务器运行./src/redis-server --port 8379 &// 调试 redis-clligdb ./src/redis-cli# gdb (gdb) b main(gdb) r -p 8379(gdb) layout src(gdb) focus cmd执行完上述步骤,我们会进入如下界面: 这时候我们就可以回到编辑器页,看看对 main 函数中哪一行比较感兴趣,就停下来研究研究。到了 2618 行,我们会看到有执行 parseOptions 这个函数,看名字,好像是初始化一些可选项。那就进去看看呗。 1.1 初始化客户端配置函数执行步骤:main -> parseOptions -> main。 ...

July 10, 2019 · 2 min · jiezi

CentOS7-linux下yum安装redis以及使用

前言继之前 window环境下安装Redis及可视化工具Redis Desktop Manager 文章后,这里记录一下Linux系统下的redis的使用安装redis检查是否有redis yum 源yum install redis下载fedora的epel仓库yum install epel-release安装redis数据库yum install redis安装完毕后,使用下面的命令启动redis服务# 启动redisservice redis start# 停止redisservice redis stop# 查看redis运行状态service redis status# 查看redis进程ps -ef | grep redis设置redis为开机自动启动chkconfig redis on进入redis服务# 进入本机redisredis-cli# 列出所有keykeys *``- 防火墙开放相应端口开启6379/sbin/iptables -I INPUT -p tcp --dport 6379 -j ACCEPT 开启6380/sbin/iptables -I INPUT -p tcp --dport 6380 -j ACCEPT 保存/etc/rc.d/init.d/iptables save centos 7下执行service iptables save`` 修改redis默认端口和密码打开配置文件vi /etc/redis.conf修改默认端口,查找 port 6379 修改为相应端口即可 修改默认密码,查找 requirepass foobared 将 foobared 修改为你的密码 ...

July 10, 2019 · 1 min · jiezi

Redis-Cluster节点故障探测算法笔记

本笔记是对Redis Cluster Spec - Failure Detection的归纳总结 状态转换图每个Node在本地维护了一张其他Node的状态表,并根据Failure Detection算法更新这张表里的Node的状态每个Node可以自行把其他Node的状态设置为GOOD(这个状态在文档和源码中均不存在,等价于不是PFAIL也不是FAIL)、PFAIL。如果要把其他Node的状态设置为FAIL则需要大多数Master Node同意才行,一旦设置成功要将这个消息传播给所有其他能连接的Node,其他Node收到这个信息后也要更新本地Node状态表,将Failed Node的状态更新为FAIL。下面是状态转换图,例举的是Node A观察Node B的例子: 少数派和多数派多数派:拥有多数Master的一方,可含有Slave。 少数派:拥有少数Master的一方,可含有Slave。 少数派视角少数派只会看到大多数Master处于PFAIL/FAIL状态,0-所有Slave处于PFAIL/FAIL状态。 多数派视角多数派只会看到少数Master处于PFAIL/FAIL状态,0-所有Slave处于PFAIL/FAIL状态。 不会存在以下情况:多数派看到大多数Master处于FAIL状态,因为大多数Master处于FAIL就意味着活着的Master们变成了少数派,这就矛盾了。 一些自问自答Q:为何少数派能够看到Master处于FAIL状态?不是说要大多数Master同意才能变成FAIL状态吗?A:考虑这个情况,在Partition发生的前一秒某些Master被决定为FAIL,随即Partition发生,那么在少数派眼里这些Master依然是处于FAIL状态的。 Q:这里的每个Node是Slave还是Master呢?A:随便,只要是Node就行。 Q:既然每个Master独占的负责Slots,那么少数派继续工作为啥不可以,反正各自管各自的。A:因为在多数派方,这个Master有可能会被Slave顶替,如果允许少数派继续工作,那么就会形成两个Master,造成split brain Q:少数派节点是如何知道自己应该停止工作的?A:它发现大多数Master变成了PFAIL / FAIL 状态时,就知道自己不能工作了,Redis源码里是这么写的。 Q:多数派节点时如何知道自己应该停止工作的?A:如果这个Cluster要求所有Slots被覆盖,那么当有一个Master处于FAIL状态时,便停止工作,见源码。如果不要求,则继续工作,只不过部分Slots的操作会报错。

July 10, 2019 · 1 min · jiezi

Redis应用限流

系列文章 Redis应用-分布式锁Redis应用-异步消息队列与延时队列Redis应用-位图Redis应用-HyperLogLogRedis应用-布隆过滤器Redis应用-限流Redis应用-Geo在高并发场景下有三把利器保护系统:缓存、降级、和限流。缓存的目的是提升系统的访问你速度和增大系统能处理的容量;降级是当服务出问题或影响到核心流程的性能则需要暂时屏蔽掉。而有些场景则需要限制并发请求量,如秒杀、抢购、发帖、评论、恶意爬虫等。 限流算法常见的限流算法有:计数器,漏桶、令牌桶。 计数器顾名思义就是来一个记一个,然后判断在有限时间窗口内的数量是否超过限制即可 function isActionAllowed($userId, $action, $period, $maxCount) { $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $key = sprintf('hist:%s:%s', $userId, $action); $now = msectime(); # 毫秒时间戳 $pipe=$redis->multi(Redis::PIPELINE); //使用管道提升性能 $pipe->zadd($key, $now, $now); //value 和 score 都使用毫秒时间戳 $pipe->zremrangebyscore($key, 0, $now - $period); //移除时间窗口之前的行为记录,剩下的都是时间窗口内的 $pipe->zcard($key); //获取窗口内的行为数量 $pipe->expire($key, $period + 1); //多加一秒过期时间 $replies = $pipe->exec(); return $replies[2] <= $maxCount;}for ($i=0; $i<20; $i++){ var_dump(isActionAllowed("110", "reply", 60*1000, 5)); //执行可以发现只有前5次是通过的}//返回当前的毫秒时间戳function msectime() { list($msec, $sec) = explode(' ', microtime()); $msectime = (float)sprintf('%.0f', (floatval($msec) + floatval($sec)) * 1000); return $msectime; }漏桶漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率.示意图如下: ...

July 9, 2019 · 3 min · jiezi

redis专题9事务

[info] 仅做了解一般不用,更多是通过lua来解决原子性问题。Redis支持简单的事务,所谓简单是因为其不支持回滚(回滚是用队列模仿的),与mysql有以下区别 typemysqlredis开启start transactionmuitl语句普通sql普通命令失败rollback 回滚discard 取消成功commitexecrollback与discard的区别:如果已经成功执行了2条语句, 第3条语句出错Rollback后,前2条的语句影响消失。discard只是取消队列,并非回滚。要用在exec前面; 在mutil后面的语句中, 语句出错可能有2种情况: 1: 语法就有问题, 这种,exec时,报错, 所有语句得不到执行 2: 语法本身没错,但适用对象有问题. 比如 zadd 操作list对象Exec之后,会执行正确的语句,并跳过有不适当的语句.(如果zadd操作list这种事怎么避免? 这一点,由程序员负责) Example127.0.0.1:6379> multi #开启事务OK127.0.0.1:6379> decrby zz 100QUEUED127.0.0.1:6379> incrby xx 100QUEUED127.0.0.1:6379> exec1) (integer) 9002) (integer) 900127.0.0.1:6379> multiOK127.0.0.1:6379> decrby zz 100QUEUED127.0.0.1:6379> sadd zz haha #语法本身没有错,那整个事务中的语句都会执行;QUEUED127.0.0.1:6379> exec1) (integer) 800 2) (error) WRONGTYPE Operation against a key holding the wrong kind of value悲观锁与乐观锁场景如下: 我正在买票Ticket -1 , money -100而票只有1张, 如果在我multi之后,和exec之前, 票被别人买了---即ticket变成0了.我该如何观察这种情景,并不再提交 悲观的想法: 世界充满危险,肯定有人和我抢, 给 ticket上锁, 只有我能操作. [悲观锁]乐观的想法: 没有那么多人和我抢,因此,我只需要注意,在下单之前看看有没有人更改ticket的值就可以了 [乐观锁]Redis的事务中,启用的是乐观锁,只负责监测key没有被改动.具体的命令:watch命令 ...

July 9, 2019 · 1 min · jiezi

在URLOS中使用Redis技术加速wordpress网站

快!快!快!我们都知道网站的加载速度直接影响用户体验。据研究发现,网站页面在3秒内加载完毕对用户而言是最佳的浏览体验。如果超过这个时间,用户跳出网站的几率会非常大。所以对于站长来说,提高速度是他们追求的终极目标之一。提高WordPress网站的其中一种方法就是使用WordPress缓存。本文我们探讨如何通过Redis技术加速网站。 为什么使用Redis技术 我们都知道WordPress采用的是动态数据库查询方式。翻译成白话,就是当用户访问文章或页面时,都会向数据库发送1个查询命令,数据库根据命令查询之后返回查询结果(这里不存在任何缓存技术)。很明显,如果访问量巨大,就会频繁的查询数据库,这会减慢网站速度。如果服务器性能不高,瞬间网站就崩溃了。 所以我们需要一种缓存技术,来减少数据库查询次数。而数据库缓存技术就是其中之一。Redis技术是其中的佼佼者。Redis是key-value分布式存储系统。简单的说,就是根据关键词值进行查询,这在很大程度上弥补了Memcached的短板。通过Redis进行数据库缓存,查询速度会更快,并发数更多。 所以Redis与WordPress的配合正可谓完美。 如何安装并启用Redis如果我们要使用Redis为WordPress缓存,则需要在服务器上安装Redis并开启它。 我们可以通过ssh工具手动输入命令行来安装和配置Redis,我们也可以使用更简单快捷的方式,通过URLOS来安装wordpress网站以及Redis。为什么推荐使用URLOS,因为它部署网站和各种服务器应用时就像使用手机安装app一样简单。通过URLOS应用市场可以直接安装wordpress和Redis,几乎是零技术。对于管理服务器来说,建议还是使用URLOS,毕竟专业事交给专业的人来做。 WordPress网站后台安装插件并开启Redis支持在WordPress管理后台,我们可以通过缓存插件的方式来开启Redis缓存,我们介绍两种使用方式。 1、使用Redis Object Cache搜索并安装激活Redis Object Cache插件,该插件安装了一个php文件,可以帮助WordPress与Redis进行通信。导航到设置→Redis,然后单击“Enable Object Cache”并确保状态显示“Connected”。 这里需要注意一点:我们需要手工修改一下/wp-content目录下的object-cache.php文件: $parameters = array( 'scheme' => 'tcp', 'host' => '127.0.0.1', 'port' => 6379 );将其中的127.0.0.1改成服务器的内网IP(URLOS自动创建集群节点,则使用172.17.0.1即可): $parameters = array( 'scheme' => 'tcp', 'host' => '172.17.0.1', 'port' => 6379 );然后在wp-config.php中,添加define ('WP_CACHE_KEY_SALT','yourURL.com')到文件中。您可以在网址中使用任何唯一字符串,但建议您使用网站的网址。 2、配合WP Super Cache使用Redis Object Cache与WP Super Cache可以同时使用,Redis成功开启之后就可以在WP Super Cache的高级配置中开启“使用对象缓存系统来存储文件”。 本文我们通过URLOS安装Redis应用来为添加WordPress缓存,通过缓存提高网站加载速度。希望对您有帮助。感兴趣的朋友可以搜索URLOS了解一下。

July 9, 2019 · 1 min · jiezi

Redis-数据类型

Redis 简介Redis 是一个速度非常快的非关系数据库, 它可以存储键与 5 种不同两类型的值之间的映射. 可以将存储在内存中的键值对持久到硬盘, 可以使用复制特性来扩展读性能, 还可以使用客户端分片来扩展写性能. 分片分片是一种将数据划分为多个部分的方法, 对数据的划分可以基于键包含的 ID, 基于键的散列值, 或者基于以上两种的某种组合.通过对数据进行分片, 用户可以将数据存储到多台机器里面, 也可以从多台机器里面获取数据, 这种方法在解决某些问题时可以获得线性级别的性能提升. Redis 实现了主从复制特性: 执行复制的从服务器会连接上主服务器, 接收主服务器发送的整个数据库的初始副本; 之后主服务器执行的写命令, 都会被发送给所有连接着的从服务器去执行, 从而实时地更新从服务器的数据集. 数据类型Redis 中的字符串STRING 可以是字符串, 整数或者浮点数. 对整个字符串或者字符串的其中一部分执行操作; 对整个数和浮点数执行自增或者自减操作. 三个简单的例子: set key value (如果存在就是修改, 不存在则是添加)get key (通过键获取值)del key (删除, 适用于所有类型)Redis 中的列表LIST 一个链表, 链表上的每个节点都包含了一个字符串. 从链表的两端推出或弹出元素; 根据偏移量对链表进行修剪; 读取单个或多个元素; 根据值查找或移除元素. 列表 (链表) 是有序的. 另外获取元素时, 元素下标是从 0(零) 开始几个简单的例子: LPUSH 从左端插入元素.RPUSH 从右端插入元素.LPOP 从列表的左端弹出一个值, 并返回弹出的值(会移除元素).RLOP 从列表的右端弹出一个值, 并返回弹出的值(会移除元素).LRANGE 获取列表在给定范围上的所有值. lrange key 0 -1 表示从零开始获取到最后.LINDEX 获取列表在给定位置上的单个元素.Redis 中的集合SET 包含字符串的无序收集器, 并且被包含的每个字符串都是不可重复的. 添加, 获取, 移除单个儿元素; 检查一个元素是否存在集合中; 计算交集, 并集, 差集; 从集合里面随机获取元素. ...

July 8, 2019 · 1 min · jiezi

redis专题8命令语法介绍之通用KEY

基础命令select num 数据库选择 默认有16[0到15]个数据库,默认自动选择0号数据库 move key num移动key到num服务器 del key [key ...]删除给定的一个或多个 key 。 exists key检查给定 key 是否存在。 expire key 整型值 设置key的生命周期 单位秒数 如果为p(pexpire)单位就变为毫秒 expireat key timestamp指定key在UNIX 时间戳(unix timestamp)变失效 KEYS pattern查找所有符合给定模式 pattern 的 key 。 KEYS * 匹配数据库中所有 key 。KEYS h?llo 通配单个字符 如 hello , hallo 和 hxllo 等。KEYS h*llo 通配任意多个字符(包括没有) 如hllo 和 heeeeello 等。KEYS h[ae]llo 通配括号内的某1个字符 如hello 和 hallo ,但不匹配 hillo 。特殊符号用 隔开。 ttl key 查询key的生命周期 默认-1,永久有效; 单位秒数 如果为(pttl)单位就变为毫秒 ...

July 8, 2019 · 4 min · jiezi

ngnixvuenodejsredis实现前后端分离的环境配置

一、 windows1. 安装配置nginx1.1. 下载安装1.2. 常用命令# 开启nginx start nginx nginx.exe# 停止nginx nginx.exe -s stop nginx.exe -s quit# 重启nginx nginx.exe -s reload1.3 配置`nginx`部分代码```# 反向代理前端vue和后端的node # 这是前端映射的端口 location / { proxy_pass http://localhost:8080; } # 后端node提供的api接口 location /api/ { proxy_pass http://localhost:8000; proxy_set_header Host $host; }```2. 安装配置redis1. 下载安装2. 常用命令运行打开一个 cmd 窗口 使用 cd 命令切换下载文件的根目录如: C:redis 运行:redis-server.exe redis.windows.conf这时候另启一个 cmd 窗口,原来的不要关闭,不然就无法访问服务端了。切换到 redis 目录下运行:redis-cli.exe -h 127.0.0.1 -p 6379[待修改]

July 8, 2019 · 1 min · jiezi

跟着大彬读源码-Redis-1-启动服务程序都干了什么

一直很羡慕那些能读 Redis 源码的童鞋,也一直想自己解读一遍,但迫于 C 大魔王的压力,解读日期遥遥无期。 相信很多小伙伴应该也都对或曾对源码感兴趣,但一来觉得自己不会 C 语言,二来也不知从何入手,结果就和博主一样,一拖再拖。 但正所谓,种一棵树的最好时间是十年前,其次就是现在。如果你真的想了解 Redis 源码,又有缘看到了这系列博文,何不跟着博主一起解读 Redis 源码,做个同行人呢?接下来,就让我们一起走入 Redis 的源码世界吧。 决定要读了,下一步就是如何读。从 github 上克隆下来源码,一看 src 目录,望天,104 个文件,我该从哪个文件开始呢?一个个文件看?不行不行,这样对我毫无诱惑力,没有诱惑力,怎么能战胜游戏、小说对我的吸引呢?苦苦思考,不得其解。然后突然想起来 HTTP 协议的那个经典面试题:从浏览器输入网址,到页面展示,这个过程发生了什么? 把这个面试题换成 Redis:输入开启 Redis 服务的命令,回车,到成功启动 Redis 服务,这个过程发生了什么? 很好,这个问题成功吸引到我了。就让我们从源码中找出这个问题的答案吧。后续的所有文章我们都尝试通过提出问题,解答问题的步骤,来深入了解 Redis。 要了解 Redis 命令的执行过程,首先要安装 Redis 服务,搭建 debug 环境。如果我们能一行行的看到命令在代码中的执行过程,解读源码也就没任何阻碍了。 后续所有文章均基于 redis3.2.13 版本。 1 搭建 debug 环境1、下载编译文件在 linux 上,下载源码文件,编译,使用 gdb(cgdb) 进行 debug。 # bashwget https://github.com/antirez/redis/archive/3.2.13.tar.gztar -zxvf 3.2.13.tar.gzmv redis-3.2.13 /opt/cd redis-3.2.13make # 编译文件,得到可执行文件 redis-server、redis-cli 等2、开启 debug # bashgdb src/redis-server # 在 redis 安装目录,进入 gdb 调试环境按我们平时调试的习惯,找到一个函数设置断点,然后一步步运行调试。对于 Redis 也一样,我们找到 server.c 文件,服务器运行的 main 函数就在此文件中。我们对 main 函数设置断点: ...

July 8, 2019 · 2 min · jiezi

基于golang和redis实现队列功能

项目地址Github: https://github.com/wuzhc/gmq 1. 概述gmq是基于redis提供的特性,使用go语言开发的一个简单易用的队列;关于redis使用特性可以参考之前本人写过一篇很简陋的文章Redis 实现队列; gmq的灵感和设计是基于有赞延迟队列设计,文章内容清晰而且很好理解,但是没有提供源码,在文章的最后也提到了一些未来架构方向; gmq不是简单按照有赞延迟队列的设计实现功能,在它的基础上,做了一些修改和优化,主要如下: 功能上 多种任务模式,不单单只是延迟队列;例如:延迟队列,普通队列,优先级队列架构上: 添加job由dispatcher调度分配各个bucket,而不是由timer每个bucket维护一个timer,而不是所有bucket一个timertimer每次扫描bucket到期job时,会一次性返回多个到期job,而不是每次只返回一个jobtimer的扫描时钟由bucket中下个job到期时间决定,而不是每秒扫描一次2. 应用场景延迟任务 延迟任务,例如用户下订单一直处于未支付状态,半个小时候自动关闭订单异步任务 异步任务,一般用于耗时操作,例如群发邮件等批量操作超时任务 规定时间内(TTR)没有执行完毕或程序被意外中断,则消息重新回到队列再次被消费,一般用于数据比较敏感,不容丢失的优先级任务 当多个任务同时产生时,按照任务设定等级优先被消费,例如a,b两种类型的job,优秀消费a,然后再消费b3. 安装3.1 源码运行配置文件位于gmq/conf.ini,可以根据自己项目需求修改配置 cd $GOPATH/src # 进入gopath/src目录git clone https://github.com/wuzhc/gmq.gitcd gmqgo get -u -v github.com/kardianos/govendor # 如果有就不需要安装了govendor sync -v # 如果很慢,可能需要翻墙go run main.go start3.2 执行文件运行cd $GOPATH/src/gmq# 编译成可执行文件go build # 启动./gmq start# 停止./gmq stop# 守护进程模式启动,不输出日志到consolenohup ./gmq start >/dev/null 2>&1 &# 守护进程模式下查看日志输出(配置文件conf.ini需要设置target_type=file,filename=gmq.log)tail -f gmq.log4. 客户端目前只实现python,go,php语言的客户端的demo,参考:https://github.com/wuzhc/demo/tree/master/mq 运行# php# 生产者php producer.php# 消费者php consumer.php# python# 生产者python producer.py# 消费者python consumer.py一条消息结构{ "id": "xxxx", # 任务id,这个必须是一个唯一值,将作为redis的缓存键 "topic": "xxx", # topic是一组job的分类名,消费者将订阅topic来消费该分类下的job "body": "xxx", # 消息内容 "delay": "111", # 延迟时间,单位秒 "TTR": "11111", # 执行超时时间,单位秒 "status": 1, # job执行状态,该字段由gmq生成 "consumeNum":1, # 被消费的次数,主要记录TTR>0时,被重复消费的次数,该字段由gmq生成}延迟任务 $data = [ 'id' => 'xxxx_id' . microtime(true) . rand(1,999999999), 'topic' => ["topic_xxx"], 'body' => 'this is a rpc test', 'delay' => '1800', // 单位秒,半个小时后执行 'TTR' => '0' ];超时任务 $data = [ 'id' => 'xxxx_id' . microtime(true) . rand(1,999999999), 'topic' => ["topic_xxx"], 'body' => 'this is a rpc test', 'delay' => '0', 'TTR' => '100' // 100秒后还未得到消费者ack确认,则再次添加到队列,将再次被被消费 ];异步任务$data = [ 'id' => 'xxxx_id' . microtime(true) . rand(1,999999999), 'topic' => ["topic_xxx"], 'body' => 'this is a rpc test', 'delay' => '0', 'TTR' => '0' ];优先级任务$data = [ 'id' => 'xxxx_id' . microtime(true) . rand(1,999999999), 'topic' => ["topic_A","topic_B","topic_C"], //优先消费topic_A,消费完后消费topic_B,最后再消费topic_C 'body' => 'this is a rpc test', 'delay' => '0', 'TTR' => '0' ];5. gmq流程图如下: ...

July 8, 2019 · 3 min · jiezi

你确定不来了解下-Redis-跳跃表的原理吗

前言本章将介绍 Redis中 set 和 zset的基本使用和内部原理.因为这两种数据结构有很多相似的地方所以把他们放到一章中介绍.并且重点介绍zset 内部一个很重要的数据结构:跳跃表. 基本介绍set先来看看 set Redis 中 set 集合很像Java 中 HashSet,键值对无序、唯一、不为空. > sadd books Java(integer 1)> sadd books Java(integer 0) # value 值重复> sadd books Go(integer 1)> smembers books # 无序1) "Go"2) "Java"zsetzset 是 Redis 中最特别的基础数据结构,其他几个都能和 Java 大致对应上.它基本上还是一个 set 但是添加了一个 score 属性去保证有序性.其内部实现为跳跃表稍后将会着重介绍. > zadd books 1 Java(integer) 1> zadd books 2 Go(integer) 1> zadd books 3 Python(integer) 1> zrange books 0 -1 #按 score 有序取出1) "Java"2) "Go"3) "Python"在 zset 中 score 的类型为 double 所以有时会出现小数点精度问题. ...

July 7, 2019 · 2 min · jiezi

redis专题7命令语法介绍之PubSub

Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。主要的目的是解耦消息发布者和消息订阅者之间的耦合,这点和设计模式中的观察者模式比较相似。pub /sub不仅仅解决发布者和订阅者直接代码级别耦合也解决两者在物理部署上的耦合。redis作为一个pub/sub server,在订阅者和发布者之间起到了消息路由的功能。订阅者可以通过subscribe和psubscribe命令向redis server订阅自己感兴趣的消息类型,redis将消息类型称为通道(channel)。当发布者通过publish命令向redis server发送特定类型的消息时。订阅该消息类型的全部client都会收到此消息。这里消息的传递是多对多的。一个client可以订阅多个channel,也可以向多个channel发送消息。 下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系: 当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端: 最明显的用法就是构建实时消息系统,比如普通的即时聊天,群聊等功能。这时每个人都是订阅者与发布者。 命令简述SUBSCRIBE channel [channel2 ...] 订阅给定的一个或多个频道的信息。 PSUBSCRIBE pattern [pattern ...] 订阅一个或多个符合给定模式的频道。每个模式以 * 作为匹配符,比如 it* 匹配所有以 it 开头的频道( it.news 、 it.blog 、 it.tweets 等等)。 news.* 匹配所有以 news. 开头的频道( news.it 、 news.global.today 等等),诸如此类。 pubsub channels [pattern]列出活跃频道(正在被subscribe监听的频道,注意不包括psubscribe监听的) pubsub numsub [channel-1 ... channel-n] 返回给定频道的订阅者数量,订阅模式的客户端不计算在内 PUBSUB NUMPAT 返回订阅模式的数量。 ...

July 7, 2019 · 1 min · jiezi

你确定不来了解一下Redis中-Hash的原理吗

前言本章接着上一节继续介绍 Redis 的基础数据结构中的Hash字典. 基本介绍Hash 也可以用来存储用户信息,和 String 不同的是 Hash 可以对用户信息的每个字段单独存储,String 则需要序列化用户的所有字段后存储.并且 String 需要以整个字符串的形式获取用户,而 hash可以只获取部分数据,从而节约网络流量.不过 hash 内存占用要大于 String,这是 hash 的缺点. > hset books java "Effective java"(integer) 1> hset books golang "concurrency in go"(integer) 1> hget books java"Effective java"> hset user age 17(integer) 1>hincrby user age 1 #单个 key 可以进行计数 和 incr 命令基本一致(integer) 18Redis 中的 Hash和 Java的 HashMap 更加相似,都是数组+链表的结构.当发生 hash 碰撞时将会把元素追加到链表上.值得注意的是在 Redis 的 Hash 中 value 只能是字符串. 内部原理看完基本介绍之后,我们先来了解下 hash 的内部结构.第一维是数组,第二维是链表.组成一个 hashtable. ...

July 7, 2019 · 1 min · jiezi

Redis基本操作之Java实现所有类型

前不久分享过Redis的基本数据结构及基本命令详解。在熟悉了redis的基本操作之后(如果还有对redis的基本操作不熟悉的,可以点击前面的连接先熟悉下),今天给大家分享下实际开发中对redis操作的Java实现版本。 Maven依赖使用maven来构建项目在当下应该已经是主流了,所以我们也不例外使用了Maven。因为使用了spring对redis封装的jar,所以也需要引入spring基本jar,Maven依赖如下: <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>5.1.5.RELEASE</version></dependency><dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.1.5.RELEASE</version></dependency><dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-redis</artifactId> <version>2.1.3.RELEASE</version></dependency><dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.2</version></dependency><dependency> <groupId></groupId> <artifactId>redis</artifactId></dependency>实现代码实现代码篇幅有点长,而且segmentfault代码div的高度有限制,建议大家把代码拷贝到开发工具中再阅读。 package spring.redis;import org.springframework.beans.factory.InitializingBean;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.dao.DataAccessException;import org.springframework.data.redis.connection.RedisConnection;import org.springframework.data.redis.core.RedisCallback;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.serializer.RedisSerializer;import org.springframework.data.redis.serializer.StringRedisSerializer;import org.springframework.stereotype.Service;import java.io.UnsupportedEncodingException;import java.util.*;import java.util.stream.Collectors;@Servicepublic class SpringRedisHandler implements InitializingBean { //redis编码 private static final String redisCode = "utf-8"; private static final String EmptyString = ""; @Autowired private RedisTemplate<String, String> jtRedis; /** * 设置key-value【不含超时时间】 * * @param key * @param value */ public void set(String key, Object value) { this.set(key, String.valueOf(value), 0L); } /** * 设置key-value【含超时时间】 * * @param key * @param value * @param liveTime */ public void set(String key, Object value, long liveTime) { this.set(key.getBytes(), String.valueOf(value).getBytes(), liveTime); } @SuppressWarnings({"unchecked", "rawtypes"}) private void set(final byte[] key, final byte[] value, final long liveTime) { jtRedis.execute(new RedisCallback() { @Override public Long doInRedis(RedisConnection connection) throws DataAccessException { connection.set(key, value); if (liveTime > 0) { connection.expire(key, liveTime); } return 1L; } }); } /** * get key的值 * * @param key * @return */ public String get(final String key) { return jtRedis.execute(new RedisCallback<String>() { @Override public String doInRedis(RedisConnection connection) throws DataAccessException { try { return new String(connection.get(key.getBytes()), redisCode); } catch (UnsupportedEncodingException e) { e.printStackTrace(); return ""; } } }); } /** * 是否存在key * * @param key * @return */ public boolean exists(final String key) { return jtRedis.execute(new RedisCallback<Boolean>() { @Override public Boolean doInRedis(RedisConnection connection) throws DataAccessException { return connection.exists(key.getBytes()); } }); } /** * 某数据中所有key的总数 * * @return */ public long dbSize() { return jtRedis.execute(new RedisCallback<Long>() { @Override public Long doInRedis(RedisConnection connection) throws DataAccessException { return connection.dbSize(); } }); } /** * 检测redis服务器是否能平通 */ public String ping() { return jtRedis.execute(new RedisCallback<String>() { @Override public String doInRedis(RedisConnection connection) throws DataAccessException { return connection.ping(); } }); } /** * value增加某个值 * * @param key * @param value * @return */ public Long incr(String key, long value) { return incr(key.getBytes(), value); } private Long incr(byte[] key, long value) { return jtRedis.execute(new RedisCallback<Long>() { @Override public Long doInRedis(RedisConnection connection) throws DataAccessException { return connection.incrBy(key, value); } }); } /** * 自增 * * @param key * @return */ public Long incr(String key) { return incr(key.getBytes(), 1); } /** * 自减 * * @param key * @return */ public Long decr(String key) { return decr(key.getBytes(), 1); } /** * value减少某个值 * * @param key * @param value * @return */ public Long decr(String key, long value) { return decr(key.getBytes(), value); } private Long decr(byte[] key, long value) { return jtRedis.execute(new RedisCallback<Long>() { @Override public Long doInRedis(RedisConnection connection) throws DataAccessException { return connection.decrBy(key, value); } }); } /** * 删除key * * @param key * @return */ public Long del(String key) { return del(key.getBytes()); } private Long del(byte[] key) { return jtRedis.execute(new RedisCallback<Long>() { @Override public Long doInRedis(RedisConnection connection) throws DataAccessException { return connection.del(key); } }); } /** * flushdb:删除db下的所有数据 */ @SuppressWarnings({"rawtypes", "unchecked"}) public void flushDb() { jtRedis.execute(new RedisCallback() { @Override public Long doInRedis(RedisConnection connection) throws DataAccessException { connection.flushDb(); return 1L; } }); } /** * 设置hash * * @param key * @param field * @param value * @return */ public Boolean hSet(String key, String field, String value) { return jtRedis.execute(new RedisCallback<Boolean>() { @Override public Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException { return redisConnection.hSet(key.getBytes(), field.getBytes(), value.getBytes()); } }); } /** * 获取hash的属性值 * * @param key * @param field * @return */ public String hGet(String key, String field) { return jtRedis.execute(new RedisCallback<String>() { @Override public String doInRedis(RedisConnection redisConnection) throws DataAccessException { return new String(redisConnection.hGet(key.getBytes(), field.getBytes())); } }); } /** * 批量设置hash * * @param key * @param values */ public void hMSet(String key, Map<String, Object> values) { jtRedis.execute(new RedisCallback<Long>() { @Override public Long doInRedis(RedisConnection redisConnection) throws DataAccessException { redisConnection.hMSet(key.getBytes(), stringObjectMapToBytes(values)); return null; } }); } /** * 批量获取hash的多个属性 * * @param key * @param fields * @return */ public List<String> hMGet(String key, String... fields) { return jtRedis.execute(new RedisCallback<List<String>>() { @Override public List<String> doInRedis(RedisConnection redisConnection) throws DataAccessException { List<String> listFileds = new ArrayList<>(); for (int i = 0; i < fields.length; i++) { listFileds.add(fields[i]); } List<byte[]> byteFileds = stringListToByte(listFileds); return bytesListToString(redisConnection.hMGet(key.getBytes(), byteFileds.toArray(new byte[byteFileds.size()][byteFileds.size()]))); } }); } /** * 获取hash的所有属性 * * @param key * @return */ public Map<String, String> hGetAll(String key) { return jtRedis.execute(new RedisCallback<Map<String, String>>() { @Override public Map<String, String> doInRedis(RedisConnection redisConnection) throws DataAccessException { return bytesMapToString(redisConnection.hGetAll(key.getBytes())); } }); } /** * 针对hash中某个属性增加指定的值 * * @param key * @param field * @param value * @return */ public Double hIncrBy(String key, String field, double value) { return jtRedis.execute(new RedisCallback<Double>() { @Override public Double doInRedis(RedisConnection redisConnection) throws DataAccessException { return redisConnection.hIncrBy(key.getBytes(), field.getBytes(), value); } }); } /** * hash是存在某属性 * * @param key * @param field * @return */ public Boolean hExists(String key, String field) { return jtRedis.execute(new RedisCallback<Boolean>() { @Override public Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException { return redisConnection.hExists(key.getBytes(), field.getBytes()); } }); } /** * 删除hash的某属性 * * @param key * @param field * @return */ public Long hDel(String key, String field) { return jtRedis.execute(new RedisCallback<Long>() { @Override public Long doInRedis(RedisConnection redisConnection) throws DataAccessException { return redisConnection.hDel(key.getBytes(), field.getBytes()); } }); } /** * 向zset中的某个key添加一个属性几分数(可以根据分数排序) * * @param key * @param score * @param field * @return */ public Boolean zAdd(String key, double score, String field) { return jtRedis.execute(new RedisCallback<Boolean>() { @Override public Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException { return redisConnection.zAdd(key.getBytes(), score, field.getBytes()); } }); } /** * 给zset中的某个key中的某个属性增加指定分数 * * @param key * @param score * @param field * @return */ public Double zIncrBy(String key, double score, String field) { return jtRedis.execute(new RedisCallback<Double>() { @Override public Double doInRedis(RedisConnection redisConnection) throws DataAccessException { return redisConnection.zIncrBy(key.getBytes(), score, field.getBytes()); } }); } /** * 从list左侧插入一个元素 * * @param key * @param values * @return */ public Long lPush(String key, String value) { return jtRedis.execute(new RedisCallback<Long>() { @Override public Long doInRedis(RedisConnection connection) throws DataAccessException { return connection.lPush(key.getBytes(), value.getBytes()); } }); } /** * 从list左侧插入多个元素 * * @param key * @param values * @return */ public Long lPush(String key, List<String> values) { return jtRedis.execute(new RedisCallback<Long>() { @Override public Long doInRedis(RedisConnection connection) throws DataAccessException { List<byte[]> bytes = stringListToByte(values); return connection.lPush(key.getBytes(), bytes.toArray(new byte[bytes.size()][bytes.size()])); } }); } /** * 从list的左侧取出一个元素 * * @param key * @return */ public String lPop(String key) { return jtRedis.execute(new RedisCallback<String>() { @Override public String doInRedis(RedisConnection connection) throws DataAccessException { if (connection.lLen(key.getBytes()) > 0) { return new String(connection.lPop(key.getBytes())); } else { return EmptyString; } } }); } /** * 向list的右侧插入一个元素 * * @param key * @param value * @return */ public Long rPush(String key, String value) { return jtRedis.execute(new RedisCallback<Long>() { @Override public Long doInRedis(RedisConnection connection) throws DataAccessException { return connection.rPush(key.getBytes(), value.getBytes()); } }); } /** * list的rpush,从右侧插入多个元素 * * @param key * @param values * @return */ public Long rPush(String key, List<String> values) { return jtRedis.execute(new RedisCallback<Long>() { @Override public Long doInRedis(RedisConnection connection) throws DataAccessException { List<byte[]> bytes = stringListToByte(values); return connection.rPush(key.getBytes(), bytes.toArray(new byte[bytes.size()][bytes.size()])); } }); } /** * 从list的右侧取出一个元素 * * @param key * @return */ public String rPop(String key) { return jtRedis.execute(new RedisCallback<String>() { @Override public String doInRedis(RedisConnection connection) throws DataAccessException { if (connection.lLen(key.getBytes()) > 0) { return new String(connection.rPop(key.getBytes())); } else { return EmptyString; } } }); } /** * 给set中添加元素 * * @param key * @param values * @return */ public Long sadd(String key, List<String> values) { return jtRedis.execute(new RedisCallback<Long>() { @Override public Long doInRedis(RedisConnection connection) throws DataAccessException { List<byte[]> bytes = stringListToByte(values); return connection.sAdd(key.getBytes(), bytes.toArray(new byte[bytes.size()][bytes.size()])); } }); } /** * 获取set中的所有元素 * * @param key * @return */ public List<String> smembers(String key) { return jtRedis.execute(new RedisCallback<List<String>>() { @Override public List<String> doInRedis(RedisConnection connection) throws DataAccessException { return bytesListToString(connection.sMembers(key.getBytes())); } }); } /** * set中是否包含某元素 * * @param key * @param value * @return */ public Boolean sIsMember(String key, String value) { return jtRedis.execute(new RedisCallback<Boolean>() { @Override public Boolean doInRedis(RedisConnection connection) throws DataAccessException { return connection.sIsMember(key.getBytes(), value.getBytes()); } }); } private byte[][] change(List<byte[]> values) { byte[][] result = {}; return result; } private List<byte[]> stringListToByte(List<String> values) { return values .stream() .map(p -> p.getBytes()) .collect( Collectors.toList() ); } private List<String> bytesListToString(Collection<byte[]> values) { return values .stream() .map(p -> new String(p)) .collect( Collectors.toList() ); } private Map<String, String> bytesMapToString(Map<byte[], byte[]> values) { Map<String, String> result = new HashMap<>(); values.forEach((k, v) -> result.put(new String(k), new String(v))); return result; } private Map<byte[], byte[]> stringObjectMapToBytes(Map<String, Object> values) { Map<byte[], byte[]> result = new HashMap<>(); values.forEach((k, v) -> result.put(k.getBytes(), String.valueOf(v).getBytes())); return result; } /** * 正则表达式获取值 * * @param pattern * @return */ public Set<String> keys(String pattern) { return jtRedis.keys(pattern); } @Override public void afterPropertiesSet() throws Exception { RedisSerializer<String> stringSerializer = new StringRedisSerializer(); jtRedis.setKeySerializer(stringSerializer); jtRedis.setValueSerializer(stringSerializer); jtRedis.setHashKeySerializer(stringSerializer); jtRedis.setHashValueSerializer(stringSerializer); }}配置文件配置文件主要有两个,一个是spirng相关配置的配置文件applicationContext-redis.xml,还有一个是redis相关配置文件redis-config.propertiesapplicationContext-redis.xml配置文件内容如下: ...

July 7, 2019 · 7 min · jiezi

redis专题6命令语法介绍之hash

可以把hash看做一个数组hset array key1 value2;,该数据类型特别适用于存储 增hset key field value作用: 把key中filed域的值设为value注:如果没有field域,直接添加,如果有,则覆盖原field域的值 hsetnx key field value作用: 将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在。若域 field 已经存在,该操作无效。如果 key 不存在,一个新哈希表被创建并执行 HSETNX 命令。 hmset key field1 value1 [field2 value2 field3 value3 ......fieldn valuen]作用: 设置field1->N 个域, 对应的值是value1->N(对应PHP理解为 $key = array(file1=>value1, field2=>value2 ....fieldN=>valueN)) 删hdel key field作用: 删除key中 field域 改hincrby key field value作用: 是把key中的field域的值增长整型值value hincrbyfloat key field value作用: 是把key中的field域的值增长浮点值value 查hget key field作用: 返回key中field域的值 hmget key field1 field2 fieldN作用: 返回key中field1 field2 fieldN域的值 ...

July 5, 2019 · 1 min · jiezi

Redis应用分布式锁

系列文章 Redis应用-分布式锁Redis应用-异步消息队列与延时队列Redis应用-位图Redis应用-HyperLogLogRedis应用-布隆过滤器Redis应用-限流Redis应用-Geo当多个进程不在同一个系统中,就需要用分布式锁控制多个进程对资源的访问。 使用redis来实现分布式锁主要用到以下命令: SETNX KEY VALUE如果key不存在,就设置key对应字符串value expire KEY seconds设置key的过期时间 del KEY删除key 代码实现如下: $redis = new Redis();$redis->connect('127.0.0.1', 6379);$ok = $redis->setNX($key, $value);if ($ok) { //获取到锁 ... do something ... $redis->del($key);}上面代码有没有问题呢?如果我们在逻辑处理过程中出现了异常情况,导致KEY没有删除,那就出现了死锁了。所以一般我们在拿到锁之后再给KEY加一个过期时间 为了保证执行的原子性,使用了multi就有了如下代码 $redis->multi();$redis->setNX($key, $value);$redis->expire($key, $ttl);$res = $redis->exec();if($res[0]) { //获取到锁 ... do something ... $redis->del($key);}但是这样的又有一个问题第一个请求成功了,之后的请求虽然没有拿到锁但是每次都刷新了锁的时间。这样我们设置锁过期时间的意义就不存在了。所以我们在拿到锁以后再进行过期时间的操作,这时候我们就可以祭出原子性操作的lua脚本,代码如下 $script = <<<EOT local key = KEYS[1] local value = KEYS[2] local ttl = KEYS[3] local ok = redis.call('setnx', key, value) if ok == 1 then redis.call('expire', key, ttl) end return okEOT;$res = $redis->eval($script, [$key,$val, $ttl], 3);if($res) { //获取到锁 ... do something ... $redis->del($key);}借助lua脚本虽然解决了问题,但是未免有些麻烦,Redis从 2.6.12 版本开始, SET 命令的行为可以通过一系列参数来修改: ...

July 5, 2019 · 1 min · jiezi

redis进阶1redis的Lua脚本控制原子性

[toc] 为什么要用lua减少网络开销:本来5次网络请求的操作,可以用一个请求完成,原先5次请求的逻辑放在redis服务器上完成。使用脚本,减少了网络往返时延。原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他进程或者进程的命令插入。(最重要)复用:客户端发送的脚本会永久存储在Redis中,意味着其他客户端可以复用这一脚本而不需要使用代码完成同样的逻辑。串讲lua相关链接与参考官方文档(英文)Lua 5.3 参考手册 (中文)菜鸟教程主要用到的语法: 注释,变量,方法调用和声明,循环,流程控制 编辑器与调试下载安装:http://luabinaries.sourceforg...IDE编辑器:Settings -> Plugins -> Marketplace -> 搜索并安装EmmyLua redis执行luaeval使用EVAL命令对 Lua 脚本进行求值 EVAL script numkeys key [key ...] arg [arg ...] [info] numkeys : keys的数量有几个。这是一个必传的参数,即使没有keys也要传个0;# 注意redis的计数是从1开始的127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second1) "key1"2) "key2"3) "first"4) "second"脚本缓存EVAL命令会将脚本添加到脚本缓存中,并且会立即对输入的脚本进行求值。 如果给定的脚本已经在缓存里面了,那么不做动作。 在脚本被加入到缓存之后,通过 EVALSHA 命令,可以使用脚本的 SHA1 校验和来调用这个脚本。 脚本可以在缓存中保留无限长的时间,直到执行SCRIPT FLUSH为止。 redis> SCRIPT LOAD "return 'hello moto'""232fd51614574cf0867b83d384a5e898cfd24e5a"# 判断脚本是否存在redis> SCRIPT EXISTS 232fd51614574cf0867b83d384a5e898cfd24e5a1) (integer) 1redis> EVALSHA 232fd51614574cf0867b83d384a5e898cfd24e5a 0"hello moto"# 清空缓存redis> SCRIPT FLUSH OKcall与pcall在 Lua 脚本中,可以使用两个不同函数来执行 Redis 命令,它们分别是: ...

July 5, 2019 · 3 min · jiezi

php-实现Redis分布式锁

简介多线程情况下访问一些共享资源需要加锁,否则就会导致数据错乱的问题分布式锁可以通过DB,Redis,Zk等方式实现,本节主要介绍php使用Redis实现分布式锁set命令setnx key value 设置一个值,当key已经存在时,返回flase,代表失败使用setnx实现分布锁有个缺陷,setnx操作无法设置key的ttl,需要配合exprie key ttl 一起使用好在set命令就集成了nx和ex操作set key name NX PX 10000$redis = new Redis();$redis->connect('127.0.0.1', 6380);$rs = $redis->set('testnx', 123, ['nx', 'ex' => 10]);var_dump($rs);Redlockset命令还有一个问题,当你要提前释放这个锁的时候,使用expire key 0或者使用del key如果expire或者del命令发送了阻塞,锁自动失效,这时候B获取了锁,expire/del命令到达,导致B获取的锁失效Redlock在加锁的时候value值要保证唯一性,在释放锁的时候要验证value是否和申请锁时value是否一致class RedLock{ private $retryDelay; private $retryCount; private $clockDriftFactor = 0.01; private $quorum; private $servers = array(); private $instances = array(); function __construct(array $servers, $retryDelay = 200, $retryCount = 3) { $this->servers = $servers; $this->retryDelay = $retryDelay; $this->retryCount = $retryCount; $this->quorum = min(count($servers), (count($servers) / 2 + 1)); } public function lock($resource, $ttl) { $this->initInstances(); $token = uniqid(); $retry = $this->retryCount; do { $n = 0; $startTime = microtime(true) * 1000; foreach ($this->instances as $instance) { if ($this->lockInstance($instance, $resource, $token, $ttl)) { $n++; } } # Add 2 milliseconds to the drift to account for Redis expires # precision, which is 1 millisecond, plus 1 millisecond min drift # for small TTLs. $drift = ($ttl * $this->clockDriftFactor) + 2; $validityTime = $ttl - (microtime(true) * 1000 - $startTime) - $drift; if ($n >= $this->quorum && $validityTime > 0) { return [ 'validity' => $validityTime, 'resource' => $resource, 'token' => $token, ]; } else { foreach ($this->instances as $instance) { $this->unlockInstance($instance, $resource, $token); } } // Wait a random delay before to retry $delay = mt_rand(floor($this->retryDelay / 2), $this->retryDelay); usleep($delay * 1000); $retry--; } while ($retry > 0); return false; } public function unlock(array $lock) { $this->initInstances(); $resource = $lock['resource']; $token = $lock['token']; foreach ($this->instances as $instance) { $this->unlockInstance($instance, $resource, $token); } } private function initInstances() { if (empty($this->instances)) { foreach ($this->servers as $server) { list($host, $port, $timeout) = $server; $redis = new \Redis(); $redis->connect($host, $port, $timeout); $this->instances[] = $redis; } } } private function lockInstance($instance, $resource, $token, $ttl) { return $instance->set($resource, $token, ['NX', 'PX' => $ttl]); } private function unlockInstance($instance, $resource, $token) { $script = ' if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end '; return $instance->eval($script, [$resource, $token], 1); }}$servers = [ ['127.0.0.1', 6379, 0.01],];$redLock = new RedLock($servers);while (true) { $lock = $redLock->lock('test', 10000); if ($lock) { print_r($lock); $redLock->unlock(['resource' => 'test', 'token' => '5d1c123121538']); } else { print "Lock not acquired\n"; }}后续Redis分布式锁还有没有问题?解决方法:引入版本号 ...

July 4, 2019 · 2 min · jiezi

redis专题5命令语法介绍之sets

关于 redis的无序集合有三个特点: 无序性, 确定性(描述准确) , 唯一性; 有点类似于数据容器; 增SADD key member1 [member2] 作用: 往集合key中增加元素注意: 集合具有唯一性,已经存在就放不进; 删SREM key member1 [member2] 作用: 删除集合中值为 value1 value2的元素返回值: 忽略不存在的元素后,真正删除掉的元素的个数 SPOP key作用: 返回并删除集合中key中1个随机元素,随机--体现了无序性 改SMOVE source dest value作用:把集合source中的value移动到集合dest中 注意:只能移动一个 查SMEMBERS key作用: 返回集合中所有的元素 SCARD key作用: 返回集合中元素的个数 SINTER key1 key2 key3作用: 求出key1 key2 key3 三个集合中的交集,并返回 sinterstore dest key1 key2 key3作用: 求出key1 key2 key3 三个集合中的交集,并赋给dest SUNIONSTORE destination key1 [key2] 作用: 所有给定集合的并集存储在 destination 集合中 SDIFFSTORE destination key1 [key2] 作用: 返回给定所有集合的差集并存储在 destination 中 ...

July 3, 2019 · 1 min · jiezi

redis专题3命令语法介绍之list

通过链表结构可以模仿队列结构与堆栈结构;关于队列结构和堆栈结构可以查看 【SPL标准库专题(6)】Datastructures:SplStack & SplQueue 增lpush key value1 value2 value3...作用: 把值插入到链表头部 rpush key value1 value2 value3...127.0.0.1:6379> rpush zimu a b c d e f(integer) 6作用: 把值插入到链接尾部 删rpop key作用: 返回并删除链表尾元素 lpop key作用: 返回并删除链表头元素 lrem key count value作用: 从key链表中删除 value值注: 删除count的绝对值个value后结束Count > 0 从表头删除Count < 0 从表尾删除 lrem key 2 b 从表头开始找b,找到就给删除,删除2个;lrem key -2 b 从表尾开始找b,找到就给删除,删除2个; 改ltrim key start stop作用: 剪切key对应的链表,切[start,stop]一段,并把该段重新赋给key lindex key index作用: 返回index索引上的值,如 lindex key 2 llen key作用:计算链接表的元素个数 ...

July 2, 2019 · 1 min · jiezi

redis专题4命令语法介绍之sortedset

有序集合可以模拟优先级队列与延时队列,排行榜等功能的实现 增zadd key score1 value1 score2 value2 ..redis 127.0.0.1:6379> zadd stu 18 lily 19 hmm 20 lilei 21 lilei(integer) 3添加元素 在redis的3.02版本还可以为zadd增加一些附加参数 ZADD key [NX|XX] [CH] [INCR] score member NX: 不存在的情况下XX: 存在的情况下(更新)CH: ??INCR: 使用该参数使得ZADD的功能类似ZINCRBY的功能 删zremrangebyscore key min maxredis 127.0.0.1:6379> zremrangebyscore stu 4 10(integer) 2redis 127.0.0.1:6379> zrange stu 0 -11) "f"作用: 按照socre来删除元素,删除score在[min,max] (包括)之间的 zrem key value1 value2 ..作用: 删除集合中的元素 zremrangebyrank key start endredis 127.0.0.1:6379> zremrangebyrank stu 0 1(integer) 2redis 127.0.0.1:6379> zrange stu 0 -11) "c"2) "e"3) "f"4) "g"作用: 按排名删除元素,删除名次在[start,end] (包括)之间的 ...

July 1, 2019 · 2 min · jiezi