使用“万金油”string,数据量大,占用内存大
刚启动redis客户端,通过info memory 命令查看内存开销,仅为728416,711.34kb
做了测试案例,查询出100万条数据,将id与内容通过string,set到内存中,占用内存为:105036784,100Mb,100w条数据增加了100Mb。
如果有几亿条数据呢?那内存占用量是相当恐怖的。
这时因为,string的数据结构,包含了两块,一块是内容,一块是元数据。
所以通过string将会保存许多我们用不到的数据。若有100M,大约有几十兆都是元数据信息。
那我们可以通过存储hash来进行优化。
hash的数据结构是压缩列表与hash表。
要注意的是,hash在压缩列表中保存了两个阈值,一旦超过了阈值,hash就会使用哈希表来保存数据了。
两个阈值分别为:
hash-max-ziplist-entries:表示用压缩列表保存时哈希集合中的最大元素个数。hash-max-ziplist-value:表示用压缩列表保存时哈希集合中单个元素的最大长度。
设置方式示例:
127.0.0.1:6379> config set hash-max-ziplist-entries 1000
OK
127.0.0.1:6379> config set hash-max-ziplist-entries 10
OK
若压缩列表中有单个值大于hash-max-ziplist-value或者每个键的所有值的集合的长度大于hash-max-ziplist-entries,hash就会转为哈希表存储,之后存储都不会再转成压缩列表了。在节省内存空间上,哈希表时不如压缩列表的。
使用hash方式存储,内存节约了20M左右。
由于没有好的数据,没有将内存节省得更多。
Java代码如下:
使用的是Jpa加载Mysql数据库,Spring Boot管理配置。
package com.redisroot.redisdemo.service;
import com.redisroot.redisdemo.entity.ProductComment;
import com.redisroot.redisdemo.repository.ProductCommentRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.List;
@Service
@Slf4j
public class RedisService {
@Autowired
private ProductCommentRepository productCommentRepository;
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void findDataLoadInRedisByString(){
ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
HashOperations<String, Object, Object> hashOperations = stringRedisTemplate.opsForHash();
log.info("开始加载所有数据");
List<ProductComment> comments = productCommentRepository.findAll();//O(n)时间复杂度
log.info("开始循环数据加载进redis");
long time = System.currentTimeMillis();
comments.forEach(productComment->{//O(n^m)时间复杂度
String id = productComment.getId();
String commentText = productComment.getCommentText();
//判断id合并数据使用hash列表,节省内存资源
//比如pc10000111
//会拆分成pc10000,111
//存储为pc10000,111,commentText
//就说明每个key底下都只有最多10000条的数据
//因为条数多,所以hash的底层数据结构就由压缩列表转成了哈希表
//提升效率,但是节省内存上,不如压缩列表
String ids = "pc";
String suffix = "";
for(int i=2;i<id.length();i++){
char chars = id.charAt(i);
String charStr = String.valueOf(chars);
if(!"0".equals(charStr)&&!"1".equals(charStr)){
suffix = id.substring(i);
break;
}else{
ids += chars;
}
}
if(StringUtils.isEmpty(suffix)){
suffix = "None";
}
hashOperations.put(ids,suffix,commentText);
//valueOperations.set(key,value);
});
log.info("redis数据加载完成,耗时:"+(System.currentTimeMillis()-time)/(1000*60));
}
}
若数据量大,要如何统计keys?
在不同的业务场景我们可能需要保存一种信息:一个key对应一个集合。
- 某APP统计每天的访问用户ID
- 某个商品对应N条评论
- 某APP统计用户每天打卡信息
- 某网站统计用户的唯一访问
以上这种场景都有可能有很大的数据量的存储与统计。面对这种场景,我们就要选择合适的集合,我们就要了解常用的集合统计模式,包括了:聚合统计、排序统计、二值状态统计、基数统计。
聚合统计指的是多个集合元素的聚合统计。
返回多个集合的共有集合(交集),返回多个集合的全部集合(并集),返回多个集合的独有元素(差集)。
交集:统计多个集合的共有集合,可以使用以下命令
SINTERSTORE DESTINATION KEY1 KEY2 KEY3
并集:统计多个集合的全部集合,可以使用以下命令
SUNIONSTORE DESTINATION KEY1 KEY2 KEY3
差集:统计多个集合的独有集合,可以使用以下命令
SDIFFSTORE DESTINATION KEY1 KEY2 KEY3
以上的意思就是统计KEY1 KEY2 KEY3三个集合,并将三个集合的统计返回的集合放在DESTINATION中并返回,多DESTINATION存在则覆盖。
注意:这三个命令都是SET的使用方法
排序统计
商品评论表,可能会需要统计最新的评论。
在redis中,List是进行顺序排序统计,Sorted set是进行权重排序统计。
list是链表的数据结构,最新的数据始终是在链表头,最旧的数据始终都在链表尾。
也就是说,若我们在list存0-9
然后我们分3页取出
LRANGE key 0 2
返回的结果就是0 1 2
再第二次分页的时候,此时有新的数据A进来,此时list的数据为A 0 1 2 3 … 9
LRANGE key 3 5
那么此时查询出来的是2 3 4,就会发现之前已经查询的2又被查询出来了。
造成这个问题的原因就是因为list是通过位置来排序的,若有新数据进入,那么所有元素都会往后移动一位。
sorted set就不会存在这个问题。
ZADD key score member [score member]
我们可以将评论时间作为权重score,并按score排序。
ZRANGEBYSCORE KEY MIN MAX [WITHSCORES] [LIMIT OFFSET COUNT]
若你不知道集合的最大值与最小值,那么MIN和MAX就可以使用-INF、+INF。就可以获取到集合中所有的数据,而如加上WITHSCORES,就会在member之后显示score值。LIMIT是将返回的集合从offset开始,取count值。
若知道score的值,那么可以使用闭区间、开区间 来查询。
1 < score < 2
(1 ( 2
1< score <=2
(1 2
1<= score <2
1 (2
二值状态统计
这种统计方式常用的是,比如:用户签到(1|0),用户是否在线(1|0)等
我们只用1表示存在,0表示不存在。
每个用户每天签到使用一个bit位,一年签到也就使用了365个bit。
我们可以使用:
SETBIT key offset 1
比如用户1111在2020年10月11日签到,可以写为
SETBIT userid:1111:20201011 10 1
offset可以记为签到的时间的天,这样就可以比较准确的记录每个月的签到天数了
由于offset偏移量是从0开始的,所以11天就为10。
若想取得某个人某一天是否有签到记录就可以使用
GETBIT userid:1111:20201010 9
这就会取得userid为1111在10月10号的签到记录,若有记录则显示记录,若没有记录则显示0
若想要知道最近10天内连续签到的人员的话,可以使用BITOP,具体写法如下:
BITOP operaters result key key …
operaters可以为AND|OR|XOR|,意思是:逻辑与、逻辑或、逻辑异或
result 就是 将多个bit集合进行逻辑运算得出的结果通过result返回
使用”逻辑与“,逻辑与的意思就是若有两个集合对比
第一个集合为1 0 0 1
第二个集合为1 0 1 1
若不相等,就为0,若都为0也为0,相等就为1。
那么得出来的结果就为 1 0 0 1
例如:
setbit bits-1 0 1
setbit bits-1 3 1
setbit bits-2 0 1
setbit bits-2 2 1
setbit bits-2 3 1
按上面命令,可以得知bits-1为 1 0 0 1,因为第0位为1,第1位没有数据所以为0,第2位没有数据所以为0,第3位为1。bits-2位1 0 1 1
bitop AND result bits-1 bits-2
通过bitop逻辑与,可以得到result结果为1 0 0 1,那么使用bitcount result就能得到结果(2),这个结果就是10天的连续签到的人数。
基数统计
需要统计网站一天访问的人员的基数(不重复数)UV的业务场景。
以看到不重复数,我们就想到可以使用Set集合,Set集合默认支持去重。
我们只需要使用
SADD key member [member]…
无论member是否存在N个重复,在set中都只有一个数据。
但是若有非常大数据量的场景,需要统计千万级别的人数,大型电商网站,可能有千百个网页,每个网页都要有set支持,那么内存消耗是非常大的。
当然我们也可以使用hash
HADD page:uv userid 1
这样就算有用户重复访问了,也只是将userid的值赋值为1。但是也存在以上set存在的问题。
那么若要又快,有节省内存
就可以使用redis 2.8.9后新增的HyperLogLog一种统计基数的数据集合类型
每个HyperLogLog都之占用为12kb的内存,支持264个元素基数。
若有一亿个基数,那么为108/8/1024/1024,大概为12M,就算统计10天的数据,也只需120M,占用内存不大。
往集合内添加数据
PFADD key element [element]…
统计集合的个数
PFCOUNT key key[…]
也可以将多个HyperLogLog合并为一个HyperLogLog
PFMERGE targetkey sourcekey [sourcekey]
例如:
127.0.0.1:6379> PFADD KEY username1 username2 usernam1 username2 username3 username2 username3 username1 usernam4
将重复的数据,不重复的数据,加入多条,然后使用PFCOUNT统计
127.0.0.1:6379> PFCOUNT KEY
(integer) 5
显示为5条,可以去重。
因为HyperLogLog是基于概率统计的,所以会有一定的误差,标准误差大概为(0.81%)。若不允许有误差的话,还是使用set、hash吧。
在不同的场景,使用不同的数据集合,可能会更好、更快的提升效率。
空间效率、时间效率都可以得到提升。