
1.Feed流推拉结合机制
Feed流是一种常见的信息展示方式,用于向用户推送内容更新,如社交媒体动态、新闻更新等。实现Feed流的方案有多种,其中推拉结合模式是一种折中的方案,兼具推和拉两种模式的优点。
推模式(写扩散)
-
定义:用户发布动态时,系统主动将动态推送到其粉丝的收件箱中。
-
优点:用户能够实时接收到内容更新,无需手动刷新。
-
缺点:内存压力大,特别是对于有大量粉丝的用户(大V),需要写入大量数据。
拉模式(读扩散)
-
定义:用户需要读取动态时,系统从其关注的用户中拉取所有动态,然后进行排序。
-
优点:节约空间,因为动态只保存在发布者的邮箱中。
-
缺点:延迟较大,特别是当用户关注了大量的用户时,需要拉取大量内容,对服务器压力较大。
推拉结合模式(读写混合)
-
定义:结合了推和拉两种模式的优点,是一种折中的方案。
-
实现方式:
-
对于普通用户,采用推模式,直接将动态写入其粉丝的收件箱。
-
对于有大量粉丝的用户(大V),采用拉模式,当粉丝需要读取动态时,再去拉取这些动态。大V的铁粉则继续使用推模式。
-
-
优点:
-
兼具推模式的实时性和拉模式的空间节约。
-
避免了大V带来的推送压力,同时保证了中小V内容的实时触达。
-
-
缺点:实现复杂。
实现逻辑
-
发布动态:
-
普通用户发布动态时,直接将动态写入其粉丝的收件箱。
-
大V发布动态时,对普通粉丝只写入自己的发件箱,不立即推送。铁粉则立即推送。
-
-
读取动态:
-
用户查看Feed流时,先读取自己的收件箱。
-
如果用户关注了大V,再去拉取大V的发件箱内容。
-
将收件箱和拉取的内容合并,按时间排序后返回给用户。
-
性能优化
-
推送优化:
-
分级推送:小V内容实时推送,中V内容批量推送。
-
粉丝分片:对中V采用粉丝分片推送,避免瞬时压力。
-
-
拉取优化:
-
预拉取机制:用户进入Feed流页面时,异步预拉取前几个头部大V的最新内容。
-
结果缓存:缓存拉模式聚合结果,减少重复计算。
-
-
存储优化:
-
内容分表:按创作者ID哈希分片存储内容表。
-
历史数据归档:超过一定时间的内容迁移至冷存储。
-

Controller
java
@GetMapping("/of/follow")
public Result queryBlogOfFollow(@RequestParam("lastId") Long max,@RequestParam(value = "offset",defaultValue = "0")Integer offset){
return blogService.queryBlogOfFollow(max,offset);
}
java
@Override
public Result saveBlog(Blog blog) {
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
boolean isSuccess = save(blog);
if(!isSuccess){
return Result.fail("发布笔记失败");
}
//查询作者所有粉丝
//推送笔记Id给所有粉丝
List<Follow> follows = followService.query().eq("follow_user_id",user.getId()).list();
//返回Id
for(Follow follow : follows){
Long userId = follow.getUserId();
//4.2推送
String key = FEED_KEY + userId;
stringRedisTemplate.opsForZSet().add(key,blog.getId().toString(),System.currentTimeMillis());
}
return Result.ok(blog.getId());
}
查询
java
@Override
public Result queryBlogOfFollow(Long max, Integer offset) {
//1.获取用户
Long userId = UserHolder.getUser().getId();
//2.查询收件箱
String key = FEED_KEY + userId;
//3.解析数据:blogId、score时间戳 offset
Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
.reverseRangeByScoreWithScores(key, 0, max, offset, 3);
//4.根据id查询blog
if(typedTuples == null || typedTuples.isEmpty()){
return Result.ok();
}
//5.封装返回
List<Long> ids = new ArrayList<>(typedTuples.size());
long minTime = 0;
int os = 1;
for(ZSetOperations.TypedTuple<String> tuple:typedTuples){
String idStr = tuple.getValue();
ids.add(Long.valueOf(idStr));
long time = tuple.getScore().longValue();
if(time == minTime){
os++;
}else{
minTime = time;
os = 1;
}
}
String idStr = StrUtil.join(",",ids);
List<Blog> blogs = query().in("id",ids).last("ORDER BY FIELD(id," + idStr + ")").list();
for(Blog blog : blogs){
//2.查关于blog有关的用户
queryBlogUser(blog);
//3.查询blog是否点赞
isBlogLiked(blog);
}
ScrollResult r = new ScrollResult();
r.setList(blogs);
r.setOffset( os);
r.setMinTime(minTime);
return Result.ok(r);
}
2.用户签到:
java
@Override
public Result sign(){
//1.获取当前用户
Long userId = UserHolder.getUser().getId();
//2.获取日期
LocalDateTime now = LocalDateTime.now();
//3.拼接key
String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
String key = USER_SIGN_KEY + userId + keySuffix;
//4.获取第几天
int dayOfMonth = now.getDayOfMonth();
//5.写入Redis set bit
stringRedisTemplate.opsForValue().setBit(key,dayOfMonth-1,true);
return Result.ok();
}
java
@Override
public Result signCount() {
//1.获取当前用户
Long userId = UserHolder.getUser().getId();
//2.获取日期
LocalDateTime now = LocalDateTime.now();
//3.拼接key
String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
String key = USER_SIGN_KEY + userId + keySuffix;
//4.获取第几天
int dayOfMonth = now.getDayOfMonth();
//5.得到签到记录 十进制数字
List<Long> result = stringRedisTemplate.opsForValue().bitField(
key, BitFieldSubCommands.create()
.get(BitFieldSubCommands.BitFieldType.
unsigned(dayOfMonth)).valueAt(0)
);
if(result == null || result.isEmpty()){
return Result.ok(0);
}
Long num = result.get(0);
if(num == null || num == 0){
return Result.ok(0);
}
//6.循环遍历
int count = 0;
while (true) {
//7.让数字与1做与运算,得到最后一个bit位
if ((num & 1) == 0){
//为0返回 没签到
break;
}else {
//不为0已签到 计数加一
count ++;
}
//数字右移 继续判断
num >>>= 1;
}
return Result.ok(count);
}
1. 使用位运算遍历
你的代码中使用了位运算来遍历二进制位,这种方法的优点是性能高且直接操作二进制位。
代码逻辑:
int count = 0;
while (true) {
if ((num & 1) == 0) {
break;
} else {
count++;
}
num >>>= 1;
}
优点:
-
性能高:位运算在 CPU 级别是非常高效的,不需要额外的内存分配。
-
直接操作二进制位:可以精确地控制每一位的操作,适合这种需要逐位检查的场景。
缺点:
- 可读性稍差:对于不熟悉位运算的开发者来说,可能需要花一点时间理解代码逻辑。
2. 转字符串遍历
如果你选择将数字转换为字符串,然后逐字符遍历,也可以实现相同的功能,但性能和可读性会有所不同。
示例代码:
int count = 0;
String binaryString = Long.toBinaryString(num);
for (int i = 0; i < binaryString.length(); i++) {
if (binaryString.charAt(i) == '1') {
count++;
} else {
break;
}
}
优点:
-
可读性好:对于不熟悉位运算的开发者来说,字符串遍历的逻辑更容易理解。
-
简洁:代码更简洁,逻辑更直观。
缺点:
-
性能低:字符串操作涉及到更多的内存分配和字符操作,性能不如位运算。
-
额外内存占用:需要将数字转换为字符串,增加了内存占用。
3.Redis Geo
Redis Geo 是 Redis 提供的一种用于处理地理位置数据的功能模块。它允许用户将地理位置信息存储在 Redis 数据库中,并执行各种与地理位置相关的操作,例如计算两点之间的距离、查找某个位置附近的其他位置等。以下是 Redis Geo 的一些主要功能和使用方法:
1. 数据结构
Redis Geo 使用有序集合(Sorted Set)来存储地理位置信息。每个地理位置由以下三部分组成:
-
经度(longitude):表示地理位置的经度。
-
纬度(latitude):表示地理位置的纬度。
-
成员(member):一个字符串,用于标识该地理位置。
2. 主要命令
Redis Geo 提供了一系列命令来操作地理位置数据,以下是一些常用的命令:
添加地理位置
-
GEOADD key longitude latitude member [longitude latitude member ...]-
功能:将一个或多个地理位置添加到指定的键中。
-
示例
GEOADD locations 116.397428 39.90923 "Beijing" GEOADD locations 121.473701 31.230393 "Shanghai"
-
计算距离
-
GEODIST key member1 member2 [unit]-
功能:计算两个地理位置之间的距离。
-
单位 :单位可以是
m(米)、km(千米)、mi(英里)、ft(英尺)。 -
示例:
GEODIST locations Beijing Shanghai km
-
查找附近的位置
-
GEORADIUS key longitude latitude radius [unit] [WITHDIST] [WITHCOORD] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]-
功能:查找指定位置周围一定范围内的其他位置。
-
示例
GEORADIUS locations 116.397428 39.90923 100 km WITHDIST
-
获取地理位置的坐标
-
GEOPOS key member [member ...]-
功能:获取一个或多个地理位置的坐标。
-
示例
GEOPOS locations Beijing
-
删除地理位置
-
ZREM key member [member ...]-
功能:从有序集合中删除指定的成员。
-
示例
ZREM locations Beijing
-
3. 应用场景
Redis Geo 在许多实际场景中非常有用,例如:
-
位置服务:如地图应用、外卖平台等,可以根据用户的位置查找附近的商家或服务。
-
社交网络:根据用户的位置推荐附近的朋友或活动。
-
物流配送:计算配送距离,优化配送路线。
java
@Override
public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
//1.判断是否根据坐标查询
if(x == null || y == null){
Page<Shop> page = query()
.eq("type_id", typeId)
.page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
return Result.ok(page.getRecords());
}
//2.计算分页参数
int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
int end = current * SystemConstants.DEFAULT_PAGE_SIZE;
//3.查询redis、按照距离排序分页
String key = SHOP_GEO_KEY + typeId;
GeoResults<RedisGeoCommands.GeoLocation<String>> results =
stringRedisTemplate.opsForGeo()
.search(key, GeoReference.fromCoordinate(x, y),
new Distance(5000),
RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs()
.includeDistance().limit(end));
//4.解析id
if(results == null ){
return Result.ok(Collections.emptyList());
}
List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
List<Long> ids = new ArrayList<>(list.size());
Map<String,Distance> distanceMap = new HashMap<>(list.size());
list.stream().skip(from).forEach(result ->{
String shopIdStr = result.getContent().getName();
ids.add(Long.valueOf(shopIdStr));
Distance distance = result.getDistance();
distanceMap.put(shopIdStr,distance);
});
// 🚨 防止 id 为空
if (ids.isEmpty()) {
return Result.ok(Collections.emptyList());
}
//获取店铺id
String idStr = StrUtil.join(",", ids);
List<Shop> shops = query().in("id",ids).last("ORDER BY FIELD(id," + idStr + ")").list();
for(Shop shop : shops){
shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
}
return Result.ok(shops);
}