简介
前面不是写了一篇点赞功能的一种实现的文章吗
当时也提出了一些问题,今天就来解决其中的部分问题
开始
先讲一讲背景吧,以免没看过之前文章的迷惑
还是以点赞功能为话题,这里主要解决之前存在的大key
问题
大key问题
由于Redis
主线程为单线程模型,大key
也会带来一些问题,如:
1、集群模式在slot
分片均匀情况下,会出现数据和查询倾斜情况,部分有大key
的Redis
节点占用内存多,QPS
高。
2、大key
相关的删除或者自动过期时,会出现qps
突降或者突升的情况,极端情况下,会造成主从复制异常,Redis
服务阻塞无法响应请求。
策略
基于之前的设计,这里进行改进
因为点赞属于经常性操作,为了避免频繁操作数据库,这里的策略是:
redis | |||
---|---|---|---|
string | key | value | |
news:like:count:%s | |||
新闻点赞数 string前缀:newsId | count | ||
修改数 | |||
hash | key | field | value |
user:like:news:%s | |||
用户点赞新闻 hash前缀:hashCode取模 | %s:%s | ||
userId:newsId | 0(未点赞)/1(点赞) | ||
set | key | value | |
user:like:news:set | |||
用户点赞操作hashKey集合 | user:like:news:%s | ||
hashCode取模 | |||
news:like:count:set | |||
新闻点赞数操作newsId集合 | %s | ||
newsId |
流程图
[图片上传失败...(image-e2375c-1648217752301)]
通过定时任务(使用的JDK
自带的ScheduledExecutorService
)将redis
数据持久化到mysql
,后来发现问题,在使用ScheduledExecutorService
时,应该是由于在非Spring组件中注入Spring组件导致的空指针异常,所以最后改为使用SpringBoot的定时任务,使用起来很简单,之前定时任务文章也提到了。
点赞会产生非常多数据,做持久化时为了不生成那么多数据,利用了valid
字段
代码
下面是部分代码,可以参考一下
Redis
工具类,主要定义一些常量和key
拼装工具
public class RedisUtils {
/**
* 默认key过期时间(s)
*/
public static final Integer DEFAULT_TTL = 300;
/**
* 默认key过期时间(minute)
*/
public static final Integer DEFAULT_TTL_MINUTES = 30;
/**
* 默认key过期时间(day)
*/
public static final Integer DEFAULT_TTL_DAYS = 7;
/**
* 模 256
*/
public static final Integer KEY_MOLD = 1 << 8;
/**
* scan count
*/
public static final Integer SCAN_COUNT = 3000;
public static <P, T> String getKey(P keyPrefix, T id) {
StringBuilder builder = new StringBuilder().append(keyPrefix).append(id);
return builder.toString();
}
public static String getUserLikeNewsKey(Long userId) {
StringBuilder builder = new StringBuilder()
.append(RedisKeyConstants.USER_LIKE_NEWS)
.append(Math.abs(userId.hashCode() & KEY_MOLD - 1));
return builder.toString();
}
public static String getUserLikeNewsField(Long userId, Long newsId) {
StringBuilder builder = new StringBuilder()
.append(userId)
.append(RedisKeyConstants.SPLITTER)
.append(newsId);
return builder.toString();
}
}
下面主要是点赞动作
@Service("userLikeService")
public class UserLikeServiceImpl implements UserLikeService {
public static final Logger LOGGER = LoggerFactory.getLogger(UserLikeServiceImpl.class);
private static final String LIKE_STATE = "1";
private static final String UNLIKE_STATE = "0";
@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
UserLikeNewsMapper userLikeNewsMapper;
@Override
public void like(Long userId, Long newsId) {
String userLikeNewsKey = RedisUtils.getUserLikeNewsKey(userId);
String userLikeNewsField = RedisUtils.getUserLikeNewsField(userId, newsId);
String newsLikeCountKey = RedisUtils.getKey(RedisKeyConstants.NEWS_LIKE_COUNT, newsId);
// 大key问题
String recordState = (String) stringRedisTemplate.opsForHash().get(userLikeNewsKey, userLikeNewsField);
if (!LIKE_STATE.equals(recordState)) {
// 未点赞,点赞
LOGGER.info("未点赞,点赞");
stringRedisTemplate.opsForHash().put(userLikeNewsKey, userLikeNewsField, LIKE_STATE);
// 操作key记录
stringRedisTemplate.opsForSet().add(RedisKeyConstants.USER_LIKE_NEWS_KEY_SET, userLikeNewsKey);
// 新闻点赞数+1
stringRedisTemplate.opsForValue().increment(newsLikeCountKey);
// 操作key记录
stringRedisTemplate.opsForSet().add(RedisKeyConstants.NEWS_LIKE_COUNT_KEY_SET, String.valueOf(newsId));
}
}
@Override
public void unlike(Long userId, Long newsId) {
String userLikeNewsKey = RedisUtils.getUserLikeNewsKey(userId);
String userLikeNewsField = RedisUtils.getUserLikeNewsField(userId, newsId);
String newsLikeCountKey = RedisUtils.getKey(RedisKeyConstants.NEWS_LIKE_COUNT, newsId);
String recordState = (String) stringRedisTemplate.opsForHash().get(userLikeNewsKey, userLikeNewsField);
if (!UNLIKE_STATE.equals(recordState)) {
// 已点赞,取消点赞
LOGGER.info("已点赞,取消点赞");
stringRedisTemplate.opsForHash().put(userLikeNewsKey, userLikeNewsField, UNLIKE_STATE);
// 操作key记录
stringRedisTemplate.opsForSet().add(RedisKeyConstants.USER_LIKE_NEWS_KEY_SET, userLikeNewsKey);
// 新闻点赞数-1
stringRedisTemplate.opsForValue().decrement(newsLikeCountKey);
// 操作key记录
stringRedisTemplate.opsForSet().add(RedisKeyConstants.NEWS_LIKE_COUNT_KEY_SET, String.valueOf(newsId));
}
}
/**
* 检查用户是否点赞新闻
* 暂未调用
*
* @param userId
* @param newsId
* @return
*/
@Override
public boolean liked(Long userId, Long newsId) {
String userLikeNewsKey = RedisUtils.getUserLikeNewsKey(userId);
String userLikeNewsField = RedisUtils.getUserLikeNewsField(userId, newsId);
String recordState = (String) stringRedisTemplate.opsForHash().get(userLikeNewsKey, userLikeNewsField);
if (Objects.nonNull(recordState)) {
return LIKE_STATE.equals(recordState);
} else {
UserLikeNews userLikeNews = userLikeNewsMapper.selectValidByUserIdAndNewsId(userId, newsId);
return Objects.nonNull(userLikeNews);
}
}
}
scan
最后关于持久化,可以先看下面文章
https://aijishu.com/a/1060000000007477
这篇讲的是利用scan
代替keys
然后再看
https://cloud.tencent.com/developer/article/1650002
https://redis.io/commands/scan
这里讲的scan
存在的问题
@Override
@Transactional(rollbackFor = Exception.class)
@Scheduled(initialDelay = 60 * 1000, fixedDelay = 5 * 60 * 1000)
public void persistUserLikeNews() {
// 定时任务持久化
Set<String> keys = stringRedisTemplate.opsForSet().members(RedisKeyConstants.USER_LIKE_NEWS_KEY_SET);
if (Objects.nonNull(keys)) {
for (String key : keys) {
// TODO cursor 问题
Cursor<Map.Entry<Object, Object>> cursor = stringRedisTemplate.opsForHash().scan(key
, ScanOptions.scanOptions().match("*").count(RedisUtils.SCAN_COUNT).build());
while (cursor.hasNext()) {
Map.Entry<Object, Object> entry = cursor.next();
String likeRecordField = (String) entry.getKey();
UserLikeNews userLikeRecord = getUserLikeNews(likeRecordField);
// ???检查用户和新闻还在不
UserLikeNews userLikeNews = userLikeNewsMapper.selectByUserIdAndNewsId(userLikeRecord.getUserId(), userLikeRecord.getNewsId());
boolean haveRecord = Objects.nonNull(userLikeNews);
String state = (String) entry.getValue();
// 点赞状态
if (LikeConstants.LIKE.equals(state)) {
// 有记录 valid true
if (haveRecord) {
userLikeNews.setValid(true);
userLikeNewsMapper.updateByPrimaryKeySelective(userLikeNews);
} else {
// 无记录 插入
userLikeNewsMapper.insertSelective(userLikeRecord);
}
} else if (LikeConstants.UNLIKE.equals(state)) {
// 取消点赞状态
if (haveRecord) {
// 有记录 valid false
userLikeNews.setValid(false);
userLikeNewsMapper.updateByPrimaryKeySelective(userLikeNews);
}
}
// 删除已持久化的field,问题若出现异常,mysql可以依据事务回滚,但redis不会
stringRedisTemplate.opsForHash().delete(key, likeRecordField);
}
try {
cursor.close();
} catch (IOException e) {
LOGGER.error("cursor关闭失败");
e.printStackTrace();
}
// 判断hashKey中是否还有元素未持久化
if (stringRedisTemplate.opsForHash().size(key) <= 0) {
// 从set中删除
stringRedisTemplate.opsForSet().remove(RedisKeyConstants.USER_LIKE_NEWS_KEY_SET, key);
}
}
}
}
定时任务代码就不贴了,有很多实现方式
总结
许多时候,我们真的需要实践才能得到真理
一开始畅想的很美好,以为实现很简单,等到手去做了,才发现总会遇到一些问题,不是那么顺畅,实践啊~实践啊