Redis 實現(xiàn)好友關注和關注推送的示例代碼
一、關注和取關
1.1 簡介
在探店圖文的詳情頁面中,可以關注發(fā)布筆記的作者:

1.2 需求描述
實現(xiàn)兩個接口:關注和取關接口、判斷是否關注的接口
1.3 代碼實現(xiàn)
涉及到的 controller 層代碼如下:
@RestController
@RequestMapping("/follow")
public class FollowController {
@Resource
private IFollowService followService;
@PutMapping("/{id}/{isFollow}")
public Result follow(@PathVariable("id") Long followUserId, @PathVariable("isFollow") Boolean isFollow) {
return followService.follow(followUserId, isFollow);
}
@GetMapping("/or/not/{id}")
public Result isFollow(@PathVariable("id") Long followUserId) {
return followService.isFollow(followUserId);
}
}涉及到的 service 層代碼如下:
@Service
public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private IUserService userService;
@Override
public Result follow(Long followUserId, Boolean isFollow) {
// 1.獲取登錄用戶
Long userId = UserHolder.getUser().getId();
String key = "follows:" + userId;
// 1.判斷到底是關注還是取關
if (isFollow) {
// 2.關注,新增數(shù)據(jù)
Follow follow = new Follow();
follow.setUserId(userId);
follow.setFollowUserId(followUserId);
boolean isSuccess = save(follow);
if (isSuccess) {
// 把關注用戶的id,放入redis的set集合 sadd userId followerUserId
stringRedisTemplate.opsForSet().add(key, followUserId.toString());
}
} else {
// 3.取關,刪除 delete from tb_follow where user_id = ? and follow_user_id = ?
boolean isSuccess = remove(new QueryWrapper<Follow>()
.eq("user_id", userId).eq("follow_user_id", followUserId));
if (isSuccess) {
// 把關注用戶的id從Redis集合中移除
stringRedisTemplate.opsForSet().remove(key, followUserId.toString());
}
}
return Result.ok();
}
@Override
public Result isFollow(Long followUserId) {
// 1.獲取登錄用戶
Long userId = UserHolder.getUser().getId();
// 2.查詢是否關注 select count(*) from tb_follow where user_id = ? and follow_user_id = ?
Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count();
// 3.判斷
return Result.ok(count > 0);
}
}二、共同關注
2.1 簡介
點擊博主頭像,可以進入博主首頁:

2.2 需求描述
利用 Redis 中恰當?shù)臄?shù)據(jù)結構,實現(xiàn)共同關注功能。在博主個人頁面展示出當前用戶與博主的共同好友。
2.3 代碼實現(xiàn)
涉及到的 controller 層代碼如下:
@RestController
@RequestMapping("/follow")
public class FollowController {
@Resource
private IFollowService followService;
@GetMapping("/common/{id}")
public Result followCommons(@PathVariable("id") Long id){
return followService.followCommons(id);
}
}涉及到的 service 層代碼如下:
@Service
public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private IUserService userService;
@Override
public Result followCommons(Long id) {
// 1.獲取當前用戶
Long userId = UserHolder.getUser().getId();
String key = "follows:" + userId;
// 2.求交集
String key2 = "follows:" + id;
Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key, key2);
if (intersect == null || intersect.isEmpty()) {
// 無交集
return Result.ok(Collections.emptyList());
}
// 3.解析id集合
List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
// 4.查詢用戶
List<UserDTO> users = userService.listByIds(ids)
.stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
return Result.ok(users);
}
}三、關注推送
3.1 簡介
關注推送也叫做 Feed 流,直譯為投喂。為用戶持續(xù)的提供 “沉浸式” 的體驗,通過無限下拉刷新獲取新的信息。

3.2 Feed 流的模式
3.2.1 TimeLine
不做內(nèi)容篩選,簡單的按照內(nèi)容發(fā)布時間排序,常用于好友或關注。例如朋友圈。
優(yōu)點:信息全面,不會有缺失。并且實現(xiàn)也相對簡單
缺點:信息噪音較多,用戶不一定感興趣,內(nèi)容獲取效率低
3.2.2 智能排序
利用智能算法屏蔽掉違規(guī)的、用戶不感興趣的內(nèi)容。推送用戶感興趣信息來吸引用戶。
優(yōu)點:投喂用戶感興趣信息,用戶粘度很高,容易沉迷
缺點:如果算法不精準,可能起到反作用
3.3 實現(xiàn)方案
本例中的個人頁面,是基于關注的好友來做 Feed 流,因此采用 Timeline 的模式。該模式的實現(xiàn)方案有三種:拉模式、推模式、推拉結合模式。
3.3.1 拉模式
拉模式也叫做讀擴散,假設現(xiàn)在有三個 up 主,張三李四和王五,它們?nèi)齻€人將來會發(fā)一些消息,此時,給他們每個人都準備一個發(fā)件箱,將來它們發(fā)送消息的時候就會發(fā)送到發(fā)件箱里面去,發(fā)送的消息除了本身以外還需要攜帶一個時間戳,因為后面要按照時間排序。
此時有一個粉絲趙六,它有一個收件箱,平常是空的,只有他要去讀消息的時候我們才會給他去拉取,即從它所關注的 up 主的發(fā)件箱去拉取消息,拉過來之后按照時間去排序,這樣就可以去讀取了。

這種模式的優(yōu)點是節(jié)省內(nèi)存空間,因為收件箱讀取完畢之后就可以清空,下次再重新拉取。缺點是每次讀消息的時候都要重新去拉取發(fā)件箱的消息,然后再做排序,這一系列的動作耗時會比較久,讀取的延遲較高。
3.3.2 推模式
推模式也叫寫擴散。假設此時有兩個 up 主,張三和李四,粉絲1關注了張三、粉絲2關注了張三和李四、粉絲3也關注了張三和李四,假設此時張三要發(fā)送消息,它所發(fā)送的消息會直接推送到所有粉絲的收件箱里面去,然后對收件箱里面的消息進行排序,此時粉絲可以直接從收件箱里面讀取消息

此種模式的優(yōu)點是延遲非常的低,彌補了拉模式的缺點。缺點是由于沒有了發(fā)件箱,不得不把消息發(fā)送給每一個粉絲,內(nèi)存占用會很高,即一個消息要寫N份
3.3.3 推拉結合模式
推拉結合模式也叫做讀寫混合,兼具推和拉兩種模式的優(yōu)點。假設現(xiàn)在有一個大 V 和普通人張三,還有兩個普通粉絲和一個活躍粉絲,每個粉絲都有自己的收件箱。假設此時普通人張三要發(fā)送消息,他的粉絲很少,就可以采用推模式,即直接將消息推送到每一個粉絲的收件箱里面。
而大 V 則不一樣,大 V 的粉絲很多,雖然粉絲多,但是粉絲存在差異,有活躍粉絲和一般粉絲。針對于活躍粉絲采用推模式,而針對于一般粉絲采取拉模式,因為大 V 需要有一個發(fā)件箱。

這種推拉結合的模式,既節(jié)省了內(nèi)存又照顧了活躍用戶的感受
3.3.4 總結

3.4 Feed 流的分頁問題
Feed 流中的數(shù)據(jù)會不斷更新,所以數(shù)據(jù)的角標也在變化,因此不能采用傳統(tǒng)的分頁模式。接下來我們分析下原因。
以下圖為例,橫線代表時間軸,此時時間來到了 t1 時刻,收件箱里面已經(jīng)有了 10 條消息了,數(shù)字越大代表越新,讀取第一頁數(shù)據(jù)沒啥問題。

此時來到了 t2 時刻,有人插入了一條新的數(shù)據(jù),此時我們的 11 數(shù)據(jù)就跑到了最前面去了

等到來到了 t3 時刻,此時需要讀取第二頁的內(nèi)容,此時就會出現(xiàn)重復讀取的問題,分頁就出現(xiàn)了混亂。

這就是 Feed 流不能采用傳統(tǒng)的分頁模式的原因。
3.5 Feed 流的滾動分頁
此時就需要采用 Feed 流的滾動分頁,即記錄每次分頁的最后一條,下次再從這個位置開始查。第一次讀取時 lastId 設置成無窮大就可以了,如下圖:

到了 t2 時刻有人插入了新的數(shù)據(jù) 11,如下圖:

等到 t3 時刻,讀取第二頁的時候,讓 lastId = 6,向后查 5 條就不會出現(xiàn)問題了,如下圖:

此時的查詢是不依賴于腳標的,所以數(shù)據(jù)不受影響。所以只能使用 zset 結構。
3.6 需求描述
基于推模式實現(xiàn)關注推送功能:
1、修改新增探店筆記的業(yè)務,在保存 blog 到數(shù)據(jù)庫的同時,推送到粉絲的收件箱
2、收件箱滿足可以根據(jù)時間戳排序,必須用 Redis 的數(shù)據(jù)結構實現(xiàn)
3、查詢收件箱數(shù)據(jù)時,可以實現(xiàn)分頁查詢
3.7 代碼實現(xiàn)
首先完成修改新增探店筆記的業(yè)務,在保存 blog 到數(shù)據(jù)庫的同時,推送到粉絲的收件箱功能,涉及到的 controller 層代碼如下:
@RestController
@RequestMapping("/blog")
public class BlogController {
@Resource
private IBlogService blogService;
@PostMapping
public Result saveBlog(@RequestBody Blog blog) {
return blogService.saveBlog(blog);
}
}涉及到的 service 層代碼如下:
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
@Resource
private IUserService userService;
@Resource
StringRedisTemplate stringRedisTemplate;
@Resource
IFollowService followService;
@Override
public Result saveBlog(Blog blog) {
// 1.獲取登錄用戶
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
// 2.保存探店筆記
boolean isSuccess = save(blog);
if(!isSuccess){
return Result.fail("新增筆記失敗!");
}
// 3.查詢筆記作者的所有粉絲 select * from tb_follow where follow_user_id = ?
List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list();
// 4.推送筆記id給所有粉絲
for (Follow follow : follows) {
// 4.1.獲取粉絲id
Long userId = follow.getUserId();
// 4.2.推送
String key = "feed:" + userId;
stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());
}
// 5.返回id
return Result.ok(blog.getId());
}
}接下來完成剩下的兩小點需求,界面請求的參數(shù)如下:

涉及到的 controller 層代碼如下:
@RestController
@RequestMapping("/blog")
public class BlogController {
@Resource
private IBlogService blogService;
@PostMapping
public Result saveBlog(@RequestBody Blog blog) {
return blogService.saveBlog(blog);
}
@GetMapping("/of/follow")
public Result queryBlogOfFollow(
@RequestParam("lastId") Long max, @RequestParam(value = "offset", defaultValue = "0") Integer offset){
return blogService.queryBlogOfFollow(max, offset);
}
}涉及到的 service 層代碼如下:
@Override
public Result queryBlogOfFollow(Long max, Integer offset) {
// 1.獲取當前用戶
Long userId = UserHolder.getUser().getId();
// 2.查詢收件箱 ZREVRANGEBYSCORE key Max Min LIMIT offset count
String key = FEED_KEY + userId;
Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
.reverseRangeByScoreWithScores(key, 0, max, offset, 2);
// 3.非空判斷
if (typedTuples == null || typedTuples.isEmpty()) {
return Result.ok();
}
// 4.解析數(shù)據(jù):blogId、minTime(時間戳)、offset
List<Long> ids = new ArrayList<>(typedTuples.size());
long minTime = 0; // 2
int os = 1; // 2
for (ZSetOperations.TypedTuple<String> tuple : typedTuples) { // 5 4 4 2 2
// 4.1.獲取id
ids.add(Long.valueOf(tuple.getValue()));
// 4.2.獲取分數(shù)(時間戳)
long time = tuple.getScore().longValue();
if(time == minTime){
os++;
}else{
minTime = time;
os = 1;
}
}
// 5.根據(jù)id查詢blog
String idStr = StrUtil.join(",", ids);
List<Blog> blogs = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();
for (Blog blog : blogs) {
// 5.1.查詢blog有關的用戶
queryBlogUser(blog);
// 5.2.查詢blog是否被點贊
isBlogLiked(blog);
}
// 6.封裝并返回
ScrollResult r = new ScrollResult();
r.setList(blogs);
r.setOffset(os);
r.setMinTime(minTime);
return Result.ok(r);
}封裝返回給前端的實體類如下:
@Data
public class ScrollResult {
private List<?> list;
private Long minTime;
private Integer offset;
}到此這篇關于 Redis 實現(xiàn)好友關注和關注推送的示例代碼的文章就介紹到這了,更多相關 Redis 好友關注和關注推送內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Govern Service 基于 Redis 的服務治理平臺安裝過程詳解
Govern Service 是一個輕量級、低成本的服務注冊、服務發(fā)現(xiàn)、 配置服務 SDK,通過使用現(xiàn)有基礎設施中的 Redis 不用給運維部署帶來額外的成本與負擔,接下來通過本文給大家分享Govern Service 基于 Redis 的服務治理平臺的相關知識,感興趣的朋友一起看看吧2021-05-05
redis連接報錯error:NOAUTH Authentication required
本文主要介紹了redis連接報錯error:NOAUTH Authentication required,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2022-05-05
redis中使用bloomfilter的白名單功能解決緩存穿透問題
本文主要介紹了redis中使用bloomfilter的白名單功能解決緩存穿透問題,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-07-07
RediSearch加RedisJSON大于Elasticsearch的搜索存儲引擎
這篇文章主要為大家介紹了RediSearch加RedisJSON大于Elasticsearch的王炸使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-07-07

