Redis20种使用场景
Redis20种使用场景
- 1缓存
- 2抽奖
- 3Set实现点赞/收藏功能
- 4排行榜
- 5PV统计(incr自增计数)
- 6UV统计(HeyperLogLog)
- 7去重(BloomFiler)
- 8用户签到(BitMap)
- 9GEO搜附近
- 10简单限流
- 11全局ID
- 12简单分布式锁
- 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 的依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2抽奖
曾几何时,抽奖是互联网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) "user9"
SpringBoot 实现
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) 1
SpringBoot 操作
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) "user3"2) "50"3) "user5"4) "25"5) "user2"6) "10"7) "user4"8) "3"9) "user1"
10) "1"
# 升序
127.0.0.1:6379> ZRANGE ranking 0 -1 WITHSCORES1) "user1"2) "1"3) "user4"4) "3"5) "user2"6) "10"7) "user5"8) "25"9) "user3"
10) "50"
SpringBoot操作
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);// 取所有Set<DefaultTypedTuple> 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 Set<DefaultTypedTuple> range(long min, long max) {// 降序Set<DefaultTypedTuple> set = redisTemplate.opsForZSet().reverseRangeWithScores(KEY_RANKING, min, max);// 升序//Set<DefaultTypedTuple> set = redisTemplate.opsForZSet().rangeWithScores(KEY_RANKING, min, max);return set;}
}
输出
所有用户排序:[DefaultTypedTuple [score=100.0, value=1003], DefaultTypedTuple [score=90.0, value=1004], DefaultTypedTuple [score=80.0, value=1002], DefaultTypedTuple [score=70.0, value=1005], DefaultTypedTuple [score=60.0, value=1001]]
前三名排序:[DefaultTypedTuple [score=100.0, value=1003], DefaultTypedTuple [score=90.0, value=1004], DefaultTypedTuple [score=80.0, value=1002]]
5PV统计(incr自增计数)
Page View(PV)指的是页面浏览量,是用来衡量流量的一个重要标准,也是数据分析很重要的一个依据;通常统计规则是页面被展示一次,就加一
功能所需命令
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) 2
6UV统计(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) 5
SpringBoot操作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) 0
SpringBoot整合
工具类 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 RedisScript<Boolean> bfreserveScript = new DefaultRedisScript<>("return redis.call('bf.reserve', KEYS[1], ARGV[1], ARGV[2])", Boolean.class);private static RedisScript<Boolean> bfaddScript = new DefaultRedisScript<>("return redis.call('bf.add', KEYS[1], ARGV[1])", Boolean.class);private static RedisScript<Boolean> 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 List<Integer> bfmadd(String key, String... values) {return (List<Integer>) redisTemplate.execute(this.generateScript(bfmaddScript, values), Arrays.asList(key), values);}/*** 批量检查元素是否存在(判断为存在时有可能是误判,不存在是一定不存在)** @param key* @param values* @return 按序 1表示存在,0表示不存在*/public List<Integer> bfmexists(String key, String... values) {return (List<Integer>) redisTemplate.execute(this.generateScript(bfmexistsScript, values), Arrays.asList(key), values);}private RedisScript<List> 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);}List<Integer> 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:false
8用户签到(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) 11
1-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 != signFlag(1011) 结果:true //4号已签到,说明连续签到1天
signFlag >>= 1 结果: 101 // 此时signFlag = 101
第二次循环
signFlag = 101 // 前一次循环计算的结果
signFlag >> 1 结果: 10
signFlag << 1 结果:100
100 != signFlag(101) 结果:true //3号已签到,说明连续签到2天
signFlag >>= 1 结果: 10 // 此时signFlag = 10
第三次循环
signFlag = 10 // 前一次循环计算的结果
signFlag >> 1 结果: 1
signFlag << 1 结果:10
10 != signFlag(10) 结果:false //2号未签到,说明连续签到从这里就断了
signFlag >>= 1 结果: 1 // 此时signFlag = 1
到这一步,遇到第一个false,说明连续签到中断;
第四次循环:
signFlag = 1 // 前一次循环计算的结果
signFlag >> 1 结果: 0
signFlag << 1 结果: 0
0 != signFlag(1) 结果: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((RedisCallback<Long>) redisConnection -> redisConnection.bitCount(key.getBytes()));}/*** 获取多字节位域*/public List<Long> 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 Map<String, Boolean> querySignedInMonth(String userId) {DateTime dateTime = new DateTime();int lengthOfMonth = dateTime.dayOfMonth().getMaximumValue();Map<String, Boolean> signedInMap = new HashMap<>(dateTime.getDayOfMonth());String signKey = this.signKeyWitMouth(userId);List<Long> 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();List<Long> 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 i=5;i<19;i++){redisTemplate.opsForValue().setBit("UserId:Sign:560:2022-08", 0, true);//}System.out.println("560用户今日是否已签到:" + this.signByMonthService.checkSign("560"));Map<String, Boolean> stringBooleanMap = this.signByMonthService.querySignedInMonth("560");System.out.println("本月签到情况:");for (Map.Entry<String, 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((RedisCallback<Long>) redisConnection -> redisConnection.bitCount(key.getBytes()));}/*** 获取多字节位域** @param key 缓存的key* @param limit 获取多少* @param offset 偏移量是多少* @return*/private List<Long> 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 Map<String, Boolean> querySigned(Integer rangeId, Integer userId, LocalDateTime start) {int days = intervalTime(start);Map<String, Boolean> signedInMap = new HashMap<>(days);String signKey = this.signKey(rangeId, userId);List<Long> 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);List<Long> 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);// 签到的详情Map<String, Boolean> stringBooleanMap = signByRangeService.querySigned(rangeId, userId, start);for (Map.Entry<String, 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) 3
geopos:获取位置坐标。
127.0.0.1:6379> GEOPOS drinks starbucks
1) 1) "116.62445157766342163"2) "39.86206038535793539"
127.0.0.1:6379> GEOPOS drinks starbucks yidiandian mxbc
1) 1) "116.62445157766342163"2) "39.86206038535793539"
2) 1) "117.35148042440414429"2) "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.0721"
m :米,默认单位。
km :千米。
mi :英里。
ft :英尺。
georadius:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
参数说明
127.0.0.1:6379> GEORADIUS drinks 116 39 100 km WITHDIST
1) 1) "xicha"2) "95.8085"
127.0.0.1:6379> GEORADIUS drinks 116 39 100 km WITHDIST WITHCOORD
1) 1) "xicha"2) "95.8085"3) 1) "116.53854042291641235"2) "39.75411928478748536"
127.0.0.1:6379> GEORADIUS drinks 116 39 100 km WITHDIST WITHCOORD WITHHASH
1) 1) "xicha"2) "95.8085"3) (integer) 40691518008823014) 1) "116.53854042291641235"2) "39.75411928478748536"127.0.0.1:6379> GEORADIUS drinks 116 39 120 km WITHDIST WITHCOORD COUNT 1
1) 1) "xicha"2) "95.8085"3) 1) "116.53854042291641235"2) "39.75411928478748536"127.0.0.1:6379> GEORADIUS drinks 116 39 120 km WITHDIST WITHCOORD COUNT 1 ASC
1) 1) "xicha"2) "95.8085"3) 1) "116.53854042291641235"2) "39.75411928478748536"127.0.0.1:6379> GEORADIUS drinks 116 39 120 km WITHDIST WITHCOORD COUNT 1 DESC
1) 1) "starbucks"2) "109.8703"3) 1) "116.62445157766342163"2) "39.86206038535793539"
m :米,默认单位。
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) "wx4f5vhb8b0"
SpringBoot 操作
通过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));List<GeoResult> 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) {List<Point> 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 List<String> getGeoHash(String... names) {List<String> hash = redisTemplate.opsForGeo().hash(KEY, names);log.info("names:{} 对应的hash:{}", names, hash);return hash;}
}
执行日志:
成功添加名称:starbucks 的坐标信息信息:Point [x=116.624450, y=39.862060]
成功添加名称:yidiandian 的坐标信息信息:Point [x=117.351479, y=38.750125]
成功添加名称:xicha 的坐标信息信息:Point [x=116.538542, y=39.754120]获取名称为:[starbucks, yidiandian, xicha] 的坐标信息:[Point [x=116.624452, y=39.862060], Point [x=117.351480, y=38.750124], Point [x=116.538540, y=39.754119]]根据坐标获取:Point [x=116.000000, y=39.000000] 120.0 KILOMETERS 范围的数据:GeoResults: [averageDistance: 102.8394 KILOMETERS, results: GeoResult [content: RedisGeoCommands.GeoLocation(name=xicha, point=Point [x=116.538540, y=39.754119]), distance: 95.8085 KILOMETERS, ],GeoResult [content: RedisGeoCommands.GeoLocation(name=starbucks, point=Point [x=116.624452, y=39.862060]), distance: 109.8703 KILOMETERS, ]]
RedisGeoCommands.GeoLocation(name=xicha, point=Point [x=116.538540, y=39.754119])
RedisGeoCommands.GeoLocation(name=starbucks, point=Point [x=116.624452, y=39.862060])根据位置:starbucks 获取: 120.0 KILOMETERS 范围的数据:GeoResults: [averageDistance: 7.03605 KILOMETERS, results: GeoResult [content: RedisGeoCommands.GeoLocation(name=starbucks, point=Point [x=116.624452, y=39.862060]), distance: 0.0 KILOMETERS, ],GeoResult [content: RedisGeoCommands.GeoLocation(name=xicha, point=Point [x=116.538540, y=39.754119]), distance: 14.0721 KILOMETERS, ]]
RedisGeoCommands.GeoLocation(name=starbucks, point=Point [x=116.624452, y=39.862060])
RedisGeoCommands.GeoLocation(name=xicha, point=Point [x=116.538540, y=39.754119])names:[starbucks, yidiandian, xicha] 对应的hash:[wx4fvbem6d0, wwgkqqhxzd0, wx4f5vhb8b0]删除名称为:[yidiandian, xicha] 的坐标信息数量:2
10简单限流
为了保证项目的安全稳定运行,防止被恶意的用户或者异常的流量打垮整个系统,一般都会加上限流,比如常见的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) 1
SpringBoot示例:
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次请求是否放行:true
11全局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) 3
12简单分布式锁
在分布式系统中,很多操作是需要用到分布式锁,防止并发操作带来一些问题;因为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) 1
set 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 的配置如下:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><exclusions><exclusion><groupId>redis.clients</groupId><artifactId>jedis</artifactId></exclusion><exclusion><artifactId>lettuce-core</artifactId><groupId>io.lettuce</groupId></exclusion></exclusions>
</dependency>
<!-- jedis客户端 -->
<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId>
</dependency>
<!-- spring2.X集成redis所需common-pool2,使用jedis必须依赖它-->
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-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 共享;
依赖
<dependency><groupId>org.springframework.session</groupId><artifactId>spring-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();Map<String, 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) {Map<String, Object> vas = new HashMap<>();// 遍历所有的session值Enumeration<String> 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和8081,8080调用赋值接口,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-5ff54fffa92a"
17商品筛选
商城类的应用,都会有类似于下图的一个商品筛选的功能,来帮用户快速搜索理想的商品;
假如现在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) "meta5000"
18购物车
商品缓存
电商项目中,商品消息,都会做缓存处理,特别是热门商品,访问用户比较多,由于商品的结果比较复杂,店铺信息,产品信息,标题、描述、详情图,封面图;为了方便管理和操作,一般都会采用 Hash 的方式来存储(key为商品ID,field用来保存各项参数,value保存对于的值)
购物车
当商品信息做了缓存,购物车需要做的,就是通过Hash记录商品ID,以及需要购买的数量(其中key为用户信息,field为商品ID,value用来记录购买的数量) ;
功能所需命令
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) "2"
19定时取消订单(key过期监听)
电商类的业务,一般都会有订单30分钟不支付,自动取消的功能,此时就需要用到定时任务框架,Quartz、xxl-job、elastic-job 是比较常用的 Java 定时任务;我们也可以通过 Redis 的定时过期、以及过期key的监听,来实现订单的取消功能;
Redis key 过期提醒配置
修改 redis 相关事件配置。找到 redis 配置文件 redis.conf,查看 notify-keyspace-events 配置项,如果没有,添加 notify-keyspace-events Ex,如果有值,则追加 Ex,相关参数说明如下:
K:keyspace 事件,事件以 keyspace@ 为前缀进行发布E:keyevent 事件,事件以 keyevent@ 为前缀进行发布g:一般性的,非特定类型的命令,比如del,expire,rename等$:字符串特定命令l:列表特定命令s:集合特定命令h:哈希特定命令z:有序集合特定命令x:过期事件,当某个键过期并删除时会产生该事件e:驱逐事件,当某个键因 maxmemore 策略而被删除时,产生该事件A:g$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时候,快递到达什么地方了,这就是一个典型的时间线列表;
数据库的做法,就是每次变更就插入一条带时间的信息记录,然后根据时间和ID(ID是必须的,如果出现两个相同的时间,单纯时间排序,会造成顺序不对),来排序生成时间线;
我们也可以通过 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除了缓存还做过什么,相信应该不成问题了。
相关文章:

Redis20种使用场景
Redis20种使用场景 1缓存2抽奖3Set实现点赞/收藏功能4排行榜5PV统计(incr自增计数)6UV统计(HeyperLogLog)7去重(BloomFiler)8用户签到(BitMap)9GEO搜附近10简单限流11全局ID12简单分…...
vue3获取原始值
在 Vue 3 中,_rawValue 是 ref 内部的一个属性,它用来存储 ref 的原始值,也就是未经响应式处理的值。这个属性主要用于 Vue 的内部逻辑,以帮助区分 ref 的当前值 (value) 和原始输入值 (_rawValue)。对于大多数开发者来说…...

“感恩遇到你,郭护士!”佛山市一医院 护士回家途中救了位老奶奶
“感恩遇见你,我感谢郭护士关爱长者、热心助人的高尚行为……”看着信件上感谢的话语,郭琳玲的内心感动不已。而这一封亲笔手写的感谢信,是来自一位将近八十岁的老奶奶。 郭琳玲是佛山市第一人民医院创伤重症功能神经外科的一名护士。4月30日…...
Java面试常见问题
操作系统 1.Q: 在操作系统中,什么时候会发生用户态到内核态的切换 A: 操作系统中,用户态和内核态是两种不同的权限级别,他们对应着不同的执行环境和执行权限。用户态事指程序在一般的运行情况下的的级别,它具有别较低的权限级别&…...

概率论 科普
符号优先级 概率公式中一共有三种符号:分号 ; 、逗号 , 、竖线 | 。 ; 分号代表前后是两类东西,以概率P(x;θ)为例,分号前面是x样本,分号后边是模型参数。分号前的 表示的是这个式子用来预测分布的随机变量x,分号后的…...

全面解读快递查询API接口,帮你轻松查询快递物流信息
随着电子商务的快速发展,快递业务正变得越来越重要。无论是买家还是卖家,都希望能够及时了解自己的快递物流信息,以便更好地掌控商品的运输过程。而现在,通过快递查询API接口,我们可以实现快速、准确地查询几百家国内快…...

【图书推荐】《JSP+Servlet+Tomcat应用开发从零开始学(第3版)》
本书目的 系统讲解JSPServletTomcat开发技术,帮助读者用最短的时间掌握Java Web应用开发技能。 内容简介 本书全面系统地介绍JSPServletTomcat开发中涉及的相关技术要点和实战技巧。本书内容讲解循序渐进,结合丰富的示例使零基础的读者能够熟练掌握JSP…...
C++容器——set
set容器 是一个关联容器,按一定的顺序存储一组唯一的元素。 set容器中的元素会根据元素的值自动进行排序,并且不允许包含重复的元素,基于二叉树实现的。 特点: 唯一性: set容器中的元素是唯一的,即容器中…...

.NET WebService \ WCF \ WebAPI 部署总结 以及 window 服务 调试
一、webservice 部署只能部署IIS上, 比较简单,就不做说明了 二、 WCF 部署 1 部署到IIS 跟部署 webservice 部署方法一样的 wcf 部署2 部署到控制台 要以管理员运行vs,或者 管理员运行 控制台的exe 在控制器项目中 创建IUserInfoService 接口…...
Centos系统实用运维命令记录(持续更新)
本文记录Centos服务器常用的运维命令,备忘 查询当前内存占用最高(前10)的进程列表和占用比例,进程ID ps -eo pid,comm,%mem,cmd --sort-%mem | head -n 11注: 内存警报时定位问题时非常有用 查询占用某个端口号的进程id lsof -i :9000注: 后面的9000…...
大势模方在修模过程中,如何导入su单体模型?
答:在单体化界面右键即可显示导入入口,若仍不可行,需要换最新版dv 模方是一款针对实景三维模型的冗余碎片、水面残缺、道路不平、标牌破损、纹理拉伸模糊等共性问题研发的实景三维模型修复编辑软件。模方4.1新增自动单体化建模功能ÿ…...

uniapp百度地图聚合
// loadBMap.js ak 百度key export default function loadBMap(ak) {return new Promise((resolve, reject) > {//聚合API依赖基础库,因此先加载基础库再加载聚合APIasyncLoadBaiduJs(ak).then(() > {// 调用加载第三方组件js公共方法加载其他资源库// 加载聚合API// Ma…...

nginx的应用部署nginx
这里写目录标题 nginxnginx的优点什么是集群常见的集群什么是正向代理、反向代理、透明代理常见的代理技术正向代理反向代理透明代理 nginx部署 nginx nginx(发音同enginex)是一款轻量级的Web服务器/反向代理服务器及电子邮件(IMAP/POP3&…...

Centos固定静态ip地址
这里我用的是Vmware虚拟机搭建的三台机器 进入 cd /etc/sysconfig/network-scripts然后使用 ip addr命令,查看自己虚拟机的以太网地址。 我这里是ens33 上面的第一个选项是本地环回地址,不用管它 然后查看刚刚进入的network-scripts目录下的文件 找到…...

豆芽机置入语音芯片WTN6040-8S:开启智能生活新篇章,让豆芽制作更便捷有趣
豆芽机的开发背景: 豆芽作为一种营养丰富、味道鲜美的食品,深受广大消费者的喜爱。然而,传统的豆芽生产过程繁琐,需要耗费大量的时间和人力,且存在生产效率低、质量不稳定等问题。随着人们生活节奏的加快和对健康饮食的…...

BLIP2预研笔记
0. 前言 文章是公司内部分享学习写的预研报告,里面有小部分文段是直接从网上借鉴的,侵删 1. 任务和方法历史进化: 在大模型等类似的预训练模型的方式(以包含“预训练阶段”等n阶段训练方式为特色)为主流之前…...
安卓开发问题:安卓Ble出现动态鉴权失败以及扫描设备一直进入不了的问题
问题1描述 1、安卓12需要动态鉴权 // 鉴权函数 requestPermissions(permissionsList.toArray(strings), MyConstants.REQUEST_CODE_PERMISSIONS);但是在鉴权回调函数中如Manifest.permission.BLUETOOTH_SCAN、Manifest.permission.BLUETOOTH_CONNECT一直显示失败&…...

DSP ARM FPGA 实验箱_音频处理_滤波操作教程:3-9 音频信号的滤波实验
一、实验目的 掌握Matlab辅助设计滤波器系数的方法,并实现音频混噪及IIR滤波器滤除,并在LCD上显示音频信号的FFT计算结果。 二、实验原理 音频接口采用的是24.576MHz(读兆赫兹)晶振,实验板上共有3个音频端口&#x…...

Rust多线程交叉打印+Send Sync特征讲解
导航 Rust多线程交叉打印Send Sync特征讲解 一、Rust多线程交叉打印二、Send Sync 特征讲解 Rust多线程交叉打印Send Sync特征讲解 一、Rust多线程交叉打印 先说背景有两个线程,分别为0号线程和1号线线程两个线程交叉打印共享值,并将共享值1当标志为fa…...
C#爬虫爬取某东商品信息
🏆作者:科技、互联网行业优质创作者 🏆专注领域:.Net技术、软件架构、人工智能、数字化转型、DeveloperSharp、微服务、工业互联网、智能制造 🏆欢迎关注我(Net数字智慧化基地),里面…...
Cursor实现用excel数据填充word模版的方法
cursor主页:https://www.cursor.com/ 任务目标:把excel格式的数据里的单元格,按照某一个固定模版填充到word中 文章目录 注意事项逐步生成程序1. 确定格式2. 调试程序 注意事项 直接给一个excel文件和最终呈现的word文件的示例,…...
Golang dig框架与GraphQL的完美结合
将 Go 的 Dig 依赖注入框架与 GraphQL 结合使用,可以显著提升应用程序的可维护性、可测试性以及灵活性。 Dig 是一个强大的依赖注入容器,能够帮助开发者更好地管理复杂的依赖关系,而 GraphQL 则是一种用于 API 的查询语言,能够提…...

MMaDA: Multimodal Large Diffusion Language Models
CODE : https://github.com/Gen-Verse/MMaDA Abstract 我们介绍了一种新型的多模态扩散基础模型MMaDA,它被设计用于在文本推理、多模态理解和文本到图像生成等不同领域实现卓越的性能。该方法的特点是三个关键创新:(i) MMaDA采用统一的扩散架构…...
镜像里切换为普通用户
如果你登录远程虚拟机默认就是 root 用户,但你不希望用 root 权限运行 ns-3(这是对的,ns3 工具会拒绝 root),你可以按以下方法创建一个 非 root 用户账号 并切换到它运行 ns-3。 一次性解决方案:创建非 roo…...

Linux-07 ubuntu 的 chrome 启动不了
文章目录 问题原因解决步骤一、卸载旧版chrome二、重新安装chorme三、启动不了,报错如下四、启动不了,解决如下 总结 问题原因 在应用中可以看到chrome,但是打不开(说明:原来的ubuntu系统出问题了,这个是备用的硬盘&a…...
LLM基础1_语言模型如何处理文本
基于GitHub项目:https://github.com/datawhalechina/llms-from-scratch-cn 工具介绍 tiktoken:OpenAI开发的专业"分词器" torch:Facebook开发的强力计算引擎,相当于超级计算器 理解词嵌入:给词语画"…...
OpenLayers 分屏对比(地图联动)
注:当前使用的是 ol 5.3.0 版本,天地图使用的key请到天地图官网申请,并替换为自己的key 地图分屏对比在WebGIS开发中是很常见的功能,和卷帘图层不一样的是,分屏对比是在各个地图中添加相同或者不同的图层进行对比查看。…...

蓝桥杯3498 01串的熵
问题描述 对于一个长度为 23333333的 01 串, 如果其信息熵为 11625907.5798, 且 0 出现次数比 1 少, 那么这个 01 串中 0 出现了多少次? #include<iostream> #include<cmath> using namespace std;int n 23333333;int main() {//枚举 0 出现的次数//因…...
【Android】Android 开发 ADB 常用指令
查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...
NPOI操作EXCEL文件 ——CAD C# 二次开发
缺点:dll.版本容易加载错误。CAD加载插件时,没有加载所有类库。插件运行过程中用到某个类库,会从CAD的安装目录找,找不到就报错了。 【方案2】让CAD在加载过程中把类库加载到内存 【方案3】是发现缺少了哪个库,就用插件程序加载进…...