redis学习笔记
本笔记大多数是对于《redis深度历险,核心原理与应用实践》进行一个学习而产生的,内容仅供参考
set lock:codehole true ex 5 nx 意思
这个命令是 Redis 中的一个分布式锁的实现,它的含义如下:
set lock:codehole true:设置 key 为 lock:codehole 的值为 true,这个 key 就是我们所说的分布式锁的 key。由于 Redis 是单线程的,因此 set 命令是原子性的,所以多个客户端同时执行这个命令时,只有一个客户端能够获得锁。
ex 5:设置 key 的过期时间为 5 秒,表示如果当前客户端在 5 秒内没有完成任务并释放锁,则锁会自动过期并被其他客户端获取。
nx:表示只有当 key 不存在时才能设置 key 的值,这个选项是为了避免多个客户端同时竞争锁时出现竞争条件的问题。
因此,这个命令的含义是:尝试获取名为 lock:codehole 的分布式锁,在获取锁的过程中,如果这个锁已经被其他客户端获取,则当前客户端会等待,直到获取锁为止。一旦当前客户端获取到了锁,它就可以开始执行任务了。锁的过期时间为 5 秒,如果当前客户端在 5 秒内没有完成任务并释放锁,则锁会自动过期并被其他客户端获取。由于使用了 nx 选项,因此如果这个锁已经被其他客户端获取了,则当前客户端会直接返回失败,不会等待获取锁。
tag = random. next int() #随机数 if redis.set(key, tag, nx=True, ex=5}: do_ something () redis.delifequals(key, tag) #假想的 delifequals 指令
意思
这段代码是 Redis 中的一种分布式锁实现方式,其中包含了以下步骤:
生成一个随机的 tag(标记),这个 tag 可以是一个随机数或者其他唯一标识符,目的是为了防止多个客户端之间使用相同的 key 同时竞争锁。
python
tag = random.randint()
使用 Redis 的 set 命令尝试获取锁,其中 nx=True 表示只有当 key 不存在时才能设置 key 的值,即只有当锁没有被其他客户端获取时才能获取锁;ex=5 表示锁的过期时间为 5秒,其中5 是一个数字,表示锁的有效期。如果获取锁成功,set 命令会返回 True,否则返回 False。
python
if redis.set(key, tag, nx=True, ex=5):
如果获取锁成功,则执行相应的操作,例如执行一段临界区代码:
python
do_something()
在临界区代码执行完毕后,使用 delifequals 命令删除 key,并且只有当 key 的值等于 tag 时才会删除 key。这个操作的目的是为了避免误删其他客户端的锁。
python
redis.delifequals(key, tag)
总的来说,这段代码的含义是:通过使用 Redis 的 set 命令尝试获取分布式锁,如果获取成功,则执行一段临界区代码,并在临界区代码执行完毕后释放锁。这种方式能够保证同一时间只有一个客户端能够获取到锁,从而保证了临界区代码的正确性和一致性。
可重入锁
java
publ class RedisWithReentrantLock {
//存储锁对象的map,String是锁的key,Integer代表加锁次数
private ThreadLocal<Map<String,Integer>> lockers= new ThreadLocal<>();
private Jedis jedis;//连接redis的对象
//构造器
public RedisWithReentrantLock(Jedis jedis) {
this.jedis = jedis;
}
//首次加锁,加锁成功返回true
private boolean _lock(String key) {
retur jedis.set(key,””,”nx”, ”ex”, 5L) != null ;
}
//打开锁
private void _unlock (String key) {
jedis.del(key);
}
//拿到并发锁
private Map<String,Integer> currentLockers() {
Map<String,Integer> refs= lockers.get();//拿到所有的锁
if (refs != null) {//假如已经有锁了
return refs;//返回所有的锁(map)
}
//假如还没有锁
lockers.set (new HashMap<>());
return lockers.get();//返回一个空的hashmap
}
//上锁,成功返回true
public boolean lock (String key) {
Map<String,Integer> refs = currentLockers();//拿到并发锁
Integer refCnt = refs.get(key);//通过传入的key,获取加锁次数
if (refCnt != null) {//加过锁了
refs.put (key, refCnt + 1);//加锁次数+1
return true;//加锁成功
}
//下面就都是没加过锁的情况(对应key)
boolean ok = this._lock(key);//首次加锁
if(!ok){
return false;//加锁失败
}
refs.put(key,1);//放入map中
return true;//加锁成功
}
//开锁,成功返回true
public boolean unlock(String key) {
Map<String, Integer> refs = currentLockers();//拿到并发锁
Integer refCnt = refs.get(key);//通过传入的key,获取加锁次数
if (refCnt == null) {//没加过锁
return false;//开锁失败,因为没有锁
}
//下面就是都是加过锁的情况(对应key)
refCnt -= 1;//锁-1
if (refCnt > 0) {//还有锁
refs.put(key, refCnt);//更新
}else{//refCnt = 0
refs.remove(key);//在map移除锁
this._unlock(key);//真正的操作redis将锁移除
}
return true;//返回成功
}
//测试
public static void main(String[] args) {
Jedis jedis = new Jedis();
RedisWi thReentrantLock redis = new Redi sWi thReentrantLock (jedis);
System.out.println(redis.lock("codehole"));
System.out.println(redis.lock("codehole"));
System.out.println(redis.unlock("codehole"));
System.out.println(redis.unlock("codehole"));
}
延时队列的实现
Redis zrem 方法是多线程多进程争抢任务的关键,它的返回值决定了当前实 例有没有抢到任务,因为 loop 方法可能会被多个线程、多个进程调用,同一个任务 可能会被多个进程、多个线程抢到,要通过 zrem 来决定唯一的属主。
同时,我们要注意一定要对 handle_msg 进行异常捕获,避免因为个别任务处理 问题导致循环异常退出。以下是 Java 版本的延时队列实现方法,因为要使用到 JSON 序列化,所以还需要 fastjson 的支持。
import java.lang.reflect.Type;
import java.util.Set;
import java.util.UUID;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import redis.clients.jedis.Jedis;
//Redis延迟队列
public class RedisDelayingQueue<T> {
static class Taskitem<T> {
public String id;
public T msg ;
}
// fastjson序列化对象存在 generic 类型,需要使用 TypeReference
private Type TaskType = new TypeReference<Taskitem<T>(){
}.getType();
private Jedis jedis;
private String queueKey;
//构造器
public RedisDelayingQueue(Jedis jedis, String queueKey) {
this.jedis = jedis;
this.queueKey = queueKey;
}
//延迟方法
public void delay (T msg) {
//创建一个任务
Taskitem<T> task= new Taskitem<T>( );
//分配自己唯一的uuid
task.id = UUID.randomUUID().toString();
task.msg = msg;
// fastjson 序列化
String s = JSON.toJSONString(task) ;
//塞入延时队列 5s 后再试
jedis.zadd (queueKey, System.currentTimeMillis() + 5000 , s);
}
//循环方法,不断从延时队列取数据
public void loop() {
while (!Thread.interrupted()) { //Thread.interrupted():其作用是中断此线程(此线程不一定是当前线程,而是指调用该方法的Thread实例所代表的线程)
//只取一条
Set<String> values = jedis.zrangeByScore(queueKey, 0, System.currentTimeMillis(), 0 , 1);
if (values.isEmpty()) { //拿到的数据为空
try {
Thread.sleep(5OO); //歇会继续
}catch (InterruptedException e) {
break;
}
continue;
}
//拿到了数据
String s = values.iterator().next();
if (jedis.zrem(queueKey, s) > 0) { //抢到了,删除redis中的数据
// fast son 反序列化
Taskitem<T> task = JSON.parseObject(s, TaskType);
this.handleMsg (task.msg);//将拿到的数据打印
}
}
}
//将拿到的数据打印
public void handleMsg(T msg){
System.out.println(msg);
}
//测试
public static void main (String[] args) {
Jedis.jedis = new Jedis() ;
RedisDelayingQueue<String> queue = new RedisDelayingQueue<>(jedis,"q-demo");//new自己这个类,key为q-demo
Thread producer = new Thread() {//生产者,向延时队列塞入数据
public void run() {
for(inti=0;i<10;i++){
queue.delay("codehole" + i) ;
}
}
};
Thread consumer = new Thread() {//消费者,向延时队列取数据
public void run() {
queue.loop() ;
}
};
//启动方法
producer.start() ;
consumer.start() ;
try{
producer.join() ;
Thread.sleep (6000) ;
consumer.interrupt() ;
consumer.join;
} catch (InterruptedException e){
}
}
}
限流
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);//拼接userid和行为作为key
long nowTs = System.currentTimeMillis();//获取当前的毫秒数,唯一标识作为score,member
Pipeline pipe = jedis.pipelined();//通道对象
pipe.multi();//标记事务开始
pipe.zadd(key,nowTs,""+nowTs);//加入管道 key:key,score:nowTs,member:“nowTs”
pipe.zremrangeByScore(key, 0, nowTs - period * 1000);//每次查询前移除key(从0-当前时间前60秒的数据)
Response<Long> count= pipe.zcard(key);//拿到zset内的key为传进来的key(相同用户相同行为)的个数
pipe.expire(key, period+ 1);//设置过期时间为 时间窗口+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("kai","reply",60,5));
}
}
}
漏斗限流
public class FunnelRateLimiter {
static class Funnel {
int capacity;//漏斗容量
float leakingRate;//漏水速率
int leftQuota;//剩余容量
long leakingTs;//上次注水时间
//构造器
public Funnel(int capacity, float leakingRate) {
this.capacity = capacity;
this.leakingRate = leakingRate;
this.leftQuota = capacity;
this.leakingTs = System.currentTimeMillis();
}
//漏水,给漏斗腾出空间
void makeSpace() {
long nowTs = System.currentTimeMillis();
long deltaTs = nowTs - leakingTs;//间隔时间
int deltaQuota = (int) (deltaTs * leakingRate);//计算漏水的容量
// 间隔时间太长,整数数字过大溢
if (deltaQuota < 0) {//将漏斗置为初始状态
this.leftQuota = capacity;
this.leakingTs = nowTs;
return;
}
//腾出空间太小 ,最小单位是1
if (deltaQuota < 1) {
return;
}
this.leftQuota += deltaQuota;//剩余容量+这段时间的漏水体积
this.leakingTs = nowTs;//更新时间
if (this.leftQuota > this.capacity) {//限制空间为一个常量
this.leftQuota = this.capacity;
}
}
//向漏斗灌水
boolean watering(int quota) {
makeSpace();//漏水,给漏斗腾出空间
if (this.leftQuota >= quota) {
this.leftQuota -= quota;
return true;
}
return false;
}
}
private Map<String,Funnel> funnels = new HashMap<>();
//能否使用
public boolean isActionAllowed(String userId,String actionKey,int capacity,float leakingRate){
String key= String.format("%s:%s", userId ,actionKey);//通过userid和行为拼接一个key
Funnel funnel = funnels.get(key);//从map中获取对应的value funnle
if(funnel == null) {//如果是第一次的行为(该用户),new funnel 并且存到map(参数(int capacity,float leakingRate)才有意义)
funnel = new Funnel(capacity, leakingRate);
funnels.put(key, funnel);
}
return funnel.watering(1); //需要1个quota,注水1单位
}
}
watch——更改账户余额
//watch使用,更改账户余额
public class TransactionDemo {
//测试
public static void main(String[ ] args) {
Jedis jedis = new Jedis();
String userId = "abc";
String key= keyFor(userId);//拿到统一格式的key
jedis.setnx(key,String.valueOf(5)) ; // setnx 做初始化 String.valueOf-->转化为字符串格式
System.out.println(doubleAccount(jedis,userId)) ;//调用带有watch发方法,返回当前账户的余额
jedis.close() ;//关闭redis连接
}
//将余额改为原来的两倍
public static int doubleAccount(Jedis jedis, String userid) {
String key = keyFor(userid);//通过userid获取key
while(true) {
jedis.watch(key);//监听账户
int value = Integer.parseInt(jedis.get(key));//拿到账户的余额
value *= 2;//加倍
Transaction tx = jedis.multi();//开启事务
tx.set(key, String.valueOf(value));//在事务执行更改操作
List<Object> res = tx.exec();//执行事务,返回值为null说明执行失败,没有进行更改操作
if (res != null) {
break; //成功了
}
}
return Integer.parseInt(jedis.get(key)); //重新获取余额
}
//传入userid转化为统一格式的key
public static String keyFor(String userId){
return String.format("account_%s",userId);
}
}
注意事项:redis禁止在multi(开启事务)和exec之间来执行watch(监听)操作,所以必须在开启事务之前对目标进行监听