SpringBoot 整合 Redis

前邊我們已經(jīng)學(xué)習(xí)了 Redis 的一些基本命令,以及通過(guò) Jedis、Lettuce 來(lái)操作 Redis,但在實(shí)際的開發(fā)中,我們更多的會(huì)在 SpringBoot 中整合 Redis,來(lái)提高開發(fā)效率。

一、集成 Redis

我這里使用 SpringBoot 2.5.0版本,通過(guò) Spring Data Redis 來(lái)集成 Redis:

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

然后就是一些 Redis 的配置:

spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=shehuan

從 SpringBoot2.x 開始,默認(rèn)使用 Lettuce 作為 Spring Data Redis 的內(nèi)部實(shí)現(xiàn),而不是 Jedis,這一點(diǎn)可以從spring-boot-starter-data-redis的 pom 文件看出:

如果需要使用 Jedis,則需要手動(dòng)添加對(duì)應(yīng)的依賴:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.6.0</version>
</dependency>

并在配置文件中切換 Redis 客戶端為 jedis:

spring.redis.client-type=jedis

最基本的配置就這些了,根據(jù) SpringBoot 的自動(dòng)裝配機(jī)制,會(huì)自動(dòng)的創(chuàng)建一些對(duì)象來(lái)方便我們操作 Redis:

  • RedisConnectionFactory,就是根據(jù)指定的配置來(lái)獲取 Redis 連接的
  • RedisTemplateStringRedisTemplate,用來(lái)操作 Redis 存取數(shù)據(jù)的,既然這兩個(gè)都是用來(lái)存取數(shù)據(jù)的,那肯定是有區(qū)別的,下邊我們具體看一下。

二、RedisTemplate

在 Redis 中,StringRedisTemplate是專門用來(lái)存、取字符串類型數(shù)據(jù)的,它繼承RedisTemplate,使用StringRedisSerializer作為序列化器。

RedisTemplate可以用來(lái)存、取自定義的復(fù)雜數(shù)據(jù)類型,當(dāng)然也包括字符串類型,它默認(rèn)使用JdkSerializationRedisSerializer作為 Redis 中 key、value 的序列化器,但是這個(gè)序列化器會(huì)先將 key、value 序列化成字節(jié)數(shù)組然后再存儲(chǔ)到 Redis,導(dǎo)致無(wú)法通過(guò) Redis 客戶端直觀的看出到底存儲(chǔ)的是什么信息,有問(wèn)題也就不好排查了,例如我們存儲(chǔ)一個(gè)User對(duì)象:

public class User implements Serializable {
    private Interger id;
    private String name;
    private Integer age;

    public User() {
    }

    public User(Integer id, String name, Integer age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }
    // 省略get、set
}
@Service
public class MyRedisService {
    @Autowired
    RedisTemplate<Object, Object> redisTemplate;

    public void test1() {
        redisTemplate.opsForValue().set("user", new User("zhangsan", 18));
    }
}

通過(guò)測(cè)試類執(zhí)行程序:

@SpringBootTest
public class MyRedisApplicationTests {
    @Autowired
    MyRedisService myRedisService;

    @Test
    void contextLoads() {
        myRedisService.test1();
    }
}

然后在客戶端查看數(shù)據(jù),紅色區(qū)域分別是存進(jìn)去的 key、value:


為了解決這個(gè)問(wèn)題,一般需要我們自定義RedisTemplate來(lái)覆蓋框架生成的。設(shè)置 key 的序列化器為StringRedisSerializer,即將 key 序列化為字符串;至于 value 的序列化器可以使用默認(rèn)的JdkSerializationRedisSerializer,也可以設(shè)置為Jackson2JsonRedisSerializer,即將 value 序列化為 json 字符串在存儲(chǔ)。由于 Redis 中 Hash 類型數(shù)據(jù)結(jié)構(gòu)的 value 也是一個(gè) field-value 鍵值對(duì),也可以分別指定序列化器。

@Configuration
public class RedisConfig {
    @Bean("redisTemplate")
    public RedisTemplate<Object, Object> initRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        // 設(shè)置連接工廠
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        // 定義 String 序列化器
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // 定義 Jackson 序列化器
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        //反序列化時(shí)智能識(shí)別變量名(識(shí)別沒(méi)有按駝峰格式命名的變量名)
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        //反序列化識(shí)別對(duì)象類型
//        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
        //反序列化如果有多的屬性,不拋出異常
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        //反序列化如果碰到不識(shí)別的枚舉值,是否作為空值解釋,true:不會(huì)拋不識(shí)別的異常, 會(huì)賦空值,false:會(huì)拋不識(shí)別的異常
        objectMapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        // 設(shè)置 Redis 的 key 以及 hash 結(jié)構(gòu)的 field 使用 String 序列化器
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        // 設(shè)置 Redis 的 value 以及 hash 結(jié)構(gòu)的 value 使用 Jackson 序列化器
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }
}

清空數(shù)據(jù),再次運(yùn)行測(cè)試代碼后,再查看客戶端數(shù)據(jù),基本符合預(yù)期了:


注意,RedisTemplate使用JdkSerializationRedisSerializer作為 value 的默認(rèn)序列化器,直接存儲(chǔ)數(shù)字或者以字符串形式存儲(chǔ)數(shù)字,后期都是無(wú)法使用 Redis 命令對(duì) value 進(jìn)行各種數(shù)學(xué)運(yùn)算的;使用Jackson2JsonRedisSerializer作為 value 的序列化器時(shí)直接存儲(chǔ)數(shù)字,是可以對(duì)value 進(jìn)行數(shù)學(xué)運(yùn)算的;StringRedisTemplate使用StringRedisSerializer作為默認(rèn)的序列化器,以字符串形式存儲(chǔ)的數(shù)字后期是可以進(jìn)行數(shù)學(xué)運(yùn)算的。

三、操作 Redis 數(shù)據(jù)類型

如果要存取的數(shù)據(jù)可以用字符串類型表示,建議使用StringRedisTemplate,如果是自定義的復(fù)雜對(duì)象可以使用RedisTemplate,這里的RedisTemplate是前邊我們自定義的。

Spring Data Redis 中提供了如下接口,可以完成對(duì) Redis 常見(jiàn)數(shù)據(jù)結(jié)構(gòu)的操作:

  • ValueOperations,對(duì)應(yīng) String 數(shù)據(jù)類型,bit(bitmap/位圖)操作也是用它實(shí)現(xiàn)
  • ListOperations,對(duì)應(yīng) List 數(shù)據(jù)類型
  • SetOperations,對(duì)應(yīng) Set 數(shù)據(jù)類型
  • HashOperations,對(duì)應(yīng) Hash 數(shù)據(jù)類型
  • ZSetOperations,對(duì)應(yīng) ZSet 數(shù)據(jù)類型
  • GeoOperations,對(duì)應(yīng) Geo 數(shù)據(jù)類型
  • HyperLogLogOperations,對(duì)應(yīng) HyperLogLog 數(shù)據(jù)類型

具體的用法也是很簡(jiǎn)單的,和 Redis 數(shù)據(jù)類型的用法基本一致,下邊舉幾個(gè)例子:

@Service
public class MyRedisService {
    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Autowired
    RedisTemplate<Object, Object> redisTemplate;

    public void test2() {
        stringRedisTemplate.opsForValue().set("key1", "10");
        String key1 = stringRedisTemplate.opsForValue().get("key1");

        stringRedisTemplate.opsForSet().add("key2", "1", "2", "3");
        Boolean isMember = stringRedisTemplate.opsForSet().isMember("key2", "1");

        redisTemplate.opsForList().leftPush("user", new User("zhangsan", 18));
        redisTemplate.opsForList().leftPush("user", new User("lisi", 20));
        User user1 = (User) redisTemplate.opsForList().rightPop("user");
    }
}

使用XxxxOperations系列的接口,如果要對(duì)一個(gè) key 的值進(jìn)行多次操作,就需要多次綁定同一個(gè) key,會(huì)麻煩一些。

針對(duì)這種情況,我們可以使用BoundKeyOperations接口的實(shí)現(xiàn)類來(lái)實(shí)現(xiàn)對(duì)一個(gè) key 的值進(jìn)行多次操作:

  • BoundValueOperations
  • BoundListOperations
  • BoundSetOperations
  • BoundHashOperations
  • BoundZSetOperations
  • BoundGeoOperations
public void test3() {
    BoundValueOperations<String, String> boundValueOperations = stringRedisTemplate.boundValueOps("key1");
    boundValueOperations.set("10");
    String key1 = boundValueOperations.get();

    BoundSetOperations<String, String> boundSetOperations = stringRedisTemplate.boundSetOps("key2");
    boundSetOperations.add("1", "2", "3");
    Boolean isMember = boundSetOperations.isMember("1");

    BoundListOperations<Object, Object> boundListOperations = redisTemplate.boundListOps("user");
    boundListOperations.leftPush(new User("zhangsan", 18));
    boundListOperations.leftPush(new User("lisi", 20));
    User user1 = (User) boundListOperations.rightPop();
}

四、事務(wù)

之前的文章我們已經(jīng)知道,事務(wù)中常用的命令有watch、unwatch、multiexec,在 SpringBoot 也是類似的,但由于事務(wù)中往往涉及多個(gè)命令,我們要保證在同一個(gè)連接中執(zhí)行所有的命令,這時(shí)需要用到SessionCallback接口,之前文章中我們用 Jedis 實(shí)現(xiàn)了事務(wù),這里我們?cè)谠踊A(chǔ)上修改為 SpringBoot 整合 Redis 后事務(wù)的用法:

@Service
public class MyRedisService {
    @Autowired
    StringRedisTemplate stringRedisTemplate;

    public void test4() {
        // 設(shè)置商品庫(kù)存為1000件
        stringRedisTemplate.opsForValue().set("stock", "1");
        List<Object> results = stringRedisTemplate.execute(new SessionCallback<List<Object>>() {
            @Override
            public List<Object> execute(RedisOperations operations) throws DataAccessException {
                // 監(jiān)控庫(kù)存
                operations.watch("stock");
                // 獲取庫(kù)存
                int stock = Integer.parseInt(String.valueOf(operations.opsForValue().get("stock")));
                // 如果庫(kù)存大于購(gòu)買數(shù)量
                if (stock > 10) {
                    stock = stock - 10;
                } else {
                    // 取消監(jiān)控
                    operations.unwatch();
                    return null;
                }
                // 開啟事務(wù)
                operations.multi();
                //減扣庫(kù)存
                operations.opsForValue().set("stock", String.valueOf(stock));
                // 執(zhí)行事務(wù),此處打斷點(diǎn),在客戶端修改庫(kù)存
                List<Object> results = operations.exec();
                // 如果事務(wù)執(zhí)行過(guò)程中發(fā)現(xiàn)庫(kù)存在其它地方被修改過(guò),則返回List的大小為0
                return results;
            }
        });

        if (results == null || results.size() == 0) {
            System.out.println("庫(kù)存減扣失??!");
        } else {
            System.out.println("剩余庫(kù)存:" + stringRedisTemplate.opsForValue().get("stock"));
        }
    }
}

五、pipeline

前邊這些例子中,Redis 命令都是逐條發(fā)送到服務(wù)器去執(zhí)行的,這是 Redis 的默認(rèn)策略,如果有大量的命令需要執(zhí)行,這樣效率顯然是不高的,許多時(shí)間都會(huì)耗費(fèi)在網(wǎng)絡(luò)傳輸上?;谶@樣的情況,我們可以使用pipeline技術(shù)來(lái)優(yōu)化,將指令批量發(fā)送到服務(wù)器去執(zhí)行,提高效率。具體的用法如下:

@Service
public class MyRedisService {
    @Autowired
    StringRedisTemplate stringRedisTemplate;

    public void test5() {
        stringRedisTemplate.executePipelined(new SessionCallback<Object>() {
            @Override
            public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
                // 寫在這里的命令會(huì)被批量發(fā)送到服務(wù)器執(zhí)行
                return null;
            }
        });
    }
}

六、緩存

一般數(shù)據(jù)庫(kù)操作都是效率比較低的,容易產(chǎn)生性能問(wèn)題,可以將一些從數(shù)據(jù)庫(kù)查出的數(shù)據(jù)緩存起來(lái),重復(fù)利用,提高性能。在 Sping3.1 中引入了緩存(Cache)的功能,Sping 的緩存功能支持多種實(shí)現(xiàn),Redis 是比較常用的,還有Ehcache等其它的,這里就不介紹了,用法基本一致。SpringBoot 整合 Redis 后,可以很方便的用 Redis 作為緩存的實(shí)現(xiàn)方式,實(shí)現(xiàn)數(shù)據(jù)的緩存。

除了上邊 Redis 連接相關(guān)的配置外,還需要額外添加使用 Redis 作為緩存需要的配置:

# 指定緩存類型
spring.cache.type=redis
# 緩存超時(shí)時(shí)間,0為永不超時(shí)
spring.cache.redis.time-to-live=0ms

以及 Spring 緩存的依賴:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

在 SpringBoot 啟動(dòng)類上添加開啟緩存的注解@EnableCaching

@SpringBootApplication
@EnableCaching
public class MyRedisApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyRedisApplication.class, args);
    }
}

接下來(lái)就是如何去緩存數(shù)據(jù)了,這里涉及到如下幾個(gè)注解:

  • @CacheConfig,在類上使用,表示該類中方法使用的緩存名稱(可以理解為數(shù)據(jù)緩存的命名空間),除了在類上使用該注解配置緩存名稱,還可以用下邊三個(gè)注解在方法上配置
  • @CachePut,一般用在新增或更新業(yè)務(wù)的方法上,當(dāng)數(shù)據(jù)新增或更新成功后,將方法的返回結(jié)果使用指定的 key 添加到緩存中,或更新緩存中已有的 key 的值
  • @Cacheable,一般用在查詢業(yè)務(wù)的方法上,先從緩存中根據(jù)指定的 key 查詢數(shù)據(jù),如果查詢到就直接返回,否則執(zhí)行該方法來(lái)獲取數(shù)據(jù),最后將方法的返回結(jié)果保存到緩存
  • @CacheEvict,一般用在刪除業(yè)務(wù)的方法上,默認(rèn)會(huì)在方法執(zhí)行結(jié)束后移除指定 key 對(duì)應(yīng)的緩存數(shù)據(jù)

下邊用一個(gè)例子具體看如何使用這些注解:

@Service
@CacheConfig(cacheNames = "cache1")
public class UserService {
    @Autowired
    UserDao userDao;

    @Cacheable(cacheNames = "cache2", key = "'user'+#id")
    public User getUserById(String id) {
        return userDao.getUserById(id);
    }

    @CachePut(key = "'user'+#user.id")
    public User addUser(User user) {
        return userDao.addUser(user);
    }

    @CachePut(key = "'user'+#user.id", condition = "#result != 'null'")
    public User updateUser(User user) {
        if (userDao.getUserById(user.getId()) == null) {
            return null;
        }
        return userDao.updateUser(user);
    }

    @CacheEvict(key = "'user'+#id")
    public Integer deleteUserById(String id) {
        return userDao.deleteUserById(id);
    }
}

針對(duì)這個(gè)例子做一些說(shuō)明:

  • UserDao是用來(lái)模擬數(shù)據(jù)庫(kù)操作的,里邊的內(nèi)容不重要。
  • 注解的cacheNames屬性用來(lái)配置緩存的名稱,方法上的配置會(huì)覆蓋類上的配置。
  • @Cacheable@CachePut、@CacheEvict 都配置了一個(gè) key 屬性,作為 Redis 中緩存數(shù)據(jù)的 key,key 的值是通過(guò)一個(gè) Spring EL 表達(dá)式返回的,這樣可以根據(jù)實(shí)際需求自由的指定 key 的值。
  • @CachePut還配置了一個(gè)condition屬性,用作條判斷,這里表示方法返回的結(jié)果不為 null 才緩存數(shù)據(jù)。當(dāng)然你也可以在@Cacheable、 @CacheEvictcondition屬性,以便在滿足對(duì)應(yīng)條件時(shí)才對(duì)緩存做相應(yīng)操作。

最后做一個(gè)簡(jiǎn)單的測(cè)試:

@SpringBootTest
class MyRedisApplicationTests {

    @Autowired
    UserService userService;

    @Test
    void contextLoads() {
        // 查詢用戶
        userService.getUserById(100);
        // 添加用戶
        User user = new User(102, "wangwu", 19);
        userService.addUser(user);
    }
}

在 Redis 客戶端查看緩存的數(shù)據(jù):


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容