点赞和取消点赞业务的执行代码
// 执行点赞Lua 脚本
long result = redisTemplate.execute(
RedisLuaScriptConstant.THUMB_SCRIPT,
Arrays.asList(tempThumbKey, userThumbKey),
loginUser.getId(),
blogId
)
// 执行取消Lua 脚本
long result = redisTemplate.execute(
RedisLuaScriptConstant.UNTHUMB_SCRIPT,
Arrays.asList(tempThumbKey, userThumbKey),
loginUser.getId(),
blogId
)
原子性执行点赞的Lua脚本
/**
* 点赞 Lua 脚本
* KEYS[1]
* KEYS[2]
* KEYS[3]
* ARGV[1]
* ARGV[2]
* 返回:
* -1: 已点赞
* 1: 操作成功
*/
public static final RedisScript<Long> THUMB_SCRIPT = new DefaultRedisScript<>(""""
local tempThumbKey = KEYS[1]
local userThumbKey = KEYS[2]
local blogTotalThumbKey = KEYS[3]
local userId = ARGV[1]
local blogId = ARGV[2]
if redis.call('HEXISTS', userThumbKey, blogId) == 1 then
return -1
end
local hashKey = userId .. ':' .. blogId
local oldNumber = tonumber(redis.call('HGET', tempThumbKey, hashKey) or 0)
local newNumber = oldNumber + 1
redis.call('HSET', tempThumbKey, hashKey, newNumber)
redis.call('HSET', userThumbKey, blogId, 1)
redis.call('ZINCRBY', blogTotalThumbKey, 1, blogId)
return 1
""", Long.class);
原子性执行取消点赞的lua脚本
/**
* 取消点赞 Lua 脚本
* KEYS[1]
* KEYS[2]
* KEYS[3]
* ARGV[1]
* ARGV[2]
* 返回:
* -1: 未点赞
* 1: 操作成功
*/
public static final RedisScript<Long> UNTHUMB_SCRIPT = new DefaultRedisScript<>("""";
local tempThumbKey = KEYS[1]
local userThumbKey = KEYS[2]
local blogTotalThumbKey = KEYS[3]
local userId = ARGV[1]
local blogId = ARGV[2]
if redis.call('HEXISTS', userThumbKey, blogId) ~= 1 then
return -1
end
local hashKey = userId .. ':' .. blogId
local oldNumber = tonumber(redis.call('HGET', tempThumbKey, hashKey) or 0)
local newNumber = oldNumber - 1
redis.call('HSET', tempThumbKey, hashKey, newNumber)
redis.call('HDEL', userThumbKey, blogId)
local newTotal = redis.call('ZINCRBY', blogTotalThumbKey, -1, blogId)
return 1
""", Long.class);
增量同步点赞记录
@Scheduled(fixedRate = 5000)
@Transactional(rollbackFor = Exception.class)
public void run() {
// log.info("开始执行")
DateTime nowDate = DateUtil.date()
String date = DateUtil.format(nowDate, "HH:mm:") + (DateUtil.second(nowDate) / 10 - 1) * 10
syncThumb2DBByDate(date)
// log.info("临时数据同步完成")
}
public void syncThumb2DBByDate(String date) {
// 获取到临时点赞和取消点赞数据
// todo 如果数据量过大,可以分批读取数据
String tempThumbKey = RedisKeyUtil.getTempThumbKey(date)
Map<Object, Object> allTempThumbMap = redisTemplate.opsForHash().entries(tempThumbKey)
boolean thumbMapEmpty = CollUtil.isEmpty(allTempThumbMap)
// 同步 点赞 到数据库
// 构建插入列表并收集blogId
Map<Long, Long> blogThumbCountMap = new HashMap<>()
if (thumbMapEmpty) {
return
}
ArrayList<Thumb> thumbList = new ArrayList<>()
LambdaQueryWrapper<Thumb> wrapper = new LambdaQueryWrapper<>()
boolean needRemove = false
for (Object userIdBlogIdObj : allTempThumbMap.keySet()) {
String userIdBlogId = (String) userIdBlogIdObj
String[] userIdAndBlogId = userIdBlogId.split(StrPool.COLON)
Long userId = Long.valueOf(userIdAndBlogId[0])
Long blogId = Long.valueOf(userIdAndBlogId[1])
// -1 取消点赞,1 点赞
Integer thumbType = Integer.valueOf(allTempThumbMap.get(userIdBlogId).toString())
if (thumbType == ThumbTypeEnum.INCR.getValue()) {
Thumb thumb = new Thumb()
thumb.setUserId(userId)
thumb.setBlogId(blogId)
thumbList.add(thumb)
} else if (thumbType == ThumbTypeEnum.DECR.getValue()) {
// 拼接查询条件,批量删除
// todo 数据量过大,可以分批操作
needRemove = true
wrapper.or().eq(Thumb::getUserId, userId).eq(Thumb::getBlogId, blogId)
} else {
if (thumbType != ThumbTypeEnum.NON.getValue()) {
log.warn("数据异常:{}", userId + "," + blogId + "," + thumbType)
}
continue
}
// 计算点赞增量
blogThumbCountMap.put(blogId, blogThumbCountMap.getOrDefault(blogId, 0L) + thumbType)
}
// 批量插入
thumbService.saveBatch(thumbList)
// 批量删除
if (needRemove) {
thumbService.remove(wrapper)
}
// 批量更新博客点赞量
if (!blogThumbCountMap.isEmpty()) {
blogMapper.batchUpdateThumbCount(blogThumbCountMap)
}
redisTemplate.delete(tempThumbKey)
}
对账补偿
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private ThumbService thumbService;
private static final String EXCHANGE_THUMB_TOPIC = "thumb.topic";
private static final String ROUTING_KEY_RECONCILE_COMPENSATE = "thumb.reconcile.compensate";
@Resource
private RabbitTemplate rabbitTemplate;
@Scheduled(cron = "0 0 2 * * ?")
public void run() {
long startTime = System.currentTimeMillis();
Set<Long> userIds = new HashSet<>();
String pattern = ThumbConstant.USER_THUMB_KEY_PREFIX + "*";
try (Cursor<String> cursor = redisTemplate.scan(ScanOptions.scanOptions().match(pattern).count(1000).build())) {
while (cursor.hasNext()) {
String key = cursor.next();
Long userId = Long.valueOf(key.replace(ThumbConstant.USER_THUMB_KEY_PREFIX, ""));
userIds.add(userId);
}
}
userIds.forEach(userId -> {
Set<Long> redisBlogIds = redisTemplate.opsForHash().keys(ThumbConstant.USER_THUMB_KEY_PREFIX + userId).stream().map(obj -> Long.valueOf(obj.toString())).collect(Collectors.toSet());
Set<Long> mysqlBlogIds = Optional.ofNullable(thumbService.lambdaQuery()
.eq(Thumb::getUserId, userId)
.list()
).orElse(new ArrayList<>())
.stream()
.map(Thumb::getBlogId)
.collect(Collectors.toSet());
Set<Long> diffBlogIds = Sets.difference(redisBlogIds, mysqlBlogIds);
sendCompensationEvents(userId, diffBlogIds);
});
log.info("对账任务完成,耗时 {}ms", System.currentTimeMillis() - startTime);
}
private void sendCompensationEvents(Long userId, Set<Long> blogIds) {
if (blogIds == null || blogIds.isEmpty()) {
return;
}
try {
ThumbReconcileCompensateMessage msg = new ThumbReconcileCompensateMessage(
userId, new ArrayList<>(blogIds), LocalDateTime.now());
rabbitTemplate.convertAndSend(EXCHANGE_THUMB_TOPIC, ROUTING_KEY_RECONCILE_COMPENSATE, msg);
} catch (Exception ex) {
log.error("补偿消息发送失败: userId={}, blogIdSize={}", userId, blogIds.size(), ex);
}
}
消费者
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(
value = "q.thumb.reconcile.compensate",
durable = "true"
),
exchange = @Exchange(
value = "thumb.topic",
type = ExchangeTypes.TOPIC,
durable = "true"
),
key = "thumb.reconcile.compensate"
),
concurrency = "1-3"
)
@Transactional(rollbackFor = Exception.class)
public void onReconcileCompensate(ThumbReconcileCompensateMessage message) {
if (message == null || message.getUserId() == null
|| message.getBlogIds() == null || message.getBlogIds().isEmpty()) {
log.warn("忽略非法补偿消息: {}", message);
return;
}
Long userId = message.getUserId();
List<Long> blogIds = message.getBlogIds().stream()
.filter(Objects::nonNull)
.distinct()
.toList();
if (blogIds.isEmpty()) {
return;
}
log.info("收到对账补偿消息(仅增加): userId={}, blogCount={}, time={}",
userId, blogIds.size(), message.getEventTime());
List<Thumb> toSave = new ArrayList<>();
for (Long blogId : blogIds) {
if (existedBlogIds.contains(blogId)) {
continue;
}
Thumb thumb = new Thumb();
thumb.setUserId(userId);
thumb.setBlogId(blogId);
thumb.setCreateTime(new Date());
toSave.add(thumb);
}
thumbService.saveBatch(toSave);
Map<Long, Long> countDelta = new HashMap<>();
for (Thumb t : toSave) {
countDelta.merge(t.getBlogId(), 1L, Long::sum);
}
blogMapper.batchUpdateThumbCount(countDelta);
}