Redis基础和应用
发布日期:2021-06-28 15:40:05 浏览次数:2 分类:技术文章

本文共 11782 字,大约阅读时间需要 39 分钟。

学习5种基本数据结构的指令和java api。以及分布式锁,位图,HyperLogLog,布隆过滤器(BloomFliter)等多种高级数据结构的指令和java api,以及具体应用场景。

数据结构

Redis五种数据结构,分别为string,list,hash,set,zset。

string

key,value数据结构。

存储结构类似java的数组。

指令

  • set key value
  • get key
  • exists key
  • del key
  • mset key1 value1 key2 value2 …
  • mget key1 key2 …
  • expire key expire_time_sec :设置过期时间
  • setex key expire_time_sec value : 创建时设置过期时间
  • setnx key value : 不存在key则创建,set会直接覆盖

测试代码

private static void testString() throws InterruptedException {
String key = "string"; // 1. 先删除指定的key jedis.del(key); // 2. 添加字符串,过期时间5s,存在则不添加 jedis.set(key, "value", new SetParams().ex(5).nx()); // 3. 查询符合模式的所有key // h?llo will match hello hallo hhllo // h*llo will match hllo heeeello // h[ae]llo will match hello and hallo, but not hillo String pattern = "str*"; System.out.println("keys, PATTERN : {" + pattern + "}, RETURN : {" + jedis.keys(pattern) + "}"); // 4. 查询过期时间 System.out.println("KEY : {" + key + "}, expire time : {" + jedis.ttl(key) + "}s"); // 5. 移除过期时间 jedis.persist(key); System.out.println("persist KEY , expire time : {" + jedis.ttl(key) + "}s"); // 6. 随机访问 String randomKey = jedis.randomKey(); // 7. 查看类型 System.out.println("RANDOM KEY : {" + randomKey + "},TYPE : {" + jedis.type(randomKey) + "}");}打印如下:keys, PATTERN : {
str*}, RETURN : {
[string]}KEY : {
string}, expire time : {
5}spersist KEY , expire time : {
-1}sRANDOM KEY : {
hash},TYPE : {
hash}

list

key, value1,value2数据结构,类似java的list。

两种存储结构:当数据量小时,使用压缩列表(ziplist);数量大大时,使用快速列表(quicklist)

struct ziplist
{ int32 zlbytes; // 整个压缩列表占用字节数 int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点 int16 zllength; // 元素个数 T[] entries; // 元素内容列表,依次紧凑存储 int8 zlend; // 标志压缩列表的结束}struct entry{ int
prevlen; // 前一个entry的字节长度 int encoding; // 元素内容编码 optional byte[] content; // 元素内容}struct quicklist{ quicklistNode* head; quicklistNode* tail; long count; // 元素总和 int nodes; // ziplist节点的个数 int compressDepth; // LZF 算法压缩度}struct quicklistNode{ quicklistNode* prev; quicklistNode* next; ziplist* zl; // 指向压缩列表 int32 size; // ziplist 的字节总数 int16 count; // ziplist 中的元素数量 int2 encoding; // 存储形式2bit}

quicklist 内部默认单个ziplist长度为8kb,超过这个字节会另起一个ziplist。可以通过参数list-max-ziplist-size决定。

指令

  • push : lpush / rpush

rpush key value1 value2 value3

  • pop : lpop / rpop

lpop key

  • lindex
  • ltrim
  • lrange

程序

private static void testList() {
// 1. 先删除指定key jedis.del("list"); // 2. 左添加 jedis.lpush("list", "listValue3", "listValue2", "listValue1"); // 右添加 jedis.rpush("list", "listValue4", "listValue5", "listValue6"); // 3. 取出所有元素 System.out.println(jedis.lrange("list", 0, -1)); // 4. 取出最后一个元素 System.out.println(jedis.lindex("list", -1)); // 5. 移除部分数据,保留1-3 jedis.ltrim("list", 1, 3); System.out.println(jedis.lrange("list", 0, -1));}打印如下:[listValue1, listValue2, listValue3, listValue4, listValue5, listValue6]listValue6[listValue2, listValue3, listValue4]

hash

key,k1,value1,k2,value2,类似java中的Map。

存储结构类似java的HashMap,使用数组加链表的方式来存储。rehash采用循环渐进的处理,两组hashtable,将旧的值一步一步的迁移到新的(hset、hdel操作),如果客户端没有操作,将会通过定时器,定时执行

指令

  • hset key k1 v1
  • hgetAll
  • hlen
  • hmget
  • hmset

程序

private static void testHash() {
String key = "hash"; // 1. 先删除指定key jedis.del(key); Map
map = new HashMap<>(); map.put("k1", "v1"); map.put("k2", "v2"); // 2. 添加数据结构 jedis.hset(key, map); // 3. 追加数据 jedis.hset(key, "k3", "v3"); // 4. 获取数据和长度 System.out.println("获取所有数据:" + jedis.hgetAll(key) + ",长度:" + jedis.hlen(key)); // 5. 批量获取 System.out.println("批量获取数据:" + jedis.hmget(key, "k1", "k3")); // 6. 计数 jedis.hset(key, "k4", 1 + ""); jedis.hincrBy(key, "k4", 1); System.out.println("获取数据:" + jedis.hget(key, "k4"));}打印日志获取所有数据:{
k3=v3, k1=v1, k2=v2},长度:3批量获取数据:[v1, v3]获取数据:2

set

key,value1,value2,类似java的Set,value不能重复。

特殊的hash,值为NULL,无序

指令

  • sadd key value1 value2
  • smembers key(无序)
  • sismember key value : 查询key是否存在value,存在则返回1,否则返回0
  • scard
  • spop

程序

private static void testSet() {
String key = "set"; // 1. 先删除指定key jedis.del(key); // 2. 添加数据结构 jedis.sadd(key, "value1", "value2"); // 3. 获取数据和长度 System.out.println("获取所有数据:" + jedis.smembers(key) + ",长度:" + jedis.scard(key)); // 4. 是否存在指定value System.out.println("是否存在数据:" + jedis.sismember(key, "value2")); // 5. 弹出数据 System.out.println("弹出数据:" + jedis.spop(key)); // 6. 获取数据 System.out.println("获取数据:" + jedis.smembers(key));}

zset

有序Set

存储结构为:跳跃列表

指令

  • zadd
  • zrange
  • zrevrange : 按score逆序列出
  • zcard
  • zscore : 获取指定key的分数
  • zrank : 获取指定key的排名
  • zrangebyscore : 遍历分值区域
  • zrem : 删除value

程序

private static void testZSet() {
String key = "zset"; // 1. 先删除指定key jedis.del(key); // 2. 添加数据结构 jedis.zadd(key, 100, "value1"); jedis.zadd(key, 50, "value2"); jedis.zadd(key, 0, "value3"); // 3. 获取数据 System.out.println("获取数据:" + jedis.zrange(key, 0, 100)); // 4. 是否存在指定value System.out.println("获取数据:" + jedis.zrevrange(key, 0, 100)); // 5. 数据量 System.out.println("数据量:" + jedis.zcard(key)); // 6. 获取数据 System.out.println("获取数据:" + jedis.zscore(key, "value2")); // 7. 获取数据 System.out.println("删除数据:" + jedis.zrem(key, "value2")); System.out.println("获取数据:" + jedis.zrange(key, 0, 100));}打印如下:获取数据:[value3, value2, value1]获取数据:[value1, value2, value3]数据量:3获取数据:50.0删除数据:1获取数据:[value3, value1]

应用

分布式锁(重要)

set key value ex expire_time_sec nx (原子操作)

设置超时时间(ex),以及存在则不添加(nx)。可以设置一个随机值作为value,在删除锁时候,判断value是否一致。

超时问题

如果函数在超时时间内无法结束,则会出现问题。解决方案:redisson插件,原理如下:

加锁时,会有一个监听线程,当持锁线程还在执行则会追加超时时间。

private static RedissonClient redissonClient;public static void main(String[] args) {
Config config = new Config(); config.useSingleServer().setAddress("redis://localhost:6379"); redissonClient = Redisson.create(config);}// 非公平锁private static void testLock() {
try {
System.out.println(Thread.currentThread().getName()); RLock rLock = redissonClient.getLock("key"); // 这一步线程阻塞 System.out.println(Thread.currentThread().getName() + ",准备加锁。。。。。。"); rLock.lock(3, TimeUnit.SECONDS); System.out.println(Thread.currentThread().getName() + "加锁成功"); Thread.sleep(5000); rLock.unlock(); } catch (InterruptedException e) {
e.printStackTrace(); }}

消息队列

使用list数据结构作为队列,lpush为生产消息,rpop为消费消息。

可以使用brpop来代替rpop。为了避免空闲连接自动断开,所以需要捕获异常,然后重试。

位图(加分)

使用位图getbit/setbit等操作对string类型进行处理。

指令

  • getbit
  • setbit
  • bitfield key get
  • bitfield key set
  • bitfield key incrby

应用场景

用户签到记录

程序

public static void main(String[] args) {
try {
jedis = new Jedis("localhost", 6379); // 实现用户的签到记录 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("YYYY-MM-DD"); long beginTime = simpleDateFormat.parse("2020-01-01").getTime(); // 位移,当天是当年的第几天 Long day = (System.currentTimeMillis() - beginTime) / 1000 * 60 * 60 * 24L; jedis.setbit("user1", day.intValue(), true); System.out.println(jedis.getbit("user1", day.intValue())); } catch (ParseException e) {
e.printStackTrace(); } finally {
jedis.close(); }}

HyperLogLog(加分)

高级数据结构HyperLogLog,提供不精确的去重技术方案,标准误差率在0.81%,占用内存空间12KB。

指令

  • pfadd
  • pfcount
  • pfmerge

注意:在计数比较小时,采用稀疏矩阵存储,空间占用很小,当计数慢慢变大、稀疏矩阵占用空间渐渐超过了阈值时,才会一次性转变为稠密矩阵,才会占用12KB空间。

应用场景

记录某个连接的UV

程序

public static void main(String[] args) throws InterruptedException {
// 大致统计用户数量 try {
jedis = new Jedis("localhost", 6379); for (int i = 0; i < 1000; i++) {
jedis.pfadd("hyper1", "user" + i); } System.out.println(jedis.pfcount("hyper1")); for (int i = 0; i < 1000; i++) {
jedis.pfadd("hyper2", "user" + (1000 + i)); } System.out.println(jedis.pfcount("hyper2")); jedis.pfmerge("hyper3", "hyper1", "hyper2"); System.out.println(jedis.pfcount("hyper3")); } finally {
jedis.close(); }}// 打印如下10119972002

布隆过滤器(重要)

高级数据结构布隆过滤器(Bloom Filter)。提供去重作用,在空间上能节省90%以上,存在一定的误判概率。

注意:需要安装插件,https://github.com/RedisBloom/RedisBloom;docker可以使用镜像redislabs/rebloom

指令

  • bf.add key value
  • bf.exists key value
  • bf.madd key value1 value2
  • bf.exists key value1 value2
  • bf.reserve key error_rate(错误率,默认0.01) initial_size(默认100)

应用场景

数据去重

缓存穿透

程序

// 添加mavan依赖
com.redislabs
jrebloom
2.0.0-m1
public static void main(String[] args) {
try {
jedis = new Jedis("localhost", 6379); // 设置布隆过滤器参数 jedis.sendCommand(io.rebloom.client.Command.RESERVE, "bf", "0.01", "5"); // 第一种场景:数据去重 // 当使用过数据,添加到redis,通过exists来判断是否存在:0为不存在,1为存在 jedis.sendCommand(io.rebloom.client.Command.ADD, "bf", "user1"); jedis.sendCommand(io.rebloom.client.Command.ADD, "bf", "user2"); System.out.println(jedis.sendCommand(io.rebloom.client.Command.EXISTS, "bf", "user1")); System.out.println(jedis.sendCommand(io.rebloom.client.Command.EXISTS, "bf", "user3")); // 第二种场景:缓存穿透 // 将所有缓存预热到指定key中,存在则查询,不存在则直接返回 Set
keys = jedis.keys("*"); keys.forEach(key->{
jedis.sendCommand(Command.ADD, "all", key); }); long result = (Long) jedis.sendCommand(io.rebloom.client.Command.EXISTS, "all", "user3"); if(result == 1){
System.out.println("执行缓存查询"); }else{
System.out.println("直接返回"); } } finally {
jedis.close(); }}// 日志打印10直接返回

原理

通过错误率和初始大小,生成位数组的大小和hash函数数量,公式如下:

hash函数数量 = 0.7 * (位数组的大小 / 初始大小)错误率 = 0.6185^(位数组的大小 / 初始大小)

布隆计算器:https://krisives.github.io/bloom-calculator

在这里插入图片描述

通过计算器可知,100个初始大小,错误率在1%的情况下,有7个hash函数和959长度的位数组,那么具体的算法如下:

  1. 7个hash函数,分别为h1,h2,h3,h4,h5,h6,h7
  2. 当添加一个value时
  3. 第一个位置:location1 = h1(value) % 959,将该位置设置为1,(setbit key location1 1);
  4. 第二个位置:location2 = h2(value) % 959,将该位置设置为1,(setbit key location2 1);
  5. 重复以上步骤,直到获取第七个位置
  6. 在判断是否存在指定value,也是类似的算法
  7. 第一个位置:location1 = h1(value) % 959,获取位置1的值,(getbit key location1);如果不为1则直接返回不存在
  8. 只有当七个位置都为1的情况下,才表示该value是存在的

错误率的原因是因为hash时,可能存在hash冲突

GeoHash(地理位置算法)

指令

  • geoadd key 经度 纬度 value1
  • 计算两个元素之间的距离(指定单位):geodist key value1 value2 m/km/ml/ft
  • 获取指定元素的经纬度:geopos key value1
  • 获取指定元素的hash:geohash key value1
  • 查询指定元素相关的元素:georadiusbymember key value1 x m/km/ml/ft [withcoord/withdist\withhash] count y asc/desc
  • 查询指定经纬度相关的元素:georadius key 经度 纬度 x m/km/ml/ft [withcoord/withdist\withhash] count y asc/desc

withcoord ;withdist:显示距离;withhash:显示hash

程序

private static Jedis jedis;public static void main(String[] args) {
try {
Jedis jedis = new Jedis("localhost", 6379); String key = "company"; jedis.geoadd(key, 116.48105, 39.996794, "掘金"); Map
map = new HashMap<>(3); map.put("掌阅", new GeoCoordinate(116.514203, 39.905409)); map.put("美团", new GeoCoordinate(116.489033, 40.007669)); map.put("京东", new GeoCoordinate(116.562108, 39.9787602)); jedis.geoadd(key, map); System.out.println("美团、京东距离:"+ jedis.geodist(key, "美团","京东", GeoUnit.M) + "米"); // 范围20公里内的三家公司 List
list = jedis.georadiusByMember(key, "京东", 20, GeoUnit.KM, GeoRadiusParam.geoRadiusParam().withDist().count(3)); list.forEach(geoRadiusResponse -> {
System.out.println(geoRadiusResponse.getMemberByString() + geoRadiusResponse.getDistance()); }); List
list2 = jedis.georadius(key, 116.562108, 39.9787602, 20, GeoUnit.KM, GeoRadiusParam.geoRadiusParam().withDist().count(3)); list2.forEach(geoRadiusResponse -> {
System.out.println(geoRadiusResponse.getMemberByString() + geoRadiusResponse.getDistance()); }); } finally {
jedis.close(); }}// 日志打印美团、京东距离:7008.1463米京东0.0美团7.0081掘金7.1929京东2.0E-4美团7.0082掘金7.193

转载地址:https://blog.csdn.net/XiaMen_BuYu/article/details/106894777 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:SpringCloud学习-更新中
下一篇:《架构师训练营》-第二周-面向对象编程

发表评论

最新留言

初次前来,多多关照!
[***.217.46.12]2024年04月28日 16时44分40秒