Redis呕心沥血的深度探险

基础及实践篇

redis可以做什么

redis可以做缓存数据库

redis可以做分布式锁

Id列表的自增

redis的安装
Docker方式安装:docker pull redis
运行redis容器:docker run --name myredis -d -p6379:6379 redis
执行容器中的客户端:docker exec -it myredis redis-cli

github源码安装方式
git clone --branch 2.8 --depth 1 [email protected]:antirez/redis.git
cd redis
make
cd src
./redis-server --daemonize yes
./redis-cli

直接安装
mac: brew install redis
ubuntu: apt-get install redis
redhat: yun install redis
运行客户端:redis-cli

5种基本的数据结构
String,list,hash,set,zset

String 类型

键值对 key-value

127.0.0.1:6379> setex name 5 ll
设置name 5秒钟失效
OK
127.0.0.1:6379> get name
"ll"
127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> setnx name code
如果name存入则设置不成功,如果不存在设置成功
(integer) 1
127.0.0.1:6379> setnx name code
(integer) 0
127.0.0.1:6379> set age 30
OK
127.0.0.1:6379> incr age
age是int类型,每次自增1
(integer) 31
127.0.0.1:6379> incr age
(integer) 32
127.0.0.1:6379> incr age 2
(error) ERR wrong number of arguments for 'incr' command
127.0.0.1:6379> incrby age 5
每次自增不是1
(integer) 37
127.0.0.1:6379> incrby age 5
(integer) 42
127.0.0.1:6379> exists name
判断key是否存在
(integer) 1
127.0.0.1:6379> exists name2
(integer) 0
127.0.0.1:6379>expire name 5
设置key失效时间,单位为s

list列表
相当于java语言种的LinkedList 是双向链表
插入删除快时间复杂度是O(1)
查询定位速度慢,时间复杂度是O(n)

模拟队列,先进先出,右进左出

127.0.0.1:6379> rpush books python java golang
右边进队列
(integer) 3
127.0.0.1:6379> llen books
books长度
(integer) 3
127.0.0.1:6379> lpop books
左边出队列
"python"
127.0.0.1:6379> lpop books
"java"
127.0.0.1:6379> lpop books
"golang"
127.0.0.1:6379> lpop books
(nil)
127.0.0.1:6379>

模拟栈 右边进右边出

127.0.0.1:6379> exists books
(integer) 0
127.0.0.1:6379> rpush books python java golang
(integer) 3
127.0.0.1:6379> rpop books
"golang"
127.0.0.1:6379> rpop books
"java"
127.0.0.1:6379> rpop books
"python"
127.0.0.1:6379>

慢操作
lindex相当于java链表的get(int index)需要堆链表的遍历
ltrim 保留 为ltrim的两个参数start_index 和end_index定义了一个区间,在区间内保留,区间外删除,可以通过Itrim实现一个定长的链表

index可以定义为负数 ,-1表示倒数第一个数 ,-2表示倒数第二个数

127.0.0.1:6379> rpush books python java golang
(integer) 3
127.0.0.1:6379> lindex books 1
"java"
127.0.0.1:6379> lrange books 0 -1
1) "python"
2) "java"
3) "golang"
127.0.0.1:6379> ltrim books 1 -1
OK
127.0.0.1:6379> lrange books 0 -1
1) "java"
2) "golang"
127.0.0.1:6379> ltrim books 1 0
清空整个列表,因为区间是负数
OK
127.0.0.1:6379> llen books
(integer) 0
127.0.0.1:6379>

压缩列表ziplist

Hash字典
相当于java中的HashMap 数据结构是数组+链表
redis种hash的值只能为字符串

127.0.0.1:6379> hset books java "think in java"
命令行有空格所有要加双引号
(integer) 1
127.0.0.1:6379> hset books golang "concurrency in go"
(integer) 1
127.0.0.1:6379> hset books python "python cookbook"
(integer) 1
127.0.0.1:6379> hgetall books
获取所有books里面的kv
1) "java"
2) "think in java"
3) "golang"
4) "concurrency in go"
5) "python"
6) "python cookbook"
127.0.0.1:6379> hlen books
(integer) 3
127.0.0.1:6379> hget books java
"think in java"
127.0.0.1:6379> hset books golang "learning go programming"
更新,所以返回0
(integer) 0
127.0.0.1:6379> hget books golang
"learning go programming"
127.0.0.1:6379> hmset books java "effective java" python "learning python" golang "modern golang programming"
批量更新返回ok
OK
127.0.0.1:6379>

自增

127.0.0.1:6379> hset li age 22
(integer) 1
127.0.0.1:6379> hincrby li age 1
(integer) 23
127.0.0.1:6379>

set集合
相当于java中的HashSet,内部的键值对是无序的,唯一的
可以存储中将用户ID,因为有去重功能

127.0.0.1:6379> sadd books python
(integer) 1
127.0.0.1:6379> sadd books python
重复 设置失败
(integer) 0
127.0.0.1:6379> sadd books java golang
(integer) 2
127.0.0.1:6379> smembers books
无序的
1) "golang"
2) "python"
3) "java"
127.0.0.1:6379> sismember books java
查看一个key是否存在
(integer) 1
127.0.0.1:6379> sismember books rust
(integer) 0
127.0.0.1:6379> scard books
查看set的长度
(integer) 3
127.0.0.1:6379> spop books
随机弹出一个
"python"
127.0.0.1:6379>

zset列表(有序列表)
类似于java中的SortedSet和HashMap的结合体
他是一个set保证内部的value唯一性,另一方面给value赋予一个score,代表权重,内部实现是一种叫做 跳跃列表的数据结构。
zset最后一value被删除时,数据结构会自动删除,内存被回收

zset可以存储粉丝列表,value值是粉丝的用户id,score是关注的时间,我们可以对粉丝列表按关注顺序排序

zset可以存储学生的成绩,value值是学生的id,score是它考试的成绩,按成绩排序得到名次

 zadd books 9.0 "think in java"
(integer) 1
127.0.0.1:6379> zadd books 8.9 "java concurrency"
(integer) 1
127.0.0.1:6379> zadd books 8.6 "java cookbook"
(integer) 1
127.0.0.1:6379> zrange books 0 -1
按score排序列出
1) "java cookbook"
2) "java concurrency"
3) "think in java"
127.0.0.1:6379> zrevrange books 0 -1
按score逆序列出
1) "think in java"
2) "java concurrency"
3) "java cookbook"
127.0.0.1:6379> zcard books
(integer) 3
127.0.0.1:6379> zscore books "java concurrency"
查看权重,内部使用double
"8.9000000000000004"
127.0.0.1:6379> zrank books "java concurrency"
排名
(integer) 1
127.0.0.1:6379> zrank books "java cookbook"
(integer) 0
127.0.0.1:6379> zrangebyscore books 0 8.91
根据分值区间遍历
1) "java cookbook"
2) "java concurrency"
127.0.0.1:6379> zrangebyscore books -inf 8.91 withscores
根据分值区间遍历 inf代表无穷大
1) "java cookbook"
2) "8.5999999999999996"
3) "java concurrency"
4) "8.9000000000000004"
127.0.0.1:6379> zrem books "java concurrency"
删除value
(integer) 1
127.0.0.1:6379> zrange books 0 -1
1) "java cookbook"
2) "think in java"

zset内部的排序功能是通过 跳跃列表来实现的
类似于一种公司管理机制

容器型数据结构的通用规则
容器型数据结构有list,hash,set,zset
1create if not exists 如果容器不存在就创建一个,在进行操作。eg:rpush如果刚开始没有列表,就会创建一个,在rpush
2 drop if no elements 如果容器中没有元素了,就会立即删除元素,释放内存

过期时间
redis所有数据结构都可以设置过期时间,过期时间到了就会自动删除对象,删除的单位是对象,而不是一个key,一个hash对象等

如果一个对象设置了过期时间,然后调用set方法修改它,那么过期时间就会消失

127.0.0.1:6379> keys pattern
查看所有key
(empty list or set)
127.0.0.1:6379> set codehole yoyo
OK
127.0.0.1:6379> expire codehole 600
(integer) 1
127.0.0.1:6379> ttl codehole
查看剩余过期时间
(integer) 595
127.0.0.1:6379> ttl codehole
(integer) 592
127.0.0.1:6379> set codehole yoyo
OK
127.0.0.1:6379> ttl codehole
(integer) -1

分布式锁

setnx(set if not exists)
setnx key value
设置分布式锁,防止死锁,设置key的过期时间

127.0.0.1:6379> setnx lock:codehole true
(integer) 1
127.0.0.1:6379> get lock:codehole
"true"
127.0.0.1:6379> expire lock:codehole 5
(integer) 1
127.0.0.1:6379> get lock:codehole
"true"
127.0.0.1:6379> get lock:codehole
(nil)
127.0.0.1:6379> get lock:codehole 

为了防止setnx和expire之间的服务器进程突然挂掉,导致无法释放锁,不能用redis事务来解决。
redis2.8中加入了set指令的扩展指令,就是setnx和expire组合在一起的原子操作。

127.0.0.1:6379> set lock:codehole true ex 5 nx
OK
127.0.0.1:6379> get lock:codehole
"true"
127.0.0.1:6379> get lock:codehole
(nil)

超时问题
Redis分布式锁不会解决超时问题,当一个线程拥有锁太长时间可能被其他线程释放掉,,
稍微安全点的办法就是:设置value值一个随机数,释放锁之前先比较随机数是否一致,但不是原子操作

可重入性
如果一个锁支持同一个线程多次加锁,这个锁就是可重入的。java中的ReentrantLock。 Redis的可重入性需要对客户端的set方法进行包装,使用线程的Threadlocal变量存储当前持有锁的计数。

Redis异步消息队列
redis不能保证消息的可靠性

Redis的list可以作为异步消息队列,用lpop和lpush操作出队列

127.0.0.1:6379> rpush notify-queue apple banana pear
(integer) 3
127.0.0.1:6379> llen notify-queue
(integer) 3
127.0.0.1:6379> lpop notify-queue
"apple"
127.0.0.1:6379> lpop notify-queue
"banana"
127.0.0.1:6379> llen notify-queue
(integer) 1
127.0.0.1:6379> lpop notify-queue
"pear"
127.0.0.1:6379> lpop notify-queue
(nil)
127.0.0.1:6379>

队列空了怎么办?客户端会陷入死循环

Thread.sleep(1000)
这个方式可能产生延迟,用阻塞读,blpop和blpush
队列没有数据的时候进入休眠状态,有数据立刻苏醒

空闲连接自动断开

锁冲突的处理
1.直接抛异常,通知用户稍后重试
2.sleep一会,然后再重试
3.讲请求转移至延迟队列,过一会再试

延迟队列的实现
可以通过redis的zset来实现,将消息的序列化成一个字符串作为zset的value,这个消息的到期处理时间作为score,然后多个线程轮询zset

Redis中的zerm方法是多线程抢夺任务的关键。

Redis限流
利用zset数据结构,通过score只留下一个区间的数据。score存一个时间戳。zcard小于等于规定时间内最大数量

package com.dl.redis;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.Response;

import java.io.IOException;

public class SimpleRateLimiter {
    private Jedis jedis;
    public SimpleRateLimiter(Jedis jedis){
        this.jedis=jedis;
    }
    public boolean isActionAllowed(String userId,String actionKey,int period,int maxCount) throws IOException {
        String key=String.format("hist:%s:%s",userId,actionKey);
        System.out.println(key);
        long nowTs=System.currentTimeMillis();
        Pipeline pipe=jedis.pipelined();
        pipe.multi();
        pipe.zadd(key,nowTs,""+nowTs);
        pipe.zremrangeByScore(key,0,nowTs-period*1000);
        Response<Long> count=pipe.zcard(key);
        pipe.expire(key,period+1);
        pipe.exec();
        pipe.close();
        return count.get()<=maxCount;
    }

    public static void main(String[] args) throws IOException {
        Jedis jedis=new Jedis();
        SimpleRateLimiter limiter=new SimpleRateLimiter(jedis);
        for (int i=0;i<20;i++){
            System.out.println(limiter.isActionAllowed("laoqian","reply",60,5));
        }
    }
}

漏斗算法–限流
限流还可以用zset,只截取范围内的数据。

cl.throttle tom:reply 15 30 60 1
//15是漏斗容量,30 60 是60秒内最多30次, 1 need 1 quota默认是1

GeoHash根据数据库算附近的人

127.0.0.1:6379> geoadd company 116.321312 39.1232421 juejin
(integer) 1
127.0.0.1:6379> geoadd company 116.514204 39.239485 ireader
(integer) 1
127.0.0.1:6379> geoadd company 116.489033 40.007434 meituan
(integer) 1
127.0.0.1:6379> geoadd company 116.489321 39.784344 jd 116.334980 40.324890 xiaomi
(integer) 2
127.0.0.1:6379> geodist company juejin ireader km
"21.0649"
127.0.0.1:6379> geodist company juejin xiaomi km
//查看juejin和xiaomi多少km
"133.6600"
127.0.0.1:6379> geopos company juejin
1) 1) "116.32131367921829224"
   2) "39.12324239655497848"
127.0.0.1:6379> geohash company ireader
1) "wwfz5r749d0"
127.0.0.1:6379> georadiusbymember company ireader 20 km count 3 asc
1) "ireader"
127.0.0.1:6379> georadiusbymember company ireader 200 km count 3 asc
//距离ireader200km内的3家公司
1) "ireader"
2) "juejin"
3) "jd"

redis的正则,Scan

keys *
查看所有key
keys com*
原创文章 41 获赞 11 访问量 1488

猜你喜欢

转载自blog.csdn.net/weixin_44038332/article/details/105519499