准备环境:一台安装了Redis的服务器/电脑, JDK环境1.8 - Springboot版本2.6.0(jdk版本可以自己选择,比如选了jdk17,那么你就需要找到对应的Springboot版本,请参考这篇文章:在Spring官网查看Springboot与Java的版本对应关系-CSDN博客

1、添加依赖

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

这里我们直接引入了spring-boot-starter-data-redis这个springBoot本身就已经提供好了的starter, 我们可以点击去看一下这个starter中包含了哪些依赖:

可以发现,里面包含了spring-data-redis和 lettuce-core两个核心包,这就是为什么说我们的spring-boot-starter-data-redis默认使用的就是lettuce这个客户端了。

如果我们想要使用jedis客户端怎么办呢?就需要排除lettuce这个依赖,再引入jedis的相关依赖就可以了。

本项目使用 默认的(lettuce这个客户端)

2、添加配置

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password: 123456
    database: 0
    lettuce:
      pool:
        max-idle: 16
        max-active: 32
        min-idle: 8

由于我们使用的lettuce客户端,所以配置的时候,在spring.redis下加上lettuce再加上pool来配置;

注意:

如果使用的是jedis,就把lettuce换成jedis(同时要注意依赖也是要换的)。

但是仅仅这在配置文件中加入,其实连接池是不会生效的。这里大家一定要注意,很多同学在配置文件上加上了这段就以为连接池已经配置好了,其实并没有,还少了最关键的一步,就是要导入一个依赖,不导入的话,这么配置也没有用,因为连接池没有生效。导入这个依赖连接池才会生效

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password:
    database: 0
    jedis:
      pool:
        max-idle: 6
        max-active: 32
        min-idle: 8
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

3、项目中使用

我们的配置工作准备就绪以后,我们就可以在项目中操作redis了,操作的话,使用spring-data-redis中为我们提供的 RedisTemplate 这个类,就可以操作了。我们先举个简单的例子,插入一个键值对(值为string)然后获取。


import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("redis")
public class RedisController {

    @Autowired
    private final StringRedisTemplate stringRedisTemplate;

    @GetMapping("getRedisKey")
    public String getRedisKey(String key){
       return "调用成功:value--》"+stringRedisTemplate.opsForValue().get(key);
    }

    @PostMapping("saveRedisValue")
    public String save(String key, String value){
        stringRedisTemplate.opsForValue().set(key, value);
        return "调用成功";
    }

}

4、测试

启动redis服务,再启动项目;

使用 postman/Apifox 等工具访问api。我使用的是postman工具

6、Redis工具类

/**
 * Redis工具类,使用之前请确保RedisTemplate成功注入
 *
 */
public class RedisUtils {
 
    private RedisUtils() {
    }
 
    @SuppressWarnings("unchecked")
    private static RedisTemplate<String, Object> redisTemplate = SpringUtils
        .getBean("redisTemplate", RedisTemplate.class);
 
    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @return true=设置成功;false=设置失败
     */
    public static boolean expire(final String key, final long timeout) {
 
        return expire(key, timeout, TimeUnit.SECONDS);
    }
 
    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @param unit 时间单位
     * @return true=设置成功;false=设置失败
     */
    public static boolean expire(final String key, final long timeout, final TimeUnit unit) {
 
        Boolean ret = redisTemplate.expire(key, timeout, unit);
        return ret != null && ret;
    }
 
    /**
     * 删除单个key
     *
     * @param key 键
     * @return true=删除成功;false=删除失败
     */
    public static boolean del(final String key) {
 
        Boolean ret = redisTemplate.delete(key);
        return ret != null && ret;
    }
 
    /**
     * 删除多个key
     *
     * @param keys 键集合
     * @return 成功删除的个数
     */
    public static long del(final Collection<String> keys) {
 
        Long ret = redisTemplate.delete(keys);
        return ret == null ? 0 : ret;
    }
 
    /**
     * 存入普通对象
     *
     * @param key Redis键
     * @param value 值
     */
    public static void set(final String key, final Object value) {
 
        redisTemplate.opsForValue().set(key, value, 1, TimeUnit.MINUTES);
    }
 
    // 存储普通对象操作
 
    /**
     * 存入普通对象
     *
     * @param key 键
     * @param value 值
     * @param timeout 有效期,单位秒
     */
    public static void set(final String key, final Object value, final long timeout) {
 
        redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
    }
 
    /**
     * 获取普通对象
     *
     * @param key 键
     * @return 对象
     */
    public static Object get(final String key) {
 
        return redisTemplate.opsForValue().get(key);
    }
 
    // 存储Hash操作
 
    /**
     * 往Hash中存入数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @param value 值
     */
    public static void hPut(final String key, final String hKey, final Object value) {
 
        redisTemplate.opsForHash().put(key, hKey, value);
    }
 
    /**
     * 往Hash中存入多个数据
     *
     * @param key Redis键
     * @param values Hash键值对
     */
    public static void hPutAll(final String key, final Map<String, Object> values) {
 
        redisTemplate.opsForHash().putAll(key, values);
    }
 
    /**
     * 获取Hash中的数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */
    public static Object hGet(final String key, final String hKey) {
 
        return redisTemplate.opsForHash().get(key, hKey);
    }
 
    /**
     * 获取多个Hash中的数据
     *
     * @param key Redis键
     * @param hKeys Hash键集合
     * @return Hash对象集合
     */
    public static List<Object> hMultiGet(final String key, final Collection<Object> hKeys) {
 
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }
 
    // 存储Set相关操作
 
    /**
     * 往Set中存入数据
     *
     * @param key Redis键
     * @param values 值
     * @return 存入的个数
     */
    public static long sSet(final String key, final Object... values) {
        Long count = redisTemplate.opsForSet().add(key, values);
        return count == null ? 0 : count;
    }
 
    /**
     * 删除Set中的数据
     *
     * @param key Redis键
     * @param values 值
     * @return 移除的个数
     */
    public static long sDel(final String key, final Object... values) {
        Long count = redisTemplate.opsForSet().remove(key, values);
        return count == null ? 0 : count;
    }
 
    // 存储List相关操作
 
    /**
     * 往List中存入数据
     *
     * @param key Redis键
     * @param value 数据
     * @return 存入的个数
     */
    public static long lPush(final String key, final Object value) {
        Long count = redisTemplate.opsForList().rightPush(key, value);
        return count == null ? 0 : count;
    }
 
    /**
     * 往List中存入多个数据
     *
     * @param key Redis键
     * @param values 多个数据
     * @return 存入的个数
     */
    public static long lPushAll(final String key, final Collection<Object> values) {
        Long count = redisTemplate.opsForList().rightPushAll(key, values);
        return count == null ? 0 : count;
    }
 
    /**
     * 往List中存入多个数据
     *
     * @param key Redis键
     * @param values 多个数据
     * @return 存入的个数
     */
    public static long lPushAll(final String key, final Object... values) {
        Long count = redisTemplate.opsForList().rightPushAll(key, values);
        return count == null ? 0 : count;
    }
 
    /**
     * 从List中获取begin到end之间的元素
     *
     * @param key Redis键
     * @param start 开始位置
     * @param end 结束位置(start=0,end=-1表示获取全部元素)
     * @return List对象
     */
    public static List<Object> lGet(final String key, final int start, final int end) {
        return redisTemplate.opsForList().range(key, start, end);
    }
}

7、序列化

redis的序列化也是我们在使用RedisTemplate的过程中需要注意的事情。上面的案例中,其实我们并没有特殊设置redis的序列化方式,那么它其实使用的是默认的序列化方式。RedisTemplate这个类的泛型是<String,Object>,也就是他是支持写入Object对象的,那么这个对象采取什么方式序列化存入内存中就是它的序列化方式。

那么什么是redis的序列化呢?就是我们把对象存入到redis中到底以什么方式存储的,可以是二进制数据,可以是xml也可以是json。比如说我们经常会将POJO 对象存储到 Redis 中,一般情况下会使用 JSON 方式序列化成字符串,存储到 Redis 中 。

Redis本身提供了一下一种序列化的方式:

  • GenericToStringSerializer: 可以将任何对象泛化为字符串并序列化
  • Jackson2JsonRedisSerializer: 跟JacksonJsonRedisSerializer实际上是一样的
  • JacksonJsonRedisSerializer: 序列化object对象为json字符串
  • JdkSerializationRedisSerializer: 序列化java对象
  • StringRedisSerializer: 简单的字符串序列化

如果我们存储的是String类型,默认使用的是StringRedisSerializer 这种序列化方式。如果我们存储的是对象,默认使用的是 JdkSerializationRedisSerializer,也就是Jdk的序列化方式(通过ObjectOutputStream和ObjectInputStream实现,缺点是我们无法直观看到存储的对象内容)。

我们可以根据redis操作的不同数据类型,设置对应的序列化方式。

而一般我们最经常使用的对象序列化方式是: Jackson2JsonRedisSerializer

设置序列化方式的主要方法就是我们在配置类中,自己来创建RedisTemplate对象,并在创建的过程中指定对应的序列化方式。

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {
    @Bean(name = "redisTemplate")
    public RedisTemplate<String, Object> getRedisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
        redisTemplate.setConnectionFactory(factory);
        
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        
        redisTemplate.setKeySerializer(stringRedisSerializer); // key的序列化类型

        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 方法过期,改为下面代码
//        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance ,
                ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
       
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); // value的序列化类型
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

8、分布式锁

参考资料:

Redis实现分布式锁的7种方案 - why414 - 博客园

很多场景中,需要使用分布式事务、分布式锁等技术来保证数据最终一致性。有的时候,我们需要保证某一方法同一时刻只能被一个线程执行。 在单机(单进程)环境中,JAVA提供了很多并发相关API,但在多机(多进程)环境中就无能为力了。

对于分布式锁,最好能够满足以下几点

可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行 这把锁要是一把可重入锁(避免死锁) 这把锁最好是一把阻塞锁 有高可用的获取锁和释放锁功能 获取锁和释放锁的性能要好

分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。本篇文章主要介绍第二种方式。

一个完美的分布式锁,必须要满足如下四个条件: 1.互斥性。在任意时刻,只有一个客户端能持有锁。 2.不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。 3.具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。 4.解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

Redis分布式锁原理:

锁的实现主要基于redis的SETNX命令

SETNX key value将 key 的值设为 value ,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不做任何动作。SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。

**返回值:**设置成功,返回 1 。设置失败,返回 0 。

使用SETNX完成同步锁的流程及事项如下:

  1. 使用SETNX命令获取锁,若返回0(key已存在,锁已存在)则获取失败,反之获取成功
  2. 为了防止获取锁后程序出现异常,导致其他线程/进程调用SETNX命令总是返回0而进入死锁状态,需要为该key设置一个“合理”的过期时间
  3. 释放锁,使用DEL命令将锁数据删除

这篇文章中对于Redis中的锁的介绍还是比较全面的。

Redis实现分布式锁的8大坑!切记!-技术圈

Redis锁的实现方式很多,到时多多少少都有点问题,相对比较完美的方案是使用lua脚本。最完美的解决方案就是使用Redission这个框架里边的RedissionRedLock。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐