【Redis】Redis 的学习教程(十三)Redis 各场景

由于Redis 支持比较丰富的数据结构,因此他能实现的功能并不仅限于缓存,而是可以运用到各种业务场景中,开发出既简洁、又高效的系统

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

1. 缓存

Redis 的几个常用数据结构都可以用于缓存

2. 抽奖(Set 结构)

抽个奖吧!一堆用户参与进来,然后随机抽取几个幸运用户给予实物/虚拟的奖品;此时,开发人员就需要写上一个抽奖的算法,来实现幸运用户的抽取;其实我们完全可以利用 Redis 的集合(Set),就能轻松实现抽奖的功能

①:所需命令:

  • SADD key member1 [member2]:添加一个或者多个参与用户
  • SRANDMEMBER KEY [count]:随机返回一个或者多个用户
  • SPOP key:随机返回一个或者多个用户,并删除返回的用户

SRANDMEMBER 和 SPOP 主要用于两种不同的抽奖模式:SRANDMEMBER 适用于一个用户可中奖多次的场景(就是中奖之后,不从用户池中移除,继续参与其他奖项的抽取);而 SPOP 就适用于仅能中一次的场景(一旦中奖,就将用户从用户池中移除,后续的抽奖,就不可能再抽到该用户); 通常 SPOP 会用的会比较多

②:Redis-cli 操作:

# 添加一个用户
127.0.0.1:6379> SADD lottery user1
(integer) 1
# 添加九个用户
127.0.0.1:6379> SADD lottery user2 user3 user4 user5 user6 user7 user8 user9 user10
(integer) 9
# 随机抽奖两个用户(不移除抽奖池)
127.0.0.1:6379> SRANDMEMBER lottery 2
1) "user5"
2) "user4"
# 随机抽奖两个用户(移除抽奖池:user2、user5)
127.0.0.1:6379> SPOP lottery 2
1) "user2"
2) "user5"
# 随机抽奖两个用户(移除抽奖池:user7、user4)
127.0.0.1:6379> SPOP lottery 2
1) "user7"
2) "user4"
127.0.0.1:6379>

③:SpringBoot 实现:

@RestController
@RequestMapping("/test")
public class TestController {private static final String LOTTERY_PREFIX = "lottery:";@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@GetMapping("/lottery")public void lottery(Integer count) {Integer lotteryId = 1;String key = LOTTERY_PREFIX + lotteryId;redisTemplate.opsForSet().add(key, 1001, 1002, 1003, 1004, 1005);// 随机抽取:抽完之后将用户移除奖池List<Object> results = redisTemplate.opsForSet().pop(key, count);// 随机抽取:抽完之后用户保留在池子里//List<Object> list = redisTemplate.opsForSet().randomMembers(key, num);System.out.println(results);}
}

3. 实现点赞/收藏功能(Set)

有互动属性APP一般都会有点赞/收藏/喜欢等功能,来提升用户之间的互动。

  • 传统的实现:用户点赞之后,在数据库中记录一条数据,同时一般都会在主题库中记录一个点赞/收藏汇总数,来方便显示;

  • Redis 方案:基于 Redis 的集合(Set),记录每个帖子/文章对应的收藏、点赞的用户数据,同时set还提供了检查集合中是否存在指定用户,用户快速判断用户是否已经点赞过

①:所需命令:

  • SADD key member1 [member2]:添加一个或者多个成员(点赞)
  • SCARD key:获取所有成员的数量(点赞数量)
  • SISMEMBER key member:判断成员是否存在(是否点赞)
  • SREM key member1 [member2]:移除一个或者多个成员(取消点赞)
  • SMEMBERS key:查看所有成员

②:Redis-cli 操作:

# user1 给文章点赞
127.0.0.1:6379> SADD like:article:1 user1
(integer) 1
# user2 给文章点赞
127.0.0.1:6379> SADD like:article:1 user2
(integer) 1
# 获取点赞数量
127.0.0.1:6379> SCARD like:article:1
(integer) 2
# 判断 user3 是否给文章点赞
127.0.0.1:6379> SISMEMBER like:article:1 user3
(integer) 0
# 判断 user1 是否给文章点赞
127.0.0.1:6379> SISMEMBER like:article:1 user1
(integer) 1
# user1 取消点赞
127.0.0.1:6379> SREM like:article:1 user1
(integer) 1
# 获取点赞数量
127.0.0.1:6379> SCARD like:article:1
(integer) 1
127.0.0.1:6379> SMEMBERS like:article:1
1) "user2"
127.0.0.1:6379>

③:SpringBoot 实现:

private static final String LIKE_ARTICLE_PREFIX = "like:article:";@GetMapping("/doLike")
public void doLike() {Integer articleId = 100;String key = LIKE_ARTICLE_PREFIX + articleId;// 点赞Long likeNum = redisTemplate.opsForSet().add(key, 1001, 1002, 1003, 1004, 1005);// 取消点赞Long unlikeNum = redisTemplate.opsForSet().remove(key, 1002);// 点赞数量Long size = redisTemplate.opsForSet().size(key);// 某个用户是否点赞Boolean aBoolean = redisTemplate.opsForSet().isMember(key, 1005);
}

4. 排行榜(ZSet)

排名、排行榜、热搜榜是很多APP、游戏都有的功能,常用于用户活动推广、竞技排名、热门信息展示等功能

  • 常规的做法:就是将用户的名次、分数等用于排名的数据更新到数据库,然后查询的时候通过Order by + limit 取出前50名显示,如果是参与用户不多,更新不频繁的数据,采用数据库的方式也没有啥问题,但是一旦出现爆炸性热点资讯,短时间会出现爆炸式的流量,瞬间的压力可能让数据库扛不住

  • 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
# 添加 4 个成员
127.0.0.1:6379> ZADD ranking 10 user2 50 user3 3 user4 25 user5
(integer) 4
# 所有成员降序排列
127.0.0.1:6379> ZREVRANGE ranking 0 -1
1) "user3"
2) "user5"
3) "user2"
4) "user4"
5) "user1"
# 前三成员降序排列
127.0.0.1:6379> ZREVRANGE ranking 0 2 WITHSCORES
1) "user3"
2) "50"
3) "user5"
4) "25"
5) "user2"
6) "10"
127.0.0.1:6379>

③:SpringBoot 实现:

private final String RANKING_PREFIX = "ranking";
@GetMapping("/doRanking")
public void doRanking() {redisTemplate.opsForZSet().add(RANKING_PREFIX, 1001, (double)60);redisTemplate.opsForZSet().add(RANKING_PREFIX, 1002, (double)70);redisTemplate.opsForZSet().add(RANKING_PREFIX, 1003, (double)80);redisTemplate.opsForZSet().add(RANKING_PREFIX, 1004, (double)75);redisTemplate.opsForZSet().add(RANKING_PREFIX, 1005, (double)50);// 获取所有Set<ZSetOperations.TypedTuple<Object>> set = redisTemplate.opsForZSet().reverseRangeWithScores(RANKING_PREFIX, 0, -1);// 前三名set = redisTemplate.opsForZSet().reverseRangeWithScores(RANKING_PREFIX, 0, 2);
}

5. PV 统计(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
127.0.0.1:6379>

6. UV 统计(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
127.0.0.1:6379>

③:SpringBoot 实现:

模拟测试 10000 个用户访问 id 为 2 的页面

private final String UV_PAGE_PREFIX = "uv:page:";
@GetMapping("/doUv")
public void doUv() {Integer pageId = 2;String key = UV_PAGE_PREFIX + pageId;for (int i = 0; i < 10000; i++) {redisTemplate.opsForHyperLogLog().add(key, i);}Long uvSize = redisTemplate.opsForHyperLogLog().size(key);
}

7. 去重(BloomFiler)

通过上面 HeyperLogLog 的学习,我们掌握了一种不精准的去重计数方案,但是有没有发现,他没办法获取某个用户是否访问过;理想中,我们是希望有一个 PFEXISTS 的命令,来判断某个key是否存在,然而 HeyperLogLog 并没有;要想实现这一需求,就得 BloomFiler 上场了。

loom Filter是由Bloom在1970年提出的一种多哈希函数映射的快速查找算法。通常应用在一些需要快速判断某个元素是否属于集合,但是并不严格要求100%正确的场合。基于一种概率数据结构来实现,是一个有趣且强大的算法。

举个例子:假如你写了一个爬虫,用于爬取网络中的所有页面,当你拿到一个新的页面时,如何判断这个页面是否爬取过?

普通做法:每爬取一个页面,往数据库插入一行数据,记录一下URL,每次拿到一个新的页面,就去数据库里面查询一下,存在就说明爬取过;

  • 缺点:少量数据,用传统方案没啥问题,如果是海量数据,每次爬取前的检索,将会越来越慢;如果你的爬虫只关心内容,对来源数据不太关心的话,这部分数据的存储,也将消耗你很大的物理资源;

此时通过 BloomFiler 就能以很小的内存空间作为代价,即可轻松判断某个值是否存在。

同样,BloomFiler 也不那么精准,在默认参数情况下,是存在 1% 左右的误差;但是 BloomFiler 是允许通过 error_rate(误差率)以及 initial_size(预计大小)来设置他的误差比例

  • error_rate:误差率,越低,需要的空间就越大;
  • initial_size:预计放入值的数量,当实际放入的数量大于设置的值时,误差率就会逐渐升高;所以为了避免误差率,可以提前做好估值,避免再次大的误差

①:所需命令:

  • bf.reserve 创建布隆过滤器
  • bf.add 添加单个元素
  • bf.madd 批量添加
  • bf.exists 检测元素是否存在
  • bf.mexists 批量检测

②:SpringBoot 实现:

需要下载布隆过滤器插件:redis安装布隆过滤器 windows redis 实现布隆过滤器

@Component
public class RedisBloomUtil {private static final RedisScript<Boolean> BFRESERVE_SCRIPT = new DefaultRedisScript<>("return redis.call('bf.reserve', KEYS[1], ARGV[1], ARGV[2])", Boolean.class);private static final RedisScript<Boolean> BFADD_SCRIPT = new DefaultRedisScript<>("return redis.call('bf.add', KEYS[1], ARGV[1])", Boolean.class);private static final RedisScript<Boolean> BFEXISTS_SCRIPT = new DefaultRedisScript<>("return redis.call('bf.exists', KEYS[1], ARGV[1])", Boolean.class);private static final String BFMEXISTS_SCRIPT = "return redis.call('bf.mexists', KEYS[1], %s)";@Autowiredprivate RedisTemplate<String, Object> redisTemplate;/*** 设置错误率和大小(需要在添加元素前调用,若已存在元素,则会报错)* 错误率越低,需要的空间越大** @param key* @param errorRate   错误率,默认0.01* @param initialSize 默认100,预计放入的元素数量,当实际数量超出这个数值时,误判率会上升,尽量估计一个准确数值再加上一定的冗余空间* @return*/public Boolean bfreserve(String key, double errorRate, int initialSize) {return redisTemplate.execute(BFRESERVE_SCRIPT, Collections.singletonList(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(BFADD_SCRIPT, Collections.singletonList(key), value);}/*** 查看元素是否存在(判断为存在时有可能是误判,不存在是一定不存在)** @param key* @param value* @return true表示存在,false表示不存在*/public Boolean bfexists(String key, String value) {return redisTemplate.execute(BFEXISTS_SCRIPT, Collections.singletonList(key), value);}/*** 批量添加元素** @param key* @param values* @return 按序 1表示添加成功,0表示添加失败*/public List<Integer> bfmadd(String key, String... values) {String bfmaddScript = "return redis.call('bf.madd', KEYS[1], %s)";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(BFMEXISTS_SCRIPT, 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);}}

测试:

private final String WEB_CRAWLER_PREFIX = "web:crawler1";
@Autowired
private RedisBloomUtil redisBloomUtil;@GetMapping("/doBloom")
public void doBloom() {Boolean hasKey = redisTemplate.hasKey(WEB_CRAWLER_PREFIX);if (Boolean.FALSE.equals(hasKey)) {redisBloomUtil.bfreserve(WEB_CRAWLER_PREFIX, 0.0001, 10000);}List<Integer> madd = redisBloomUtil.bfmadd(WEB_CRAWLER_PREFIX, "baidu", "google");Boolean baidu = redisBloomUtil.bfexists(WEB_CRAWLER_PREFIX, "baidu");Boolean bing = redisBloomUtil.bfexists(WEB_CRAWLER_PREFIX, "bing");
}

8. 用户签到(BitMap)

很多 APP 为了拉动用户活跃度,往往都会做一些活动,比如连续签到领积分/礼包等等

  • 传统做法:用户每次签到时,往是数据库插入一条签到数据,展示的时候,把本月(或者指定周期)的签到数据获取出来,用于判断用户是否签到、以及连续签到情况;此方式,简单,理解容易;

  • Redis 做法:由于签到数据的关注点就2个:是否签到(0/1)、连续性,因此就完全可以利用 BitMap(位图)来实现;

在这里插入图片描述
如上图所示,将一个月的 31 天,用 31 个位(4个字节)来表示,偏移量(offset)代表当前是第几天,0/1 表示当前是否签到,连续签到只需从右往左校验连续为 1 的位数;

由于 String 类型的最大上限是 512M,转换为 bit 则是 2^32 个 bit 位

①:所需命令:

  • SETBIT key offset value:向指定位置 offset 存入一个 0 或 1
  • GETBIT 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 号,检测本月的签到情况,用户 ID 为 8899 分别于 1、3、4 号签到过

②:Redis-cli 操作:

# 8月1号的签到
127.0.0.1:6379> SETBIT rangeId:sign:1:8899 0 1
(integer) 0
# 8月3号的签到
127.0.0.1:6379> SETBIT rangeId:sign:1:8899 2 1
(integer) 0
# 8月4号的签到
127.0.0.1:6379> SETBIT rangeId:sign:1:8899 3 1
(integer) 0
# 查询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
# 查询指定区间的签到情况
127.0.0.1:6379> BITFIELD rangeId:sign:1:8899 GET u4 0
1) (integer) 11
127.0.0.1:6379>

1-4号的签到情况为:1011(二进制) ==> 11(十进制)

9. 搜附近(GEO)

Redis 在 3.2 的版本新增了 Redis GEO,用于存储地址位置信息,并对支持范围搜索;基于 GEO 就能轻松且快速的开发一个搜索附近的功能

①:所需命令:

  • GEOADD key longitude latitude member [longitude latitude member ...]:新增位置坐标

  • GEOPOS key member [member ...]:获取位置坐标

  • GEODIST key member1 member2 [unit]:计算两个位置之间的距离

    • m :米,默认单位
    • km :千米
    • mi :英里
    • ft :英尺
  • GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC]:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合

    • m|km|ft|mi:同上
    • WITHDIST: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回
    • WITHCOORD:将位置元素的经度和纬度也一并返回
    • WITHHASH:以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。这个选项主要用于底层应用或者调试, 实际中的作用并不大
    • COUNT:限定返回的记录数
    • ASC:查找结果根据距离从近到远排序
    • DESC:查找结果根据从远到近排序
  • GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC]:根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合

    功能和上面的georadius类似,只是georadius是以经纬度坐标为中心,这个是以某个地点为中心

  • GEOHASH key member [member ...]:返回一个或多个位置对象的 geohash 值

②:Redis-cli 操作:

# 新增位置坐标(3 个)
127.0.0.1:6379> GEOADD drinks 116.62445 39.86206 starbucks 117.3514785 38.7501247 yidiandian 116.538542 39.75412 xicha
(integer) 3
# 获取位置坐标
127.0.0.1:6379> GEOPOS drinks starbucks yidiandian
1) 1) "116.62445157766342163"2) "39.86206038535793539"
2) 1) "117.35148042440414429"2) "38.75012383773680114"
# 计算两个位置之间的距离
127.0.0.1:6379> GEODIST drinks starbucks xicha
"14072.1255"
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"

10. 简单限流

为了保证项目的安全稳定运行,防止被恶意的用户或者异常的流量打垮整个系统,一般都会加上限流,比如常见的 sentialhystrix,都是实现限流控制;如果项目用到了 Redis,也可以利用 Redis,来实现一个简单的限流功能

①:所需命令:

  • INCR:将 key 中储存的数字值增一
  • Expire:设置 key 的有效期

11. 全局 ID

在分布式系统中,很多场景下需要全局的唯一 ID,由于 Redis 是独立于业务服务的其他应用,就可以利用 Incr 的原子性操作来生成全局的唯一递增 ID

①:所需命令:

  • INCR:将 key 中储存的数字值增一

12. 简单分布式锁

在分布式系统中,很多操作是需要用到分布式锁,防止并发操作带来一些问题;因为 Redis 是独立于分布式系统外的其他服务,因此就可以利用 Redis,来实现一个简单的不完美分布式锁

①:所需命令:

  • SETNX:key 不存在,设置;key 存在,不设置

13. 认识的人/好友推荐(Set)

在支付宝、抖音、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"
127.0.0.1:6379>

14. 发布/订阅

发布/订阅是比较常用的一种模式;在分布式系统中,如果需要实时感知到一些变化,比如:某些配置发生变化需要实时同步,就可以用到发布,订阅功能

①:所需命令:

  • PUBLISH channel message:将消息推送到指定的频道
  • SUBSCRIBE channel [channel …]:订阅给定的一个或多个频道的信息

15. 消息队列(List)

说到消息队列,常用的就是Kafka、RabbitMQ等等,其实 Redis 利用 List 也能实现一个消息队列

①:所需命令:

  • RPUSH key value1 [value2]:在列表中添加一个或多个值
  • BLPOP key1 [key2] timeout:移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
  • BRPOP key1 [key2] timeout:移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止

16. 商品筛选(Set)

商城类的应用,都会有类似于下图的一个商品筛选的功能,来帮用户快速搜索理想的商品

①:所需命令:

  • SADD key member [member …]:添加一个或多个元素
  • SINTER key [key …]:返回给定所有集合的交集

②:Redis-cli 操作:

假如现在iphone 100 、华为mate 5000 已发布,在各大商城上线

# 将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"

17. 购物车(Hash)

商品缓存: 电商项目中,商品消息,都会做缓存处理,特别是热门商品,访问用户比较多,由于商品的结果比较复杂,店铺信息,产品信息,标题、描述、详情图,封面图;为了方便管理和操作,一般都会采用 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"

18. 物流信息(List)

寄快递、网购的时候,查询物流信息,都会给我们展示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除了缓存还做过什么,相信聊上个1把小时,应该不成问题了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/198928.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

第三节:提供者、消费者、Eureka

一、 提供者 消费者&#xff08;就是个说法、定义&#xff0c;以防别人叭叭时听不懂&#xff09; 服务提供者&#xff1a;业务中被其他微服务调用的服务。&#xff08;提供接口给其他服务调用&#xff09;服务消费者&#xff1a;业务中调用其他微服务的服务。&#xff08;调用…

QThread之moveToThread用法

简介 使用moveToThread函数的流程如下&#xff1a; 1、创建一个类继承自QObject类或其子类&#xff0c;并在其中定义所要执行的多个任务&#xff0c;执行多个任务就要定义相应的信号。 2、任务通过moveToThread指定所要执行的线程。 3、线程通过start启动 4、通过信号与槽机制…

【AI绘画】万字长文——(超详细)ControlNet的详细介绍使用Stable Diffusion的艺术二维码完全生成攻略

目录 前言一、名词解释1-1、Stable Diffusion介绍1-2、ControlNet介绍1-2-1、ControlNet介绍&工作原理1-2-2、ControlNet控制方法介绍 1-3、案例分析1-3-1、室内装修设计1-3-2、品牌创意海报 1-4、stable-diffusion-webui 的参数解释 二、生成方法2-1、图像到图像2-1-1、二…

Python使用netmiko配置华为交换机

一、netmiko介绍 1.更适合网络设备的自动化运维模块。 二、场景 1、批量查询 2、批量配置变更、备份 三、项目地址 GitHub - ktbyers/netmiko: Multi-vendor library to simplify Paramiko SSH connections to network devices 三、使用步骤 1.安装netmiko pip install ne…

D6208单片双向马达驱动电路国产芯片,工作电源电压范围宽(4.5V~15.0V),内设保护二极管采用SOP8封装

D6208 是一块单片双向马达驱动电路&#xff0c;它使用TTL电平的逻辑信号就能控制卡式录音机和其它电子设备中的双向马达。该电路由一个逻辑部分和一个功率输出部分组成。逻辑部分控制马达正、反转向及制动&#xff0c;功率输出部分根据逻辑控制能提供100mA&#xff08;典型值&a…

腾讯地图系列(二):微信小程序添加插件(三种方法)以及插件AppId获取

目录 第一章 前言 第二章 添加插件 2.1 微信小程序添加插件方法一&#xff08;微信公众平台添加插件&#xff09; 2.2 微信小程序添加插件方法二&#xff08;通过项目配置添加插件&#xff09; 2.3 微信小程序添加插件方法三&#xff08;微信公众平台服务市场添加插件&…

spring mvc理解

spring mvc M&#xff1a;model 模型 V&#xff1a;view 视图 C&#xff1a;controller 控制器 S: service 服务处理 D: Dao 数据持久化 视图 我理解就是web页面&#xff0c;帮助用户调用后端接口。 前后端分离之后&#xff0c;view似乎就和后端没什么关系了。 模型 格式…

zabbix6.4监控交换机发现ICMP报错Ping item must have target or host interface specified

报错信息&#xff1a; 查看监控项&#xff1a; 修改键值&#xff1a; 保存再次检查&#xff0c;发现又报错/usr/sbin/fping: [2] No such file or directory 原因是&#xff0c;zabbix-server上没有安装fping工具 解决方法&#xff1a;yum install fping -y 之后数据采集正常…

【unity3D】Transform组件(如何访问和获取Transform组件)

&#x1f497; 未来的游戏开发程序媛&#xff0c;现在的努力学习菜鸡 &#x1f4a6;本专栏是我关于游戏开发的学习笔记 &#x1f236;本篇是unity的Transform组件 Transform组件 基础知识介绍三个成员变量常用属性扩展 Transform的相关查找方法静态方法 基础知识 介绍 在Unit…

mapbox实现框选要素

成果图 参考博客 https://blog.csdn.net/ScapeD/article/details/89158755 原理与源码 利用mapbox的queryRenderedFeatures方法可以获取范围内的要素&#xff0c;但是这个只能是点和矩形和范围内的全屏要素&#xff0c;并不支持多边形&#xff0c;所以实现这个的思路就是画完框…

【JavaEE进阶】 Spring核⼼与设计思想

文章目录 &#x1f332;Spring 是什么&#xff1f;&#x1f384;什么是IoC呢&#xff1f;&#x1f388;传统程序开发&#x1f388;传统程序开发的缺陷&#x1f388;如何解决传统程序的缺陷&#xff1f;&#x1f388;控制反转式程序开发&#x1f388;对⽐总结规律 &#x1f340;…

适用于 Windows 的最佳(免费/付费)数据恢复软件

借助最佳数据恢复工具从 Windows PC 恢复丢失和删除的数据 您是否正在寻找一种巧妙的方法来从计算机中取消删除或恢复已删除的文件&#xff1f;如果是&#xff0c;那么这篇文章就是为您准备的&#xff01;在本教程中&#xff0c;我们整理了一份全面的数据恢复软件列表&#xf…

机器人学习目标

学习目标&#xff1a; 若干年后&#xff0c;我们都将化为尘土&#xff0c;无人铭记我们的存在。那么&#xff0c;何不趁现在&#xff0c;尽己所能&#xff0c;在这个世界上留下一些痕迹&#xff0c;让未来的时光里&#xff0c;仍有人能感知到我们的存在。 机器人协会每届每个阶…

外包干了4年,技术退步明显...

先说情况&#xff0c;大专毕业&#xff0c;18年通过校招进入湖南某软件公司&#xff0c;干了接近4年的功能测试&#xff0c;今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了四年的功能测试&#xf…

postgres pg_dump: fe_sendauth: no password supplied问题处理

postgres pg_dump: fe_sendauth: no password supplied问题处理 1、pg_dump: fe_sendauth: no password supplied问题处理&#xff08;root 用户&#xff09;2、解决方法2.1、创建 .pgpass文件2.2、chmod 600 ~/.pgpass 3、额外情况 1、pg_dump: fe_sendauth: no password supp…

文件重命名:删除文件名中的空格,提高文件可读性和可管理性的方法

在计算机科学中&#xff0c;有效的文件管理对于提高工作效率和保持数据的一致性至关重要。工作中经常会遇到文件名中包含空格的情况&#xff0c;这不仅会使文件在某些情况下难以读取&#xff0c;而且可能导致管理上的困扰。在文件名中添加空格可能会使文件名变得模糊和不明确&a…

我有才知识付费小程序免费搭建:轻松拥有知识付费平台

用户管理 提供会员特权和积分奖励&#xff0c;提高用户忠诚度和购买力。 用户通过在平台上进行消费、签到、参与活动等方式获取积分&#xff0c;用于兑换丰厚奖品或提升会员等级。增强用户的参与感与忠诚度&#xff0c;提高用户粘性&#xff0c;并刺激用户的购买力。 为用户打…

Arduino学习笔记2023年11月30日

目录 1 编程软件下载2 代码结构3 IO引脚控制3.1 引脚初始化3.2 引脚使用数字量输出数字量输入模拟量输出模拟量输入 4 串口串口初始化串口输出串口输入 5 外部中断6 函数6.1 映射区间函数6.2 延时函数 总结 1 编程软件下载 官网链接&#xff1a;https://www.arduino.cc/ 下载链…

SpringBoot 配置文件使用@ @取值

目录 一、背景 二、遇到的问题 三、解决办法 一、背景 &#xff08;1&#xff09;我在项目中引入了如下依赖&#xff0c;目的是开启SpringBoot为我们提供的监控(Actuator)功能。 <!-- 引入SpringBoot 监控功能 --> <dependency><groupId>org.springframew…

PWM控制器电路D9741,定时闩锁、短路保护电路,输出基准电压(2.5V) 采用SOP16封装形式

D9741是一块脉宽调制方三用于也收路像机和笔记本电的等设备上的直流转换器。在便携式的仪器设备上。 主要特点&#xff1a;● 高精度基准电路 ● 定时闩锁、短路保护电路 ● 低电压输入时误操作保护电路 ● 输出基准电…