Redis的常见配置
spring.redis.pool.max-active=8 # 连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-wait=-1 # 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-idle=8 # 连接池中的最大空闲连接
spring.redis.pool.min-idle=0 # 连接池中的最小空闲连接
spring.redis.timeout=0 # 接超时时间(毫秒)
Redis内存淘汰机制
简介:内存的淘汰机制的初衷是为了更好地使用内存,用一定的缓存miss来换取内存的使用效率。我们可以通过配置redis.conf中的maxmemory这个值来开启内存淘汰功能,maxmemory为0的时候表示我们对Redis的内存使用没有限制。
Redis提供了下面几种淘汰策略供用户选择,其中默认的策略为noeviction策略:
noeviction:不做任何的清理工作,在redis的内存超过限制之后,所有的写入操作都会返回错误;但是读操作都能正常的进行
- volatile-ttl:在设置了过期时间的键空间中,具有更早过期时间的key优先移除
- allkeys-lru:在主键空间中,优先移除最近未使用的key
- volatile-lru:在设置了过期时间的键空间中,优先移除最近未使用的key
- allkeys-random:在主键空间中,随机移除某个key
- volatile-random:在设置了过期时间的键空间中,随机移除某个key
- allkeys-lfu:从主键空间中,选择某段时间之内使用频次最少的键值对清除
- volatile-lfu:从设置了过期时间的键空间中,选择某段时间之内使用频次最小的键值对清除掉
- 这里补充一下主键空间和设置了过期时间的键空间,举个例子,假设我们有一批键存储在Redis中,则有那么一个哈希表用于存储这批键及其值,如果这批键中有一部分设置了过期时间,那么这批键还会被存储到另外一个哈希表中,这个哈希表中的值对应的是键被设置的过期时间。设置了过期时间的键空间为主键空间的子集。
- 淘汰策略的选择可以通过下面的配置指定:# maxmemory-policy noeviction
- Redis的高可用
- Redis-Sentinell集群机制
- Redis-Sentinel是在master-slave机制上加入监控机制哨兵Sentinel实现的。
- Sentinel主要功能就是为Redis Master-Slave集群提供:
- 监控(Monitoring): Sentinel以每秒钟一次的频率向它所知的Master,Slave以及其他Sentinel实例发送一个PING
- 提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
- 自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器。
- PS:写操作只能在主服务器中,从服务器不允许写,从服务器只允许读
- PS:当主服务器挂掉后,从服务器中进行投票机制选举出主服务器
- PS:哨兵模式最好是链式结构,这样只有一个从服务器向主服务器发送同步命令的请求,其他从服务器向各自上游从服务器发送请求
- Redis-Sentinel原理(执行步骤)
- 从数据库向主数据库发送sync命令。
- 主数据库接收sync命令后,执行BGSAVE命令(保存快照),创建一个RDB文件,在创建RDB文件期间的命令将保存在缓冲区中。
- 当主数据库执行完BGSAVE时,会向从数据库发送RDB文件,而从数据库会接收并载入该文件。
- 主数据库将缓冲区的所有写命令发给从服务器执行。
- 以上处理完之后,之后主数据库每执行一个写命令,都会将被执行的写命令发送给从数据库。
- Redis-Sentinel的缺点
- 是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
- 只能主库写,从库不能写,不能水平扩容
- Redis-Cluster集群
- 工作原理:
- 客户端与Redis节点直连,不需要中间的Proxy层,直接连接任意一个Master节点
- 根据公式计算映射到相应的节点
- 优点:
- 无需Sentinel哨兵监控,如果Master挂了,内部会自动将Slave切换Master
- 可以水平的扩容
- 支持自动化迁移,当出现某个Slave宕机了,那么就只有Master,万一Master在宕机了呢?针对这种情况,如果说其他Master有多余的Slave,集群自动把多余的Slave迁移到没有Slave的Master
- redis cluster 为了保证数据的高可用性,加入了主从模式,一个主节点对应一个或多个从节点,主节点提供数据存取,从节点则是从主节点拉取数据备份,当这个主节点挂掉后,就会有这个从节点选取一个来充当主节点,从而保证集群不会挂掉。
- PS:集群有ABC三个主节点, 如果这3个节点都没有加入从节点,如果B挂掉了,我们就无法访问整个集群了。A和C的slot也无法访问。
- PS:所以我们在集群建立的时候,一定要为每个主节点都添加了从节点, 比如像这样, 集群包含主节点A、B、C, 以及从节点A1、B1、C1, 那么即使B挂掉系统也可以继续正确工作。
- PS:B1节点替代了B节点,所以Redis集群将会选择B1节点作为新的主节点,集群将会继续正确地提供服务。 当B重新开启后,它就会变成B1的从节点。
- PS:不过需要注意,如果节点B和B1同时挂了,Redis集群就无法继续正确地提供服务了。
- 缺点
- 批量操作是个坑(批量操作是分批次发送的,再加上根据算法不同的key会进入不同的片段)
- 资源隔离性较差,容易出现相互影响的情况
- 只使用单个sentinel进程来监控redis集群是不可靠的,当sentinel进程宕掉后(sentinel本身也有单点问题,single-point-of-failure)整个集群系统将无法按照预期的方式运行。所以有必要将sentinel集群,这样有几个好处:
- 即使有一些sentinel进程宕掉了,依然可以进行redis集群的主备切换;
- 如果只有一个sentinel进程,如果这个进程运行出错,或者是网络堵塞,那么将无法实现redis集群的主备切换(单点问题);
- 如果有多个sentinel,redis的客户端可以随意地连接任意一个sentinel来获得关于redis集群中的信息。
- Redis持久化
- RDB 持久化(原理是将 Reids 在内存中的数据库记录定时 dump 到磁盘上的 RDB 持久化)
- RDB 持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是 fork 一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
- AOF持久化(原理是将 Reids 的操作日志以追加的方式写入文件)
- AOF 持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
- 二者优缺点
- RDB 存在哪些优势呢?
- 一旦采用该方式,那么你的整个 Redis 数据库将只包含一个文件,这对于文件备份而言是非常完美的。比如,你可能打算每个小时归档一次最近 24 小时的数据,同时还要每天归档一次最近 30 天的数据。通过这样的备份策略,一旦系统出现灾难性故障,我们可以非常容易的进行恢复。
- 对于灾难恢复而言,RDB 是非常不错的选择。因为我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。
- 性能最大化。对于 Redis 的服务进程而言,在开始持久化时,它唯一需要做的只是 fork 出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行 IO 操作了。
- 相比于 AOF 机制,如果数据集很大,RDB 的启动效率会更高。
- RDB 又存在哪些劣势呢?
- 如果你想保证数据的高可用性,即最大限度的避免数据丢失,那么 RDB 将不是一个很好的选择。因为系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。
- 由于 RDB 是通过 fork 子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是 1 秒钟。
- AOF 的优势有哪些呢?
- 该机制可以带来更高的数据安全性,即数据持久性。Redis 中提供了 3 中同步策略,即每秒同步、每修改同步和不同步。事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会被立即记录到磁盘中。可以预见,这种方式在效率上是最低的。至于无同步,无需多言,我想大家都能正确的理解它。
- 由于该机制对日志文件的写入操作采用的是 append 模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。然而如果我们本次操作只是写入了一半数据就出现了系统崩溃问题,不用担心,在 Redis 下一次启动之前,我们可以通过 redis-check-aof 工具来帮助我们解决数据一致性的问题。
- 如果日志过大,Redis 可以自动启用 rewrite 机制。即 Redis 以 append 模式不断的将修改数据写入到老的磁盘文件中,同时 Redis 还会创建一个新的文件用于记录此期间有哪些修改命令被执行。因此在进行 rewrite 切换时可以更好的保证数据安全性。
- AOF 包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。事实上,我们也可以通过该文件完成数据的重建。
- AOF 的劣势有哪些呢?
- 对于相同数量的数据集而言,AOF 文件通常要大于 RDB 文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
- 根据同步策略的不同,AOF 在运行效率上往往会慢于 RDB。总之,每秒同步策略的效率是比较高的,同步禁用策略的效率和 RDB 一样高效。
- 二者选择的标准,就是看系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能,待手动运行 save 的时候,再做备份(rdb)。rdb 这个就更有些 eventually consistent 的意思了。
- 缓存穿透
- 正常情况:是先查询缓存,缓存有的不查询数据库
- 异常情况:查询一个数据库不存在的数据,会一直穿透缓存,导致DB挂掉
- 解决缓存穿透:
- 方法一:参数校验,不合法的不仅如此数据库查询
- 方法二:当我们从数据库找不到的时候,我们也将这个空对象设置到缓存里边去。下次再请求的时候,就可以从缓存里边获取了。这种情况我们一般会将空对象设置一个较短的过期时间
- 并发量不算高缓存与数据库双写一致问题
- 错误办法:先更新数据库,在删除缓存。这样如果出现更新数据库成功,删除缓存失败,就会出现数据库和缓存不一致
- 正确办法:先删除缓存,在更新数据库,这样及时出现删除缓存成功,数据库更新失败,下次读取缓存没有,在读取数据库,立刻反写缓存,也不会出现数据库和缓存不一致的问题
- 为什么是删除缓存而不是更新缓存呢
- 因为在某些场景,更新缓存并不仅仅是更新某个字段,而是需要更新数据库中某张表的字段,然后和其他几张表联合计算出来的,这种情况如果我们删除缓存,下次计算走现有逻辑重新计算放入缓存,简单高效。如果更新就需要在这一次更新的时候还需要增加计算的逻辑。删除更新可以看成一次懒加载,下次需要用到的时候在加载数据
- 并发量很高(一天上亿并发)的缓存与数据库双写一致问题
- 先删除缓存,在更新数据库,在更改数据库但是没有更新完的时候,这是又来了一个读请求,这时候查询数据库,计入缓存,这时候数据库更新完成,出现不一致的情况了。
- 解决办法一个队列对应一个工作线程,每个工作线程串行拿到对应的操作,然后一条一条执行。这样的话,一个数据变更操作,先删除缓存,然后更新数据库,但是数据库还没有完成更新,此时进来一个读请求过来,没有读到缓存,那么先将这次请求发送到队列中,此时队列中会积压,然后串行执行每个请求。
- 这里有一个优化点,一个队列中,其实多个更新缓存请求串在一起是没有意义的(可以多个相同的业务id),如果发现队列中已经包含,不需要放入队列中。等待前面操作完成即可。操作完成之后放入缓存
- 高并发可能衍生的问题就是读请求长时阻塞,大量更新请求读不到缓存,积压到队列,直接导致很多查询直接访问数据库
- 第二个问题多个服务,必须保证更新操作和缓存操作,都通过nginx服务器路由到相同的服务实例上,比如同一个商品,可以通过某个参数hash路由,使他进入同一台机器上。
- Redis支持的五种数据结构以及应用场景
- String
- set key value 【ex seconds】【px millionseconds】【nx | xx】
- set wzc 911024 ex 50(设置这个值有效时间是50秒)
- set wzc 911024 px 50(设置这个值有效时间是50毫秒)
- set wzc 911024 nx(设置的这个key不存在,才会成功)分布式锁
- set wzc 911024 xx(设置的这个key存在,才会成功)更新操作
- get key(获取值)
- mset key val key2 val key3 val(批量设置)
- mget key key2 key3(批量获取)
- incr key(自增),decr key(自减),incrby key increment(指定数字自增),decrby key decrement(指定数字自减)
- 四种情况
- 如果key对应的value不是整数,会报错
- 如果key对应的value是整数,则返回自增或自减后的结果
- 如果key不存在,设置这个key的value是0并自增或自减1(指定了increment或decrement除外),返回自增后的结果
- 若key对应的value值自增后超过integer的最大值也就是2的64次方,也会报错
- strlen key(获取字符串长度)
- 应用场景
- 缓存个人数据信息,缓存权限信息
- session共享,token
- 常规计数,比如点赞数,收藏数incresby,decreaseby
- 设置验证码有效期setex
- 访问限制
//60秒内短信验证码
用户请求了我们的发送短信验证码服务
先从换从里取出这个用户对应的key(可以是电话号码拼接点东西)
mKey = redisCli.get(key);
if(mKey != null){
如果缓存中存在了,就是已经请求过验证码了,比如我系统参数设置的至少60秒发送一次短信
那就代表60之内再次请求了,返回false阻止
return false;
}else{
否则就是第一次请求短信验证码或者等了60再次请求的验证码
发送验证码
sendMsg()
放入redis缓存并设置失效时间60秒
redisCli.set(key,value,60)
}
//一天内限制五次
try {
//先判断Redis中是否有该key值
if (RedisHelper.exists(key)) {
//取出访问次数
int times = Integer.parseInt(RedisHelper.get(key));
//判断访问是否大于最大次数
if (times >= maxTimes) {
return true;
}
//若不大于则将访问次数加1
RedisHelper.incr(key, 1L);
} else {
//若没有则创建并设置失效时间
RedisHelper.set(key, "1", days, TimeUnit.DAYS);
}
}
catch (Exception e) {
throw new Exception
return false;
}
- hash
- hmset user:001 name "李三" age 18 birthday "20010101" (key是user:001,value是map)
- hmset user:001 age 30
- 应用场景:购物车ID,map(filed是name,count)
- list
- 常用命令:
- RPUSH key value [value ...]:从队列的右边入队一个元素或多个元素,复杂度O(1)。将所有指定的值插入存于key的列表的尾部(从右侧插入)。如果key不存在,那么PUSH之前,会先自动创建一个空的列表。如果key对应的值不是一个list的话,则会返回一个错误。如果同时push多个值的话,值会依次从左到右PUSH从尾部进入list。
redis> rpush queue a
(integer) 1
redis> rpush queue b
(integer) 2
redis> rpush queue c
(integer) 3
redis> rpush queue d e f
(integer) 6
redis> lrange queue 0 -1
1) "a"
2) "b"
3) "c"
4) "d"
5) "e"
6) "f"
- LPUSH key value [value ...]
- 从队列的左边入队一个或多个元素,复杂度O(1)。这个指令和RPUSH几乎一样,只是插入节点的方向相反了,是从list的头部(左侧)进行插入的。
- RPUSHX key value
- 从队列的右边入队一个元素,仅队列存在时有效。当队列不存在时,不进行任何操作。
# 之前queue已经存在,且有a、b、c、d、e、f、g这6个元素
redis> rpushx queue z
(integer) 7
redis> del queue
(integer) 1
redis> rpushx queue z
(integer) 0
可以看出,最开始rpushx向queue中新增了一个节点,但当我们删掉了queue时,再rpushx,就没有插入成功(返回值为0)。
- LPUSHX key value
- 从队列的左边入队一个元素,仅队列存在时有效。当队列不存在时,不进行任何操作。
- LPOP:从队列的左边出队一个元素,复杂度O(1)。如果list为空,则返回nil。
redis> del queue
(integer) 0
redis> rpush queue a b c d e f
(integer) 6
redis> lrange queue 0 -1
1) "a"
2) "b"
3) "c"
4) "d"
5) "e"
6) "f"
redis> lpop queue
"a"
redis> lpop queue
"b"
redis> lrange queue 0 -1
1) "c"
2) "d"
3) "e"
4) "f"
redis> rpop queue
"f"
redis> rpop queue
"e"
redis> lrange queue 0 -1
1) "c"
2) "d"
- RPOP key
- 从队列的右边出队一个元素,复杂度O(1)。如果list为空,则返回nil。
- BLPOP key [key ...] timeout
- 删除,并获得该列表中的第一元素,或阻塞,直到有一个可用。这是LPOP的阻塞版本。在LPOP的时候,如果队列中没有值,则会返回一个nil。而BLPOP则会等待一段时间,如果list中有值(等待的时候,被添加的),则返回对应值;如果在给定时间内仍没有得到结果,则返回nil。
redis> lrange queue 0 -1
1) "c"
2) "d"
redis> BLPOP queue 1
1) "queue"
2) "c"
redis> BLPOP queue 1
1) "queue"
2) "d"
redis> BLPOP queue 1
(nil)
(1.10s)
redis> LPOP queue
(nil)
我们仍接着上面的实验继续,这时queue里面只有2个元素了,我们使用BLPOP取值,前两次都成功地得到了值,效果和LPOP一样。但第三次的时候,由于list已经为空,但是BLPOP并没有立刻返回nil,而是阻塞了一点时间(timeout的时间),之后才返回了nil。最后,我们试验了一下LPOP,证实了LPOP是立刻返回结果的。
timeout表示等待的时间,单位是秒。当设为0时,表示永远阻塞,非0时,表示等待的最长时间。
要注意的是,LBPOP支持多个key,也就是说可以同时监听多个list,并按照key的顺序,依次检查list是否为空,如果不为空,则返回最优先的list中的值。如果都为空,则阻塞,直到有一个list不为空,那么返回这个list对应的值。
- BRPOP key [key ...] timeout
- 删除,并获得该列表中的最后一个元素,或阻塞,直到有一个可用。参考BLPOP。
- RPOPLPUSH source destination
- 删除列表中的最后一个元素,将其追加到另一个列表。这个命令可以原子性地返回并删除source对应的列表的最后一个元素(尾部元素),并将钙元素放入destination对应的列表的第一个元素位置(列表头部)。
redis> rpush q1 1 2 3 4 5
(integer) 5
redis> lrange q1 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
redis> rpoplpush q1 q2
"5"
redis> rpoplpush q1 q2
"4"
redis> lrange q1 0 -1
1) "1"
2) "2"
3) "3"
redis> lrange q2 0 -1
1) "4"
2) "5"
我们简单的看一下上述的例子,首先我们初始化一个q1,内容是{1, 2, 3, 4, 5}。这是q2没有定义,可以理解是一个空的list {}。
之后使用rpoplpush,从q1右边pop出一个元素5,然后在q2左侧push进。则现在的q1为{1, 2, 3, 4},q2为{5}。
再进行一次rpoplpush,从q1右边pop出一个元素4,然后在q2左侧push进。则现在的q1为{1, 2, 3},q2为{4, 5}。
- BRPOPLPUSH source destination timeout
- 弹出一个列表的值,将它推到另一个列表,并返回它;或阻塞,直到有一个可用。RPOPLPUSH的阻塞版本。timeout的单位是秒,当timeout为0的时候,表示无限期阻塞。
- LLEN key
- 获得队列(List)的长度
- LRANGE key start stop
- 从列表中获取指定返回的元素。我们在前面用到了很多次。LRANGE可以获取list的指定范围的值。范围用start和stop表示。负数表示从右向左数。需要注意的是,超出范围的下标不会产生错误:如果start>end,会得到空列表,如果end超过队尾,则Redis会将其当做列表的最后一个元素。
- LINDEX key index
- 获取一个元素,通过其索引列表
- 我们之前介绍的操作都是对list的两端进行的,所以算法复杂度都只有O(1)。而这个操作是指定位置来进行的,每次操作,list都得找到对应的位置,因此算法复杂度为O(N)。list的下表是从0开始的,index为负的时候是从右向左数。-1表示最后一个元素。当下标超出的时候,会返回nul。所以不用像操作数组一样担心范围越界的情况。
- LSET key index value
- 设置队列里面一个元素的值
- 这个操作和LINDEX类似,只不过LINDEX是读,而LSET是写。下标的用法和LINDX相同。不过当index越界的时候,这里会报异常。
- LREM key count value
- 从列表中删除元素
- 该命令用于从key对应的list中,移除前count次出现 的值为value的元素。count参数有三种情况:
- count > 0: 表示从头向尾(左到右)移除值为value的元素。
- count < 0: 表示从尾向头(右向左)移除值为value的元素。
- count = 0: 表示移除所有值为value的元素。
- LTRIM key start stop
- 修剪到指定范围内的清单
- 这个命令和LRANGE十分相似,LRANGE会将指定范围的元素返回给客户端,而LTRIM会对list进行修剪,使其只包含指定范围的元素。start和stop表示范围。超出范围的下标不会产生错误:如果start>end,会得到空列表,如果end超过队尾,则Redis会将其当做列表的最后一个元素。
- LINSERT key BEFORE|AFTER pivot value
- 在列表中的另一个元素之前或之后插入一个元素
- 该命令将value插入值key对应的列表的基准值pivot的前面或是后面。
- 当key不存在时,这个list被视为空列表,任何操作都不会发生。
- 当key存在,但保存的不是list,则会报error。
- 该命令会返回修改之后的list的长度,如果找不到pivot,则会返回-1。
sadd <key> <value1> <value2>
将多个元素加入到key中,重复值忽略
smembers <key>
取出该集合的所有值
sismember <key> <value>
判断集合key中是否有该value值 有就1 没有0
scard <key>
返回该集合的元素个数
srem <key> <value1> <value2>
删除集合中的某个元素
spop <key>
随机吐出该集合一个值
srandmember <key> <n>
随机从集合中取出n个值,不会从集合中删除
smove <key1> <key2> <value>
将key1中的value 移动到key2 中
sinter <key1> <key2>
返回两个集合的交集元素
sunion <key1> <key2>
返回两个集合的并集
1. “共同好友列表”
社交类应用中,获取两个人或多个人的共同好友,两个人或多个人共同关注的微博这样类似的功能,用 MySQL 的话操作很复杂,可以把每个人的好友 id 存到集合中,获取共同好友的操作就可以简单到一个取交集的命令就搞定
// 这里为了方便阅读,把 id 替换成姓名
sadd user:wade james melo paul kobe
sadd user:james wade melo paul kobe
sadd user:paul wade james melo kobe
sadd user:melo wade james paul kobe
// 获取 wade 和 james 的共同好友
sinter user:wade user:james
/* 输出:
* 1) "kobe"
* 2) "paul"
* 3) "melo"
*/
// 获取香蕉四兄弟的共同好友
sinter user:wade user:james user:paul user:melo
/* 输出:
* 1) "kobe"
*/
/*
类似的需求还有很多 , 必须把每个标签下的文章 id 存到集合中,可以很容易的求出几个不同标签下的共同文章;
把每个人的爱好存到集合中,可以很容易的求出几个人的共同爱好。
*/
2.抽奖
# 添加用户
sadd key {userId1,userId2}
# 获取对应的集合的所有成员
smembers key
# 从集合的右侧(尾部)移除一个成员,并将其返回
spop(key2)
# 从key2对应的集合中随机获取 numbers 个元素
srandmember(name, numbers)
zadd<key><score1><value1><score2><value2>
将一个或多个元素以及score加入zset
zrange<key><start><stop> withscore
返回下标在区间内的集合,带有score
zrangebyscore <key> <min> <max>[withscore] [limit offset count]
返回key中 score介于min和max中的成员,升序排列
zrevrangerbyscore <key> <min> <max> [withscore] [limit offset count]
降序
zincrby <key> <increment> <value>
在key集合中的value上增加increment
zrem <key> <value>
删除key集合下的指定元素
zcount <key> <min><max>
统计 区间内的元素个数
zcard <key>
获取集合中的元素个数
zrank <key><value>
查询value在key中的排名,从0开始
// 用元素的分数(score)表示与好友的亲密度
zadd user:kobe 80 james 90 wade 85 melo 90 paul
// 根据“亲密度”给好友排序
zrevrange user:kobe 0 -1
/**
* 输出:
* 1) "wade"
* 2) "paul"
* 3) "melo"
* 4) "james"
*/
// 增加好友的亲密度
zincrby user:kobe 15 james
// 再次根据“亲密度”给好友排序
zrevrange user:kobe 0 -1
/**
* 输出:
* 1) "james"
* 2) "wade"
* 3) "paul"
* 2) "melo"
*/
//类似的需求还出现在根据文章的阅读量或点赞量对文章列表排序
//用户登录次数
//将登录次数和用户统一存储在一个sorted set里
zadd login:login_times 5 1
zadd login:login_times 1 2
zadd login:login_times 2 3
//当用户登录时,对该用户的登录次数自增1
ret = r.zincrby("login:login_times", 1, uid)
//那么如何获得登录次数最多的用户呢,逆序排列取得排名前N的用户
ret = r.zrevrange("login:login_times", 0, N-1)
防止连续点赞
String key = "document-collect-" + userId;
Set<ZSetOperations.TypedTuple<String>> tuples = new HashSet<>();
ZSetOperations.TypedTuple<String> tuple0 = new DefaultTypedTuple<String>(String.valueOf(itemId) + "-" + String.valueOf(sourceType), 1d);
tuples.add(tuple0);
Long i = vo.add(key, tuples);
redisTemplate.expire(key, 1, TimeUnit.HOURS);
//大于0,才会进入操作
if (i > 0) {
}else{
}
- 应用场景
- 评论系统:我们在看完一条微博之后,常常会评论一番,或者看看其他人的吐槽。每条评论的记录都是按照时间顺序排序的。我们读的时候也是这个顺序。这时,队列就是一个很好的存储结构。每提交一次评论,都向list的末尾添加一个新的节点。
- 并行转串行:用户每时每刻都可能发出请求,而且请求的频率经常变化。这时,后台的程序不可能立刻响应每一个用户的请求,尤其是请求特别占资源的服务的时候。我们需要一个排队系统。根据用户的请求时间,将用户的请求放入队列中,后台程序依次从队列中获取任务,处理并将结果返回到结果队列。通过队列,我们将并行的请求转换成串行的任务队列,之后依次处理(当然后台的程序也可以多核并行处理)。
- 消息列表:两个老师,一个学生
- A老师发送消息 给学生
- lpush msg::studnet_001 100(消息编号100)
- B老师发送消息
- lpush msg::studnet_001 200(消息编号200)
- 假如想拿最近的10条消息就可以执行如下命令(最新的消息一定在list的最左边)
- lrange msg::studnet_001 0 9 #下标从0开始,闭区间都包含
- 需求作业列表查询学生最新的作业
- 在Redis中我们的根据时间查询的最新列表每个ID使用了常驻缓存,这是一直更新的。但是需要限制不能超过5000个ID,因此我们的获取ID函数会一直询问Redis。只有在start/count参数超出了这个范围的时候,才需要去访问数据库。
- bitmap
- 语法:redis.setbit(key,offset,1)
- 语法:redis.getbit(key,offset)
- 语法:redis.bitCount(key)
- 语法:jedis.bitop(BitOP.AND, destkey, key1, key2);//求逻辑并
- 语法:jedis.bitop(BitOP.OR, destkey, key1, key2);//求逻辑或
- 语法:jedis.bitop(BitOP.XOR, destkey, key1, key2);//异或:不相同是1,相同时0,最后合计
- 应用场景
- 统计日活量:
- RedisUtils.setbit("userlogin2019-01-10", 1L, "1");
- RedisUtils.bitCount("userlogin2019-01-10");
- 统计一首歌 每天被多少用户听
- RedisUtils.setbit("musicId2019-01-10", userId, "1");
- RedisUtils.bitCount("musicId2019-01-10");
- 连续两天登录的用户
- RedisUtils.bitopAnd("activeUserlogin2019-01-09/10", "userlogin2019-01-10", "userlogin2019-01-09");
- Redis分布式锁实现原理(悲观锁)
- Redis因为是单线程的,所以本身没有锁的概念。所以分布式锁的实现原理是往Redis当中写入一个key(调用方法setnx),写入成功相当于获取锁成功返回1,获取锁成功;写入失败也即是setnx方法返回0,获取锁失败。注意锁的失效时间,否则容易造成死锁。
- String requestid = UUID.randomUUID().toString().trim().replaceAll("-", "");
- String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
- System.out.println(result);
- if ("OK".equals(result)) {
- return true;
- }
- NX代表只有key不存在才会成功
- PX代表在过期时间后会自动释放
- 使用随机值的原因是如果某个获取到锁的客户端阻塞了很长时间,导致它获取到的锁自动释放。此时可能其他客户端已经获取到锁,如果直接删除就会出现问题,所以需要随机值
- redis乐观锁
- multi:开启redis事务,置客户端为事务态
- exec:提交事务,执行从multi到此之前的命令队列,置客户端为非事务态
- discard:取消事务,置客户端为非事务态
- watch:监视键值对,作用提交事务exec时,事务是否发生变化,发生变化,事务取消
jedis.watch(key);
Transaction transaction = jedis.multi();
transaction.set(key, String.valueOf(prdNum - 1));
List<Object> result = transaction.exec();
if (result == null || result.isEmpty()) {
System.out.println("悲剧了,顾客:" + clientName + "没有抢到商品");// 可能是watch-key被外部修改,或者是数据操作被驳回
} else {
jedis.sadd(clientList, clientName);// 抢到商品记录一下
System.out.println("好高兴,顾客:" + clientName + "抢到商品");
break;
}