1. 达人探店案例
1.1 点赞功能
@PutMapping("/like/{id}")
public Result likeBlog(@PathVariable("id") Long id) {
// 修改点赞数量
blogService.update()
.setSql("liked = liked + 1").eq("id", id).update();
return Result.ok();
}
一个基本的点赞功能如上代码所示
这样做的后果是每次都访问数据库,最终压力都全部会打到数据库上,而且点赞没有去重判断的功能一人一单
类。
@Override
public Result queryBlogById(Long id) {
//1.查询blog
Blog blog = this.query().eq("id", id).one();
if (blog == null) {
return Result.fail("博客不存在!");
}
//2.查询blog有关的信息
this.getUser(blog);
isBolgLiked(blog);
return Result.ok(blog);
}
private void getUser(Blog blog){
User user = userService.query().eq("id", blog.getUserId()).one();
blog.setName(user.getNickName());
blog.setIcon(user.getIcon());
}
@Override
public Result queryBlogById(Integer current) {
// 根据用户查询
Page<Blog> page = query()
.orderByDesc("liked")
.page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
// 获取当前页数据
List<Blog> records = page.getRecords();
// 查询用户
records.forEach(blog->{
getUser(blog);
isBolgLiked(blog);
});
// 查询blog是否被点赞
return Result.ok(records);
}
@Override
public Result likeBlog(Long id) {
//1.判断当前是否点赞
UserDTO user = UserHolder.getUser();
Long userId = user.getId();
String key = RedisConstants.BLOG_LIKED_KEY+id;
Boolean isLiked = stringRedisTemplate.opsForSet().isMember(key, userId);
//2.如果未点赞
if(!BooleanUtil.isTrue(isLiked)){
//数据库的点赞数+1
boolean success = update().setSql("liked = liked +1").eq("id", id).update();
//将用户信息保存到redis中
if(success){
stringRedisTemplate.opsForSet().add(key,userId.toString());
}
}else{
//3.已经点赞,取消点赞,点赞数-1
boolean success = update().setSql("liked = liked -1").eq("id", id).update();
//把用户从redis中的set集合中删除
if(success){
stringRedisTemplate.opsForSet().remove(key,userId.toString());
}
}
return Result.ok();
}
private void isBolgLiked(Blog blog){
UserDTO user = UserHolder.getUser();
Long userId = user.getId();
String key = RedisConstants.BLOG_LIKED_KEY+blog.getId();
Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId);
blog.setIsLike(BooleanUtil.isTrue(isMember));
}
1.2 点赞排行榜
在上面的实现中,我们是基于无序的set
进行实现的,然而我们想要实现的功能是按照点赞时间来排序,得到点赞top5的用户
List | Set | SortedSet | |
---|---|---|---|
排序方式 | 按照添加顺序排序 | 无法排序 | 根据score值排序 |
唯一性 | 不唯一 | 唯一 | 唯一 |
查找方式 | 按索引查找或者首尾查找 | 根据元素查找 | 根据元素查找 |
@Override
public Result likeBlog(Long id) {
//1.判断当前是否点赞...
Double score = stringRedisTemplate.opsForZSet().score(key, userId);
//2.如果未点赞
if(score == null){
//数据库的点赞数+1
//将用户信息保存到redis中 zadd key value score
if(success){
stringRedisTemplate.opsForZSet().add(key,userId.toString(),System.currentTimeMillis());
}
}else{
//3.已经点赞,取消点赞,点赞数-1
//把用户从redis中的set集合中删除
if(success){
stringRedisTemplate.opsForZSet().remove(key,userId.toString());
}
}
return Result.ok();
}
private void isBolgLiked(Blog blog){
...
Double score = stringRedisTemplate.opsForZSet().score(key, userId);
blog.setIsLike(score != null);
}
TopN点赞实现
@Override
public Result queryBlogLikes(Long id) {
String key = RedisConstants.BLOG_LIKED_KEY+id;
//查询TOP5
Set<String> topN = stringRedisTemplate.opsForZSet().range(key, 0, 4);
if(topN == null || topN.isEmpty()){
return Result.ok(Collections.emptyList());
}
//解析相关信息
List<Long> ids = topN.stream().map(Long::valueOf).collect(Collectors.toList());
List<UserDTO> users = userService.listByIds(ids)
.stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
//数据脱敏
return Result.ok(users);
}
2. 好友关注案例
2.1 关注和取关
@Resource
private FollowMapper followMapper;
@Override
@Transactional
public Result follow(Long followUserId, Boolean isFollow) {
UserDTO loginUser = UserHolder.getUser();
//取决于你传的参数,到底是关注还是取关
if(BooleanUtil.isTrue(isFollow)){
Follow follow = new Follow();
follow.setUserId(loginUser.getId());
follow.setFollowUserId(followUserId);
save(follow);
}else{
//取关,删除 delete from tb_follow where user_id = ? and follower_id = ?
followMapper.cancelFollow(loginUser.getId(),followUserId);
}
return Result.ok("操作成功");
}
<select id="followOrNot" resultType="int">
select count(id) from tb_follow where user_id = #{userId} and follow_user_id = #{followUserId}
</select>
@Override
public Result followOrNot(Long followUserId) {
UserDTO loginUser = UserHolder.getUser();
Long userId = loginUser.getId();
//查询是否关注
int result = followMapper.followOrNot(userId, followUserId);
return Result.ok(result != 0);
}
2.2 共同关注
共同关注实际上就是将{用户关注用户的集合}∩{目标用户关注用户的集合}
,显示出来就可以了
这里的思路是使用redis
中的set
这种数据结构
@Override
@Transactional
public Result follow(Long followUserId, Boolean isFollow) {
UserDTO loginUser = UserHolder.getUser();
//取决于你传的参数,到底是关注还是取关
String key = "follows:"+ loginUser.getId().toString();
if(BooleanUtil.isTrue(isFollow)){
Follow follow = new Follow();
follow.setUserId(loginUser.getId());
follow.setFollowUserId(followUserId);
save(follow);
stringRedisTemplate.opsForSet().add(key,followUserId.toString());
}else{
//取关,删除 delete from tb_follow where user_id = ? and follower_id = ?
Boolean isSuccess = followMapper.cancelFollow(loginUser.getId(), followUserId);
if(BooleanUtil.isTrue(isSuccess)){
stringRedisTemplate.opsForSet().remove(key,followUserId.toString());
}
}
return Result.ok("操作成功");
}
public Result commonFollow(Long otherUserId) {
Long userId = UserHolder.getUser().getId();
String key4ThisUser = "follows"+userId;
String key4OtherUser = "follows"+otherUserId;
//使用redis
Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key4ThisUser, key4OtherUser);
if(intersect == null || intersect.isEmpty()){
return Result.ok(Collections.emptyList());
}
List<Long> userIds = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
List<UserDTO> userDTOs = userService.listByIds(userIds).
stream().
map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
//数据脱敏
return Result.ok(userDTOs);
}
2.3 关注推送
feed流实现方案分析
关注推送也叫做Feed
流,直接翻译为投喂,为用户持续地提供沉浸式的体验,通过无限下拉刷新获取新的信息
实现的有两种模式
Timeline
:不做内容筛选,简单的按照内容发布时间进行排序,常用于好友关注,例如朋友圈
- 优点:信息全面,不会有缺失,并且实现也相对简单
- 缺点:信息噪音比较多,用户不一定感兴趣,内容获取的效率低
智能排序
:利用智能算法屏蔽掉用户不感兴趣,推送用户感兴趣的内容来吸引用户
- 优点:投喂用户感兴趣的信息,用户粘度很高,容易沉迷
- 缺点:如果算法不精准,可能起到反作用
基于Timeline
的模式的实现
- 拉取(pull)模式 :这也叫做读扩散
如图所示,首先张三和李四发布了两条消息,这两条消息除了自身的数据内容之外,还带有一个时间戳,用来标识这个消息产生的时间,当赵六与张三和李四建立了关注的关系后,就可以通过创建消息摘要(就是将消息的概要信息,比如说谁发送的,啥时候推送的,而不是直接将消息数据复制一份到收件箱中,每一次数据的读取都需要到对应的发件箱中读取消息
)的方式,将张三和李四所发布的消息拉取到赵六的收件箱中,在收件箱做时间戳的排序,供给用户查看
这种模式的优缺点:
它首先是节省内容的,因为同一个消息内容,在存储中只占一份,不会导致信息的冗余
会导致一定的延迟,这是因为每次都需要去拉取对应的信息
- 推送(push)模式:写扩散
这种模式的特点是,没有发件箱的这个角色存在,因此需要在收件箱中都保存一份副本。
首先它的延迟是很低的,因为不需要用户进行实时拉取,一旦接收到推送的消息就可以使用
内存浪费,相同的内容可能会存储相当多次
- 推送/拉取模式:读写混合模式
简单来说就是对用户进行分类,对于那些活跃用户,为了提高他们的体验,降低延时,使用推送模式
对于普通用户,为了避免内存的浪费,使用拉取模式
基于推模式实现关注推送功能
需要采用滚动分页模式进行实现,记录每一次查询的最后一条,下一次查询从这记录的最后一条开始
那么最后一条记录什么呢,SortedSet
可以记录每一条记录的score
,同时具有范围查询的功能,因此我们可以记录下查询的最后一条元素的score
,去查询一定范围内的score
的记录,同时维护这个最后一条元素score
,最终实现滚动查询
public Result saveBlog(Blog blog) {
// 获取登录用户
Long userId = UserHolder.getUser().getId();
blog.setUserId(userId);
// 保存探店博文,推送到粉丝收件箱(redis)
boolean isSuccess = this.save(blog);
if(!isSuccess){
return Result.fail("推送失败!");
}
//查询笔记作者的所有粉丝
List<Follow> allFollowers = followMapper.findAllFollower(userId);
for (Follow follower : allFollowers) {
//4.1 获取粉丝ID
Long followerId = follower.getId();
//4.2 推送
String key = "feed:"+followerId;
stringRedisTemplate.opsForZSet().add(key,blog.getId().toString(),System.currentTimeMillis());
}
// 返回id
return Result.ok(blog.getId());
}
public Result pushByPage(Integer offset, Long lastId) {
//实现滚动分页查询逻辑
//1. 获取当前用户
UserDTO user = UserHolder.getUser();
Long userId = user.getId();
//2. 查询收件箱
String key = RedisConstants.FEED_KEY+userId;
//3. 解析数据:blogId、minTime、offset
//Zrevrange key max min LIMIT offset count
Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, 0, lastId, offset, 3);
if(typedTuples == null || typedTuples.isEmpty()){
return Result.ok(Collections.emptyList());
}
//4. 数据脱敏,封装返回
List<Long> ids = new ArrayList<>(typedTuples.size());
long minTime = 0;
int os = 1;
for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {
//获取id
String id = typedTuple.getValue();
//获取时间戳
long time = typedTuple.getScore().longValue();
if(time == minTime){
os++;
}else{
minTime = time;
os = 1;
}
ids.add(Long.valueOf(id));
}
List<Blog> blogs = this.listByIds(ids);
//处理博客
for (Blog blog : blogs) {
getUser(blog);
isBolgLiked(blog);
}
ScrollResult scrollResult = new ScrollResult();
scrollResult.setOffset(os);
scrollResult.setMinTime(minTime);
scrollResult.setList(blogs);
return Result.ok(scrollResult);
}
3. 用户签到案例
3.1 BitMap
用0和1来标示业务的状态,这种思路称之为位图
Redis底层是使用String
类型的数据结构来实现BitMap
的,因此最大的上限是512M
,转化为bit就是2^32
个bit位,基础指令如:
setbit
:向指定的位置offset
存一个0或者1getbit
:获取指定位置offset
的bit值bitcount
:统计BitMap
中值为1的bit位的数量bitfield
:操作(查询、修改、自增)
bitmap中bit数组中的指定位置的offset
的值bitfield_ro
:获取BitMap
中bit数组,并以十进制的形式返回bitop
:将多个BITMap
的结果做位运算(与、异或、或)
bitpos
:查找bit数组中第一个0或者第一个1出现的位置
3.2 实现签到功能
因为BitMap底层是基于
String
实现的,因此其相关的操作也都封装在字符串的相关操作中了
@Override
public Result sign() {
Long userId = UserHolder.getUser().getId();
LocalDateTime now = LocalDateTime.now();
//获取当前年份/月份,组装key
int year = now.getYear();
int month = now.getMonth().getValue();
String key = RedisConstants.USER_SIGN_KEY+userId.toString()+year+":"+month;
//获取当前是月份的第几天,setBit
int day = now.getDayOfMonth();
Boolean isSuccess = stringRedisTemplate.opsForValue().setBit(key, day-1, true);
if(BooleanUtil.isTrue(isSuccess)){
return Result.ok("签到成功!");
}
return Result.fail("签到失败,请重试!");
}
3.3 统计连续签到功能
@Override
public Result signCount() {
Long userId = UserHolder.getUser().getId();
//获取天数
LocalDateTime now = LocalDateTime.now();
int month = now.getMonthValue();
int day = now.getDayOfMonth();
int year = now.getYear();
String key = RedisConstants.USER_SIGN_KEY+userId.toString()+year+":"+month;
List<Long> result = stringRedisTemplate.opsForValue().bitField(
key,
BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(day - 1)).valueAt(0)
);
if(result == null || result.isEmpty()){
return Result.ok();
}
Long resultNum = result.get(0);
if(resultNum == null || resultNum == 0L){
return Result.ok();
}
int cnt=0;
while(resultNum!=0){
if((resultNum &1) == 0L){
break;
}
cnt++;
resultNum>>>=1;
}
return Result.ok(cnt);
}
4.UV统案例
4.1 HyperLogLog用法
UV
:全称为Unique Visitor
,也叫做独立访客量,是指通过互联网访问、浏览这个网页的自然人, 1天内同一个访客多次访问该网站,只记录一次PV
:全称为Page View
,也叫作页面访问量或者点击量,用户每访问网站的一个页面,记录一次PV
,用户多次打开页面,则记录多次PV
,往往用来衡量网站的流量
HyperLogLog
是从LogLog
算法派生出来的概率算法,用于确定非常大的集合的基数,而不需要存储所有值,Redis是基于String
结构实现的,单个HLL
的内存永远小于16kb
,其测量结果是有概率性的,有误差存在
String[] values = new String[1000];
for (int i = 0; i < 1000000; i++) {
int j = i%1000;
values[j] = "user"+i;
if(j == 999){
stringRedisTemplate.opsForHyperLogLog().add("hl2",values);
}
}
//统计
Long count = stringRedisTemplate.opsForHyperLogLog().size("hl2");
System.out.println(count);