- VisualStudio2022插件的安装及使用-编程手把手系列文章
- pprof-在现网场景怎么用
- C#实现的下拉多选框,下拉多选树,多级节点
- 【学习笔记】基础数据结构:猫树
在笔者 3 年的 Java 一线开发经历中,尤其是一些移动端、用户量大的互联网项目,经常会使用到 Redis 作为缓存中间件的基本工具来解决一些特定的问题.
下面是笔者总结梳理的一些常见的 Redis 缓存应用场景,例如常见的 String 类型 Key-Value、对时效性要求高的场景、Hash 结构的场景以及对实时性要求高的场景等,基本涵盖了 Redis 中所有的 5 种基本类型.
如果你也在项目中经常使用 Redis 来作为缓存的中间件,那么你一定不会对下面的内容感到陌生。如果你还是刚入行不久,暂时还没接触到 Redis 这样的缓存中间件,那么也没关系,本篇文章对你也会有一定的帮助.
关于缓存的一些基本概念,大家可以看这里再回顾一下:https://www.cnblogs.com/CodeBlogMan/p/18022719 。
首先介绍的是项目开发中常见的一些String 类型的 key-value 结构场景,如:
下面用简单的 demo 来演示一下如何获取用户登录信息.
@RestController
@RequestMapping("/member")
public class MemberController {
@Resource
private MemberService memberService;
/**
* 通过 userUuid 获取会员信息
* @param userUuid
* @return 会员信息
*/
@GetMapping("/info")
public Response<MemberVO> getMemberInfo(@RequestParam(value = "userUuid") String userUuid) {
return ResponseBuilder.buildSuccess(this.memberService.info(userUuid));
}
}
@Resource
private RedisTemplate<String, String> redisTemplate;
private final String MEMBER_INFO_USER_UUID_KEY = "initial.member.user.uuid.key";
@Override
public MemberVO info(String userUuid) {
//先查缓存
String memberStr = redisTemplate.opsForValue().get(RedisKey.MEMBER_INFO_USER_UUID_KEY.concat(userUuid));
if (StringUtils.isNotBlank(memberStr)){
return JSON.parseObject(memberStr, MemberVO.class);
}
//缓存没有再查数据库
LambdaQueryWrapper<Member> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Member::getMemberUuid, userUuid)
.eq(Member::getEnableStatus, NumberUtils.INTEGER_ZERO)
.eq(Member::getDataStatus, NumberUtils.INTEGER_ZERO);
return this.getOne(wrapper).convertExt(MemberVO.class);
}
/**
* 仅部分核心属性
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class MemberVO extends BaseVO {
/**
* 用户唯一uuid
*/
private String memberUuid;
/**
* 登录 token
*/
private String token;
/**
* 用户昵称
*/
private String nickName;
/**
* 电话号码
*/
private String mobile;
/**
* 头像地址
*/
private String avatarImg;
/**
* 性别:1-male,2-female,3-unknown
*/
private Integer gender;
}
在开发的时候我们经常会碰到时效性强的一些场景,从业务上对过期时间的要求比较高,比如:
下面举两个例子,demo 虽然简单但是可以运行,不是伪代码:
/**
* 活动预览链接 30 分钟后过期
*/
@Test
public void testPreviewLinkExpire(){
String baseUrl = "http://localhost:8089/initial/ealbum/preview/detail";
Long projectId = UUIDUtils.generateUUIDToLong();
String projectUuid = UUIDUtils.generateUUID();
//这里是链接的签名,如果签名过期,那么意味着整个链接过期
String tempLinkSign = DigestUtils.md5Hex(projectUuid);
String signKey = RedisKey.INITIAL_EALBUM_TEMP_LINK_SIGN_KEY.concat(".").concat(projectId.toString());
stringRedisTemplate.opsForValue().set(signKey, tempLinkSign, Duration.ofMinutes(30));
String sign = stringRedisTemplate.opsForValue().get(signKey);
if (StringUtils.isNotBlank(sign)){
//拼接临时地址
StringBuilder tempLinkUrl = new StringBuilder(baseUrl)
.append("?projectId=").append(projectId)
.append("&sign=").append(sign);
log.info("打印看下预览地址:{}", tempLinkUrl);
}
throw new BusinessException("生成预览链接失败!");
}
/**
* 剪切板口令码1小时过期
*/
@Test
public void testClipboardTextKey(){
//这里先随机生产一个 uuid 作为例子
String articleId = UUIDUtils.generateUUID();
String redisKey = RedisKey.CLIP_BOARD_TEXT_KEY.concat(".").concat(articleId);
String value = JSON.toJSONString(ClipboardVO.builder()
.articleId(articleId)
.articleTitle("测试标题")
.copyright(NumberUtils.INTEGER_ZERO).build());
//很简单的一个结构,就是常见的 String 类型的 key-value,但是对时效性有要求,一个小时后直接失效
stringRedisTemplate.opsForValue().set(redisKey, value, Duration.ofHours(1));
//这里去拿 value,判断是否过期
ClipboardVO vo = Optional.ofNullable(stringRedisTemplate.opsForValue().get(redisKey))
.filter(StringUtils::isNotBlank)
.map(val -> JSON.parseObject(val, ClipboardVO.class))
.orElse(null);
log.info("打印一下返回的vo:{}", vo);
}
关于计数器也是 Redis 的一个常见应用场景了,比如以下几点:
点赞数/收藏数:文章的点赞数,文章的收藏数,可以同步到文章表作为一个属性; 。
文章评论数:采用 String 结构,redis-key 可以是文章 id 标识,redis-value 则是评论数,也可以同步到文章表作为一个属性; 。
未读消息数:采用 Hash 结构,常量 redis-key,用户标识为 hash-key,数量为 hash-value,需要同步到通知表; 。
加入购物车的商品数:采用 BoundHash 结构,redis-key 为用户标识,hash-key 为商品 id 标识,hash-value 为数量,需要同步到购物车的表.
下面举两个例子吧,部分实体、DTO/VO 之类的没有写明,大家能意会就好,重要的是思路:
/**
* 用户未读通知数量计数
*/
@Test
public void testNoticeCountNum() {
//关于热 Key 这里场景有点不够:因为通知数量只有启动app和点击按钮的时候才会调用,并不是那么地频繁,QPS 几万应该没问题
//但有大 key 的可能性,那么:1、定期清理缓存,配合数据库解决;2、Hash 底层会压缩数据;3、看占用内存的大小或者元素的数量;4、数据分片
NoticeAddDTO dto = NoticeAddDTO.builder()
.targetUserUuid("123qwe456rty789uio")
.superType(NumberUtils.INTEGER_TWO)
.subType(5)
.content("内容内容").build();
//无论何种业务系统的何种类型消息,先入数据库
Notification notification = this.notificationService.addNotice(dto);
//1、按消息的 tab 类型来做,可以为每一种类型都新建一个Redis-Key来单独存,这样是可行的,从某种程度上来说是拆 Key
//2、思考了一下还是用 Hash 结构,用 List 或者 String 可以解决类型的问题,但是难以按照用户来取值
HashOperations<String, String, Integer> hashOperations = redisTemplate.opsForHash();
//由于一个小时会清除全部缓存,当前未读数量需要查数据库来确认
this.notificationService.checkUnReadCount(notification);
if (NumberUtils.INTEGER_ONE.equals(notification.getSuperType()) && NumberUtils.INTEGER_ONE.equals(notification.getSubType())) {
//业务系统产生的消息,未读消息数+1;
Long increment = hashOperations.increment(RedisKey.INITIAL_NOTICE_COMMENT_NUM_PERFIX, dto.getTargetUserUuid(), 1);
log.info("新增的评论tab消息数量:{}", increment);
}
if (NumberUtils.INTEGER_TWO.equals(dto.getSuperType())) {
Long increment = hashOperations.increment(RedisKey.INITIAL_NOTICE_NUM_PERFIX, dto.getTargetUserUuid(), 1);
log.info("新增的通知tab消息数量:{}", increment);
}
//业务系统撤回消息,未读数-1
if (NumberUtils.INTEGER_TWO.equals(dto.getSuperType())) {
//Redis里不存在去做自减一,得到的就是-1;所以一定有值,不用判空只需判断正负
Long num = hashOperations.increment(RedisKey.INITIAL_NOTICE_NUM_PERFIX, dto.getTargetUserUuid(), -1);
log.info("撤回后通知tab消息数量:{}", num);
int intNUm = num.intValue();
//为了避免出现负数,要拿0作为界限来比
int result = intNUm < NumberUtils.INTEGER_ZERO ? NumberUtils.INTEGER_ZERO : intNUm;
hashOperations.put(RedisKey.INITIAL_NOTICE_NUM_PERFIX, dto.getTargetUserUuid(), result);
}
}
/**
* 用户加入购物车的商品计数
*/
@Resource
private ShoppingCarService shoppingCarService;
@Test
public void testUserShoppingCarInfo(){
//首选方案:boundHashOps() 在使用上的主要区别就是需要先绑定 Redis-Key,方便后续操作;而 opsForHash() 则是直接操作数据
String userUuid = "1656698374114156635";
String gooId = "3523465836543623675";
Long shopId = 34776547437357643L;
//1、首先是入参DTO的信息应该至少包含哪些?
ShoppingCarGoodInfoDTO dto = ShoppingCarGoodInfoDTO.builder()
.userUuid(userUuid).goodId(gooId)
.goodName("商品名称").goodDesc("618专属活动商品")
.price(BigDecimal.valueOf(98.99D))
.shopId(shopId).shopName("xx品牌旗舰店")
.quantities(2).manufactureTime(1720146162L).build();
ShoppingCar shoppingCar = dto.convertExt(ShoppingCar.class);
//2、先入数据库
this.shoppingCarService.save(shoppingCar);
//3、再入 redis:将用户 uuid 作为 redis-key,goodId 作为 hash-key,商品的具体信息为 hash-value
BoundHashOperations<String, String, String> operations = redisTemplate.boundHashOps(userUuid);
//这样就是有多少个用户,就有多少个Redis-Key;虽然一般来说用不完,也不会造成大 Key 问题,但数量多了无疑是对资源的一种巨大消耗,要考虑成本
String goodInfo = JSON.toJSONString(shoppingCar);
operations.put(gooId, goodInfo);
//入 redis 的时候直接设置7天过期时间,这样可以定期删除 Key 保证空间
operations.expire(Duration.ofDays(7));
//计数
Long size = operations.size();
log.info("打印看下数量:{}", size);
//todo: 更新购物车时也是先入数据库,再更新 Redis,并设置过期时间;查询时先查 Redis,没有再查数据库,然后重新写入数据库,设置过期时间
}
实时性要求高的场景,一般指的是:用户在使用某个功能时,服务能够近乎实时地提供结果。且并发量高时,如果每次都去请求数据库,那么所花费的开销对系统来说无疑是种挑战和压力。如果将数据存储在 Redis 中进行取用,那么其响应速度将会是极快的.
下面举 2 个例子:
/**
* 性能要求高,评论即进即出,而且可以入库
*/
@Test
public void testCommentList(){
TestCommentAddDTO dto = TestCommentAddDTO.builder()
.parentId(NumberUtils.INTEGER_ZERO)
.articleType(NumberUtils.INTEGER_ONE)
.articleTitle("新闻06").articleId(12)
.content("评论一下看看").creatorName("用户375368")
.creatorUuid("abc123def789UUID").createTime(new Date())
.build();
//评论队列先入 Redis 缓存,从队列左边进入,值得注意的是,List 结构只是表示队列的形式,具体的数据结构是 String 类型的
Long num = stringRedisTemplate.opsForList().leftPush(RedisKey.COMMENT_IMPORT_LIST, JSON.toJSONString(dto));
//这里返回的数量是该队列的大小
log.info("评论队列先入 Redis 缓存,数量为:{}", num);
//经验证,这里的方法是根据Redis-Key 弹出(即删除)全部的 Value,并且设置超时时间为 5 秒;leftPull 配合 rightPop 就是先进先出
String str = stringRedisTemplate.opsForList().rightPop(RedisKey.COMMENT_IMPORT_LIST,5, TimeUnit.SECONDS);
log.info("从右边弹出全部 Redis 队列缓存的内容,内容为:{}", str);
//反序列化解析
TestCommentAddDTO result = Optional.ofNullable(str)
.filter(StringUtils::isNotBlank)
.map(val -> JSON.parseObject(val, TestCommentAddDTO.class))
.orElse(null);
log.info("打印一下:{}", result);
//todo:接下来可以入数据库
}
/**
* 性能要求高,立即同步文章选用的媒体信息到媒资系统
*/
@Test
public void testMediaSet(){
ArrayList<String> imageList = new ArrayList<>();
imageList.add("20240702165612_image_xxx_filename_Media.png");
imageList.add("20240702164556_image_xxx_filename_Media.png");
ArrayList<String> videoList = new ArrayList<>();
videoList.add("20240702152319_video_xxx_filename_Media.mp4");
ArticleMediaDTO dto = ArticleMediaDTO.builder()
.articleId(123L)
.articleTitle("测文章")
.articleType(NumberUtils.INTEGER_TWO)
.imageMediaId(imageList)
.videoMediaId(videoList).build();
//这里每次只添加一条,但是需要保证整个队列没有重复的元素,故选择Set
stringRedisTemplate.opsForSet().add(RedisKey.INITIAL_ARTICLE_MEDIA_KEY_SET, JSON.toJSONString(dto));
//这里pop()是随机弹出一个元素,由于每次都是及时弹出的,所以队列里有的话只会有一个,否则为空
String str = stringRedisTemplate.opsForSet().pop(RedisKey.INITIAL_ARTICLE_MEDIA_KEY_SET);
ArticleMediaDTO result = Optional.ofNullable(str)
.filter(StringUtils::isNotBlank)
.map(val -> JSON.parseObject(val, ArticleMediaDTO.class))
.orElse(null);
log.info("打印一下弹出的内容:{}", result);
//todo: 接下来还可以与 MQ 配合进行通知操作
}
顾名思义,排行的场景很好理解,无论是 web 网页应用还是 app 客户端,都有很多需要排行的场景,如:
而 Redis 提供的 ZSet 集合数据类型结构能很好地实现各种复杂的排行榜需求,下面举一个 demo 来简单实现.
/**
* 计算用户成绩排名,ZSet 的 Score 需要根据一个权重来生成,最终 Redis 就会根据 Score 来排序
*/
@Test
public void testUserScoreRanking(){
//1、首先看分数(平均分、总分、最高分),总之按照配置会得到有一个分数;如果是整数那么就直接看用时,如果是小数会取小数点后两位
//2、如果分数相同,那么比谁的用时少(平均用时、总用时、最短用时),总之会得到一个用时,时间统一都精确到毫秒
//3、如果分数和用时都完全一样,那么就看交卷时间,谁的的交卷时间早谁就排前面,这里的时间也精确到毫秒
ScoreData scoreData = ScoreData.builder()
.finalScore(82.36D)
.spendTime(368956L)
.submitTime(1719915961902L).build();
Long activityId = UUIDUtils.generateUUIDToLong();
String userUuid = UUIDUtils.generateUUID();
//注意:分数取大,用时取小,这样的组合无法组成权重;如果将用时取反,即剩余时间,那么剩余时间取大,两者都成正比就能形成一个权重了
//具体:1、分数右移两位,组成权重的整数部分;
BigDecimal scoreBigDecimal = BigDecimal.valueOf(scoreData.getFinalScore()).movePointRight(NumberUtils.INTEGER_TWO);
//2、用一天的毫秒数 - 用时毫秒树 = 剩余时间,剩余时间小数点左移8位,组成权重的小数部分;
long time = Constants.MAX_DAY_TIME - scoreData.getSpendTime();
BigDecimal spendTimeBig = BigDecimal.valueOf(time).movePointLeft(8);
//3、整数部分+小数部分,即为 ZSet 的权重
BigDecimal weight = scoreBigDecimal.add(spendTimeBig);
//Java 的 BigDecimal 类提供了任意精度的计算,能完全满足对当代数学算术运算结果的精度要求,广泛运用于金融、科学计算等领域。
String rankingKey = RedisKey.INITIAL_ACTIVITY_PLAYER_SCORE_RANKING_KET.concat(".").concat(activityId.toString());
//最后到这里就可以写进 ZSet 了
stringRedisTemplate.opsForZSet().add(rankingKey, userUuid, weight.doubleValue());
}
到这里本篇文章就结束了,关于在实际 Java 互联网项目开发中常见的 Redis 缓存应用场景其实还有很多,以上只是我做的一些总结.
今天的分享就到这里,如有不足和错误,还请大家指正。或者你有其它想说的,也欢迎大家在评论区交流! 。
最后此篇关于【解决方案】Java互联网项目中常见的Redis缓存应用场景的文章就讲到这里了,如果你想了解更多关于【解决方案】Java互联网项目中常见的Redis缓存应用场景的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我有一个关于 Redis Pubsub 的练习,如下所示: 如果发布者发布消息但订阅者没有收到服务器崩溃。订阅者如何在重启服务器时收到该消息? 请帮帮我,谢谢! 最佳答案 在这种情况下,消息将永远消失
我们正在使用 Service Stack 的 RedisClient 的 BlockingDequeue 来保存一些数据,直到它可以被处理。调用代码看起来像 using (var client =
我有一个 Redis 服务器和多个 Redis 客户端。每个 Redis 客户端都是一个 WebSocket+HTTP 服务器,其中包括管理 WebSocket 连接。这些 WebSocket+HTT
我有多个 Redis 实例。我使用不同的端口创建了一个集群。现在我想将数据从预先存在的 redis 实例传输到集群。我知道如何将数据从一个实例传输到集群,但是当实例多于一个时,我无法做到这一点。 最佳
配置:三个redis集群分区,跨三组一主一从。当 Master 宕机时,Lettuce 会立即检测到中断并开始重试。但是,Lettuce 没有检测到关联的 slave 已经将自己提升为 master
我想根据从指定集合中检索这些键来删除 Redis 键(及其数据集),例如: HMSET id:1 password 123 category milk HMSET id:2 password 456
我正在编写一个机器人(其中包含要禁用的命令列表),用于监视 Redis。它通过执行禁用命令,例如 (rename-command ZADD "")当我重新启动我的机器人时,如果要禁用的命令列表发生变化
我的任务是为大量听众使用发布/订阅。这是来自 docs 的订阅的简化示例: r = redis.StrictRedis(...) p = r.pubsub() p.subscribe('my-firs
我一直在阅读有关使用 Redis 哨兵进行故障转移的内容。我打算有1个master+1个slave,如果master宕机超过1分钟,就把slave变成master。我知道这在 Sentinel 中是
与仅使用常规 Redis 和创建分片相比,使用 Redis 集群有哪些优势? 在我看来,Redis Cluster 更注重数据安全(让主从架构解决故障)。 最佳答案 我认为当您需要在不丢失任何数据的情
由于 Redis 以被动和主动方式使 key 过期, 有没有办法得到一个 key ,即使它的过期时间已过 (但 在 Redis 中仍然存在 )? 最佳答案 DEBUG OBJECT myKey 将返回
我想用redis lua来实现monitor命令,而不是redis-cli monitor。但我不知道怎么办。 redis.call('monitor') 不起作用。 最佳答案 您不能从 Redis
我读过 https://github.com/redisson/redisson 我发现有几个 Redis 复制设置(包括对 AWS ElastiCache 和 Azure Redis 缓存的支持)
Microsoft.AspNet.SignalR.Redis 和 StackExchange.Redis.Extensions.Core 在同一个项目中使用。前者需要StackExchange.Red
1. 认识 Redis Redis(Remote Dictionary Server)远程词典服务器,是一个基于内存的键值对型 NoSQL 数据库。 特征: 键值(key-value)型,value
1. Redis 数据结构介绍 Redis 是一个 key-value 的数据库,key 一般是 String 类型,但 value 类型多种多样,下面就举了几个例子: value 类型 示例 Str
1. 什么是缓存 缓存(Cache) 就是数据交换的缓冲区,是存贮数据的临时地方,一般读写性能较高。 缓存的作用: 降低后端负载 提高读写效率,降低响应时间 缓存的成本: 数据一致性成本 代码维护成本
我有一份记录 list 。对于我的每条记录,我都需要进行一些繁重的计算,因为我要在Redis中创建反向索引。为了达到到达记录,需要在管道中执行多个redis命令(sadd为100 s + set为1
我有一个三节点Redis和3节点哨兵,一切正常,所有主服务器和从属服务器都经过验证,并且哨兵配置文件已与所有Redis和哨兵节点一起更新,但是问题是当Redis主服务器关闭并且哨兵希望选举失败者时再次
我正在尝试计算Redis中存储的消息之间的响应时间。但是我不知道该怎么做。 首先,我必须像这样存储chat_messages的时间流 ZADD conversation:CONVERSATION_ID
我是一名优秀的程序员,十分优秀!