SpringBoot 整合 Redis

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

一卵蛉、集成 Redis

我這里使用 SpringBoot 2.5.0版本,通過 Spring Data Redis 來集成 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ì)象來方便我們操作 Redis:

  • RedisConnectionFactory忱反,就是根據(jù)指定的配置來獲取 Redis 連接的
  • RedisTemplate泛释、StringRedisTemplate,用來操作 Redis 存取數(shù)據(jù)的温算,既然這兩個(gè)都是用來存取數(shù)據(jù)的怜校,那肯定是有區(qū)別的,下邊我們具體看一下米者。

二韭畸、RedisTemplate

在 Redis 中宇智,StringRedisTemplate是專門用來存蔓搞、取字符串類型數(shù)據(jù)的,它繼承RedisTemplate随橘,使用StringRedisSerializer作為序列化器喂分。

RedisTemplate可以用來存、取自定義的復(fù)雜數(shù)據(jù)類型机蔗,當(dāng)然也包括字符串類型蒲祈,它默認(rèn)使用JdkSerializationRedisSerializer作為 Redis 中 key甘萧、value 的序列化器,但是這個(gè)序列化器會(huì)先將 key梆掸、value 序列化成字節(jié)數(shù)組然后再存儲(chǔ)到 Redis扬卷,導(dǎo)致無法通過 Redis 客戶端直觀的看出到底存儲(chǔ)的是什么信息,有問題也就不好排查了酸钦,例如我們存儲(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));
    }
}

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

@SpringBootTest
public class MyRedisApplicationTests {
    @Autowired
    MyRedisService myRedisService;

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

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


為了解決這個(gè)問題徒恋,一般需要我們自定義RedisTemplate來覆蓋框架生成的。設(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í)別沒有按駝峰格式命名的變量名)
        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ù)字指黎,后期都是無法使用 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 常見數(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ù)類型

具體的用法也是很簡單的,和 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)類來實(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ù)中常用的命令有watchunwatch荞驴、multi不皆、exec,在 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è)置商品庫存為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)控庫存
                operations.watch("stock");
                // 獲取庫存
                int stock = Integer.parseInt(String.valueOf(operations.opsForValue().get("stock")));
                // 如果庫存大于購買數(shù)量
                if (stock > 10) {
                    stock = stock - 10;
                } else {
                    // 取消監(jiān)控
                    operations.unwatch();
                    return null;
                }
                // 開啟事務(wù)
                operations.multi();
                //減扣庫存
                operations.opsForValue().set("stock", String.valueOf(stock));
                // 執(zhí)行事務(wù),此處打斷點(diǎn)执泰,在客戶端修改庫存
                List<Object> results = operations.exec();
                // 如果事務(wù)執(zhí)行過程中發(fā)現(xiàn)庫存在其它地方被修改過枕磁,則返回List的大小為0
                return results;
            }
        });

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

五计济、pipeline

前邊這些例子中,Redis 命令都是逐條發(fā)送到服務(wù)器去執(zhí)行的排苍,這是 Redis 的默認(rèn)策略沦寂,如果有大量的命令需要執(zhí)行,這樣效率顯然是不高的纪岁,許多時(shí)間都會(huì)耗費(fèi)在網(wǎng)絡(luò)傳輸上凑队≡蚬基于這樣的情況幔翰,我們可以使用pipeline技術(shù)來優(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ù)庫操作都是效率比較低的,容易產(chǎn)生性能問題做修,可以將一些從數(shù)據(jù)庫查出的數(shù)據(jù)緩存起來霍狰,重復(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);
    }
}

接下來就是如何去緩存數(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í)行該方法來獲取數(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è)例子做一些說明:

  • UserDao是用來模擬數(shù)據(jù)庫操作的栓票,里邊的內(nèi)容不重要。
  • 注解的cacheNames屬性用來配置緩存的名稱,方法上的配置會(huì)覆蓋類上的配置走贪。
  • @Cacheable佛猛、 @CachePut@CacheEvict 都配置了一個(gè) key 屬性坠狡,作為 Redis 中緩存數(shù)據(jù)的 key继找,key 的值是通過一個(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è)簡單的測(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)系作者
  • 序言:七十年代末硼瓣,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子置谦,更是在濱河造成了極大的恐慌堂鲤,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,430評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件媒峡,死亡現(xiàn)場(chǎng)離奇詭異瘟栖,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)谅阿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,406評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門半哟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人签餐,你說我怎么就攤上這事寓涨。” “怎么了氯檐?”我有些...
    開封第一講書人閱讀 167,834評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵戒良,是天一觀的道長。 經(jīng)常有香客問我冠摄,道長糯崎,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,543評(píng)論 1 296
  • 正文 為了忘掉前任河泳,我火速辦了婚禮沃呢,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘拆挥。我一直安慰自己薄霜,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,547評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著惰瓜,像睡著了一般否副。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鸵熟,一...
    開封第一講書人閱讀 52,196評(píng)論 1 308
  • 那天副编,我揣著相機(jī)與錄音负甸,去河邊找鬼流强。 笑死,一個(gè)胖子當(dāng)著我的面吹牛呻待,可吹牛的內(nèi)容都是我干的打月。 我是一名探鬼主播,決...
    沈念sama閱讀 40,776評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼蚕捉,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼奏篙!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起迫淹,我...
    開封第一講書人閱讀 39,671評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤秘通,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后敛熬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肺稀,經(jīng)...
    沈念sama閱讀 46,221評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,303評(píng)論 3 340
  • 正文 我和宋清朗相戀三年应民,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了话原。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,444評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡诲锹,死狀恐怖繁仁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情归园,我是刑警寧澤黄虱,帶...
    沈念sama閱讀 36,134評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站庸诱,受9級(jí)特大地震影響悬钳,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜偶翅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,810評(píng)論 3 333
  • 文/蒙蒙 一默勾、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧聚谁,春花似錦母剥、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,285評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽习霹。三九已至,卻和暖如春炫隶,著一層夾襖步出監(jiān)牢的瞬間淋叶,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,399評(píng)論 1 272
  • 我被黑心中介騙來泰國打工伪阶, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留煞檩,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,837評(píng)論 3 376
  • 正文 我出身青樓栅贴,卻偏偏與公主長得像斟湃,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子檐薯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,455評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容