缓存学习(八):Redis的Java客户端

本文详细介绍了Redis的Java客户端Jedis,包括基本使用、连接池JedisPool、管道、事务、发布/订阅以及新加入的streams支持。通过实例展示了如何实现一个简单的分布式锁,并提供了相关测试,帮助读者全面掌握Jedis的使用技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

1 基本使用

2 高级特性

2.1 连接池JedisPool

2.2 管道

2.3 事务

2.4 发布/订阅

2.5 streams支持

3 利用Jedis实现一个简单的分布式锁

3.1 构造方法

3.2 lock方法

3.3 unlock方法

3.4 测试


Jedis是Redis的Java客户端实现,支持Redis的全部特性,如:事务、管道、发布/订阅、集群等,还支持连接池等特性。

1 基本使用

首先需要引入依赖,目前最新的3.1.0-m1版本已经提供了对Redis 5 streams的支持:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.1.0-m1</version>
</dependency>

Jedis支持Redis的全部操作,下面是一个使用set、get命令的单连接示例:

public static void main(String[] args) {
    Jedis jedis=new Jedis("localhost",6379);
    System.out.println(jedis.set("hello","world"));
    System.out.println(jedis.get("hello"));
    jedis.close();
}

运行后输出“OK”和“world”。 Jedis的构造方法还支持配置连接超时时间和读写超时时间。set方法和get方法支持byte[]类型的参数,意味着可以将对象序列化后传入Redis。

2 高级特性

由于还没有介绍哨兵和集群机制,所以这里仅介绍事务、管道、发布/订阅机制等已经介绍了的特性,脚本机制比较简单就不做介绍了。

2.1 连接池JedisPool

连接池相比于单连接,具有开销少、易控制的优点,Jedis也提供了连接池实现:JedisPool。下面将上一节的例子改编为连接池实现:

public static void main(String[] args) {
    JedisPool jedisPool=new JedisPool("localhost",6379);
    try(Jedis jedis=jedisPool.getResource()) {
        System.out.println(jedis.set("hello", "world"));
        System.out.println(jedis.get("hello"));
    }
    jedisPool.close();
}

输出不变。这里使用了 try-with-resource ,会在try块执行完毕后自动关闭Jedis连接,其close方法如下:

public void close() {
    if (this.dataSource != null) {
        JedisPoolAbstract pool = this.dataSource;
        this.dataSource = null;
        if (this.client.isBroken()) {
            pool.returnBrokenResource(this);
        } else {
            pool.returnResource(this);
        }
    } else {
        super.close();
    }
}

可见如果是通过连接池方式获取连接实例,其close方法会归还连接。 

JedisPool的构造方法可以传入GenericObjectPoolConfig对象,对连接池进行配置,比较常用的属性有:

参数含义默认值
maxTotal最大连接数8
maxIdle最大空闲连接数8
minIdle最小空闲连接数0
maxWaitMillis连接池资源耗尽后,请求最大等待时间-1,代表一直等待
jmxEnabled是否启用jmx监控true
jmxNameBase在jmx中的名称null
jmxNamePrefix在jmx中的名称前缀pool
minEvictableIdleTimeMillis连接的最小空闲时间,达到此值后会被移除1800000(30分钟)
numTestsPerEvictionRun空闲连接检测时的取样数3
testOnBorrow从连接池取出连接时是否检测有效性(发送ping命令检测),失效连接会被移除,下同false
testOnCreate连接池创建连接时是否检测有效性,false
testOnReturn归还连接时是否检测有效性false
testWhileIdle是否在连接空闲时检测有效性false
timeBetweenEvictionRunsMillis空闲连接的检测周期-1,即不检测
blockWhenExhausted连接池资源耗尽时,请求是否等待,该值为true时,maxWaitMillis才有意义true
evictionPolicy连接移除策略DefaultEvictionPolicy
fairness连接池内部存放空闲连接的的阻塞队列是否是公平的false

2.2 管道

Redis原生的管道一大缺陷就是用户必须将命令编写为RESP格式,非常冗长,还容易出错,Jedis的Pipeline实现提供了类似Jedis类的操作,非常友好:

public static void main(String[] args) {
    JedisPool jedisPool=new JedisPool("localhost",6379);
    try(Jedis jedis=jedisPool.getResource()) {
        Pipeline pipeline=jedis.pipelined();
        pipeline.set("hello","world");
        Response<String> response=pipeline.get("hello");
        pipeline.sync();
        System.out.println(response.get());
        pipeline.close();
    }
    jedisPool.close();
}

可以看到,Jedis的Pipeline使用和Jedis类使用具有良好的统一性,完全不需要了解RESP消息格式。Jedis实现管道的原理是,每次执行操作时,就讲命令写入RedisOutputStream,当调用sync方法时,再调用flush将数据一同发送出去,之后读取RedisInputStream,将数据写入Response队列即可。

Response类似于Future,仅表示未来的数据,如果这里将resposne.get()移动到pipeline.sync()前面,则会报出如下异常:

Exception in thread "main" redis.clients.jedis.exceptions.JedisDataException: Please close pipeline or multi block before calling this method.
	at redis.clients.jedis.Response.get(Response.java:33)
	at JedisTest.main(JedisTest.java:16)

2.3 事务

Jedis的事务机制和管道机制有类似之处:

public static void main(String[] args) {
    JedisPool jedisPool=new JedisPool("localhost",6379);
    try(Jedis jedis=jedisPool.getResource()) {
        Transaction transaction=jedis.multi();
        transaction.set("hello","world");
        Response<String> response=transaction.get("hello");
        transaction.exec();
        System.out.println(response.get());
    }
    jedisPool.close();
}

都使用到了Response。实际上,如果看源码的话就能发现,Jedis的Transaction类正是继承了PipelineBase,和管道机制师出同门。

同样地,Jedis也支持watch:

public static void main(String[] args) {
    JedisPool jedisPool=new JedisPool("localhost",6379);
    try(Jedis jedis=jedisPool.getResource()) {
        jedis.watch("hello");
        Transaction transaction=jedis.multi();
        Thread.sleep(10000L);
        transaction.set("hello","world");
        Response<String> response=transaction.get("hello");
        transaction.exec();
        System.out.println(response.get());
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    jedisPool.close();
}

在线程睡眠的10秒钟内,修改hello对应的值的话,会有如下输出,表明事务没有成功提交:

Exception in thread "main" redis.clients.jedis.exceptions.JedisDataException: Please close pipeline or multi block before calling this method.
	at redis.clients.jedis.Response.get(Response.java:33)
	at JedisTest.main(JedisTest.java:15)

2.4 发布/订阅

发布比较简单,只要调用publish方法即可,重点是订阅机制。无论是subscribe,还是psubscribe,其参数列表中都有一个JedisPubSub对象:

void subscribe(JedisPubSub jedisPubSub, String... channels);

可以将它理解为一个事件监听器, 它是一个抽象类,没有默认实现,需要我们编写事件处理逻辑,下面是其可以重写的方法列表:

我们以onMessage方法为例:

public static void main(String[] args) {
    JedisPool jedisPool=new JedisPool("localhost",6379);
    try(Jedis jedis=jedisPool.getResource()) {
        jedis.subscribe(new TestListener(),"test");
    }
    jedisPool.close();
}

static class TestListener extends JedisPubSub{
    @Override
    public void onMessage(String channel, String message) {
        System.out.println("Received Message:"+message+" from "+channel);
    }
}

然后在命令行客户端输入 publish test helloworld,从而得到如下响应:

Received Message:helloworld from test

需要注意的是,subscribe会阻塞调用线程,一直等待消息,最好是在子线程里进行订阅,这样可以在主线程调用unsubscribe来退订。 

2.5 streams支持

Jedis 3.1.0最大的更新就是支持了Redis 5 的streams数据类型,以下是一个例子:

public static void main(String[] args) {
    JedisPool jedisPool=new JedisPool("localhost",6379);
    try(Jedis jedis=jedisPool.getResource()) {
        Map<String,String> hash=new HashMap<>();
        hash.put("name","zhangsan");
        hash.put("age","10");
        hash.put("sexual","male");
        jedis.xadd("myteststream",StreamEntryID.NEW_ENTRY,hash);
        System.out.println(jedis.xlen("myteststream"));
    }
    jedisPool.close();
}

hash就是field-string对,最终输出的结果是1,代表成功向myteststream流中写入了1个条目。 而StreamEntryID.NEW_ENTRY就是“*”,除此之外还有代表最后一个Entry的“$”(LAST_ENTRY)和未接收Entry的“>”(UNRECEIVED_ENTRY):

    public static final StreamEntryID NEW_ENTRY = new StreamEntryID() {
        private static final long serialVersionUID = 1L;

        public String toString() {
            return "*";
        }
    };
    public static final StreamEntryID LAST_ENTRY = new StreamEntryID() {
        private static final long serialVersionUID = 1L;

        public String toString() {
            return "$";
        }
    };
    public static final StreamEntryID UNRECEIVED_ENTRY = new StreamEntryID() {
        private static final long serialVersionUID = 1L;

        public String toString() {
            return ">";
        }
    };

3 利用Jedis实现一个简单的分布式锁

基于Redis的分布式锁框架有很多,官网列出的有:

这里使用Jedis实现一个互斥锁。该锁有两个方法:lock和unlock。

3.1 构造方法

由于需要借助Jedis实例来加解锁,因此该类需要有一个Jedis对象,同时需要有相应的构造方法:

private Jedis jedis;

public JedisLock(Jedis jedis) {
    this.jedis = jedis;
}

同时还需要考虑无参的情况,于是我们再添加一个JedisPool成员变量:

private JedisPool pool;

public JedisLock() {
    pool=new JedisPool(PropertiesUtil.get("lock.host"),Integer.parseInt(PropertiesUtil.get("lock.port")));
}

PropertiesUtil是自己实现的类,用来读取classpath下的lock.properties文件中的配置。 

3.2 lock方法

互斥锁获取时,要求该锁必须空闲,获取后,该锁必须处于占用状态。这一性质使用setnx就可以实现。

另一个需要考虑的问题是,等待获取锁的超时时间,和锁本身的存活时间,前者可以通过传入参数解决,后者可以通过expire/pexpire命令解决

这里结合以上两个问题,使用set nx px来获取锁。

现在开始实现,首先判断jedis是否为null,是的话需要从连接池取出一个连接,flag的作用是在收尾时判断是否需要重新将连接设为null:

boolean flag=false;
if(jedis==null) {
    jedis = pool.getResource();
    flag = true;
}

接下来,根据wait和expire参数是否大于0,选择合适的分支进行获取锁,获取成功则返回true,这里wait单位是秒,expire单位是毫秒:

if(expire>0){
    if(wait>0){
        while(wait>0){
            if(jedis.get(key)==null){
                String result=jedis.set(key,"1",new SetParams().px(expire).nx());
                if(result!=null&&result.equals("OK")){
                    jedis.set(id,"1");
                    close(flag,jedis);
                    return true;
                }
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            wait--;
        }
    }else{
        if(jedis.get(key)==null){
            String result=jedis.set(key,"1",new SetParams().px(expire).nx());
            if(result!=null&&result.equals("OK")){
                jedis.set(id,"1");
                close(flag,jedis);
                return true;
            }
        }
    }
}else{
    if(wait>0){
        while(wait>0){
            if(jedis.get(key)==null) {
                String result = jedis.set(key, "1", new SetParams().nx());
                if (result != null && result.equals("OK")) {
                    jedis.set(id, "1");
                    close(flag, jedis);
                    return true;
                }
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            wait--;
        }
    }else{
        if(jedis.get(key)==null){
            String result=jedis.set(key,"1",new SetParams().nx());
            if(result!=null&&result.equals("OK")){
                jedis.set(id,"1");
                close(flag,jedis);
                return true;
            }
        }
    }
}

key是锁的名称,全局一致,id则是全局唯一ID。如果未能成功获取锁,则返回false:

close(flag,jedis);
return false;

close的逻辑就是如果flag为true,则关闭jedis并设置jedis为null:

private void close(boolean flag, Jedis jedis) {
    if(flag){
        jedis.close();
        jedis=null;
    }
}

3.3 unlock方法

unclock的思路比较简单,只要验证一下自己的id是否存在于Redis即可,是则删除key,否则说明自己不是锁的所有者,没有资格解锁:

    public void unlock(String key)  throws IllegalAccessException {
        boolean flag=false;
        if(jedis==null) {
            jedis = pool.getResource();
            flag = true;
        }
        if(jedis.get(id)==null){
            throw new IllegalAccessException("Permission Denied.");
        }else{
            jedis.del(key);
            jedis.del(id);
        }
        close(flag,jedis);
    }

3.4 测试

我们编写一个长度为10的循环,创建10个子线程,每个子线程都尝试获取锁,获取到锁后,等待2000毫秒再释放锁:

public static void main(String[] args) {
    for(int i=0;i<10;i++){
        new Thread(()->{
            JedisLock lock=new JedisLock();
            if(lock.lock(Thread.currentThread().getName(),10,0)){
                System.out.println("线程"+Thread.currentThread().getName()+"获得锁啦");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                try {
                    lock.unlock(Thread.currentThread().getName());
                System.out.println("线程"+Thread.currentThread().getName()+"释放锁啦");
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
            lock.close();
        }).start();
    }
}

这里就用线程名作为id字段,理论上应该有10/2=5个线程获得锁,输出如下:

线程Thread-9获得锁啦
线程Thread-9释放锁啦
线程Thread-7获得锁啦
线程Thread-7释放锁啦
线程Thread-1获得锁啦
线程Thread-1释放锁啦
线程Thread-0获得锁啦
线程Thread-0释放锁啦
线程Thread-3获得锁啦
线程Thread-3释放锁啦

符合预期。以上代码已上传Github:https://github.com/Yanghanchen/jedistest

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值