简介
前面不是写了一篇点赞功能的一种实现的文章吗
当时也提出了一些问题,今天就来解决其中的部分问题
开始
先讲一讲背景吧,以免没看过之前文章的迷惑
还是以点赞功能为话题,这里主要解决之前存在的大key
问题
大key问题
由于Redis
主线程为单线程模型,大key
也会带来一些问题,如:
1、集群模式在slot
分片均匀情况下,会出现数据和查询倾斜情况,部分有大key
的Redis
节点占用内存多,QPS
高。
2、大key
相关的删除或者自动过期时,会出现qps
突降或者突升的情况,极端情况下,会造成主从复制异常,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 |
|
流程图
通过定时任务(使用的JDK
自带的ScheduledExecutorService
)将redis
数据持久化到mysql
,后来发现问题,在使用ScheduledExecutorService
时,应该是由于在非Spring组件中注入Spring组件导致的空指针异常,所以最后改为使用SpringBoot的定时任务,使用起来很简单,之前定时任务文章也提到了。
点赞会产生非常多数据,做持久化时为了不生成那么多数据,利用了valid
字段
代码
下面是部分代码,可以参考一下
Redis
工具类,主要定义一些常量和key
拼装工具
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| public class RedisUtils {
public static final Integer DEFAULT_TTL = 300;
public static final Integer DEFAULT_TTL_MINUTES = 30;
public static final Integer DEFAULT_TTL_DAYS = 7;
public static final Integer KEY_MOLD = 1 << 8;
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(); } }
|
下面主要是点赞动作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| @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);
String recordState = (String) stringRedisTemplate.opsForHash().get(userLikeNewsKey, userLikeNewsField); if (!LIKE_STATE.equals(recordState)) { LOGGER.info("未点赞,点赞"); stringRedisTemplate.opsForHash().put(userLikeNewsKey, userLikeNewsField, LIKE_STATE); stringRedisTemplate.opsForSet().add(RedisKeyConstants.USER_LIKE_NEWS_KEY_SET, userLikeNewsKey); stringRedisTemplate.opsForValue().increment(newsLikeCountKey); 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); stringRedisTemplate.opsForSet().add(RedisKeyConstants.USER_LIKE_NEWS_KEY_SET, userLikeNewsKey); stringRedisTemplate.opsForValue().decrement(newsLikeCountKey); stringRedisTemplate.opsForSet().add(RedisKeyConstants.NEWS_LIKE_COUNT_KEY_SET, String.valueOf(newsId)); } }
@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
存在的问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| @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) { 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)) { if (haveRecord) { userLikeNews.setValid(true); userLikeNewsMapper.updateByPrimaryKeySelective(userLikeNews); } else { userLikeNewsMapper.insertSelective(userLikeRecord); } } else if (LikeConstants.UNLIKE.equals(state)) { if (haveRecord) { userLikeNews.setValid(false); userLikeNewsMapper.updateByPrimaryKeySelective(userLikeNews); } } stringRedisTemplate.opsForHash().delete(key, likeRecordField); } try { cursor.close(); } catch (IOException e) { LOGGER.error("cursor关闭失败"); e.printStackTrace(); } if (stringRedisTemplate.opsForHash().size(key) <= 0) { stringRedisTemplate.opsForSet().remove(RedisKeyConstants.USER_LIKE_NEWS_KEY_SET, key); } } } }
|
定时任务代码就不贴了,有很多实现方式
总结
许多时候,我们真的需要实践才能得到真理
一开始畅想的很美好,以为实现很简单,等到手去做了,才发现总会遇到一些问题,不是那么顺畅,实践啊~实践啊