怎么自己做免费网站,临湘做网站,网站建设标新立异,金顺广州外贸网站建设Redis20种使用场景 1缓存2抽奖3Set实现点赞/收藏功能4排行榜5PV统计#xff08;incr自增计数#xff09;6UV统计#xff08;HeyperLogLog#xff09;7去重#xff08;BloomFiler#xff09;8用户签到#xff08;BitMap#xff09;9GEO搜附近10简单限流11全局ID12简单分… Redis20种使用场景 1缓存2抽奖3Set实现点赞/收藏功能4排行榜5PV统计incr自增计数6UV统计HeyperLogLog7去重BloomFiler8用户签到BitMap9GEO搜附近10简单限流11全局ID12简单分布式锁13认识的人/好友推荐14发布/订阅15消息队列16数据共享session共享17商品筛选18购物车19定时取消订单key过期监听20物流信息时间线 后端程序员不管是出去面试还是当面试官Redis几乎是100%会问到的技术点究其原因主要是因为他实在过于强大、使用率太高了导致项目中几乎无处不在。
那Redis部分不出意外第一个问题就是你做的项目用Redis干了些啥大部分人的回答都会是缓存当问到是否还有其他场景中使用时部分用的少的朋友就会微微摇头
其实也没错Redis绝不部分使用场景就是用来做缓存但是由于Redis 支持比较丰富的数据结构因此他能实现的功能并不仅限于缓存而是可以运用到各种业务场景中开发出既简洁、又高效的系统;
下面整理了20种 Redis 的妙用场景每个方案都用一个实际的业务需求并结合数据结构的API来讲解希望大家能够理解其底层的实现方式学会举一反三并运用到项目的方方面面
1缓存
本文假定你已经了解过Redis并知晓Redis最基础的一些使用如果你对Redis的基础API还不了解可以先看一下菜鸟教程https://www.runoob.com/redis那么缓存部分及基础API的演示就不过多来讲解了
但是基本的数据结构在这里再列举一下方便后续方案的理解 依赖
以下所有通过SpringBoot测试的用例都需要引入 Redis 的依赖
dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactId
/dependency2抽奖
曾几何时抽奖是互联网APP热衷的一种推广、拉新的方式节假日没有好的策划那就抽个奖吧一堆用户参与进来然后随机抽取几个幸运用户给予实物/虚拟的奖品此时开发人员就需要写上一个抽奖的算法来实现幸运用户的抽取其实我们完全可以利用Redis的集合Set就能轻松实现抽奖的功能
功能实现需要的API
SADD key member1 [member2]添加一个或者多个参与用户
SRANDMEMBER KEY [count]随机返回一个或者多个用户
SPOP key随机返回一个或者多个用户并删除返回的用户SRANDMEMBER 和 SPOP 主要用于两种不同的抽奖模式SRANDMEMBER 适用于一个用户可中奖多次的场景就是中奖之后不从用户池中移除继续参与其他奖项的抽取而 SPOP 就适用于仅能中一次的场景一旦中奖就将用户从用户池中移除后续的抽奖就不可能再抽到该用户 通常 SPOP 会用的会比较多。 Redis-cli 操作
127.0.0.1:6379 SADD raffle user1
(integer) 1
127.0.0.1:6379 SADD raffle user2 user3 user4 user5 user6 user7 user8 user9 user10
(integer) 9
127.0.0.1:6379 SRANDMEMBER raffle 2
1) user5
2) user2
127.0.0.1:6379 SPOP raffle 2
1) user3
2) user4
127.0.0.1:6379 SPOP raffle 2
1) user10
2) user9SpringBoot 实现
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;import java.util.List;Slf4j
SpringBootTest
public class RaffleMain {private final String KEY_RAFFLE_PROFIX raffle:;AutowiredRedisTemplate redisTemplate;Testvoid test() {Integer raffleId 1;join(raffleId, 1000, 1001, 2233, 7890, 44556, 74512);List lucky lucky(raffleId, 2);log.info(活动{} 的幸运中奖用户是{}, raffleId, lucky);}public void join(Integer raffleId, Integer... userIds) {String key KEY_RAFFLE_PROFIX raffleId;redisTemplate.opsForSet().add(key, userIds);}public List lucky(Integer raffleId, long num) {String key KEY_RAFFLE_PROFIX raffleId;// 随机抽取 抽完之后将用户移除奖池List list redisTemplate.opsForSet().pop(key, num);// 随机抽取 抽完之后用户保留在池子里//List list redisTemplate.opsForSet().randomMembers(key, num);return list;}}3Set实现点赞/收藏功能
有互动属性APP一般都会有点赞/收藏/喜欢等功能来提升用户之间的互动。
传统的实现用户点赞之后在数据库中记录一条数据同时一般都会在主题库中记录一个点赞/收藏汇总数来方便显示
Redis方案基于Redis的集合Set记录每个帖子/文章对应的收藏、点赞的用户数据同时set还提供了检查集合中是否存在指定用户用户快速判断用户是否已经点赞过
功能实现需要的API
SADD key member1 [member2]添加一个或者多个成员点赞
SCARD key获取所有成员的数量点赞数量
SISMEMBER key member判断成员是否存在是否点赞
SREM key member1 [member2] 移除一个或者多个成员点赞数量Redis-cli API操作
127.0.0.1:6379 sadd like:article:1 user1
(integer) 1
127.0.0.1:6379 sadd like:article:1 user2
(integer) 1
# 获取成员数量点赞数量
127.0.0.1:6379 SCARD like:article:1
(integer) 2
# 判断成员是否存在是否点在
127.0.0.1:6379 SISMEMBER like:article:1 user1
(integer) 1
127.0.0.1:6379 SISMEMBER like:article:1 user3
(integer) 0
# 移除一个或者多个成员取消点赞
127.0.0.1:6379 SREM like:article:1 user1
(integer) 1
127.0.0.1:6379 SCARD like:article:1
(integer) 1SpringBoot 操作
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;Slf4j
SpringBootTest
public class LikeMain {private final String KEY_LIKE_ARTICLE_PROFIX like:article:;AutowiredRedisTemplate redisTemplate;Testvoid test() {long articleId 100;Long likeNum like(articleId, 1001, 1002, 2001, 3005, 4003);unLike(articleId, 2001);likeNum likeNum(articleId);boolean b2001 isLike(articleId, 2001);boolean b3005 isLike(articleId, 3005);log.info(文章{} 点赞数量{} 用户2001的点赞状态{} 用户3005的点赞状态{}, articleId, likeNum, b2001, b3005);}/*** 点赞** param articleId 文章ID* return 点赞数量*/public Long like(Long articleId, Integer... userIds) {String key KEY_LIKE_ARTICLE_PROFIX articleId;Long add redisTemplate.opsForSet().add(key, userIds);return add;}public Long unLike(Long articleId, Integer... userIds) {String key KEY_LIKE_ARTICLE_PROFIX articleId;Long remove redisTemplate.opsForSet().remove(key, userIds);return remove;}public Long likeNum(Long articleId) {String key KEY_LIKE_ARTICLE_PROFIX articleId;Long size redisTemplate.opsForSet().size(key);return size;}public Boolean isLike(Long articleId, Integer userId) {String key KEY_LIKE_ARTICLE_PROFIX articleId;return redisTemplate.opsForSet().isMember(key, userId);}}4排行榜
排名、排行榜、热搜榜是很多APP、游戏都有的功能常用于用户活动推广、竞技排名、热门信息展示等功能 常规的做法就是将用户的名次、分数等用于排名的数据更新到数据库然后查询的时候通过Order by limit 取出前50名显示如果是参与用户不多更新不频繁的数据采用数据库的方式也没有啥问题但是一旦出现爆炸性热点资讯比如大陆收复湾湾xxx某些绿了等等短时间会出现爆炸式的流量瞬间的压力可能让数据库扛不住
Redis方案将热点资讯全页缓存采用Redis的有序队列Sorted Set来缓存热度SCORES即可瞬间缓解数据库的压力同时轻松筛选出热度最高的50条
功能实现需要的命令
ZADD key score1 member1 [score2 member2]添加并设置SCORES支持一次性添加多个
ZREVRANGE key start stop [WITHSCORES] 根据SCORES降序排列
ZRANGE key start stop [WITHSCORES] 根据SCORES降序排列Redis-cli操作
# 单个插入
127.0.0.1:6379 ZADD ranking 1 user1
(integer) 1
# 批量插入
127.0.0.1:6379 ZADD ranking 10 user2 50 user3 3 user4 25 user5
(integer) 4
# 降序排列 不带SCORES
127.0.0.1:6379 ZREVRANGE ranking 0 -1
1) user3
2) user5
3) user2
4) user4
5) user1
# 降序排列 带SCORES
127.0.0.1:6379 ZREVRANGE ranking 0 -1 WITHSCORES1) user32) 503) user54) 255) user26) 107) user48) 39) user1
10) 1
# 升序
127.0.0.1:6379 ZRANGE ranking 0 -1 WITHSCORES1) user12) 13) user44) 35) user26) 107) user58) 259) user3
10) 50SpringBoot操作
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.DefaultTypedTuple;
import org.springframework.data.redis.core.RedisTemplate;import java.util.Set;SpringBootTest
Slf4j
public class RankingTest {private final String KEY_RANKING ranking;AutowiredRedisTemplate redisTemplate;Testvoid test() {add(1001, (double) 60);add(1002, (double) 80);add(1003, (double) 100);add(1004, (double) 90);add(1005, (double) 70);// 取所有SetDefaultTypedTuple range range(0, -1);log.info(所有用户排序{}, range);// 前三名range range(0, 2);log.info(前三名排序{}, range);}public Boolean add(Integer userId, Double score) {Boolean add redisTemplate.opsForZSet().add(KEY_RANKING, userId, score);return add;}public SetDefaultTypedTuple range(long min, long max) {// 降序SetDefaultTypedTuple set redisTemplate.opsForZSet().reverseRangeWithScores(KEY_RANKING, min, max);// 升序//SetDefaultTypedTuple set redisTemplate.opsForZSet().rangeWithScores(KEY_RANKING, min, max);return set;}
}输出
所有用户排序[DefaultTypedTuple [score100.0, value1003], DefaultTypedTuple [score90.0, value1004], DefaultTypedTuple [score80.0, value1002], DefaultTypedTuple [score70.0, value1005], DefaultTypedTuple [score60.0, value1001]]
前三名排序[DefaultTypedTuple [score100.0, value1003], DefaultTypedTuple [score90.0, value1004], DefaultTypedTuple [score80.0, value1002]]5PV统计incr自增计数
Page ViewPV指的是页面浏览量是用来衡量流量的一个重要标准也是数据分析很重要的一个依据通常统计规则是页面被展示一次就加一
功能所需命令
INCR将 key 中储存的数字值增一 Redis-cli 操作
127.0.0.1:6379 INCR pv:article:1
(integer) 1
127.0.0.1:6379 INCR pv:article:1
(integer) 26UV统计HeyperLogLog
前面介绍了通过INCR方式来实现页面的PV除了PV之外UV独立访客也是一个很重要的统计数据
但是如果要想通过计数INCR的方式来实现UV计数就非常的麻烦增加之前需要判断这个用户是否访问过那判断依据就需要额外的方式再进行记录。
你可能会说不是还有Set嘛一个页面弄个集合来一个用户塞SADD一个用户进去要统计UV的时候再通过SCARD汇总一下数量就能轻松搞定了此方案确实能实现UV的统计效果但是忽略了成本如果是普通页面几百、几千的访问可能造成的影响微乎其微如果一旦遇到爆款页面动辄上千万、上亿用户访问时就一个页面UV将会带来非常大的内存开销对于如此珍贵的内存来说这显然是不划算的。
此时HeyperLogLog数据结构就能完美的解决这一问题它提供了一种不精准的去重计数方案注意这里强调一下是不精准的会存在误差不过误差也不会很大标准的误差率是0.81%这个误差率对于统计UV计数是能够容忍的所以不要将这个数据结构拿去做精准的去重计数。
另外HeyperLogLog 是会占用12KB的存储空间虽然说Redis 对 HeyperLogLog 进行了优化在存储数据比较少的时候采用了稀疏矩阵存储只有在数据量变大稀疏矩阵空间占用超过阈值时才会转为空间为12KB的稠密矩阵相比于成千、上亿的数据量这小小的12KB简直是太划算了但是还是建议不要将其用于数据量少且频繁创建 HeyperLogLog 的场景避免使用不当造成资源消耗没减反增的不良效果。 功能所需命令
PFADD key element [element ...]增加计数统计UV
PFCOUNT key [key ...]获取计数货物UV
PFMERGE destkey sourcekey [sourcekey ...]将多个 HyperLogLog 合并为一个 HyperLogLog多个合起来统计Redis-cli 操作
# 添加三个用户的访问
127.0.0.1:6379 PFADD uv:page:1 user1 user2 user3
(integer) 1
# 获取UV数量
127.0.0.1:6379 PFCOUNT uv:page:1
(integer) 3
# 再添加三个用户的访问 user3是重复用户
127.0.0.1:6379 PFADD uv:page:1 user3 user4 user5
(integer) 1
# 获取UV数量 user3是重复用户 所以这里返回的是5
127.0.0.1:6379 PFCOUNT uv:page:1
(integer) 5SpringBoot操作HeyperLogLog 模拟测试10000个用户访问id为2的页面
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;SpringBootTest
Slf4j
public class UVTest {private final String KEY_UV_PAGE_PROFIX uv:page:;AutowiredRedisTemplate redisTemplate;Testpublic void uvTest() {Integer pageId 2;for (int i 0; i 10000; i) {uv(pageId, i);}for (int i 0; i 10000; i) {uv(pageId, i);}Long uv getUv(pageId);log.info(pageId:{} uv:{}, pageId, uv);}/*** 用户访问页面* param pageId* param userId* return*/private Long uv(Integer pageId, Integer userId) {String key KEY_UV_PAGE_PROFIX pageId;return redisTemplate.opsForHyperLogLog().add(key, userId);}/*** 统计页面的UV* param pageId* return*/private Long getUv(Integer pageId) {String key KEY_UV_PAGE_PROFIX pageId;return redisTemplate.opsForHyperLogLog().size(key);}
}日志输出
pageId:2 uv:10023由于存在误差这里访问的实际访问的数量是1万统计出来的多了23个在标准的误差0.81%范围内加上UV数据不是必须要求准确因此这个误差是可以接受的。
7去重BloomFiler
通过上面HeyperLogLog的学习我们掌握了一种不精准的去重计数方案但是有没有发现他没办法获取某个用户是否访问过理想中我们是希望有一个PFEXISTS的命令来判断某个key是否存在然而HeyperLogLog并没有要想实现这一需求就得 BloomFiler 上场了。
什么是Bloom Filter
Bloom Filter是由Bloom在1970年提出的一种多哈希函数映射的快速查找算法。 通常应用在一些需要快速判断某个元素是否属于集合但是并不严格要求100%正确的场合。 基于一种概率数据结构来实现是一个有趣且强大的算法。
举个例子假如你写了一个爬虫用于爬取网络中的所有页面当你拿到一个新的页面时如何判断这个页面是否爬取过
普通做法每爬取一个页面往数据库插入一行数据记录一下URL每次拿到一个新的页面就去数据库里面查询一下存在就说明爬取过
普通做法的缺点少量数据用传统方案没啥问题如果是海量数据每次爬取前的检索将会越来越慢如果你的爬虫只关心内容对来源数据不太关心的话这部分数据的存储也将消耗你很大的物理资源
此时通过 BloomFiler 就能以很小的内存空间作为代价即可轻松判断某个值是否存在。
同样BloomFiler 也不那么精准在默认参数情况下是存在1%左右的误差但是 BloomFiler 是允许通过error_rate误差率以及initial_size预计大小来设置他的误差比例
error_rate误差率越低需要的空间就越大
initial_size预计放入值的数量当实际放入的数量大于设置的值时误差率就会逐渐升高所以为了避免误差率可以提前做好估值避免再次大的误差BloomFiler 安装 为了方便测试这里使用 Docker 快速安装
docker run -d -p 6379:6379 redislabs/rebloom功能所需的命令
bf.add 添加单个元素
bf.madd 批量田间
bf.exists 检测元素是否存在
bf.mexists 批量检测Redis-cli操作
127.0.0.1:6379 bf.add web:crawler baidu
(integer) 1
127.0.0.1:6379 bf.madd web:crawler tencent bing
1) (integer) 1
2) (integer) 1
127.0.0.1:6379 bf.exists web:crawler baidu
(integer) 1
127.0.0.1:6379 bf.mexists web:crawler baidu 163
1) (integer) 1
2) (integer) 0SpringBoot整合 工具类 RedisBloom
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;import java.util.Arrays;
import java.util.List;/*** redis布隆过滤器**/
Component
public class RedisBloom {private static RedisScriptBoolean bfreserveScript new DefaultRedisScript(return redis.call(bf.reserve, KEYS[1], ARGV[1], ARGV[2]), Boolean.class);private static RedisScriptBoolean bfaddScript new DefaultRedisScript(return redis.call(bf.add, KEYS[1], ARGV[1]), Boolean.class);private static RedisScriptBoolean bfexistsScript new DefaultRedisScript(return redis.call(bf.exists, KEYS[1], ARGV[1]), Boolean.class);private static String bfmaddScript return redis.call(bf.madd, KEYS[1], %s);private static String bfmexistsScript return redis.call(bf.mexists, KEYS[1], %s);Autowiredprivate StringRedisTemplate redisTemplate;/*** 设置错误率和大小需要在添加元素前调用若已存在元素则会报错* 错误率越低需要的空间越大** param key* param errorRate 错误率默认0.01* param initialSize 默认100预计放入的元素数量当实际数量超出这个数值时误判率会上升尽量估计一个准确数值再加上一定的冗余空间* return*/public Boolean bfreserve(String key, double errorRate, int initialSize) {return redisTemplate.execute(bfreserveScript, Arrays.asList(key), String.valueOf(errorRate), String.valueOf(initialSize));}/*** 添加元素** param key* param value* return true表示添加成功false表示添加失败存在时会返回false*/public Boolean bfadd(String key, String value) {return redisTemplate.execute(bfaddScript, Arrays.asList(key), value);}/*** 查看元素是否存在判断为存在时有可能是误判不存在是一定不存在** param key* param value* return true表示存在false表示不存在*/public Boolean bfexists(String key, String value) {return redisTemplate.execute(bfexistsScript, Arrays.asList(key), value);}/*** 批量添加元素** param key* param values* return 按序 1表示添加成功0表示添加失败*/public ListInteger bfmadd(String key, String... values) {return (ListInteger) redisTemplate.execute(this.generateScript(bfmaddScript, values), Arrays.asList(key), values);}/*** 批量检查元素是否存在判断为存在时有可能是误判不存在是一定不存在** param key* param values* return 按序 1表示存在0表示不存在*/public ListInteger bfmexists(String key, String... values) {return (ListInteger) redisTemplate.execute(this.generateScript(bfmexistsScript, values), Arrays.asList(key), values);}private RedisScriptList generateScript(String script, String[] values) {StringBuilder sb new StringBuilder();for (int i 1; i values.length; i) {if (i ! 1) {sb.append(,);}sb.append(ARGV[).append(i).append(]);}return new DefaultRedisScript(String.format(script, sb.toString()), List.class);}}测试
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;import java.util.List;SpringBootTest
Slf4j
public class BFTest {private final String KEY_WEB_CRAWLER web:crawler1;AutowiredRedisBloom bloom;AutowiredRedisTemplate redisTemplate;Testpublic void test() {Boolean hasKey redisTemplate.hasKey(KEY_WEB_CRAWLER);log.info(bloom hasKey:{}, hasKey);if (!hasKey) {// 不存在的时候 再去初始化Boolean bfreserve bloom.bfreserve(KEY_WEB_CRAWLER, 0.0001, 10000);log.info(bloom bfreserve:{}, bfreserve);}ListInteger madd bloom.bfmadd(KEY_WEB_CRAWLER, baidu, google);log.info(bloom bfmadd:{}, madd);Boolean baidu bloom.bfexists(KEY_WEB_CRAWLER, baidu);log.info(bloom bfexists baidu:{}, baidu);Boolean bing bloom.bfexists(KEY_WEB_CRAWLER, bing);log.info(bloom bfexists bing:{}, bing);}
}日志输出
com.ehang.redis.bloom_filter.BFTest : bloom hasKey:false
com.ehang.redis.bloom_filter.BFTest : bloom bfreserve:true
com.ehang.redis.bloom_filter.BFTest : bloom bfmadd:[1, 1]
com.ehang.redis.bloom_filter.BFTest : bloom bfexists baidu:true
com.ehang.redis.bloom_filter.BFTest : bloom bfexists bing:false8用户签到BitMap
很多APP为了拉动用户活跃度往往都会做一些活动比如连续签到领积分/礼包等等
传统做法用户每次签到时往是数据库插入一条签到数据展示的时候把本月或者指定周期的签到数据获取出来用于判断用户是否签到、以及连续签到情况此方式简单理解容易
Redis做法由于签到数据的关注点就2个是否签到0/1、连续性因此就完全可以利用BitMap位图来实现 由于String类型的最大上限是512M转换为bit则是2^32个bit位。
所需命令
SETBIT key offset value向指定位置offset存入一个0或1GETBIT key offset获取指定位置offset的bit值BITCOUNT key [start] [end]统计BitMap中值为1的bit位的数量BITFIELD: 操作查询修改自增BitMap中bit 数组中的指定位置offset的值这里最不容易理解的就是BITFIELD详情可参考https://deepinout.com/redis-cmd/redis-bitmap-cmd/redis-cmd-bitfield.html 而且这部分还必须理解了否则该需求的核心部分就没办法理解了
需求假如当前为8月4号检测本月的签到情况用户分别于1、3、4号签到过 Redis-cli 操作
# 8月1号的签到
127.0.0.1:6379 SETBIT RangeId:Sign:1:8899 0 1
(integer) 1# 8月3号的签到
127.0.0.1:6379 SETBIT RangeId:Sign:1:8899 2 1
(integer) 1# 8月4号的签到
127.0.0.1:6379 SETBIT RangeId:Sign:1:8899 3 1
(integer) 1# 查询各天的签到情况
# 查询1号
127.0.0.1:6379 GETBIT RangeId:Sign:1:8899 0
(integer) 1
# 查询2号
127.0.0.1:6379 GETBIT RangeId:Sign:1:8899 1
(integer) 0
# 查询3号
127.0.0.1:6379 GETBIT RangeId:Sign:1:8899 2
(integer) 1
# 查询4号
127.0.0.1:6379 GETBIT RangeId:Sign:1:8899 3
(integer) 1# 查询指定区间的签到情况
127.0.0.1:6379 BITFIELD RangeId:Sign:1:8899 get u4 0
1) (integer) 111-4号的签到情况为1011二进制 11十进制 是否签到、连续签到判断 签到功能中最不好理解的就是是否签到、连续签到的判断在下面SpringBoot代码中就是通过这样的signFlag 1 1 ! signFlag来判断的稍微有一点不好理解在这里提前讲述一下
上面测试了1-4号的签到情况通过BITFIELD获取出来signFlag 11十进制 1011二进制
连续签到的判断依据就是从右往左计算连续为1的BIT个数二进制 1011 表示连续签到的天数就是2天2天的计算过程如下
第一步获取signFlag第二步循环天数以上测试用例是4天的签到情况for循环也就是4次第三步从右往左循环判断连续签到遇到第一个false的时候终止并得到连续天数
签到详情循环所有天数true就表示当前签到了false表示当天未签到
第一次循环
signFlag 1011
signFlag 1 结果 101
signFlag 1 结果1010
1010 ! signFlag1011 结果true //4号已签到说明连续签到1天
signFlag 1 结果 101 // 此时signFlag 101第二次循环
signFlag 101 // 前一次循环计算的结果
signFlag 1 结果 10
signFlag 1 结果100
100 ! signFlag101 结果true //3号已签到说明连续签到2天
signFlag 1 结果 10 // 此时signFlag 10第三次循环
signFlag 10 // 前一次循环计算的结果
signFlag 1 结果 1
signFlag 1 结果10
10 ! signFlag10 结果false //2号未签到说明连续签到从这里就断了
signFlag 1 结果 1 // 此时signFlag 1到这一步遇到第一个false说明连续签到中断
第四次循环
signFlag 1 // 前一次循环计算的结果
signFlag 1 结果 0
signFlag 1 结果 0
0 ! signFlag1 结果true //1号已签到到此根据BITFIELD获取出来11十进制就能得到1、3、4号已签到2号未签到连续签到2天 理解上面的逻辑之后再来看下面的SpringBoot代码就会容易很多了
SpringBoot实现签到 签到的方式一般就两种按月周/ 自定义周期下面将两种方式的签到全部列举出来以供大家参考
按月签到 签到工具类
import lombok.extern.slf4j.Slf4j;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** title: 按月签到*/
Slf4j
Service
public class SignByMonthServiceImpl {AutowiredStringRedisTemplate stringRedisTemplate;private int dayOfMonth() {DateTime dateTime new DateTime();return dateTime.dayOfMonth().get();}/*** 按照月份和用户ID生成用户签到标识 ** param userId 用户id* return*/private String signKeyWitMouth(String userId) {DateTime dateTime new DateTime();DateTimeFormatter fmt DateTimeFormat.forPattern(yyyy-MM);StringBuilder builder new StringBuilder(UserId:Sign:);builder.append(userId).append(:).append(dateTime.toString(fmt));return builder.toString();}/*** 设置标记位* 标记是否签到** param key* param offset* param tag* return*/public Boolean sign(String key, long offset, boolean tag) {return this.stringRedisTemplate.opsForValue().setBit(key, offset, tag);}/*** 统计计数** param key 用户标识* return*/public long bitCount(String key) {return stringRedisTemplate.execute((RedisCallbackLong) redisConnection - redisConnection.bitCount(key.getBytes()));}/*** 获取多字节位域*/public ListLong bitfield(String buildSignKey, int limit, long offset) {return this.stringRedisTemplate.opsForValue().bitField(buildSignKey, BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(limit)).valueAt(offset));}/*** 判断是否被标记** param key* param offest* return*/public Boolean container(String key, long offest) {return this.stringRedisTemplate.opsForValue().getBit(key, offest);}/*** 用户今天是否签到** param userId* return*/public int checkSign(String userId) {DateTime dateTime new DateTime();String signKey this.signKeyWitMouth(userId);int offset dateTime.getDayOfMonth() - 1;int value this.container(signKey, offset) ? 1 : 0;return value;}/*** 查询用户当月签到日历** param userId* return*/public MapString, Boolean querySignedInMonth(String userId) {DateTime dateTime new DateTime();int lengthOfMonth dateTime.dayOfMonth().getMaximumValue();MapString, Boolean signedInMap new HashMap(dateTime.getDayOfMonth());String signKey this.signKeyWitMouth(userId);ListLong bitfield this.bitfield(signKey, lengthOfMonth, 0);if (!CollectionUtils.isEmpty(bitfield)) {long signFlag bitfield.get(0) null ? 0 : bitfield.get(0);DateTimeFormatter fmt DateTimeFormat.forPattern(yyyy-MM-dd);for (int i lengthOfMonth; i 0; i--) {DateTime dateTime1 dateTime.withDayOfMonth(i);signedInMap.put(dateTime1.toString(fmt), signFlag 1 1 ! signFlag);signFlag 1;}}return signedInMap;}/*** 用户签到** param userId* return*/public boolean signWithUserId(String userId) {int dayOfMonth this.dayOfMonth();String signKey this.signKeyWitMouth(userId);long offset (long) dayOfMonth - 1;boolean re false;if (Boolean.TRUE.equals(this.sign(signKey, offset, Boolean.TRUE))) {re true;}// 查询用户连续签到次数,最大连续次数为7天long continuousSignCount this.queryContinuousSignCount(userId, 7);return re;}/*** 统计当前月份一共签到天数** param userId*/public long countSignedInDayOfMonth(String userId) {String signKey this.signKeyWitMouth(userId);return this.bitCount(signKey);}/*** 查询用户当月连续签到次数** param userId* return*/public long queryContinuousSignCountOfMonth(String userId) {int signCount 0;String signKey this.signKeyWitMouth(userId);int dayOfMonth this.dayOfMonth();ListLong bitfield this.bitfield(signKey, dayOfMonth, 0);if (!CollectionUtils.isEmpty(bitfield)) {long signFlag bitfield.get(0) null ? 0 : bitfield.get(0);DateTime dateTime new DateTime();// 连续不为0即为连续签到次数当天未签到情况下for (int i 0; i dateTime.getDayOfMonth(); i) {if (signFlag 1 1 signFlag) {if (i 0) break;} else {signCount 1;}signFlag 1;}}return signCount;}/*** 以7天一个周期连续签到次数** param period 周期* return*/public long queryContinuousSignCount(String userId, Integer period) {//查询目前连续签到次数long count this.queryContinuousSignCountOfMonth(userId);//按最大连续签到取余if (period ! null period count) {long num count % period;if (num 0) {count period;} else {count num;}}return count;}
}测试类
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;import java.util.Map;SpringBootTest
Slf4j
public class SignTest2 {Autowiredprivate SignByMonthServiceImpl signByMonthService;Autowiredprivate StringRedisTemplate redisTemplate;/*** 测试用户按月签到*/Testpublic void querySignDay() {//模拟用户签到//for(int i5;i19;i){redisTemplate.opsForValue().setBit(UserId:Sign:560:2022-08, 0, true);//}System.out.println(560用户今日是否已签到: this.signByMonthService.checkSign(560));MapString, Boolean stringBooleanMap this.signByMonthService.querySignedInMonth(560);System.out.println(本月签到情况:);for (Map.EntryString, Boolean entry : stringBooleanMap.entrySet()) {System.out.println(entry.getKey() : (entry.getValue() ? √ : -));}long countSignedInDayOfMonth this.signByMonthService.countSignedInDayOfMonth(560);System.out.println(本月一共签到: countSignedInDayOfMonth 天);System.out.println(目前连续签到: this.signByMonthService.queryContinuousSignCount(560, 7) 天);}
}执行日志
c.e.r.bitmap_sign_by_month.SignTest2 : 560用户今日是否已签到:0
c.e.r.bitmap_sign_by_month.SignTest2 : 本月签到情况:
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-12: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-11: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-10: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-31: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-30: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-19: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-18: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-17: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-16: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-15: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-14: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-13: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-23: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-01: √
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-22: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-21: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-20: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-09: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-08: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-29: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-07: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-28: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-06: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-27: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-05: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-26: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-04: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-25: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-03: √
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-24: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-02: -
c.e.r.bitmap_sign_by_month.SignTest2 : 本月一共签到:2天
c.e.r.bitmap_sign_by_month.SignTest2 : 目前连续签到:1天指定时间签到 签到工具类 import lombok.extern.slf4j.Slf4j;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;Slf4j
Service
public class SignByRangeServiceImpl {AutowiredStringRedisTemplate stringRedisTemplate;/*** 根据区间的id 以及用户id 拼接key** param rangeId 区间ID 一般是指定活动的ID等* param userId 用户的ID* return*/private String signKey(Integer rangeId, Integer userId) {StringBuilder builder new StringBuilder(RangeId:Sign:);builder.append(rangeId).append(:).append(userId);return builder.toString();}/*** 获取当前时间与起始时间的间隔天数** param start 起始时间* return*/private int intervalTime(LocalDateTime start) {return (int) (LocalDateTime.now().toLocalDate().toEpochDay() - start.toLocalDate().toEpochDay());}/*** 设置标记位* 标记是否签到** param key 签到的key* param offset 偏移量 一般是指当前时间离起始时间活动开始的天数* param tag 是否签到 true:签到 false:未签到* return*/private Boolean setBit(String key, long offset, boolean tag) {return this.stringRedisTemplate.opsForValue().setBit(key, offset, tag);}/*** 统计计数** param key 统计的key* return*/private long bitCount(String key) {return stringRedisTemplate.execute((RedisCallbackLong) redisConnection - redisConnection.bitCount(key.getBytes()));}/*** 获取多字节位域** param key 缓存的key* param limit 获取多少* param offset 偏移量是多少* return*/private ListLong bitfield(String key, int limit, long offset) {return this.stringRedisTemplate.opsForValue().bitField(key, BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(limit)).valueAt(offset));}/*** 判断是否签到** param key 缓存的key* param offest 偏移量 指当前时间距离起始时间的天数* return*/private Boolean container(String key, long offest) {return this.stringRedisTemplate.opsForValue().getBit(key, offest);}/*** 根据起始时间进行签到** param rangeId* param userId* param start* return*/public Boolean sign(Integer rangeId, Integer userId, LocalDateTime start) {int offset intervalTime(start);String key signKey(rangeId, userId);return setBit(key, offset, true);}/*** 根据偏移量签到** param rangeId* param userId* param offset* return*/public Boolean sign(Integer rangeId, Integer userId, long offset) {String key signKey(rangeId, userId);return setBit(key, offset, true);}/*** 用户今天是否签到** param userId* return*/public Boolean checkSign(Integer rangeId, Integer userId, LocalDateTime start) {long offset intervalTime(start);String key this.signKey(rangeId, userId);return this.container(key, offset);}/*** 统计当前月份一共签到天数** param userId*/public long countSigned(Integer rangeId, Integer userId) {String signKey this.signKey(rangeId, userId);return this.bitCount(signKey);}public MapString, Boolean querySigned(Integer rangeId, Integer userId, LocalDateTime start) {int days intervalTime(start);MapString, Boolean signedInMap new HashMap(days);String signKey this.signKey(rangeId, userId);ListLong bitfield this.bitfield(signKey, days 1, 0);if (!CollectionUtils.isEmpty(bitfield)) {long signFlag bitfield.get(0) null ? 0 : bitfield.get(0);DateTimeFormatter fmt DateTimeFormatter.ofPattern(yyyy-MM-dd);for (int i days; i 0; i--) {LocalDateTime localDateTime start.plusDays(i);signedInMap.put(localDateTime.format(fmt), signFlag 1 1 ! signFlag);signFlag 1;}}return signedInMap;}/*** 查询用户当月连续签到次数** param userId* return*/public long queryContinuousSignCount(Integer rangeId, Integer userId, LocalDateTime start) {int signCount 0;String signKey this.signKey(rangeId, userId);int days this.intervalTime(start);ListLong bitfield this.bitfield(signKey, days 1, 0);if (!CollectionUtils.isEmpty(bitfield)) {long signFlag bitfield.get(0) null ? 0 : bitfield.get(0);DateTime dateTime new DateTime();// 连续不为0即为连续签到次数当天未签到情况下for (int i 0; i dateTime.getDayOfMonth(); i) {if (signFlag 1 1 signFlag) {if (i 0) break;} else {signCount 1;}signFlag 1;}}return signCount;}
}测试工具类
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;SpringBootTest
Slf4j
public class SignTest {AutowiredSignByRangeServiceImpl signByRangeService;Testvoid test() {DateTimeFormatter isoDateTime DateTimeFormatter.ISO_DATE_TIME;// 活动开始时间LocalDateTime start LocalDateTime.of(2022, 8, 1, 1, 0, 0);Integer rangeId 1;Integer userId 8899;log.info(签到开始时间: {}, start.format(isoDateTime));log.info(活动ID: {} 用户ID: {}, rangeId, userId);// 手动指定偏移量签到signByRangeService.sign(rangeId, userId, 0);// 判断是否签到Boolean signed signByRangeService.checkSign(rangeId, userId, start);log.info(今日是否签到: {}, signed ? √ : -);// 签到Boolean sign signByRangeService.sign(rangeId, userId, start);log.info(签到操作之前的签到状态{} -表示今日第一次签到√表示今天已经签到过了, sign ? √ : -);// 签到总数long countSigned signByRangeService.countSigned(rangeId, userId);log.info(总共签到: {} 天, countSigned);// 连续签到的次数long continuousSignCount signByRangeService.queryContinuousSignCount(rangeId, userId, start);log.info(连续签到: {} 天, continuousSignCount);// 签到的详情MapString, Boolean stringBooleanMap signByRangeService.querySigned(rangeId, userId, start);for (Map.EntryString, Boolean entry : stringBooleanMap.entrySet()) {log.info(签到详情 {} : {}, entry.getKey(), (entry.getValue() ? √ : -));}}
}输出日志
签到开始时间: 2022-08-01T01:00:00
活动ID: 1 用户ID: 8899
今日是否签到: √
签到操作之前的签到状态√ -表示今日第一次签到√表示今天已经签到过了
总共签到: 3 天
连续签到: 2 天
签到详情 2022-08-01 : √
签到详情 2022-08-04 : √
签到详情 2022-08-03 : √
签到详情 2022-08-02 : -9GEO搜附近
很多生活类的APP都具备一个搜索附近的功能比如美团搜索附近的商家 如果自己想要根据经纬度来实现一个搜索附近的功能是非常麻烦的但是Redis 在3.2的版本新增了Redis GEO用于存储地址位置信息并对支持范围搜索基于GEO就能轻松且快速的开发一个搜索附近的功能
GEO API 及Redis-cli 操作 geoadd新增位置坐标。
127.0.0.1:6379 GEOADD drinks 116.62445 39.86206 starbucks 117.3514785 38.7501247 yidiandian 116.538542 39.75412 xicha
(integer) 3geopos获取位置坐标。
127.0.0.1:6379 GEOPOS drinks starbucks
1) 1) 116.624451577663421632) 39.86206038535793539
127.0.0.1:6379 GEOPOS drinks starbucks yidiandian mxbc
1) 1) 116.624451577663421632) 39.86206038535793539
2) 1) 117.351480424404144292) 38.75012383773680114
3) (nil)geodist计算两个位置之间的距离。
单位参数
127.0.0.1:6379 GEODIST drinks starbucks yidiandian
138602.4133
127.0.0.1:6379 GEODIST drinks starbucks xicha
14072.1255
127.0.0.1:6379 GEODIST drinks starbucks xicha m
14072.1255
127.0.0.1:6379 GEODIST drinks starbucks xicha km
14.0721m 米默认单位。
km 千米。
mi 英里。
ft 英尺。georadius根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
参数说明
127.0.0.1:6379 GEORADIUS drinks 116 39 100 km WITHDIST
1) 1) xicha2) 95.8085
127.0.0.1:6379 GEORADIUS drinks 116 39 100 km WITHDIST WITHCOORD
1) 1) xicha2) 95.80853) 1) 116.538540422916412352) 39.75411928478748536
127.0.0.1:6379 GEORADIUS drinks 116 39 100 km WITHDIST WITHCOORD WITHHASH
1) 1) xicha2) 95.80853) (integer) 40691518008823014) 1) 116.538540422916412352) 39.75411928478748536127.0.0.1:6379 GEORADIUS drinks 116 39 120 km WITHDIST WITHCOORD COUNT 1
1) 1) xicha2) 95.80853) 1) 116.538540422916412352) 39.75411928478748536127.0.0.1:6379 GEORADIUS drinks 116 39 120 km WITHDIST WITHCOORD COUNT 1 ASC
1) 1) xicha2) 95.80853) 1) 116.538540422916412352) 39.75411928478748536127.0.0.1:6379 GEORADIUS drinks 116 39 120 km WITHDIST WITHCOORD COUNT 1 DESC
1) 1) starbucks2) 109.87033) 1) 116.624451577663421632) 39.86206038535793539m 米默认单位。
km 千米。
mi 英里。
ft 英尺。
WITHDIST: 在返回位置元素的同时 将位置元素与中心之间的距离也一并返回。
WITHCOORD: 将位置元素的经度和纬度也一并返回。
WITHHASH: 以 52 位有符号整数的形式 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试 实际中的作用并不大。
COUNT 限定返回的记录数。
ASC: 查找结果根据距离从近到远排序。
DESC: 查找结果根据从远到近排序。georadiusbymember根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合。
功能和上面的georadius类似只是georadius是以经纬度坐标为中心这个是以某个地点为中心
geohash返回一个或多个位置对象的 geohash 值。
127.0.0.1:6379 GEOHASH drinks starbucks xicha
1) wx4fvbem6d0
2) wx4f5vhb8b0SpringBoot 操作 通过SpringBoot操作GEO的示例如下
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.geo.*;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.RedisTemplate;import java.util.List;SpringBootTest
Slf4j
public class GEOTest {private final String KEY geo:drinks;AutowiredRedisTemplate redisTemplate;Testpublic void test() {add(starbucks, new Point(116.62445, 39.86206));add(yidiandian, new Point(117.3514785, 38.7501247));add(xicha, new Point(116.538542, 39.75412));get(starbucks, yidiandian, xicha);GeoResults nearByXY getNearByXY(new Point(116, 39), new Distance(120, Metrics.KILOMETERS));ListGeoResult content nearByXY.getContent();for (GeoResult geoResult : content) {log.info({}, geoResult.getContent());}GeoResults nearByPlace getNearByPlace(starbucks, new Distance(120, Metrics.KILOMETERS));content nearByPlace.getContent();for (GeoResult geoResult : content) {log.info({}, geoResult.getContent());}getGeoHash(starbucks, yidiandian, xicha);del(yidiandian, xicha);}private void add(String name, Point point) {Long add redisTemplate.opsForGeo().add(KEY, point, name);log.info(成功添加名称{} 的坐标信息信息{}, name, point);}private void get(String... names) {ListPoint position redisTemplate.opsForGeo().position(KEY, names);log.info(获取名称为{} 的坐标信息{}, names, position);}private void del(String... names) {Long remove redisTemplate.opsForGeo().remove(KEY, names);log.info(删除名称为{} 的坐标信息数量{}, names, remove);}/*** 根据坐标 获取指定范围的位置** param point* param distance* return*/private GeoResults getNearByXY(Point point, Distance distance) {Circle circle new Circle(point, distance);RedisGeoCommands.GeoRadiusCommandArgs args RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance(). // 包含距离includeCoordinates(). // 包含坐标sortAscending(). // 排序 还可选sortDescending()limit(5); // 获取前多少个GeoResults geoResults redisTemplate.opsForGeo().radius(KEY, circle, args);log.info(根据坐标获取{} {} 范围的数据{}, point, distance, geoResults);return geoResults;}/*** 根据一个位置获取指定范围内的其他位置** param name* param distance* return*/private GeoResults getNearByPlace(String name, Distance distance) {RedisGeoCommands.GeoRadiusCommandArgs args RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance(). // 包含距离includeCoordinates(). // 包含坐标sortAscending(). // 排序 还可选sortDescending()limit(5); // 获取前多少个GeoResults geoResults redisTemplate.opsForGeo().radius(KEY, name, distance, args);log.info(根据位置{} 获取 {} 范围的数据{}, name, distance, geoResults);return geoResults;}/*** 获取GEO HASH** param names* return*/private ListString getGeoHash(String... names) {ListString hash redisTemplate.opsForGeo().hash(KEY, names);log.info(names{} 对应的hash{}, names, hash);return hash;}
}执行日志 成功添加名称starbucks 的坐标信息信息Point [x116.624450, y39.862060]
成功添加名称yidiandian 的坐标信息信息Point [x117.351479, y38.750125]
成功添加名称xicha 的坐标信息信息Point [x116.538542, y39.754120]获取名称为[starbucks, yidiandian, xicha] 的坐标信息[Point [x116.624452, y39.862060], Point [x117.351480, y38.750124], Point [x116.538540, y39.754119]]根据坐标获取Point [x116.000000, y39.000000] 120.0 KILOMETERS 范围的数据GeoResults: [averageDistance: 102.8394 KILOMETERS, results: GeoResult [content: RedisGeoCommands.GeoLocation(namexicha, pointPoint [x116.538540, y39.754119]), distance: 95.8085 KILOMETERS, ],GeoResult [content: RedisGeoCommands.GeoLocation(namestarbucks, pointPoint [x116.624452, y39.862060]), distance: 109.8703 KILOMETERS, ]]
RedisGeoCommands.GeoLocation(namexicha, pointPoint [x116.538540, y39.754119])
RedisGeoCommands.GeoLocation(namestarbucks, pointPoint [x116.624452, y39.862060])根据位置starbucks 获取 120.0 KILOMETERS 范围的数据GeoResults: [averageDistance: 7.03605 KILOMETERS, results: GeoResult [content: RedisGeoCommands.GeoLocation(namestarbucks, pointPoint [x116.624452, y39.862060]), distance: 0.0 KILOMETERS, ],GeoResult [content: RedisGeoCommands.GeoLocation(namexicha, pointPoint [x116.538540, y39.754119]), distance: 14.0721 KILOMETERS, ]]
RedisGeoCommands.GeoLocation(namestarbucks, pointPoint [x116.624452, y39.862060])
RedisGeoCommands.GeoLocation(namexicha, pointPoint [x116.538540, y39.754119])names[starbucks, yidiandian, xicha] 对应的hash[wx4fvbem6d0, wwgkqqhxzd0, wx4f5vhb8b0]删除名称为[yidiandian, xicha] 的坐标信息数量210简单限流
为了保证项目的安全稳定运行防止被恶意的用户或者异常的流量打垮整个系统一般都会加上限流比如常见的sential、hystrix都是实现限流控制如果项目用到了Redis也可以利用Redis来实现一个简单的限流功能
功能所需命令
INCR将 key 中储存的数字值增一
Expire设置key的有效期Redis-cli操作
127.0.0.1:6379 INCR r:f:user1
(integer) 1
# 第一次 设置一个过期时间
127.0.0.1:6379 EXPIRE r:f:user1 5
(integer) 1
127.0.0.1:6379 INCR r:f:user1
(integer) 2
# 等待5s 再次增加 发现已经重置了
127.0.0.1:6379 INCR r:f:user1
(integer) 1SpringBoot示例
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;import java.util.concurrent.TimeUnit;SpringBootTest
Slf4j
public class FreqTest {// 单位时间秒private static final Integer TIME 5;// 允许访问上限次数private static final Integer MAX 100;AutowiredRedisTemplate redisTemplate;Testpublic void test() throws Exception {String userName user1;int tag 1;boolean frequency frequency(userName);log.info(第{}次是否放行{}, tag, frequency);for (int i 0; i 100; i) {tag 1;frequency(userName);}frequency frequency(userName);log.info(第{}次是否放行{}, tag, frequency);Thread.sleep(5000);frequency frequency(userName);log.info(模拟等待5s后第{}次是否放行{}, tag, frequency);}/*** 校验访问频率** param uniqueId 用于限流的唯一ID 可以是用户ID、或者客户端IP等* return true放行 false拦截*/private boolean frequency(String uniqueId) {String key r:q: uniqueId;Long increment redisTemplate.opsForValue().increment(key);if (increment 1) {redisTemplate.expire(key, TIME, TimeUnit.SECONDS);}if (increment MAX) {return true;}return false;}
}运行日志
user1 第1次请求是否放行true
user1 第101次请求是否放行false
模拟等待5s后user1 第101次请求是否放行true11全局ID
在分布式系统中很多场景下需要全局的唯一ID由于Redis是独立于业务服务的其他应用就可以利用Incr的原子性操作来生成全局的唯一递增ID
功能所需命令
INCR将 key 中储存的数字值增一Redis-cli 客户端测试
127.0.0.1:6379 incr g:uid
(integer) 1
127.0.0.1:6379 incr g:uid
(integer) 2
127.0.0.1:6379 incr g:uid
(integer) 312简单分布式锁
在分布式系统中很多操作是需要用到分布式锁防止并发操作带来一些问题因为redis是独立于分布式系统外的其他服务因此就可以利用redis来实现一个简单的不完美分布式锁
功能所需命令
SETNX key不存在设置key存在不设置 # 加锁
127.0.0.1:6379 SETNX try_lock 1
(integer) 1
# 释放锁
127.0.0.1:6379 del try_lock
(integer) 1set key value [ex seconds] [nx | xx]
上面的方式虽然能够加锁但是不难发现很容易出现死锁的情况比如a用户在加锁之后突然系统挂了此时a就永远不会释放他持有的锁了从而导致死锁为此我们可以利用redis的过期时间来防止死锁问题
set try_lock 1 ex 5 nx不完美的锁
上面的方案虽然解决了死锁的问题但是又带来了一个新的问题执行时间如果长于自动释放的时间比如自动释放是5秒但是业务执行耗时了8秒那么在第5秒的时候锁就自动释放了此时其他的线程就能正常拿到锁简单流程如下 此时相同颜色部分的时间区间是由多线程同时在执行。而且此问题在此方案下并没有完美的解决方案只能做到尽可能的避免
方式一value设置为随机数如1234在程序释放锁的时候检测一下是不是自己加的锁比如A线程在第8s释放的锁就是线程B加的此时在释放的时候就可以检验一下value是不是自己当初设置的值1234是的就释放不是的就不管了
方式二只在时间消耗比较小的业务上选用此方案尽可能的避免执行时间超过锁的自动释放时间13认识的人/好友推荐
在支付宝、抖音、QQ等应用中都会看到好友推荐 好友推荐往往都是基于你的好友关系网来推荐将你可能认识的人推荐给你让你去添加好友如果随意在系统找个人推荐给你那你认识的可能性是非常小的此时就失去了推荐的目的
比如A和B是好友B和C是好友此时A和C认识的概率是比较大的就可以在A和C之间的好友推荐
基于这个逻辑就可以利用 Redis 的 Set 集合缓存各个用户的好友列表然后以差集的方式来实现好友推荐
功能所需的命令
SADD key member [member …]集合中添加元素缓存好友列表
SDIFF key [key …]取两个集合间的差集找出可以推荐的用户Redis-cli 客户端测试
# 记录 用户1 的好友列表
127.0.0.1:6379 SADD fl:user1 user2 user3
(integer) 2
# 记录 用户2 的好友列表
127.0.0.1:6379 SADD fl:user2 user1 user4
(integer) 2# 用户1 可能认识的人 把自己user1去掉user4是可能认识的人
127.0.0.1:6379 SDIFF fl:user2 fl:user1
1) user1
2) user4# 用户2 可能认识的人 把自己user2去掉user3是可能认识的人
127.0.0.1:6379 SDIFF fl:user1 fl:user2
1) user3
2) user2不过这只是推荐机制中的一种因素可以借助其他条件来增强推荐的准确度
14发布/订阅
发布/订阅是比较常用的一种模式在分布式系统中如果需要实时感知到一些变化比如某些配置发生变化需要实时同步就可以用到发布订阅功能
常用API
PUBLISH channel message将消息推送到指定的频道
SUBSCRIBE channel [channel …]订阅给定的一个或多个频道的信息15消息队列
说到消息队列常用的就是Kafka、RabbitMQ等等其实 Redis 利用 List 也能实现一个消息队列
功能所需的指令
RPUSH key value1 [value2]在列表中添加一个或多个值
BLPOP key1 [key2] timeout移出并获取列表的第一个元素 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
BRPOP key1 [key2] timeout移出并获取列表的最后一个元素 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。依赖调整
Spring Boot 从 2.0版本开始将默认的Redis客户端Jedis替换为Lettuce在测试这块阻塞的时候会出现一个超时的异常io.lettuce.core.RedisCommandTimeoutException: Command timed out after 1 minute(s)没有找到一个好的解决方式所以这里将 Lettuce 换回成 Jedis 就没有问题了pom.xml 的配置如下
dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactIdexclusionsexclusiongroupIdredis.clients/groupIdartifactIdjedis/artifactId/exclusionexclusionartifactIdlettuce-core/artifactIdgroupIdio.lettuce/groupId/exclusion/exclusions
/dependency
!-- jedis客户端 --
dependencygroupIdredis.clients/groupIdartifactIdjedis/artifactId
/dependency
!-- spring2.X集成redis所需common-pool2使用jedis必须依赖它--
dependencygroupIdorg.apache.commons/groupIdartifactIdcommons-pool2/artifactId
/dependency测试代码
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;import java.util.concurrent.TimeUnit;SpringBootTest
Slf4j
public class QueueTest {private static final String REDIS_LP_QUEUE redis:lp:queue;private static final String REDIS_RP_QUEUE redis:rp:queue;AutowiredStringRedisTemplate stringRedisTemplate;/*** 先进后出队列*/Testpublic void rightMonitor() {while (true) {Object o stringRedisTemplate.opsForList().rightPop(REDIS_LP_QUEUE, 0, TimeUnit.SECONDS);log.info(先进后出队列 接收到数据{}, o);}}/*** 先进先出队列*/Testpublic void leftMonitor() {while (true) {Object o stringRedisTemplate.opsForList().leftPop(REDIS_RP_QUEUE, 0, TimeUnit.SECONDS);log.info(先进先出队列 接收到数据{}, o);}}
}不过对消息的可靠性要求比较高的场景建议还是使用专业的消息队列框架当值被弹出之后List 中就已经不存在对应的值了假如此时程序崩溃就会出现消息的丢失无法保证可靠性虽然说也有策略能够保证消息的可靠性比如在弹出的同时将其保存到另外一个队列BRPOPLPUSH成功之后再从另外的队列中移除当消息处理失败或者异常再重新进入队列执行只是这样做就得不偿失了。
16数据共享session共享
既然Redis能持久化数据就可以用它来实现模块间的数据共享SpringBoot Session 利用的这个机制来实现 Session 共享
依赖
dependencygroupIdorg.springframework.session/groupIdartifactIdspring-session-data-redis/artifactId
/dependency开启session共享
Configuration
EnableRedisHttpSession
public class RedisSessionConfig {
}测试代码
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;/*** author session 共享* title: RedisSessionController* projectName ehang-spring-boot*/
RestController
RequestMapping(session)
public class RedisSessionController {/*** 设置session的值* param request* return*/GetMapping(set)public Map set(HttpServletRequest request) {String id request.getSession().getId();MapString, String vas new HashMap();String key key;String value value;vas.put(id, id);vas.put(key, value);// 自定义session的值request.getSession().setAttribute(key, value);return vas;}/*** 获取session的值* param request* return*/GetMapping(get)public Map get(HttpServletRequest request) {MapString, Object vas new HashMap();// 遍历所有的session值EnumerationString attributeNames request.getSession().getAttributeNames();while (attributeNames.hasMoreElements()) {String k attributeNames.nextElement();Object va request.getSession().getAttribute(k);vas.put(k, va);}vas.put(id, request.getSession().getId());return vas;}
}测试
开启两个服务分别接听8080和80818080调用赋值接口8081调用获取接口可以看到两个服务共享了一份Session数据
Redis中保存的数据
127.0.0.1:6379 keys spring:*
1) spring:session:sessions:expires:6f1d7d53-fe01-4e80-9e6a-5ff54fffa92a
2) spring:session:expirations:1659688680000
3) spring:session:sessions:6f1d7d53-fe01-4e80-9e6a-5ff54fffa92a17商品筛选
商城类的应用都会有类似于下图的一个商品筛选的功能来帮用户快速搜索理想的商品 假如现在iphone 100 、华为mate 5000 已发布在各大商城上线下面就通过 Redis 的 set 来实现上述的商品筛选功能
功能所需命令
SADD key member [member …]添加一个或多个元素
SINTER key [key …]返回给定所有集合的交集Redis-cli 客户端测试
# 将iphone100 添加到品牌为苹果的集合
127.0.0.1:6379 sadd brand:apple iphone100
(integer) 1# 将meta5000 添加到品牌为苹果的集合
127.0.0.1:6379 sadd brand:huawei meta5000
(integer) 1# 将 meta5000 iphone100 添加到支持5T内存的集合
127.0.0.1:6379 sadd ram:5t iphone100 meta5000
(integer) 2# 将 meta5000 添加到支持10T内存的集合
127.0.0.1:6379 sadd ram:10t meta5000
(integer) 1# 将 iphone100 添加到操作系统是iOS的集合
127.0.0.1:6379 sadd os:ios iphone100
(integer) 1# 将 meta5000 添加到操作系统是Android的集合
127.0.0.1:6379 sadd os:android meta5000
(integer) 1# 将 iphone100 meta5000 添加到屏幕为6.0-6.29的集合中
127.0.0.1:6379 sadd screensize:6.0-6.29 iphone100 meta5000
(integer) 2# 筛选内存5T、屏幕在6.0-6.29的机型
127.0.0.1:6379 sinter ram:5t screensize:6.0-6.29
1) meta5000
2) iphone100# 筛选内存10T、屏幕在6.0-6.29的机型
127.0.0.1:6379 sinter ram:10t screensize:6.0-6.29
1) meta5000# 筛选内存5T、系统为iOS的机型
127.0.0.1:6379 sinter ram:5t screensize:6.0-6.29 os:ios
1) iphone100# 筛选内存5T、屏幕在6.0-6.29、品牌是华为的机型
127.0.0.1:6379 sinter ram:5t screensize:6.0-6.29 brand:huawei
1) meta500018购物车
商品缓存
电商项目中商品消息都会做缓存处理特别是热门商品访问用户比较多由于商品的结果比较复杂店铺信息产品信息标题、描述、详情图封面图为了方便管理和操作一般都会采用 Hash 的方式来存储key为商品IDfield用来保存各项参数value保存对于的值
购物车
当商品信息做了缓存购物车需要做的就是通过Hash记录商品ID以及需要购买的数量其中key为用户信息field为商品IDvalue用来记录购买的数量
功能所需命令
HSET key field value : 将哈希表 key 中的字段 field 的值设为 value ;
HMSET key field1 value1 [field2 value2 ] 同时将多个 field-value (域-值)对设置到哈希表 key 中。
HGET key field获取存储在哈希表中指定字段的值。
HGETALL key 获取在哈希表中指定 key 的所有字段和值
HINCRBY key field increment 为哈希表 key 中的指定字段的整数值加上增量 increment 。
HLEN key获取哈希表中字段的数量Redis-cli 客户端测试
# 购物车添加单个商品
127.0.0.1:6379 HSET sc:u1 c001 1
(integer) 1
# 购物车添加多个商品
127.0.0.1:6379 HMSET sc:u1 c002 1 coo3 2
OK
# 添加商品购买数量
127.0.0.1:6379 HINCRBY sc:u1 c002 1
(integer) 2
# 减少商品的购买数量
127.0.0.1:6379 HINCRBY sc:u1 c003 -1
(integer) 1
# 获取单个的购买数量
127.0.0.1:6379 HGET sc:u1 c002
2
# 获取购物车的商品数量
127.0.0.1:6379 HLEN sc:u1
(integer) 3
# 购物车详情
127.0.0.1:6379 HGETALL sc:u1
1) c001
2) 1
3) c002
4) 2
5) coo3
6) 219定时取消订单key过期监听
电商类的业务一般都会有订单30分钟不支付自动取消的功能此时就需要用到定时任务框架Quartz、xxl-job、elastic-job 是比较常用的 Java 定时任务我们也可以通过 Redis 的定时过期、以及过期key的监听来实现订单的取消功能
Redis key 过期提醒配置
修改 redis 相关事件配置。找到 redis 配置文件 redis.conf查看 notify-keyspace-events 配置项如果没有添加 notify-keyspace-events Ex如果有值则追加 Ex相关参数说明如下
Kkeyspace 事件事件以 keyspace 为前缀进行发布Ekeyevent 事件事件以 keyevent 为前缀进行发布g一般性的非特定类型的命令比如delexpirerename等$字符串特定命令l列表特定命令s集合特定命令h哈希特定命令z有序集合特定命令x过期事件当某个键过期并删除时会产生该事件e驱逐事件当某个键因 maxmemore 策略而被删除时产生该事件Ag$lshzxe的别名因此”AKE”意味着所有事件添加RedisKeyExpirationListener的监听
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;Configuration
public class RedisListenerConfig {BeanRedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {RedisMessageListenerContainer container new RedisMessageListenerContainer();container.setConnectionFactory(connectionFactory);return container;}
}KeyExpirationEventMessageListener 接口监听所有 db 的过期事件 keyevent*:expired
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;/*** 监听所有db的过期事件__keyevent*__:expired*/
Component
Slf4j
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {super(listenerContainer);}/*** 针对 redis 数据失效事件进行数据处理** param message* param pattern*/Overridepublic void onMessage(Message message, byte[] pattern) {// 获取到失效的 key进行取消订单业务处理// 由于这里是监听了所有的key如果只处理特定数据的话需要做特殊处理String expiredKey message.toString();log.info(过期的Key{}, expiredKey);}
}测试
为了快速验证效果这里 将过期时间调整为2秒
注意由于过期之后Redis中的Key已经不存在了因此一定要将订单号作为key不能作为值保存否则监听到过期Key之后将拿不到过期的订单号 不推荐使用
基于这一套机制确实能够实现订单的超时取消但是还是不太建议使用这里仅作为一个思路原因主要有以下几个
redis 的过期删除策略是采用定时离线扫描或者访问时懒性检测删除并没有办法保证时效性有可能key已经到期了但Redis并没有扫描到导致通知的延迟
消息发送即忘fire and forget并不会保证消息的可达性如果此时服务不在线或者异常通知就再也收不到了20物流信息时间线
寄快递、网购的时候查询物流信息都会给我们展示xxx时候快递到达什么地方了这就是一个典型的时间线列表 数据库的做法就是每次变更就插入一条带时间的信息记录然后根据时间和IDID是必须的如果出现两个相同的时间单纯时间排序会造成顺序不对来排序生成时间线
我们也可以通过 Redis 的 List 来实现时间线功能由于 List 采用的是双向链表因此升序降序的时间线都能正常满足
RPUSH key value1 [value2]在列表中添加一个或多个值升序时间线
LPUSH key value1 [value2]将一个或多个值插入到列表头部降序时间线
LRANGE key start stop获取列表指定范围内的元素Redis-cli 客户端测试 升序
127.0.0.1:6379 RPUSH time:line:asc 20220805170000
(integer) 1
127.0.0.1:6379 RPUSH time:line:asc 20220805170001
(integer) 2
127.0.0.1:6379 RPUSH time:line:asc 20220805170002
(integer) 3
127.0.0.1:6379 LRANGE time:line:asc 0 -1
1) 20220805170000
2) 20220805170001
3) 20220805170002降序
127.0.0.1:6379 LPUSH time:line:desc 20220805170000
(integer) 1
127.0.0.1:6379 LPUSH time:line:desc 20220805170001
(integer) 2
127.0.0.1:6379 LPUSH time:line:desc 20220805170002
(integer) 3
127.0.0.1:6379 LRANGE time:line:desc 0 -1
1) 20220805170002
2) 20220805170001
3) 20220805170000好了关于Redis 的使用场景就介绍到这里有了这些个场景的运用下次再有面试官问你Redis除了缓存还做过什么相信应该不成问题了。