前言
最近在做一个微信小程序的项目,然后这个项目有一个论坛功能,前端哥说希望做到点赞超过一定数量之后,会自动取消超过500后的最早那一批的点赞记录,她说这个思路的实现来自于B站,希望我们也能做一下,所以作为负责优化项目的我,就开始着手思考这个内容。
其实基本可以马上就想到就是数据库的操作,但是想了一想,效率肯定很低,因为点赞这种操作很频繁,虽然说可以让点赞的请求累积到一定数量然后通过MQ异步更新数据库,但是效率依旧有点问题,毕竟磁盘操作效率就是更加的低。
所以我就考虑能不能使用内存上的操作,也就是缓存,比如Redis。
然后我就想到了Bitmap这个数据结构。
简单的介绍一下,bitmap其实就是由一个又一个的bit位组成的,也就是0/1,而刚刚好点赞和没点赞就是0/1就能表示。同时,bitmap的基本单位(额,我是怎么理解的)就是一个又一个的byte,由8个bit组成,那么其实这就非常节省空间了,因此如果使用bitmap,那么在时间和空间上,都有相对于使用数据库更好的效率。
bitmap的简单介绍
思路
那么,上面简单的带过了一下基本思路后,现在来聊一聊到底如何实现比较合理。
bitmap有包含,key,offset,value。
其中key就是找到唯一的bitmap的方式,offset就是某一个索引位,value就是0/1。
同时,由于还得做到删除掉最早期的数据,因此我还得做一个能存储用户给那些文章点赞的时间集合,然后如果超过了设定的点赞上限的大小,就把最早的集合中的数据删掉。
在项目中我是用的是一个ArrayList来作为用户点赞的时间排序,因为其实我们并不需要真的去记录用户是什么时候点赞的,只要知道它最早点赞的那一批数据是那些即可了。
所以,我要做的就是,编写操作bitmap的接口,然后对某个offset上的数据置0/1,offset对应的就是被点赞的文章的id,而key就是用户id,value就是用户是否点赞。
然后我还做了一个存储list的集合,这个list保存着用户点赞的那一批文章的id以及顺序。
同时,我还得做到,当用户初始化论坛的时候,必须直接加载出来所有的,他点赞过的文章。
也就是我需要遍历bitmap,并且传递回来所有的位为1的offset。
第一种思路,就是遍历每一个位,然后加一点优化,也就是使用bitcount方法判断当前段上是否有为1的位,而如果没有,那么我们就不再需要判断这个位了,直接跳过这个段即可。大概代码如下
不过,我上面已经设定过了一个list,那么我直接返回这个list给前端即可。
如下
Redis服务包
因为是SpringBoot项目,那么其实直接对RedisTemplagte进行封装即可。
@Component
public class RedisService
{
@Autowired
public RedisTemplate redisTemplate;
/**
* 使用bitmap并且设定某一个位值
* @param key bitmap缓存的键值
* @param offset bitmap对应的索引位
* @param value bitmap对应的值 0/1
* @return 是否设置成功
*/
public boolean setBit(final String key,final Long offset,final Boolean value){
return redisTemplate.opsForValue().setBit(key,offset,value);
}
/**
* 获取bitmap某一个位上的值
* @param key bitmap缓存的键值
* @param offset bitmap对应的索引位
* @return 该位键值是0/1
*/
public boolean getBit(final String key,final Long offset){
return redisTemplate.opsForValue().getBit(key,offset);
}
/**
* 返回bitmap的长度
* @param key bitmap缓存的键值
* @return 返回bitmap的长度
*/
public long bitmapSize(String key){
return redisTemplate.opsForValue().size(key);
}
/**
* 获取某个bitmap上的1的个数
* @param key bitmap缓存的键值
* @return 返回1的个数
*/
public Long bitCount(final String key){
//Long start = 0L; // 起始位置
//Long end = -1L; // 结束位置,-1表示计算整个bitmap的长度
Long count = (Long) redisTemplate.execute((RedisCallback<Long>) connection ->
connection.bitCount(key.getBytes()));
return count;
}
/**
* 获取某个bitmap上某一段上的1的个数
* @param key bitmap缓存的键值
* @return 返回1的个数
*/
public Long bitCountRange(final String key,final Long start,final Long end){
Long length = (Long) redisTemplate.execute((RedisCallback<Long>) con ->
con.bitCount(key.getBytes(),start,end));
return length;
}
/**
* 是哦那个bitfield获取连续为1的天数
* @param buildSignKey bitmap缓存的键值
* @param limit
* @param offset
* @return
*/
@Deprecated
public List<Long> bitField(final String buildSignKey,final Integer limit,final Long offset){
return (List<Long>) redisTemplate.execute((RedisCallback<List<Long>>)con->
con.bitField(buildSignKey.getBytes(),
BitFieldSubCommands.create()
.get(BitFieldSubCommands.BitFieldType
.unsigned(limit)).valueAt(offset)));
}
/**
* 缓存List数据
*
* @param key 缓存的键值
* @param dataList 待缓存的List数据
* @return 缓存的对象
*/
public <T> long setCacheList(final String key, final List<T> dataList)
{
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
return count == null ? 0 : count;
}
/**
* 获得缓存的list对象
*
* @param key 缓存的键值
* @return 缓存键值对应的数据
*/
public <T> List<T> getCacheList(final String key)
{
return redisTemplate.opsForList().range(key, 0, -1);
}
}
代码实现
按照上面的思路,假设我们对userId为22的用户,点赞articleId为1000的文章,那么就有如下代码
@Test
public void bitmapCode(){
//15.4k byte 1024k = 1m
long userId = 22;
long articleId = 1000;
//设定喜欢的文章 设定状态为相反
boolean bit = redisService.getBit(RedisServiceConstants.USER_LIKE_ARTICLE + userId,
Long.valueOf(articleId));
redisService.setBit(RedisServiceConstants.USER_LIKE_ARTICLE + userId
,Long.valueOf(articleId),!bit);
List<Long> likeList = redisService.getCacheList(
RedisServiceConstants.USER_LIKE_TIME + userId);
if (bit){
likeList.remove(articleId);
}else{
likeList.add(articleId);
}
//去重后放入list
ArrayList<Long> likeList1 = new ArrayList<>(new HashSet<Long>(likeList));
redisService.deleteObject(RedisServiceConstants.USER_LIKE_TIME+userId);
redisService.setCacheList(RedisServiceConstants.USER_LIKE_TIME+userId,
likeList1);
}
发送点赞请求之后,就会出现如下的情况。
而再一次点击之后,就是删除请求了。
可以发现time的那个键就已经被删除了,也就是取消点赞之后,就会把对应的文章的id删除,而这个用户的点赞记录的bitmap还是存在,不过本来为1的位已经变为了0