需求背景
近日项目的后台系统中需要新增公告通知功能.
需求分析
公告条数不会很多, 用户数量级在十万左右, 是典型的一对多的场景, 且有明显的布尔特征, 即 已读/未读.
方案设计
可以考虑使用 Redis 存储关联关系并使用 bitmap 来进行用户是否阅读公告的记录.
快速入门Redis中bitmap的使用
这里摘录一些本次需要注意的:
Bitmap 不属于 Redis 的基本数据类型, 而是基于 String 类型进行的位操作.
Redis 中字符串的最大长度是 512M, 所以 bitmap 的 offset 值也是有上限的, 其最大值是 8 * 1024 * 1024 * 512 = 2^32
由于不同端(运营, 用户, 顾客)的用户数据可能在相同的表, 也可能在不同的表, 且公告需要支持只通知到某一些端, 故 Redis 的key设计如下
1
|
notice:通知端数字枚举:数据库中的消息id
|
新增或修改公告
根据公告设置的不同端, 调用 setBit 方法进行设置.
考虑后面查询脚本的编写方便及性能, 可以在公告的不同端中设置固定用户为已读
1
2
3
4
5
6
7
|
public static final String notice_key = "notice:%d:%d";
// platform: 通知端数字枚举
// msgId: 数据库中的消息id
// userId: 不同用户表(运营, 用户, 顾客)的主键id
// isRead: 是否阅读
redisTemplate.opsForValue().setBit(String.format(notice_key, platform, msgId), userId, isRead);
|
删除公告
删除公告直接组装 该公告需要删除的端的key 成List, 然后调用delete的重载方法进行删除
1
2
|
redisTemplate.delete(K key)
redisTemplate.delete(Collection<K> keys)
|
设置已读/未读
直接组装key, 设置 isRead 即可
1
|
redisTemplate.opsForValue().setBit(String.format(notice_key, productType, msgId), userId, isRead);
|
查询用户是否已读
用户查看公告列表(分页)时, 直接简单的循环查询公告是否已读
1
|
redisTemplate.opsForValue().getBit(String.format(notice_key, productType, msgId), userId);
|
未读/已读数量统计
需要使用 lua 脚本循环所有消息来进行统计.
用户登录后, 根据用户所在端可以拼出该端所有的消息的key:
1
|
"notice:" + productType + ":*"
|
然后调用如下 lua 脚本, 即可统计出未读和已读数量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
private final String READ_COUNT_SCRIPT =
// 第一步: 拿到所有的消息key
"local noticeKeys = redis.call('keys', KEYS[1]);" +
"local unread = 0;" +
"local read = 0;" +
// 第二步: 如果key不存在则直接返回
"if next(noticeKeys) == nil then return unread .. ':' .. read;end;" +
// 第三步: 拿到所有消息key的值
"local values = redis.call('mget', unpack(noticeKeys));" +
// 第四步: 循环消息key, 使用getbit命令判断是否已读, 分别对未读数量和已读数量加一
"for i = 1, #noticeKeys do " +
" if(redis.call('getbit', noticeKeys[i], ARGV[1]) == 0) " +
" then unread = unread + 1;" +
" else read = read + 1;" +
" end;" +
"end;" +
// 第五步: 返回未读数量和已读数量(lua中使用 .. 来连接字符串)
"return unread .. ':' .. read;";
List<String> keys = new ArrayList<>(1);
keys.add("notice:" + productType + ":*");
String unreadAndRead = redisTemplate.execute(new DefaultRedisScript<>(READ_COUNT_SCRIPT, String.class),
new StringRedisSerializer(), new StringRedisSerializer(), keys, String.valueOf(userId));
|
后续踩坑记
原来的 lua 脚本第一行是
1
|
local noticeKeys = redis.call('keys', 'notice:' .. KEYS[1] .. ':*');
|
导致该功能上线后, 立即爆出异常
1
|
-ERR bad lua script for redis cluster, all the keys that the script uses should be passed using the KEYS arrayrn
|
搜索相关报错后发现是阿里云的redis产品限制, 所有key都应该由 KEYS 数组来传递.