redis学习笔记

89 阅读7分钟

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(监听)操作,所以必须在开启事务之前对目标进行监听