精华内容
下载资源
问答
  • redis应用场景及实例
    2020-12-08 14:39:30

    redis应用场景及实例

    前言

    Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。在这篇文章中,我们将阐述 Redis 最常用的使用场景,以及那些影响我们选择的不同特性。
    Redis 的 5 个常见使用场景

    1、会话缓存(Session Cache)

    最常用的一种使用Redis的情景是会话缓存(session cache)。用Redis缓存会话比其他存储(如Memcached)的优势在于:Redis提供持久化。当维护一个不是严格要求一致性的缓存时,如果用户的购物车信息全部丢失,大部分人都会不高兴的,现在,他们还会这样吗?
    幸运的是,随着 Redis 这些年的改进,很容易找到怎么恰当的使用Redis来缓存会话的文档。甚至广为人知的商业平台Magento也提供Redis的插件。

    2、全页缓存(FPC)

    除基本的会话token之外,Redis还提供很简便的FPC平台。回到一致性问题,即使重启了Redis实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,这是一个极大改进,类似PHP本地FPC。
    再次以Magento为例,Magento提供一个插件来使用Redis作为全页缓存后端。
    此外,对WordPress的用户来说,Pantheon有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面。

    3、队列

    Reids在内存存储引擎领域的一大优点是提供 list 和 set 操作,这使得Redis能作为一个很好的消息队列平台来使用。Redis作为队列使用的操作,就类似于本地程序语言(如Python)对 list 的 push/pop 操作。
    如果你快速的在Google中搜索“Redis queues”,你马上就能找到大量的开源项目,这些项目的目的就是利用Redis创建非常好的后端工具,以满足各种队列需求。例如,Celery有一个后台就是使用Redis作为broker,你可以从这里去查看。

    4、排行榜/计数器

    Redis在内存中对数字进行递增或递减的操作实现的非常好。集合(Set)和有序集合(Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis只是正好提供了这两种数据结构。所以,我们要从排序集合中获取到排名最靠前的10个用户–我们称之为“user_scores”,我们只需要像下面一样执行即可:
    当然,这是假定你是根据你用户的分数做递增的排序。如果你想返回用户及用户的分数,你需要这样执行:
    ZRANGE user_scores 0 10 WITHSCORES
    Agora Games就是一个很好的例子,用Ruby实现的,它的排行榜就是使用Redis来存储数据的,你可以在这里看到。

    5、发布/订阅

    最后(但肯定不是最不重要的)是Redis的发布/订阅功能。发布/订阅的使用场景确实非常多。我已看见人们在社交网络连接中使用,还可作为基于发布/订阅的脚本触发器,甚至用Redis的发布/订阅功能来建立聊天系统!(不,这是真的,你可以去核实)。
    Redis提供的所有特性中,我感觉这个是喜欢的人最少的一个,虽然它为用户提供如果此多功能。

    详解 Redis 应用场景及应用实例

    MySql+Memcached架构的问题
    实际MySQL是适合进行海量数据存储的,通过Memcached将热点数据加载到cache,加速访问,很多公司都曾经使用过这样的架构,但随着业务数据量的不断增加,和访问量的持续增长,我们遇到了很多问题:

    1.MySQL需要不断进行拆库拆表,Memcached也需不断跟着扩容,扩容和维护工作占据大量开发时间。

    2.Memcached与MySQL数据库数据一致性问题。

    3.Memcached数据命中率低或down机,大量访问直接穿透到DB,MySQL无法支撑。

    4.跨机房cache同步问题。

    众多NoSQL百花齐放,如何选择

    最近几年,业界不断涌现出很多各种各样的NoSQL产品,那么如何才能正确地使用好这些产品,最大化地发挥其长处,是我们需要深入研究和思考的问题,实际归根结底最重要的是了解这些产品的定位,并且了解到每款产品的tradeoffs,在实际应用中做到扬长避短,总体上这些NoSQL主要用于解决以下几种问题

    1.少量数据存储,高速读写访问。此类产品通过数据全部in-momery 的方式来保证高速访问,同时提供数据落地的功能,实际这正是Redis最主要的适用场景。

    2.海量数据存储,分布式系统支持,数据一致性保证,方便的集群节点添加/删除。

    3.这方面最具代表性的是dynamo和bigtable 2篇论文所阐述的思路。前者是一个完全无中心的设计,节点之间通过gossip方式传递集群信息,数据保证最终一致性,后者是一个中心化的方案设计,通过类似一个分布式锁服务来保证强一致性,数据写入先写内存和redo log,然后定期compat归并到磁盘上,将随机写优化为顺序写,提高写入性能。

    4.Schema free,auto-sharding等。比如目前常见的一些文档数据库都是支持schema-free的,直接存储json格式数据,并且支持auto-sharding等功能,比如mongodb。
    面对这些不同类型的NoSQL产品,我们需要根据我们的业务场景选择最合适的产品。

    Redis最适合所有数据in-momory的场景,虽然Redis也提供持久化功能,但实际更多的是一个disk-backed的功能,跟传统意义上的持久化有比较大的差别,那么可能大家就会有疑问,似乎Redis更像一个加强版的Memcached,那么何时使用Memcached,何时使用 Redis呢?

    如果简单地比较Redis与Memcached的区别,大多数都会得到以下观点:

    1 、Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。

    2 、Redis支持数据的备份,即master-slave模式的数据备份。

    3 、Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。

    Redis常用数据类型

    Redis最为常用的数据类型主要有以下:
    String
    Hash
    List
    Set
    Sorted set
    pub/sub
    Transactions
    在具体描述这几种数据类型之前,我们先通过一张图了解下Redis内部内存管理中是如何描述这些不同数据类型的:

    在这里插入图片描述

    首先Redis内部使用一个redisObject对象来表示所有的key和value,redisObject最主要的信息如上图所示:
    type代表一个value对象具体是何种数据类型,
    encoding是不同数据类型在redis内部的存储方式,
    比如:type=string代表value存储的是一个普通字符串,那么对应的encoding可以是raw或者是int,如果是int则代表实际 redis内部是按数值型类存储和表示这个字符串的,当然前提是这个字符串本身可以用数值表示,比如:”123″ “456″这样的字符串。
    这里需要特殊说明一下vm字段,只有打开了Redis的虚拟内存功能,此字段才会真正的分配内存,该功能默认是关闭状态的,该功能会在后面具体描述。通过上图我们可以发现Redis使用redisObject来表示所有的key/value数据是比较浪费内存的,当然这些内存管理成本的付出主要也是为了给 Redis不同数据类型提供一个统一的管理接口,实际作者也提供了多种方法帮助我们尽量节省内存使用,我们随后会具体讨论。

    各种数据类型应用和实现方式
    下面我们先来逐一的分析下这7种数据类型的使用和内部实现方式:

    String:

    Strings 数据结构是简单的key-value类型,value其实不仅是String,也可以是数字。
    常用命令: set,get,decr,incr,mget 等。
    应用场景:String是最常用的一种数据类型,普通的key/ value 存储都可以归为此类。即可以完全实现目前 Memcached 的功能,并且效率更高。还可以享受Redis的定时持久化,操作日志及 Replication等功能。除了提供与 Memcached 一样的get、set、incr、decr 等操作外,Redis还提供了下面一些操作:
    获取字符串长度
    往字符串append内容
    设置和获取字符串的某一段内容
    设置及获取字符串的某一位(bit)
    批量设置一系列字符串的内容
    实现方式:String在redis内部存储默认就是一个字符串,被redisObject所引用,当遇到incr,decr等操作时会转成数值型进行计算,此时redisObject的encoding字段为int。

    Hash

    常用命令:hget,hset,hgetall 等。
    应用场景:在Memcached中,我们经常将一些结构化的信息打包成HashMap,在客户端序列化后存储为一个字符串的值,比如用户的昵称、年龄、性别、积分等,这时候在需要修改其中某一项时,通常需要将所有值取出反序列化后,修改某一项的值,再序列化存储回去。这样不仅增大了开销,也不适用于一些可能并发操作的场合(比如两个并发的操作都需要修改积分)。而Redis的Hash结构可以使你像在数据库中Update一个属性一样只修改某一项属性值。
    我们简单举个实例来描述下Hash的应用场景,比如我们要存储一个用户信息对象数据,包含以下信息:
    用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息,如果用普通的key/value结构来存储,主要有 2种存储方式。
    第一种方式将用户ID作为查找key,把其他信息封装成一个对象以序列化的方式存储,这种方式的缺点是,增加了序列化/反序列化的开销,并且在需要修改其中一项信息时,需要把整个对象取回,并且修改操作需要对并发进行保护,引入CAS等复杂问题。
    第二种方法是这个用户信息对象有多少成员就存成多少个key-value对儿,用用户ID+对应属性的名称作为唯一标识来取得对应属性的值,虽然省去了序列化开销和并发问题,但是用户ID为重复存储,如果存在大量这样的数据,内存浪费还是非常可观的。
    那么Redis提供的Hash很好的解决了这个问题,Redis的Hash实际是内部存储的Value为一个HashMap,并提供了直接存取这个Map成员的接口。
    也就是说,Key仍然是用户ID, value是一个Map,这个Map的key是成员的属性名,value是属性值,这样对数据的修改和存取都可以直接通过其内部Map的 Key(Redis里称内部Map的key为field), 也就是通过 key(用户ID) + field(属性标签) 就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题。很好的解决了问题。
    这里同时需要注意,Redis提供了接口(hgetall)可以直接取到全部的属性数据,但是如果内部Map的成员很多,那么涉及到遍历整个内部 Map的操作,由于Redis单线程模型的缘故,这个遍历操作可能会比较耗时,而另其它客户端的请求完全不响应,这点需要格外注意。
    实现方式:
    上面已经说到Redis Hash对应Value内部实际就是一个HashMap,实际这里会有2种不同实现,这个Hash的成员比较少时Redis为了节省内存会采用类似一维数组的方式来紧凑存储,而不会采用真正的HashMap结构,对应的value redisObject的encoding为zipmap,当成员数量增大时会自动转成真正的HashMap,此时encoding为ht。

    List

    常用命令:lpush,rpush,lpop,rpop,lrange等。
    应用场景:
    Redis list的应用场景非常多,也是Redis最重要的数据结构之一,比如twitter的关注列表,粉丝列表等都可以用Redis的list结构来实现。
    Lists 就是链表,相信略有数据结构知识的人都应该能理解其结构。使用Lists结构,我们可以轻松地实现最新消息排行等功能。Lists的另一个应用就是消息队列,
    可以利用Lists的PUSH操作,将任务存在Lists中,然后工作线程再用POP操作将任务取出进行执行。Redis还提供了操作Lists中某一段的api,你可以直接查询,删除Lists中某一段的元素。
    实现方式:
    Redis list的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销,Redis内部的很多实现,包括发送缓冲队列等也都是用的这个数据结构。

    Set

    常用命令:
    sadd,spop,smembers,sunion 等。
    应用场景:
    Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
    Sets 集合的概念就是一堆不重复值的组合。利用Redis提供的Sets数据结构,可以存储一些集合性的数据,比如在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis还为集合提供了求交集、并集、差集等操作,可以非常方便的实现如共同关注、共同喜好、二度好友等功能,对上面的所有集合操作,你还可以使用不同的命令选择将结果返回给客户端还是存集到一个新的集合中。
    实现方式:
    set 的内部实现是一个 value永远为null的HashMap,实际就是通过计算hash的方式来快速排重的,这也是set能提供判断一个成员是否在集合内的原因。
    Sorted Set

    常用命令:
    zadd,zrange,zrem,zcard等
    使用场景:
    Redis sorted set的使用场景与set类似,区别是set不是自动有序的,而sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。当你需要一个有序的并且不重复的集合列表,那么可以选择sorted set数据结构,比如twitter 的public timeline可以以发表时间作为score来存储,这样获取时就是自动按时间排好序的。

    另外还可以用Sorted Sets来做带权重的队列,比如普通消息的score为
    1,重要消息的score为
    2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。

    实现方式:

    Redis sorted set的内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员,排序依据是HashMap里存的score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。
    Pub/Sub
    Pub/Sub 从字面上理解就是发布(Publish)与订阅(Subscribe),在Redis中,你可以设定对某一个key值进行消息发布及消息订阅,当一个 key值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能。
    Transactions
    谁说NoSQL都不支持事务,虽然Redis的Transactions提供的并不是严格的ACID的事务(比如一串用EXEC提交执行的命令,在执行中服务器宕机,那么会有一部分命令执行了,剩下的没执行),但是这个Transactions还是提供了基本的命令打包执行的功能(在服务器不出问题的情况下,可以保证一连串的命令是顺序在一起执行的,中间有会有其它客户端命令插进来执行)。Redis还提供了一个Watch功能,你可以对一个key进行 Watch,然后再执行Transactions,在这过程中,如果这个Watched的值进行了修改,那么这个Transactions会发现并拒绝执行。

    Redis实际应用场景

    Redis在很多方面与其他数据库解决方案不同:它使用内存提供主存储支持,而仅使用硬盘做持久性的存储;它的数据模型非常独特,用的是单线程。另一个大区别在于,你可以在开发环境中使用Redis的功能,但却不需要转到Redis。
    转向Redis当然也是可取的,许多开发者从一开始就把Redis作为首选数据库;但设想如果你的开发环境已经搭建好,应用已经在上面运行了,那么更换数据库框架显然不那么容易。另外在一些需要大容量数据集的应用,Redis也并不适合,因为它的数据集不会超过系统可用的内存。所以如果你有大数据应用,而且主要是读取访问模式,那么Redis并不是正确的选择。
    然而我喜欢Redis的一点就是你可以把它融入到你的系统中来,这就能够解决很多问题,比如那些你现有的数据库处理起来感到缓慢的任务。这些你就可以通过 Redis来进行优化,或者为应用创建些新的功能。在本文中,我就想探讨一些怎样将Redis加入到现有的环境中,并利用它的原语命令等功能来解决 传统环境中碰到的一些常见问题。在这些例子中,Redis都不是作为首选数据库。

    1、显示最新的项目列表

    下面这个语句常用来显示最新项目,随着数据多了,查询毫无疑问会越来越慢。
    SELECT * FROM foo WHERE … ORDER BY time DESC LIMIT 10
    在Web应用中,“列出最新的回复”之类的查询非常普遍,这通常会带来可扩展性问题。这令人沮丧,因为项目本来就是按这个顺序被创建的,但要输出这个顺序却不得不进行排序操作。
    类似的问题就可以用Redis来解决。比如说,我们的一个Web应用想要列出用户贴出的最新20条评论。在最新的评论边上我们有一个“显示全部”的链接,点击后就可以获得更多的评论。
    我们假设数据库中的每条评论都有一个唯一的递增的ID字段。
    我们可以使用分页来制作主页和评论页,使用Redis的模板,每次新评论发表时,我们会将它的ID添加到一个Redis列表:
    LPUSH latest.comments
    我们将列表裁剪为指定长度,因此Redis只需要保存最新的5000条评论:
    LTRIM latest.comments 0 5000
    每次我们需要获取最新评论的项目范围时,我们调用一个函数来完成(使用伪代码):
    FUNCTION get_latest_comments(start, num_items):
    id_list = redis.lrange(“latest.comments”,start,start+num_items – 1)
    IF id_list.length < num_items
    id_list = SQL_DB(“SELECT … ORDER BY time LIMIT …”)
    END
    RETURN id_list
    END
    这里我们做的很简单。在Redis中我们的最新ID使用了常驻缓存,这是一直更新的。但是我们做了限制不能超过5000个ID,因此我们的获取ID函数会一直询问Redis。只有在start/count参数超出了这个范围的时候,才需要去访问数据库。
    我们的系统不会像传统方式那样“刷新”缓存,Redis实例中的信息永远是一致的。SQL数据库(或是硬盘上的其他类型数据库)只是在用户需要获取“很远”的数据时才会被触发,而主页或第一个评论页是不会麻烦到硬盘上的数据库了。

    2、删除与过滤

    我们可以使用LREM来删除评论。如果删除操作非常少,另一个选择是直接跳过评论条目的入口,报告说该评论已经不存在。
    有些时候你想要给不同的列表附加上不同的过滤器。如果过滤器的数量受到限制,你可以简单的为每个不同的过滤器使用不同的Redis列表。毕竟每个列表只有5000条项目,但Redis却能够使用非常少的内存来处理几百万条项目。

    3、排行榜相关

    另一个很普遍的需求是各种数据库的数据并非存储在内存中,因此在按得分排序以及实时更新这些几乎每秒钟都需要更新的功能上数据库的性能不够理想。
    典型的比如那些在线游戏的排行榜,比如一个Facebook的游戏,根据得分你通常想要:
    – 列出前100名高分选手
    – 列出某用户当前的全球排名
    这些操作对于Redis来说小菜一碟,即使你有几百万个用户,每分钟都会有几百万个新的得分。
    模式是这样的,每次获得新得分时,我们用这样的代码:
    ZADD leaderboard
    你可能用userID来取代username,这取决于你是怎么设计的。
    得到前100名高分用户很简单:ZREVRANGE leaderboard 0 99。
    用户的全球排名也相似,只需要:ZRANK leaderboard 。

    4、按照用户投票和时间排序

    排行榜的一种常见变体模式就像Reddit或Hacker News用的那样,新闻按照类似下面的公式根据得分来排序:
    score = points / time^alpha
    因此用户的投票会相应的把新闻挖出来,但时间会按照一定的指数将新闻埋下去。下面是我们的模式,当然算法由你决定。
    模式是这样的,开始时先观察那些可能是最新的项目,例如首页上的1000条新闻都是候选者,因此我们先忽视掉其他的,这实现起来很简单。
    每次新的新闻贴上来后,我们将ID添加到列表中,使用LPUSH + LTRIM,确保只取出最新的1000条项目。
    有一项后台任务获取这个列表,并且持续的计算这1000条新闻中每条新闻的最终得分。计算结果由ZADD命令按照新的顺序填充生成列表,老新闻则被清除。这里的关键思路是排序工作是由后台任务来完成的。

    5、处理过期项目

    另一种常用的项目排序是按照时间排序。我们使用unix时间作为得分即可。
    模式如下:
    – 每次有新项目添加到我们的非Redis数据库时,我们把它加入到排序集合中。这时我们用的是时间属性,current_time和time_to_live。
    – 另一项后台任务使用ZRANGE…SCORES查询排序集合,取出最新的10个项目。如果发现unix时间已经过期,则在数据库中删除条目。

    6、计数

    Redis是一个很好的计数器,这要感谢INCRBY和其他相似命令。
    我相信你曾许多次想要给数据库加上新的计数器,用来获取统计或显示新信息,但是最后却由于写入敏感而不得不放弃它们。
    好了,现在使用Redis就不需要再担心了。有了原子递增(atomic increment),你可以放心的加上各种计数,用GETSET重置,或者是让它们过期。
    例如这样操作:
    INCR user: EXPIRE
    user: 60
    你可以计算出最近用户在页面间停顿不超过60秒的页面浏览量,当计数达到比如20时,就可以显示出某些条幅提示,或是其它你想显示的东西。

    7、特定时间内的特定项目

    另一项对于其他数据库很难,但Redis做起来却轻而易举的事就是统计在某段特点时间里有多少特定用户访问了某个特定资源。比如我想要知道某些特定的注册用户或IP地址,他们到底有多少访问了某篇文章。
    每次我获得一次新的页面浏览时我只需要这样做:
    SADD page:day1:
    当然你可能想用unix时间替换day1,比如time()-(time()%3600*24)等等。
    想知道特定用户的数量吗?只需要使用SCARD page:day1: 。
    需要测试某个特定用户是否访问了这个页面?SISMEMBER page:day1: 。

    8、实时分析正在发生的情况,用于数据统计与防止垃圾邮件等

    我们只做了几个例子,但如果你研究Redis的命令集,并且组合一下,就能获得大量的实时分析方法,有效而且非常省力。使用Redis原语命令,更容易实施垃圾邮件过滤系统或其他实时跟踪系统。

    9、Pub/Sub

    Redis的Pub/Sub非常非常简单,运行稳定并且快速。支持模式匹配,能够实时订阅与取消频道。

    10、队列

    你应该已经注意到像list push和list pop这样的Redis命令能够很方便的执行队列操作了,但能做的可不止这些:比如Redis还有list pop的变体命令,能够在列表为空时阻塞队列。
    现代的互联网应用大量地使用了消息队列(Messaging)。消息队列不仅被用于系统内部组件之间的通信,同时也被用于系统跟其它服务之间的交互。消息队列的使用可以增加系统的可扩展性、灵活性和用户体验。非基于消息队列的系统,其运行速度取决于系统中最慢的组件的速度(注:短板效应)。而基于消息队列可以将系统中各组件解除耦合,这样系统就不再受最慢组件的束缚,各组件可以异步运行从而得以更快的速度完成各自的工作。
    此外,当服务器处在高并发操作的时候,比如频繁地写入日志文件。可以利用消息队列实现异步处理。从而实现高性能的并发操作。

    11、缓存

    Redis的缓存部分值得写一篇新文章,我这里只是简单的说一下。Redis能够替代memcached,让你的缓存从只能存储数据变得能够更新数据,因此你不再需要每次都重新生成数据了。

    更多相关内容
  • 前言 Redis是一个key-value存储系统,现在在各种系统中的使用越来越多,大部分情况下是因为其高性能的特性,被...缓存是Redis最常见的应用场景,之所有这么使用,主要是因为Redis读写性能优异。而且逐渐有取代memcache
  • Remote Dictionary Server(Redis) 是一个开源的由Salvatore Sanfilippo使用ANSI C语言开发的key-value数据存储服务器。其值(value)可以是 字符串(String), 哈希(Map), 列表(list), 集合(sets) 和 有序集合(sorted ...

    Remote Dictionary Server(Redis) 是一个开源的由Salvatore Sanfilippo使用ANSI C语言开发的key-value数据存储服务器。其值(value)可以是 字符串(String), 哈希(Map), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型,所以它通常也被称为数据结构服务器。

    d7a8d4a14af3df3a3a79d3e4a37810ed.png

    Redis特点

    redis足够简单和稳定

    支持丰富的数据结构

    内存存储读写性能优秀

    提供持久化的支持

    支持事务操作

    提供主从复制功能

    Redis与memcache性能压力测试比较

    62f9e1f84809bdca4b4103c80b9c198b.png

    Redis的典型应用场景:

    一:缓存热点数据

    热点数据(经常会被查询,但是不经常被修改或者删除的数据),首选是使用redis缓存,redis的性能非常优秀。

    二:计数器

    诸如统计点击数、访问数、点赞数、评论数、浏览数等应用,由于单线程,可以避免并发问题,保证数据的正确性,并且100%毫秒级性能,同时开启Redis持久化,以便于持久化数据。

    三:单线程机制

    验证前端的重复请求,可以自由扩展类似情况),可以通过redis进行过滤,比如,每次请求将Request IP、参数、接口等hash作为key存储redis(幂等性请求),设置多长时间有效期,然后下次请求过来的时候先在redis中检索有没有这个key,进而验证是不是一定时间内过来的重复提交;再比如,限制用户登录的次数,比如一天错误登录次数10次等。

    秒杀系统,基于redis是单线程特征,防止出现数据库超卖;

    全局增量ID生成等;

    四:排行榜

    谁得分高谁排名在前,比如点击率最高、活跃度最高、销售数量最高、投票最高的前10名排行等等;

    五:分布式锁

    使用redis可以实现分布式锁,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

    互斥性,在任意时刻,只有一个客户端能持有锁。

    不会发生死锁,即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。

    具有容错性,只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。

    解铃还须系铃人,加锁和解锁必须是同一个客户端,客户端不能解他人加的锁。

    六:Session存储

    使用Redis的进行会话缓存(session cache)是非常常见的一种场景。用Redis缓存会话比其他存储(如Memcached)的优势在于:Redis提供持久化,目前大量的方案均采用了redis作为session的存储方案。

    展开全文
  • redis使用场景及案例

    万次阅读 多人点赞 2019-08-19 14:49:30
    最近在写一个篮球社区项目,其中redis的使用场景还挺多的,于是结合项目总结一下redis的使用场景 一、缓存 项目场景:用户登录或注册时的验证码存储,用户名 set Code:1:code 1232 EX 100 NX OK get Code...

    最近在写一个篮球社区项目,其中redis的使用场景还挺多的,于是结合项目总结一下redis的使用场景

    一、缓存

    项目场景:用户登录或注册时的验证码存储,用户名

    set Code:1:code 1232 EX 100 NX

    OK

    get Code:1:code

    “1232”

    set User:1:name bob EX 100 NX

    OK

    get User:1:name

    “bob”
    缓存是 redis 出镜率最高的一种使用场景,仅仅使用 set/get 就可以实现,不过也有一些需要考虑的点
    1、如何更好的设置缓存
    2、如何保持缓存与上游数据的一致性
    3、如何解决缓存雪崩,缓存击穿问题(这两个问题会单独写一篇)

    二:消息队列

    lpush UserEmailQueue 1 2 3 4

    lpop UserEmailQueue

    rpop UserEmailQueue

    1

    rpop UserEmailQueue

    2

    可以把redis的队列看为分布式队列,作为消息队列时,生产者在一头塞入数据。消费者另一条取出数据:(lpush/rpop,rpush/lpop),不过也有一些不足,而这些不足有可能是致命的,不过对于一些丢几条消息也没关系的场景还是可以考虑的
    1、没有ack(消息确认机制),有可能丢消息
    2、需要做redis的持久化配置
    用redis实现消息队列

    三:过滤器(dupefilter)

    sadd UrlSet http://1
    (integer) 1
    sadd UrlSet http://2
    (integer) 1
    sadd UrlSet http://2
    (integer) 0

    smembers UrlSet

    1. “http://1”
    2. “http://2”
      scrapy-redis作为分布式的爬虫框架,便是使用了 redis 的 Set 这个数据结构来对将要爬取的 url 进行去重处理。
    def request_seen(self, request):  
      """Returns True if request was already seen.   
       Parameters    
       ----------   
        request : scrapy.http.Request    
        Returns    
        -------
         bool    
         """    
         fp = self.request_fingerprint(request)    
         added = self.server.sadd(self.key, fp)    
         return added == 0
    

    不过当 url 过多时,会有内存占用过大的问题

    四、分布式锁

    分布式锁,这个是除了 KV 缓存之外最为常用的另一个特色功能。

    set Lock:User:10086 06be97fc-f258-4202-b60b-8d5412dd5605 EX 60 NX
    释放锁,一段 LUA 脚本
    
    if redis.call("get",KEYS[1]) == ARGV[1] then
    	  return redis.call("del",KEYS[1])
    else
    	 return 0
    end
    

    这是一个最简单的单机版的分布式锁,有以下要点
    1)EX 表示锁会过期释放
    2)NX 保证原子性
    解锁时对比资源对应产生的 UUID,避免误解锁
    当你使用分布式锁是为了解决一些性能问题,如分布式定时任务防止执行多次 (做好幂等性),而且鉴于单点 redis 挂掉的可能性很小,可以使用这种单机版的分布式锁。
    举个例子说明:
    比如一个很能干的资深工程师,开发效率很快,代码质量也很高,是团队里的明星。所以呢诸多产品经理都要来烦他,让他给自己做需求。如果同一时间来了一堆产品经理都找他,它的思路呢就会陷入混乱,再优秀的程序员,大脑的并发能力也好不到哪里去。所以呢他就在自己的办公室的门把上挂了一个请勿打扰的牌子,当一个产品经理来的时候先看看门把上有没有这个牌子,如果没有呢就可以进来找工程师谈需求,谈之前要把牌子挂起来,谈完了再把牌子摘了。这样其它产品经理也要来烦他的时候,如果看见这个牌子挂在那里,就可以选择睡觉等待或者是先去忙别的事。如是这位明星工程师从此获得了安宁。

    一定要设置这个过期时间,因为遇到特殊情况 —— 比如地震(进程被 kill -9,或者机器宕机),产品经理可能会选择从窗户上跳下去,没机会摘牌,导致了死锁饥饿,让这位优秀的工程师成了一位大闲人,造成严重的资源浪费。同时还需要注意这个 owner_id,它代表锁是谁加的 —— 产品经理的工号。以免你的锁不小心被别人摘掉了。释放锁时要匹配这个 owner_id,匹配成功了才能释放锁。这个 owner_id 通常是一个随机数,存放在 ThreadLocal 变量里(栈变量)。官方其实并不推荐这种方式,因为它在集群模式下会产生锁丢失的问题 —— 在主从发生切换的时候。官方推荐的分布式锁叫 RedLock,作者认为这个算法较为安全,推荐我们使用。不过我们一直还使用上面最简单的分布式锁。为什么我们不去使用 RedLock 呢,因为它的运维成本会高一些,需要 3 台以上独立的 Redis 实例,用起来要繁琐一些。另外,Redis 集群发生主从切换的概率也并不高,即使发生了主从切换出现锁丢失的概率也很低,因为主从切换往往都有一个过程,这个过程的时间通常会超过锁的过期时间,也就不会发生锁的异常丢失。还有呢就是分布式锁遇到锁冲突的机会也不多,这正如一个公司里明星程序员也比较有限一样,总是遇到锁排队那说明结构上需要优化。

    五:定时任务

    分布式定时任务有多种实现方式,最常见的一种是 master-workers 模型。
    master 负责管理时间,到点了就将任务消息仍到消息中间件里,然后worker们负责监听这些消息队列来消费消息。
    著名的 Python 定时任务框架 Celery 就是这么干的。但是 Celery 有一个问题,那就是 master 是单点的,如果这个 master 挂了,整个定时任务系统就停止工作了。
    在这里插入图片描述

    另一种实现方式是 multi-master 模型。这个模型什么意思呢,就类似于 Java 里面的 Quartz 框架,采用数据库锁来控制任务并发。
    会有多个进程,每个进程都会管理时间,时间到了就使用数据库锁来争抢任务执行权,抢到的进程就获得了任务执行的机会,然后就开始执行任务,这样就解决了 master 的单点问题。

    这种模型有一个缺点,那就是会造成竞争浪费问题,不过通常大多数业务系统的定时任务并没有那么多,所以这种竞争浪费并不严重。
    还有一个问题它依赖于分布式机器时间的一致性,如果多个机器上时间不一致就会造成任务被多次执行,这可以通过增加数据库锁的时间来缓解。
    在这里插入图片描述
    现在有了 Redis 分布式锁,那么我们就可以在 Redis 之上实现一个简单的定时任务框架。

    #注册定时任务
    hset tasks name trigger_rule
    #获取定时任务列表
    hgetall tasks
    # 争抢任务
    set lock:$(name) true nx ex=5
    # 任务列表变空
    # 轮询版本号,有变化就重新加载任务列表,重新调度时间有变化的任务
    set tasks_version $new_version
    get tasks_version
    

    六、频率控制

    项目的社区功能里,不可避免的总是会遇到垃圾内容,一觉醒来你会发现首页突然会被某些恶意的帖子和广告刷屏了,如果不采取适当的机制来控制就会导致用户体验受到严重的影响
    控制广告垃圾贴的策略很多,高级一点的可以通过AI,最简单的方式是通过关键词扫描,还有比较常用的一种方式是频率控制,限制单个用户内容的生产速度,不通等级的用户会有不同的频率控制参数
    频率控制就可以使用redis来实现,我们将用户的行为理解为一个时间序列,我们要保证在一定的时间内限制单个用户的时间序列的长度,超过这个长度就禁止用户的行为,它可以是用redis的zset(有序集合,zset详解)来实现
    在这里插入图片描述
    图中绿色的部门就是我们要保留的一个时间段的时间序列信息,灰色的段会被砍掉。统计绿色段中时间序列记录的个数就知道是否超过了频率的阈值。

    下面的代码控制用户的ugc行为为每小时最对N次
    hist_key:"ugc:${user_id}"
    with redis.pipeline() as pipe:
    # 记录当前的行为
    	pipe.zadd(hist_key,ts,uuid)
    	#保留1小时内的行为序列
    	pipe.zremrangebyscore(hist_key, 0, now_ts -3600)
    	# 获取这1小时的行为数量
    	pipe.zcard(hist_key)
    	# 设置过期时间,节约内存
    	pipe.expire(hist_key, 3600)
    	# 批量执行
    	_ , _ , count, _ =pipe.exec()
    	return count > N
    	
    

    七、服务发现

    如果想要技术成熟度再高一些,有的企业会有服务发现的基础设施。通常我们都会选用zookeeper、etcd,consul等分布式配置数据库来作为服务列表的存储
    它们有非常及时的通知机制来通知服务消费者服务列表发生了变更。那我们该如何使用 Redis 来做服务发现呢?
    在这里插入图片描述
    这里我们要再次使用 zset 数据结构,我们使用 zset 来保存单个服务列表。多个服务列表就使用多个 zset 来存储。
    zset 的 value 和 score 分别存储服务的地址和心跳的时间。服务提供者需要使用心跳来汇报自己的存活,每隔几秒调用一次 zadd。服务提供者停止服务时,使用 zrem 来移除自己。

    zadd service_key heartbeat_ts addr
    zrem service_key addr
    

    这样还不够,因为服务有可能是异常终止,根本没机会执行钩子,所以需要使用一个额外的线程来清理服务列表中的过期项

    zremrangebyscore service_key 0 now_ts -30 # 30s都没来心跳
    

    接下来还有一个重要的问题是如何通知消费者服务列表发生了变更,这里我们同样使用版本号轮询机制,当服务列表变更时,递增版本号。消费者通过轮询版本号的变化来重加载服务列表

    if zadd() >0 || zrem() >0 ||zremrangebuscore() >0:
    		incr service_version_key
    

    但是还有一个问题,如果消费者依赖了很多的服务列表,那么它就需要轮询很多的版本号,这样的IO效率会比较低下。

    这是我们可以再增加一个全局版本号,在任意的服务类表版本号发生变化时,递增全局版本号
    这样在正常情况下消费者只需要轮询全局版本号就可以了。当全局版本号发生变更时再挨个对依赖的服务类表的子版本号,然后加载有变更的服务列表
    在这里插入图片描述

    八、位图

    项目里需要做一个球队成员的签到系统,当用户量比较少的时候,设计上比较简单,就是将用户的签到状态用redis的hash结构来存储,签到一次就再hash结构里记录一条,签到有三种状态:未签到,已签到和部签到,分别是0,1,2三个整数值

    hset sign:$(user_id) 2019-08-11 1
    hset sign:$(user_id) 2019-08-12 0
    hset sign:$(user_id) 2019-08-14 2
    
    

    这个其实非常浪费用户空间,后来想做全部用户的签到,技术leader指出,这时候的再用hash就有问题了,他讲到当用户过千万的时候,内存可能会飚到 30G+,我们线上实例通常过了 20G 就开始报警,30G 已经属于严重超标了。
    这时候我们就开始着手解决这个问题,去优化存储。我们选择使用位图来记录签到信息,一个签到状态需要两个位来记录,一个月的存储空间只需要 8 个字节。这样就可以使用一个很短的字符串来存储用户一个月的签到记录。

    在这里插入图片描述
    但是位图也有一个缺点,它的底层是字符串,字符串是连续存储空间,位图会自动扩展,比如一个很大的位图 8m 个位,只有最后一个位是 1,其它位都是零,这也会占用1m 的存储空间,这样的浪费非常严重。
    所以呢就有了咆哮位图这个数据结构,它对大位图进行了分段存储,全位零的段可以不用存。
    另外还对每个段设计了稀疏存储结构,如果这个段上置 1 的位不多,可以只存储它们的偏移量整数。这样位图的存储空间就得到了非常显著的压缩。

    九、 模糊计数

    上面提到的签到系统,如果产品经理需要知道这个签到的日活月活怎么办呢?
    通常我们会直接甩锅——请找数据部门。
    但是数据部门的数据往往不是很实时,经常前一天的数据需要第二天才能跑出来,离线计算是通常是定时的一天一次。那如何实现一个实时的活跃计数?
    最简单的方案就是在 Redis 里面维护一个 set 集合,来一个用户,就 sadd 一下,最终集合的大小就是我们需要的 UV 数字。
    但是这个空间浪费严重怎么办?这时候就需要使用redis提供的HyperLogLog模糊计数功能,它是一种概率计数,有一定的误差,大约是0.81%。
    但是空间占用很小,其底层是一个位图,它最多只会占用12k的存储空间,而且在计数值比较小的时候,位图使用稀疏存储,空间占用就更小了。

    #记录用户
    pfadd sign_uv_${day} user_id
    #获取记录数量
    p count sign_uv_${day}
    
    

    微信公众号文章的阅读数可以使用它,网页的 UV 统计它都可以完成。但是如果产品经理非常在乎数字的准确性,比如某个统计需求和金钱直接挂钩,那么你可以考虑一下前面提到的咆哮位图。

    它使用起来会复杂一些,需要提前将用户 ID 进行整数序列化。Redis 没有原生提供咆哮位图的功能,但是有一个开源的 Redis Module 可以拿来即用。

    十、布隆过滤器

    如果系统即将会有大量的新用户涌入时,布隆过滤器就会非常有价值,可以显著降低缓存的穿透率,降低数据库的压力
    这个新用户的涌入不一定是业务系统的大规模铺开,也可能是因为来自外部的缓存穿透攻击

    def get_user_state(user_id):
    		state = cache.get(user_id)
    		if not state:
    			state = db.get(user_id) or{}
    			cache.set(user_id,state)
    		return state
    def save_user_state(user_id,state):
    		cache.set(user_id,state)
    		db.set_async(user_id,state)
    

    比如就上面这个业务系统的用户状态查询接口代码,现在一个新用户过来,会先去缓存里查询有没有这个用户的状态数据
    因为是新用户,所以肯定缓存里没有,然后它就要去数据库里查,结过数据库也没有,如果这样的新用户大批量瞬间涌入,那么可以遇见数据库的压力会比较大,会存在大量的空查询
    我们非常希望redis里面有这样一个set,它存放了所有的用户id,这样通过查询这个set集合就知道是不是新用户来了
    当用户量非常庞大的时候,维护这样的一个集合需要的存储空间是很大的
    这时候就可以使用布隆过滤器,它相当于一个set,但是又不同于set,它需要的存储空间要小的多
    比如存储一个用户id需要64个字节,而布隆过滤器存储一个用户ID只需要1个字节多点,其实它存的不是用户id,而是用户id的指纹,所以会存在一定的小概率误判,它是一个具备模糊过滤能力的容器
    在这里插入图片描述

    当它说用户 id 不在容器中时,那么就肯定不在。当它说用户 id 在容器里时,99% 的概率下它是正确的,还有 1% 的概率它产生了误判。

    不过在这个案例中,这个误判并不会产生问题,误判的代价只是缓存穿透而已。
    相当于有 1% 的新用户没有得到布隆过滤器的保护直接穿透到数据库查询,而剩下的 99% 的新用户都可以被布隆过滤器有效的挡住,避免了缓存穿透
    在这里插入图片描述
    布隆过滤器的原理有一个很好的比喻,那就是在冬天一片白雪覆盖的地面上,如果你从上面走过,就会留下你的脚印。如果地面上有你的脚印,那么就可以大概率断定你来过这个地方,但是也不一定,也许别人的鞋正好和你穿的一模一样。可是如果地面上没有你的脚印,那么就可以 100% 断定你没来过这个地方

    说明:该文写到一半时,偶然看到了下面简书作者的这篇文章,原文写的很好,跟我的项目中的一些业务逻辑很吻合,故将其引用过来,如有疑惑,请去原文阅读Java老王
    展开全文
  • Redis项目应用场景实例汇总

    千次阅读 2021-01-25 16:10:29
    文章目录一、背景1.1 开发环境1.2 项目配置二、字符串的应用场景:封锁一个IP地址三、Hash的应用场景:存储用户信息四、List的应用场景:队列实现五、Set的应用场景:自动去重 一、背景 本篇文章是将以前Redis实战...

    一、背景

    • 本篇文章是将以前Redis实战的系列文章进行汇总,针对Redis中常用的一些数据结构,进行实战模拟。
    stringshasheslistssetssorted sets
    封锁一个IP地址存储用户信息模拟消息队列自动排重以某一个条件为权重,进行排序

    1.1 开发环境

    1. JDK 1.8
    2. SpringBoot 2.2.5
    3. JPA
    4. Spring Security
    5. Mysql 8.0
    6. Redis Server 3.2.1
    7. Redis Desktop Manager
    8. Swagger2

    1.2 项目配置

    • SpringBoot集成Redis, 添加依赖
    <!--pom.xl-->
    	   <!--Redis-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
    
    • 项目配置文件 添加redis连接配置
    <!-- application.yml-->
    server:
      port: 8000
    
    spring:
        freemarker:
          check-template-location: false
        profiles:
          active: dev
        jackson:
          time-zone: GMT+8
        data:
          redis:
            repositories:
              enabled: false
    
        #配置 Jpa
        jpa:
          properties:
            hibernate:
              dialect: org.hibernate.dialect.MySQL5InnoDBDialect
          open-in-view: true
    
        redis:
          database: 0
          host: 127.0.0.1
          port: 6379
          password:
    
    
    • 增加RedisConfig配置类
      在这里插入图片描述
    /**
     * Redis配置类
     *
     * @author zhuhuix
     */
    @Configuration
    @EnableCaching
    @ConditionalOnClass(RedisOperations.class)
    @EnableConfigurationProperties(RedisProperties.class)
    public class RedisConfig extends CachingConfigurerSupport {
    
        /**
         *设置 redis 数据默认过期时间
         */
        @Bean
        public RedisCacheConfiguration redisCacheConfiguration(){
            FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
            RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig();
            configuration = configuration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(fastJsonRedisSerializer)).entryTtl(Duration.ofHours(Constant.CACHE_TIMEOUT_HOUR));
            return configuration;
        }
    
    
        @Bean
        public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
            RedisTemplate<Object, Object> template = new RedisTemplate<>();
            template.setConnectionFactory(redisConnectionFactory);
    
            FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer(Object.class);
            template.setValueSerializer(fastJsonRedisSerializer);
            template.setHashValueSerializer(fastJsonRedisSerializer);
    
            ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    
            template.setKeySerializer(new StringRedisSerializer());
            template.setHashKeySerializer(new StringRedisSerializer());
    
            template.afterPropertiesSet();
            return template;
        }
    
        /**
         * 参考:https://blog.csdn.net/qq_15071263/article/details/84335632
         * 自定义缓存key生成策略,默认将使用该策略
         */
        @Bean
        @Override
        public KeyGenerator keyGenerator() {
            return (target, method, params) -> {
                Map<String,Object> container = new HashMap<>(3);
                Class<?> targetClassClass = target.getClass();
                // 类地址
                container.put("class",targetClassClass.toGenericString());
                // 方法名称
                container.put("methodName",method.getName());
                // 包名称
                container.put("package",targetClassClass.getPackage());
                // 参数列表
                for (int i = 0; i < params.length; i++) {
                    container.put(String.valueOf(i),params[i]);
                }
                // 转为JSON字符串
                String jsonString = JSON.toJSONString(container);
                // 做SHA256 Hash计算,得到一个SHA256摘要作为Key
                return DigestUtils.sha256Hex(jsonString);
            };
        }
    
    
    • 增加RedisUtils工具类:实现对各种数据结构的封装
    /**
     * Redis工具类
     *
     * @author zhuhuix
     */
    @Component
    @AllArgsConstructor
    public class RedisUtils {
        private RedisTemplate<Object, Object> redisTemplate;
    
        /**
         * HashGet根据键值得到对象
         *
         * @param key  键值 @NotNull
         * @param item 项目 @NotNull
         * @return 对象
         */
        public Object hashGet(String key, String item) {
            return redisTemplate.opsForHash().get(key, item);
        }
    
        /**
         * 根据键值向hash表中写入对象
         *
         * @param key   键值 @NotNull
         * @param item  项目 @NotNull
         * @param value 对象 @NotNull
         * @return true 成功 false失败
         */
        public boolean hashSet(String key, String item, Object value) {
            try {
                redisTemplate.opsForHash().put(key, item, value);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
    
        }
    
        /**
         * 根据键值向hash表中写入对象,并设置过期时间
         *
         * @param key   键值 @NotNull
         * @param item  项目 @NotNull
         * @param value 对象 @NotNull
         * @param time  过期时间(秒) @NotNull
         * @return true 成功 false失败
         */
        public boolean hashSet(String key, String item, Object value, long time) {
            try {
                redisTemplate.opsForHash().put(key, item, value);
                if (time > 0) {
                    expire(key, time);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 根据键值对某一项目的进行累加计数
         *
         * @param key 键值
         * @param l   累加数
         */
        public long increment(String key, long l) {
            return redisTemplate.opsForValue().increment(key, l);
        }
    
        /**
         * 根据键值对某一项目的进行累加计数,并设置过期时间
         *
         * @param key  键值
         * @param l    累加数
         * @param time 过期时间(秒)
         */
        public long increment(String key, long l, long time) {
            long count = redisTemplate.opsForValue().increment(key, l);
            if (time > 0) {
                expire(key, time);
            }
            return count;
        }
    
        /**
         * 入队
         *
         * @param key   队列键值
         * @param value 元素
         * @return 添加数量
         */
        public long leftPush(String key, Object value) {
            return redisTemplate.opsForList().leftPush(key, value);
        }
    
        /**
         * 向队列头部添加全部集合元素
         *
         * @param key  队列键值
         * @param list 集合
         * @return 返回添加的数量
         */
        public long leftPushAll(String key, List<Object> list) {
            return redisTemplate.opsForList().leftPushAll(key, list);
        }
    
        /**
         * 统计队列中所有元素数量
         *
         * @param key 队列键值
         * @return 队列中元素数量
         */
        public long size(String key) {
            return redisTemplate.opsForList().size(key);
        }
    
        /**
         * 返回队列中从起始位置到结束位置的集合元素
         *
         * @param key   队列键值
         * @param start 起始位置
         * @param end   结束位置
         * @return 返回集合
         */
        public List<Object> range(String key, long start, long end) {
            return redisTemplate.opsForList().range(key, start, end);
        }
    
        /**
         * 出队
         *
         * @param key 队列键值
         * @return 元素
         */
        public Object rightPop(String key) {
            return redisTemplate.opsForList().rightPop(key);
        }
    
        /**
         * 弹出队列最新元素
         *
         * @param key 队列键值
         * @return 元素
         */
        public Object leftPop(String key) {
            return redisTemplate.opsForList().leftPop(key);
        }
    
        /**
         * 删除队列所有元素
         *
         * @param key 队列键值
         */
        public void deleteAll(String key) {
            redisTemplate.opsForList().trim(key, 0, 0);
            redisTemplate.opsForList().leftPop(key);
        }
    
        /**
         * 向集合中增加元素
         *
         * @param key   集合键值
         * @param value 元素
         * @return 添加数量
         */
        public long setAdd(String key, Object value) {
            return redisTemplate.opsForSet().add(key,value);
        }
    
        /**
         * 向集合中批量增加元素
         *
         * @param key  集合键值
         * @param list 元素列表
         * @return 添加数量
         */
        public long setAdd(String key, List<Object> list) {
            return redisTemplate.opsForSet().add(key,list);
        }
    
        /**
         * 集合删除指定元素
         *
         * @param key   集合键值
         * @param value 指定元素
         * @return 删除数量
         */
        public long setRemove(String key, Object value) {
            return redisTemplate.opsForSet().remove(key, value);
        }
    
        /**
         * 集合批量删除指定元素
         *
         * @param key  集合键值
         * @param list 指定元素列表
         * @return 删除数量
         */
        public long setRemove(String key, List<Object> list) {
            return redisTemplate.opsForSet().remove(key, list);
        }
    
        /**
         * 取出两信集合的交集
         *
         * @param key1 集合1键值
         * @param key2 集合2键值
         * @return 交集
         */
        public Set<Object> setInter(String key1, String key2) {
            return redisTemplate.opsForSet().intersect(key1, key2);
        }
    
        /**
         * 取出多个集合的交集
         *
         * @param keys 键值列表
         * @return 交集
         */
        public Set<Object> setInter(List<Object> keys) {
            return redisTemplate.opsForSet().intersect(keys);
        }
    
        /**
         * 取出两个集合的差集
         *
         * @param key1 集合1键值
         * @param key2 集合2键值
         * @return 差集
         */
        public Set<Object> setDifference(String key1,String key2){
            return redisTemplate.opsForSet().difference(key1,key2);
        }
    
        /**
         * 指定缓存的失效时间
         *
         * @param key  键值 @NotNull
         * @param time 时间(秒) @NotNull
         */
        public boolean expire(String key, long time) {
            try {
                if (time > 0) {
                    redisTemplate.expire(key, time, TimeUnit.SECONDS);
                }
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
            return true;
        }
    
    }
    
    

    二、字符串的应用场景:封锁一个IP地址

    • 创建SpringBoot后台服务程序,实现用户登录及JWT认证;
    • 通过Redis缓存限制在1分钟内同一IP请求登录不能超过5次。

    在这里插入图片描述

    • 登录实现类增加Redis计数判断
    /**
     * 授权登录接口实现类
     *
     * @author zhuhuix
     */
    @Slf4j
    @Service
    @Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
    public class AuthServiceImpl implements AuthService {
    
        @Value("${wxMini.appId}")
        private String appId;
        @Value("${wxMini.secret}")
        private String secret;
    
        private final JwtTokenUtils jwtTokenUtils;
        private final WxMiniApi wxMiniApi;
        private final UserService userService;
        private final JwtSecurityProperties properties;
        private final RedisUtils redisUtils;
    
        public AuthServiceImpl(JwtTokenUtils jwtTokenUtils, WxMiniApi wxMiniApi, UserService userService, JwtSecurityProperties properties, RedisUtils redisUtils) {
            this.jwtTokenUtils = jwtTokenUtils;
            this.wxMiniApi = wxMiniApi;
            this.userService = userService;
            this.properties = properties;
            this.redisUtils = redisUtils;
        }
    
        @Override
        @Transactional(rollbackFor = Exception.class)
        public Result<AuthUserDto> login(AuthUserDto authUserDto, HttpServletRequest request) {
            // 通过缓存判断同一IP某一时间段内的登录次数是否超出限定次数
            String ip = NetworkUtils.getIp(request);
            String requestLoginIp = "request_login_".concat(ip);
            long loginCount = redisUtils.increment(requestLoginIp, 1L);
            if (loginCount == 1) {
                redisUtils.expire(requestLoginIp, Constant.REQUEST_LOGIN_LIMIT_TIME);
            }
            if (loginCount > Constant.REQUEST_LOGIN_LIMIT_COUNT) {
                log.warn("IP:[".concat(ip).concat("]已超出限定次数"));
                throw new RuntimeException("时间段内已超出限定次数,请不要频繁登录!");
            }
    
           ...
           ...
        }
    }
    
    
    • 测试与验证
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述

    三、Hash的应用场景:存储用户信息

    创建SpringBoot后台服务程序,实现微信小程序登录及JWT认证;
    通过Redis缓存记录该用户最后一次登录时间及登录累计次数。

    /**
     * 授权登录接口实现类--增加redis缓存哈希表应用
     *
     * @author zhuhuix
     */
    @Slf4j
    @Service
    @Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
    public class AuthServiceImpl implements AuthService {
    		....
    		....
    		
     		// 将当前用户信息与登录时间写入Redis缓存的哈希表
     		// 以微信登录用户的openId作为哈希键值
            String key = authUserDto.getUserInfo().getOpenId();
            
            redisUtils.hashSet(key, "id", authUserDto.getUserInfo().getId());
            redisUtils.hashSet(key, "nickName", authUserDto.getUserInfo().getNickName());
            redisUtils.hashSet(key, "getAvatarUrl", authUserDto.getUserInfo().getAvatarUrl());
            redisUtils.hashSet(key, "lastLoginTime", Timestamp.valueOf(LocalDateTime.now()));
            
            // 读取缓存中当前哈希值对应的用户的登录次数
            Long loginCount = 1L;
            Object obj = redisUtils.hashGet(key, "loginCount");
            if (obj != null) {
                loginCount += Long.valueOf(String.valueOf(obj));
            }
            // 累加后回写到哈希表中
            redisUtils.hashSet(key, "loginCount", loginCount);
            ...
      }
    
    • 测试与验证
      在这里插入图片描述
      在这里插入图片描述

    四、List的应用场景:队列实现

    • 创建SpringBoot上传文件WebApi服务接口;
    • 通过Redis缓存队列记录最新10笔用户上传文件的信息。

    缓存队列时序图

    • 图片上传服务源码中增加Redis队列
    /**
     * 微信小程序CRM实现类:实现Redis队列
     *
     * @author zhuhuix
     */
    @Slf4j
    @AllArgsConstructor
    @Service
    @Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
    public class WxMiniCrmImpl implements WxMiniCrm {
    
        ...
    
        @Override
        @Transactional(rollbackFor = Exception.class)
        public Result<CrmIndex> uploadCrmIndex(String json, String openId, String realName, MultipartFile multipartFile) {
            try {
                JSONObject jsonObject = JSONObject.parseObject(json);
    
                String createTime = jsonObject.getString("create");
                String employeeCode = jsonObject.getString("employeeCode");
                String customerCode = jsonObject.getString("customerCode");
                String customerName = jsonObject.getString("customerName");
                String type = jsonObject.getString("type");
    
                if (StringUtils.isEmpty(createTime) || StringUtils.isEmpty(employeeCode) || StringUtils.isEmpty(customerCode)
                        || StringUtils.isEmpty(customerName) || StringUtils.isEmpty(type)) {
                    throw new RuntimeException("上传信息中缺少关键资料");
                }
    
                UploadFile uploadFile = uploadFileTool.upload(openId, realName, multipartFile);
                if (uploadFile == null) {
                    throw new RuntimeException("上传文件失败!");
                }
                CrmIndex crmIndex = new CrmIndex();
                DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm");
                crmIndex.setCreateTime(Timestamp.valueOf(LocalDateTime.parse(createTime, dateTimeFormatter)));
                crmIndex.setEmployeeCode(employeeCode);
                crmIndex.setCustomerCode(customerCode);
                crmIndex.setCustomerName(customerName);
                crmIndex.setType(type);
                crmIndex.setJson(json);
                crmIndex.setOpenId(openId);
                crmIndex.setPath(uploadFile.getPath());
    
                // 将最新10条上传的信息放入redis缓存
                if (redisUtils.size(Constant.REDIS_UPLOAD_QUEUE_NAME) >= Constant.REDIS_UPLOAD_QUEUE_COUNT) {
                    log.warn(Constant.REDIS_UPLOAD_QUEUE_NAME.concat("队列已满,移除最旧上传信息:") + redisUtils.rightPop(Constant.REDIS_UPLOAD_QUEUE_NAME));
                }
                log.info(Constant.REDIS_UPLOAD_QUEUE_NAME.concat("队列增加上传信息:").concat(crmIndex.toString()));
                redisUtils.leftPush(Constant.REDIS_UPLOAD_QUEUE_NAME, crmIndex);
    
                return new Result<CrmIndex>().ok(crmIndexRepository.save(crmIndex));
    
            } catch (JSONException ex) {
                throw new RuntimeException("json转换失败:" + ex.getMessage());
            }
    
        }
    	...
    }
    
    

    文件上传的原理与实现可参考该文章《SpringBoot实现微信小程序文件上传的完整案例》

    • 测试与验证

    • 微信小程序端
      – 前端将识别信息与图片上传至服务器
      在这里插入图片描述
      在这里插入图片描述

    • Redis缓存队列
      – 队列中只保存最新10条(数量可自行调整)信息.:

    在这里插入图片描述

    五、Set的应用场景:自动去重

    • 创建SpringBoot添加客户信息服务接口;
    • 通过Redis集合缓存客户信息,要求自动去重,不得重复记录。

    在这里插入图片描述

    • 客户实体类
    /**
     * CRM客户信息
     *
     * @author zhuhuix
     */
    @Entity
    @Getter
    @Setter
    @Table(name = "customer")
    public class Customer implements Serializable {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @NotNull(groups = Update.class)
        private Long id;
    
        @Column(name = "open_id")
        private String openId;
    
        /**
         * 客户代码
         */
        @Column(name = "customer_code")
        private String customerCode;
    
        /**
         * 客户名称
         */
        @Column(name = "customer_name")
        private String customerName;
    
        /**
         * 首字母
         */
        @Column(name = "first_letter")
        private String firstLetter;
    
        /**
         * 创建时间
         */
        @Column(name = "create_time")
        @CreationTimestamp
        private Timestamp createTime;
    
        /**
         * 更新时间
         */
        @Column(name = "update_time")
        @UpdateTimestamp
        private Timestamp updateTime;
    
        @Override
        public String toString() {
            return "Customer{" +
                    "customerCode='" + customerCode + '\'' +
                    ", customerName='" + customerName + '\'' +
                    '}';
        }
    }
    
    
    • 客户信息WebApi
    	@ApiOperation(value = "通过扫一扫功能上传客户信息")
        @PostMapping(value = "/crmScan/{openId}")
        public ResponseEntity crmScan(@RequestBody WxScanDto wxScanDto, @PathVariable String openId) {
    
            return ResponseEntity.ok(wxMiniCrm.wxScan(wxScanDto, openId));
    
        }
    
    • 记录并缓存客户信息实现类
    /**
     * 微信小程序CRM实现类
     *
     * @author zhuhuix
     */
    @Slf4j
    @AllArgsConstructor
    @Service
    @Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
    public class WxMiniCrmImpl implements WxMiniCrm {
    
        private final UploadFileTool uploadFileTool;
        private final CrmIndexRepository crmIndexRepository;
        private final CustomerRepository customerRepository;
        private final UserService userService;
        private final RedisUtils redisUtils;
    
    	...
    	
        @Override
        @Transactional(rollbackFor = Exception.class)
        public Result<WxScanDto> wxScan(WxScanDto wxScanDto, String openId) {
    
            //微信扫一扫保存客户信息
            if (Constant.SAVE_CUSTOMER_INFO.equals(wxScanDto.getScanType()) && wxScanDto.getJsonObject() != null) {
                try {
                    Customer customer = JSONObject.parseObject(wxScanDto.getJsonObject().toJSONString(), Customer.class);
                    Customer target = customerRepository.findByCustomerCodeAndOpenId(customer.getCustomerCode(), openId);
                    if (target != null) {
                        BeanUtils.copyProperties(customer, target, RepositoryUtil.getNullPropertyNames(customer));
                    } else {
                        target = customer;
                        target.setOpenId(openId);
                    }
                    wxScanDto.setReturnObject(customerRepository.save(target));
                    // 将用户增加的客户信息添加到redis集合中
                    redisUtils.setAdd(openId.concat("_customer"),customer.toString());
    
                    return new Result<WxScanDto>().ok(wxScanDto);
                } catch (JSONException ex) {
                    throw new RuntimeException("json转换失败:" + ex.getMessage());
                }
    
            }
            return new Result<WxScanDto>().error("无法处理扫一扫功能");
        }
    
    
    }
    
    
    • 测试与验证
    1. 相同信息自动去重:通过swagger2进行接口测试,多次提交相同的客户信息
      在这里插入图片描述
    • Redis Desktop Manager验证数据
      – 查看集合中的数据,实现自动去重
      在这里插入图片描述
    1. 不同集合之间的交集与差集:用户1通过接口添加4个客户
      在这里插入图片描述
      –用户2通过接口添加3个客户
      在这里插入图片描述
      – 获取用户1与用户2相同及不同的客户信息
    /**
     * Redis测试
     * 
     * @author zhuhuix
     */
    @SpringBootTest
    @Slf4j
    public class TestSet {
        @Test
        void test() {
            RedisUtils redisUtils = SpringContextHolder.getBean(RedisUtils.class);
            //获取交集:相同客户
            Set<Object> setInter=redisUtils.setInter("openId1_customer","openId2_customer");
            Iterator iterator = setInter.iterator();
            log.info("openId1_customer与openId2_customer相同的客户为:");
            while(iterator.hasNext()){
                log.info(iterator.next().toString());
            }
            //获取差集:不同客户
            Set<Object> setDiff=redisUtils.setDifference("openId1_customer","openId2_customer");
            iterator = setDiff.iterator();
            log.info("openId1_customer与openId2_customer不同的客户为:");
            while(iterator.hasNext()){
                log.warn(iterator.next().toString());
            }
            
     		//获取差集:不同客户
            Set<Object> setDiff1=redisUtils.setDifference("openId2_customer","openId1_customer");
            iterator = setDiff1.iterator();
            log.info("openId2_customer与openId1_customer不同的客户为:");
            while(iterator.hasNext()){
                log.warn(iterator.next().toString());
            }
        }
    }
    
    
    • 测试结果
      在这里插入图片描述
    展开全文
  • redis应用场景及应用实例详解

    千次阅读 2018-06-13 11:37:01
    概述Redis在很多方面与其他数据库解决方案不同:它使用内存提供主存储支持,而仅使用硬盘做持久性的存储;...但设想如果你的开发环境已经搭建好,应用已经在上面运行了,那么更换数据库框架显然不那么容易。另外在一...
  • 需要把用户的信息(id,登录时间,ip等)记录下来以记录活跃人数,但是再次访问接口时就不需要记录了,如果每次访问都去查询数据库是否已经记录活跃度,就比较耗费资源,此时就可以同时存入redis一条信息,每次查询...
  • Redis应用场景

    2020-10-15 11:24:25
    转载:Redis应用场景 NoSQL主要用于解决以下几种问题 1.少量数据存储,高速读写访问。此类产品通过数据全部in-momery 的方式来保证高速访问,同时提供数据落地的功能,实际这正是Redis最主要的适用场景。 ...
  • Redis应用场景以及分布式缓存的实现

    千次阅读 2022-04-16 22:38:42
    1. Redis应用场景 利用 redis 中字符串类型完成 项目中手机验证码存储的实现 验证码一般都具有时效性,我们在redis中可以设置一个key的超时时间, 当用户在超时时间之内响应时,会与redis中的数据进行对比,验证...
  • Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。从2010年3月15日起,Redis的开发工作由VMware主持。 1. MySql+Memcached架构的...
  • Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。从2010年3月15日起,Redis的开发工作由VMware主持。 MySQL+Memcached架构的问题 实际...
  • Redis 应用场景和应用实例详解

    千次阅读 2017-10-04 11:04:31
    Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。从2010年3月15日起,Redis的开发工作由VMware主持。1.MySQL+Memcached架构的问题实际MySQL...
  • 3. springboot使用场景 (1)作为缓存 (2)作为分布式 (3) 作为点赞量videaId,0 incr(videaId),排行榜,转发量。 4. redis的常见面试题。 1. springboot连接redis 使用redisTemplate该类可以存放任意类型的...
  • Redis底层采用全局哈希表,时间复杂度为常数,性能极高。本文章主要用于分析Redis的各种应用场景
  • Redis应用场景

    2018-08-12 18:51:45
     可以应用于缓存(加强版的memache):处理高并发问题,还可以减轻数据库读取数据的压力  命中率:访问数据是通过redis来获取的,命中。  session的共享  Redis保存分布式web服务器总所有的session,实现了...
  • redis应用场景

    万次阅读 多人点赞 2018-07-17 10:47:58
    毫无疑问,Redis开创了一种新的数据存储思路,使用Redis,我们不用在面对功能单调的数据库时,把精力放在如何把大象放进冰箱这样的问题上,而是利用Redis灵活多变的数据结构和数据操作,为不同的大象构建不同的冰箱...
  • 本篇文章,主要介绍利用php使用Redis,主要的应用场景。 简单字符串缓存实战 $redis->connect('127.0.0.1', 6379); $strCacheKey = 'Test_bihu'; //SET 应用 $arrCacheData = [ 'name' => 'job', 'sex' =...
  • 概述 redis cluster大家喜欢叫它redis集群,也有人喜欢叫分片集群...首先客户端会对key进行CRC16计算,计算出来得到一个slot号,然后数据读写就在对应的redis实例上。和哨兵模式对比,分片集群不仅保证了高可用,同时还
  • 在目前广泛的Web应用中,都会出现一种场景:在某一个时刻,网站会迎来一个用户请求的高峰期(比如:淘宝的双十一购物狂欢节,12306的春运抢票节等),一般的设计中,用户的请求都会被直接写入数据库或文件中,在高...
  • redis安装网上很多windows、Linux的安装教程,可根据自己的需要寻找对应教程安装redis-py安装pip install redisredis可视化工具API的使用redis-py 的API的使用可以分类为:连接方式直接连接连接池操作String 操作...
  • Redis开创了一种新的数据存储思路,使用Redis,我们不用在面对功能单调的数据库时,把精力放在如何把大象放进冰箱这样的问题上,而是利用Redis灵活多变的数据结构和数据操作,为不同的大象构建不同的冰箱。...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 73,027
精华内容 29,210
关键字:

redis应用场景及实例