Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da9fadef67 | ||
|
|
33ea0a5a7e | ||
|
|
4386afc703 |
@@ -2,6 +2,9 @@
|
||||
|
||||
🧐 高并发点赞系统
|
||||
|
||||
> **🔗 参考资料**
|
||||
> - [B站千亿级点赞系统服务架构设计 - 哔哩哔哩技术团队](https://www.bilibili.com/opus/758312609901445367)
|
||||
|
||||
## 模块
|
||||
|
||||
### 用户模块
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package cn.hezhaohui.thumb.constant;
|
||||
|
||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||
import org.springframework.data.redis.core.script.RedisScript;
|
||||
|
||||
public class RedisLuaScriptConstant {
|
||||
|
||||
/**
|
||||
* 点赞 Lua 脚本
|
||||
* KEYS[1] -- 临时计数键
|
||||
* KEYS[2] -- 用户点赞状态键
|
||||
* ARGV[1] -- 用户 ID
|
||||
* ARGV[2] -- 博客 ID
|
||||
* 返回:
|
||||
* -1: 已点赞
|
||||
* 1: 操作成功
|
||||
*/
|
||||
public static final RedisScript<Long> THUMB_SCRIPT = new DefaultRedisScript<>("""
|
||||
local tempThumbKey = KEYS[1] -- 临时计数键(如 thumb:temp:{timeSlice})
|
||||
local userThumbKey = KEYS[2] -- 用户点赞状态键(如 thumb:{userId})
|
||||
local userId = ARGV[1] -- 用户 ID
|
||||
local blogId = ARGV[2] -- 博客 ID
|
||||
|
||||
-- 1. 检查是否已点赞(避免重复操作)
|
||||
if redis.call('HEXISTS', userThumbKey, blogId) == 1 then
|
||||
return -1 -- 已点赞,返回 -1 表示失败
|
||||
end
|
||||
|
||||
-- 2. 获取旧值(不存在则默认为 0)
|
||||
local hashKey = userId .. ':' .. blogId
|
||||
local oldNumber = tonumber(redis.call('HGET', tempThumbKey, hashKey) or 0)
|
||||
|
||||
-- 3. 计算新值
|
||||
local newNumber = oldNumber + 1
|
||||
|
||||
-- 4. 原子性更新:写入临时计数 + 标记用户已点赞
|
||||
redis.call('HSET', tempThumbKey, hashKey, newNumber)
|
||||
redis.call('HSET', userThumbKey, blogId, 1)
|
||||
|
||||
return 1 -- 返回 1 表示成功
|
||||
""", Long.class);
|
||||
|
||||
/**
|
||||
* 取消点赞 Lua 脚本
|
||||
* 参数同上
|
||||
* 返回:
|
||||
* -1: 未点赞
|
||||
* 1: 操作成功
|
||||
*/
|
||||
public static final RedisScript<Long> UNTHUMB_SCRIPT = new DefaultRedisScript<>("""
|
||||
local tempThumbKey = KEYS[1] -- 临时计数键(如 thumb:temp:{timeSlice})
|
||||
local userThumbKey = KEYS[2] -- 用户点赞状态键(如 thumb:{userId})
|
||||
local userId = ARGV[1] -- 用户 ID
|
||||
local blogId = ARGV[2] -- 博客 ID
|
||||
|
||||
-- 1. 检查用户是否已点赞(若未点赞,直接返回失败)
|
||||
if redis.call('HEXISTS', userThumbKey, blogId) ~= 1 then
|
||||
return -1 -- 未点赞,返回 -1 表示失败
|
||||
end
|
||||
|
||||
-- 2. 获取当前临时计数(若不存在则默认为 0)
|
||||
local hashKey = userId .. ':' .. blogId
|
||||
local oldNumber = tonumber(redis.call('HGET', tempThumbKey, hashKey) or 0)
|
||||
|
||||
-- 3. 计算新值并更新
|
||||
local newNumber = oldNumber - 1
|
||||
|
||||
-- 4. 原子性操作:更新临时计数 + 删除用户点赞标记
|
||||
redis.call('HSET', tempThumbKey, hashKey, newNumber)
|
||||
redis.call('HDEL', userThumbKey, blogId)
|
||||
|
||||
return 1 -- 返回 1 表示成功
|
||||
""", Long.class);
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package cn.hezhaohui.thumb.service.impl;
|
||||
|
||||
import cn.hezhaohui.thumb.constant.RedisLuaScriptConstant;
|
||||
import cn.hezhaohui.thumb.exception.BusinessException;
|
||||
import cn.hezhaohui.thumb.exception.ErrorCode;
|
||||
import cn.hezhaohui.thumb.mapper.ThumbMapper;
|
||||
import cn.hezhaohui.thumb.model.dto.DoThumbRequest;
|
||||
import cn.hezhaohui.thumb.model.entity.Blog;
|
||||
import cn.hezhaohui.thumb.model.entity.Thumb;
|
||||
import cn.hezhaohui.thumb.model.entity.User;
|
||||
import cn.hezhaohui.thumb.model.enums.LuaStatusEnum;
|
||||
import cn.hezhaohui.thumb.service.BlogService;
|
||||
import cn.hezhaohui.thumb.service.ThumbService;
|
||||
import cn.hezhaohui.thumb.service.UserService;
|
||||
import cn.hezhaohui.thumb.util.RedisKeyUtil;
|
||||
import cn.hutool.core.date.DateTime;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* @author 23117
|
||||
* @description 针对表【thumb】的数据库操作Service实现
|
||||
* @createDate 2025-10-18 14:51:00
|
||||
*/
|
||||
@Service
|
||||
public class ThumbServiceRedisImpl extends ServiceImpl<ThumbMapper, Thumb>
|
||||
implements ThumbService{
|
||||
|
||||
@Resource
|
||||
private UserService userService;
|
||||
|
||||
@Resource
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Override
|
||||
public Boolean doThumb(DoThumbRequest doThumbRequest, HttpServletRequest request) {
|
||||
if (doThumbRequest == null || doThumbRequest.getBlogId() == null) {
|
||||
throw new RuntimeException("参数异常");
|
||||
}
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
Long blogId = doThumbRequest.getBlogId();
|
||||
String timeSlice = this.getTimeSlice();
|
||||
// Redis Key
|
||||
String tempThumbKey = RedisKeyUtil.getTempThumbKey(timeSlice);
|
||||
String userThumbKey = RedisKeyUtil.getUserThumbKey(loginUser.getId());
|
||||
// 执行 Lua 脚本
|
||||
long result = redisTemplate.execute(
|
||||
RedisLuaScriptConstant.THUMB_SCRIPT,
|
||||
// local tempThumbKey = KEYS[1] -- 临时计数键(如 thumb:temp:{timeSlice})
|
||||
// local userThumbKey = KEYS[2] -- 用户点赞状态键(如 thumb:{userId})
|
||||
Arrays.asList(tempThumbKey, userThumbKey),
|
||||
// local userId = ARGV[1] -- 用户 ID
|
||||
loginUser.getId(),
|
||||
// local blogId = ARGV[2] -- 博客 ID
|
||||
blogId
|
||||
);
|
||||
// 成功 SUCCESS(1L),
|
||||
// 失败 FAIL(0L);
|
||||
if (LuaStatusEnum.FAIL.getValue() == result) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "用户已点赞");
|
||||
}
|
||||
|
||||
return LuaStatusEnum.SUCCESS.getValue() == result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean undoThumb(DoThumbRequest doThumbRequest, HttpServletRequest request) {
|
||||
if (doThumbRequest == null || doThumbRequest.getBlogId() == null) {
|
||||
throw new RuntimeException("参数异常");
|
||||
}
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
Long blogId = doThumbRequest.getBlogId();
|
||||
String timeSlice = this.getTimeSlice();
|
||||
// Redis Key
|
||||
String tempThumbKey = RedisKeyUtil.getTempThumbKey(timeSlice);
|
||||
String userThumbKey = RedisKeyUtil.getUserThumbKey(loginUser.getId());
|
||||
// 执行 Lua 脚本
|
||||
long result = redisTemplate.execute(
|
||||
RedisLuaScriptConstant.UNTHUMB_SCRIPT,
|
||||
// local tempThumbKey = KEYS[1] -- 临时计数键(如 thumb:temp:{timeSlice})
|
||||
// local userThumbKey = KEYS[2] -- 用户点赞状态键(如 thumb:{userId})
|
||||
Arrays.asList(tempThumbKey, userThumbKey),
|
||||
// local userId = ARGV[1] -- 用户 ID
|
||||
loginUser.getId(),
|
||||
// local blogId = ARGV[2] -- 博客 ID
|
||||
blogId
|
||||
);
|
||||
// 成功 SUCCESS(1L),
|
||||
// 失败 FAIL(0L);
|
||||
if (LuaStatusEnum.FAIL.getValue() == result) {
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "用户未点赞");
|
||||
}
|
||||
return LuaStatusEnum.SUCCESS.getValue() == result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean hasThumb(Long blogId, Long userId) {
|
||||
return redisTemplate.opsForHash().hasKey(RedisKeyUtil.getUserThumbKey(userId), blogId.toString());
|
||||
}
|
||||
|
||||
private String getTimeSlice() {
|
||||
DateTime nowDate = DateUtil.date();
|
||||
return DateUtil.format(nowDate, "HH:mm:" + (DateUtil.second(nowDate) / 10) * 10);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user