Redis Java客户端相关命令介绍 Redisson 分布式锁

一、客户端介绍

  • Jedis

以Redis命令作为方法名称,学习成本低,简单实用。但是Jedis实例是线程不安全的,多线程环境下需要基于连接池来使用。

  • lettuce

Lettuce是基于Netty实现的,支持同步、异步和响应式编程方式,并且是线程安全的。支持Redis的哨兵模式、集群模式和管道模式。

  • Redisson

Redisson是一个基于Redis实现的分布式、可伸缩的Java数据结构集合。包含了诸如Map、Queue、Lock、Semaphore、AtomicLong等强大功能

二、Jedis

Jedis官方文档:https://github.com/redis/jedis

2.1 引入依赖

    <!--jedis依赖-->
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>4.4.0-m2</version>
    </dependency>

2.2 建立连接

    @BeforeEach
    public void setUp() {
        // 建立连接
        jedis = new Jedis("101.32.333.188", 6379);
        // 设置密码
        jedis.auth("123456");
        // 选择库
        jedis.select(0);
    }

2.3 测试使用

    // 测试string
    @Test
    public void testString() {
        // 插入数据,jedis命令和redis命令一致,直接使用即可
        String result = jedis.set("name", "测试");
        System.out.println("result = " + result);
        // 获取数据
        String name = jedis.get("name");
        System.out.println("name = " + name);
    }
    
    // 测试hash
    @Test
    public void testHash() {
        // 插入hash数据
        jedis.hset("user:1", "name", "测试");
        jedis.hset("user:1", "age", "25");

        // 获取key单个数值
        String name = jedis.hget("user:1", "name");
        // 获取key所有数据
        Map<String, String> hashMap = jedis.hgetAll("user:1");
        System.out.println("name = " + name);
        System.out.println("map = " + hashMap);
    }

2.4 释放jedis

    @AfterEach
    public void release() {
        // 释放资源
        if (jedis != null) {
            jedis.close();
        }
    }

三、Jedis连接池

Jedis本身是线程不安全的,并且频繁的创建喝销毁连接会有性能损耗,因此推荐使用Jedis连接池代替Jedis直连方式

3.1 建立配置类

public class JedisConnectFactory {

    private static final JedisPool jedisPool;

    static {
        // 配置连接池
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        // 最大连接
        jedisPoolConfig.setMaxTotal(8);
        // 最大空闲连接
        jedisPoolConfig.setMaxIdle(8);
        // 最小空闲连接
        jedisPoolConfig.setMinIdle(0);
        // 设置最长等待时间,单位ms
        jedisPoolConfig.setMaxWait(Duration.ofMillis(200));
        // 连接
        jedisPool = new JedisPool(jedisPoolConfig, "101.32.333.188", 6379, 1000, "123456");
    }

    // 获取jedis对象
    public static Jedis getJedis() {
        return jedisPool.getResource();
    }
}

3.2 测试使用

@BeforeEach
    public void setUp() {
        // 建立连接,直接调用连接池方法获取Jedis实例
        jedis = JedisConnectFactory.getJedis();
        // 设置密码
        jedis.auth("123456");
        // 选择库
        jedis.select(0);
    }

四、SpringDataRedis

SpringData是Spring中数据操作的模块,包含对各种数据库的集成,其中对Redis的集成模块就叫做SpringDataRedis,官网地址:https://spring.io/projects/spring-data-redis

  • 提供了对不同Redis客户端的整合(Lettuce和Jedis)
  • 提供了RedisTemplate统一API来操作Redis
  • 支持Redis的发布订阅模型
  • 支持Redis哨兵和Redis集群
  • 支持基于Lettuce的响应式编程
  • 支持基于JDK、JSON、字符串、Spring对象的数据序列化及反序列化
  • 支持基于Redis的JDKCollection实现

4.1 相关API命令

SpringDataRedis中提供了RedisTemplate工具类,其中封装了各种对Redis的操作。并且将不同数据类型的操作API封装到了不同的类型中:

API返回值类型说明
redisTemplate.opsForValueValueOperations操作String类型数据
redisTemplate.opsForHashHashOperations操作Hash类型数据
redisTemplate.opsForListListOperations操作List类型数据
redisTemplate.opsForSetSetOperations操作Set类型数据
redisTemplate.opsForZSetZSetOperations操作SortedSet类型数据
redisTemplate通用的命令

4.2 引入依赖

SpringBoot已经提供了对SpringDataRedis的支持,直接引入依赖即可:

        <!--redis依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!--连接池依赖-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

4.3 配置文件

注:spring-data-redis 默认就引入了lettuce依赖,如果需要使用Jedis,则需额外添加jedis依赖

        <!--jedis依赖-->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
spring:
  redis:
    host: 101.32.333.188
    port: 6379
    password: 123456
    lettuce:
      pool:
        max-active: 8 # 最大连接
        max-idle: 8 # 最大空闲连接
        min-idle: 0 # 最小空闲连接
        max-wait: 100 # 最长连接等待时间

4.4 注入RedisTemplate

    @Resource
    private RedisTemplate redisTemplate;

4.5 编写测试

@SpringBootTest
public class RedisDemoApplicationTests {

    @Resource
    private RedisTemplate redisTemplate;

    @Test
    void testString() {
        // 插入一条String数据
        redisTemplate.opsForValue().set("name", "实践");

        Object name = redisTemplate.opsForValue().get("name");
        System.out.println("name = " + name);
    }
}

4.6 SpringDataRedis的序列化方式

RedisTemplate可以接收任意Object作为值写入Redis,只不过写入前会把Object序列化为字节形式,默认是采用JDK序列化,得到的结果是这样的:

存入的key变成了这样:"\xac\xed\x00\x05t\x00\x04name"

缺点:

  • 可读性差
  • 内存占用较大

序列化工具类:

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        // 创建RedisTemplate对象
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        // 设置连接工厂
        redisTemplate.setConnectionFactory(connectionFactory);
        // 创建JSON的序列化工具
        GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        // 设置key的序列化
        redisTemplate.setKeySerializer(RedisSerializer.string());
        redisTemplate.setHashKeySerializer(RedisSerializer.string());
        // 设置value的序列化
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        // 返回
        return redisTemplate;
    }
}

// 如果只是简单demo测试,没添加Jackson依赖会出现报错,因为使用Json的序列化工具,正常SpringMvc中是已经包含Jackson依赖,不需要额外添加

        <!--Jackson依赖-->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
4.6.1 测试存入对象
  1. 创建对象
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {

    private String name;

    private Integer age;

}
  1. 单元测试
    @Test
    void testSaveUser() {
        // 写入数据
        redisTemplate.opsForValue().set("user:23", new User("cy", 25));
        // 获取数据
        User user = (User) redisTemplate.opsForValue().get("user:23");
        System.out.println("user = " + user);
    }
  1. 输出结果
1、控制台输出
user = User(name=cy, age=25)
2、redis可视化工具查看,对象value已经被转成json格式存储了
{
  "@class": "com.cy.common.pojo.User",
  "name": "cy",
  "age": 25
}
4.6.2 JSON序列化器弊端
{
  "@class": "com.cy.common.pojo.User",
  "name": "cy",
  "age": 25
}

为了在反序列化时知道对象的类型,JSON序列化器会将类的class类型写入json结果中,存入Redis,会带来额外的内存开销。

为了节省内存空间,我们并不会使用JSON序列化器来处理value,而是统一使用String序列化器,要求只能存储String类型的key和value。当需要存储Java对象时,手动完成对象的序列化和反序列化。

4.6.3 StringRedisTemplate

Spring默认提供了一个StringRedisTemplate类,它的key和value的序列化方式默认就是String方式。省去了自定义RedisTemplate的过程:

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    
    @Test
    void testStringTemplate() {
        // 创建对象
        User user = new User("cyy", 25);
        // 手动序列化
        String json = JSONObject.toJSONString(user);
        // 写入数据
        stringRedisTemplate.opsForValue().set("user:24", json);
        // 读取数据
        String value = stringRedisTemplate.opsForValue().get("user:24");
        // 反序列化
        User user1 = JSONObject.parseObject(value, User.class);
        System.out.println("user =" + user1);
    }
4.6.4 RedisTemplate的两种序列化方案:

方案一:

  • 自定义RedisTemplate
  • 修改RedisTemplate的序列化器为GenericJackson2JsonRedisSerializer

方案二:

  • 使用StringRedisTemplate
  • 写入Redis时,手动把对象序列化为JSON
  • 读取Redis时,手动把读取到的JSON反序列化为对象

五、Redisson

Redisson官方文档

Redisson是一个Redis的基础上实现的Java驻内存数据网络(In-Memory Data Frid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qg4vtevY-1677636456404)(https://note.youdao.com/yws/api/personal/file/43CC4A08D9794A2D803A18CE9E4EDB02?method=download&shareKey=c3fe38bb66e51537b4940c2605b2f702)]

基于setnx实现的分布式锁存在下面的问题:

  • 不可重入:同一个线程无法多次获取同一把锁
  • 不可重试:获取锁只尝试一次就返回false,没有重试机制
  • 超时释放:锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患。
  • 主从一致性:如果Redis提供了主从集群,主从同步存在延迟,当主宕机时,如果从未同步主中锁数据,则会出现锁失效

5.1 Redisson上手使用

  1. 引入依赖
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.19.3</version>
        </dependency>

2.配置Redisson客户端:

@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient(){
        // 配置类
        Config config = new Config();
        // 添加redis地址,setAddress添加单节点地址,也可以使用 config.useClusterServers().setNodeAddresses() 添加集群地址
        config.useSingleServer().setAddress("redis://111.22.268.127:6379").setPassword("123456");
        // 创建客户端
        return Redisson.create(config);
    }
}
  1. 使用Redisson的分布式锁
    // 引入Redisson客户端
    @Resource
    RedissonClient redissonClient;
    
    // 创建锁
    RLock lock = redisson.getLock("lockKey");
    // 最常见的使用方法
    lock.lock();
    
    // 加锁以后10秒钟自动解锁
    // 无需调用unlock方法手动解锁
    lock.lock(10, TimeUnit.SECONDS);

    // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
    boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
    if (res) {
        try {
            ...
        } finally {
       lock.unlock();
        }
    }

5.2 Redisson可重入锁原理

在原有判断是否是同一线程标识的基础上,增加可重入逻辑,同一线程执行业务获取锁,增加锁计数器+1,执行完毕锁计数器-1,直到锁计数器为0,则释放锁。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1J2OyBUR-1677636456414)(https://note.youdao.com/yws/api/personal/file/151A609E1613476AB4B383CC9575CDD6?method=download&shareKey=db981c6d893000a385adc3dcd333f007)]

// RedissonLock 源码中的获取锁 lua脚本


if ((redis.call('exists', KEYS[1]) == 0) or 
(redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then
 redis.call('hincrby', KEYS[1], ARGV[2], 1); 
 redis.call('pexpire', KEYS[1], ARGV[1]); 
 return nil; 
 end;
 return redis.call('pttl', KEYS[1]);
// RedissonLock 源码中的释放锁 lua脚本
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil;
end;
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
else
redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
return nil;

5.3 Redisson的锁充实和WatchDog机制

获取锁流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SoplrnJw-1677636456415)(https://note.youdao.com/yws/api/personal/file/F4A4F4540E6C4F75B8B7A49C6C7892F2?method=download&shareKey=c94727231fff7307ca6d699e1f1bb0cc)]

释放锁流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I1x7NdcZ-1677636456416)(https://note.youdao.com/yws/api/personal/file/F5BD911795624469A97F1674DF191645?method=download&shareKey=ae1e4f5978e0f76545193c5f99f9a4e0)]

Redisson分布式锁原理:

  • 可重入:利用hash结构记录线程id和重入次数
  • 可重试:利用信号量和PubSub功能实现等待、唤醒,获取锁失败的重试机制
  • 超时续约:利用watchDog,每隔一段时间(releaseTime/3),重置超时时间

5.4 Redisson分布式锁主从一致性问题

5.4.1 不可重入的Redis分布式锁:
  • 原理:利用setnx的互斥性;利用ex避免死锁;释放锁时判断线程标识
  • 缺陷:不可重入、无法重试、锁超时失效
5.4.2 可重入的Redis分布式锁:
  • 原理:利用hash结构,记录线程标识和冲入次数;利用watchDog机制延续锁时间;利用信号量控制锁重试等待
  • 缺陷:redis宕机引起锁失效问题
5.4.3 Redisson的multiLock:
  • 原理:多个独立的Redis节点,必须在所有节点都获取重入锁,才能算获取锁成功
  • 缺陷:运维成本高、实现复杂
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值