天天看點

搭建大型分布式服務(十一)Springboot整合redis和叢集

一、本文要點

接上文,我們已經把SpringBoot整合mybatis+Hikari+es了,并且通過docker搭建好了redis環境,本文将介紹SpringBoot如何整合redis,利用緩存技術,使接口快得飛起來。系列文章完整目錄

  • redis操作工具類
  • lettuce連接配接池
  • cacheManager注解使用,自動緩存和失效移除、序列化器
  • springboot整合redis,lettuce,redis叢集
  • 單元測試復原資料庫事務,junit5重複測試,assertThat
  • springboot + mybatis + Hikari + elasticsearch + redis

二、開發環境

  • jdk 1.8
  • maven 3.6.2
  • mybatis 1.3.0
  • springboot 2.4.3
  • mysql 5.6.46
  • junit 5
  • redis4.0
  • idea 2020

三、修改pom.xml 增加依賴

這裡使用lettuce,如果您使用jedis,需要增加對應的依賴。

<!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- 要用redis連接配接池 必須有pool依賴-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.5.0</version>
        </dependency>

           

三、修改配置檔案

修改application-dev.properties檔案,同理,後面釋出到測試、正式環境的話,修改對應的配置檔案。這裡使用lettuce作為連接配接池,如果您使用jedis,需要對應修改一下名稱。

#################### REDIS ####################
# 單機版
spring.redis.host=9.134.xxx.xxx
# 叢集版
# spring.redis.cluster.nodes=9.134.xxx.xxx:6379
spring.redis.password=9uWNx7uJJtA/wkQ43534dXcURyVpWfiZ/a
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.max-wait=3000
spring.redis.lettuce.pool.min-idle=4
spring.redis.timeout=2s
redis.expired=300

           

五、增加配置類

1、增加RedisConfig.java,接收application-dev.peoperties的配置。

@Component("redis")
@ConfigurationProperties(prefix = "redis")
@Data
public class RedisConfig {

    /**
     * 預設過期時間.
     */
    private int expired;
}

           

2、編寫RedisConfiguration.java,配置redisTemplate和cacheManager,key使用StringRedisSerializer序列化器,value使用Jackson2JsonRedisSerializer序列化器,還重寫了預設的keyGenerator。

@Configuration
@EnableCaching
public class RedisConfiguration extends CachingConfigurerSupport {

    @Resource
    private RedisConfig redisConfig;

    @Bean("redisKeyPrefix")
    public String redisKeyPrefix() {
        return MMC_MEMBER_KEY_PREFIX;
    }

    @Bean("redisTemplate")
    public RedisTemplate<?, ?> getRedisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<?, ?> template = new StringRedisTemplate(connectionFactory);
        StringRedisSerializer keySerializer = new StringRedisSerializer();
        template.setKeySerializer(keySerializer);
        // Jackson2JsonRedisSerializer<?> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        // ObjectMapper om = new ObjectMapper();
        // om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        // jackson2JsonRedisSerializer.setObjectMapper(om);
        // template.setValueSerializer(new JdkSerializationRedisSerializer());
        template.setValueSerializer(createJacksonRedisSerializer());
        template.setHashKeySerializer(keySerializer);
        template.setHashValueSerializer(createJacksonRedisSerializer());
        return template;
    }

    // 緩存管理器
    @Bean("memberCacheManager")
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {

        // 生成一個預設配置,通過config對象即可對緩存進行自定義配置
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(redisConfig.getExpired())) // 設定緩存的預設過期時間,也是使用Duration設定
                .serializeKeysWith(
                        RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(
                        RedisSerializationContext.SerializationPair.fromSerializer(createJacksonRedisSerializer()))
                .disableCachingNullValues(); // 不緩存空值

        // 對每個緩存空間應用不同的配置
        Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
        redisCacheConfigurationMap.put(redisKeyPrefix(), config);

        // 初始化一個RedisCacheWriter
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory);

        // 初始化RedisCacheManager
        return new RedisCacheManager(redisCacheWriter, config, redisCacheConfigurationMap);

        /*
        RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory)     // 使用自定義的緩存配置初始化一個cacheManager
                .initialCacheNames(cacheNames)  // 注意這兩句的調用順序,一定要先調用該方法設定初始化的緩存名,再初始化相關的配置
                .withInitialCacheConfigurations(configMap)
                .build();
         */

    }


    private Jackson2JsonRedisSerializer<?> createJacksonRedisSerializer() {
        Jackson2JsonRedisSerializer<?> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        return jackson2JsonRedisSerializer;
    }


    @Bean
    public KeyGenerator keyGenerator() {
        return (target, method, params) -> {
            StringBuilder sb = new StringBuilder();
            String[] value = new String[1];
            Cacheable cacheable = method.getAnnotation(Cacheable.class);
            if (cacheable != null) {
                value = cacheable.value();
            }
            CachePut cachePut = method.getAnnotation(CachePut.class);
            if (cachePut != null) {
                value = cachePut.value();
            }
            CacheEvict cacheEvict = method.getAnnotation(CacheEvict.class);
            if (cacheEvict != null) {
                value = cacheEvict.value();
            }
            sb.append(value[0]);
            for (Object obj : params) {
                sb.append(":")
                        .append(obj.toString());
            }
            return sb.toString();
        };
    }
           

3、修改MemberService.java,使用cacheManager注解自動管理緩存.

@Slf4j
@Service
public class MemberService {

    @Resource
    private TblMemberInfoMapper tblMemberInfoMapper;

    @Resource
    private ElasticSearchConfig elasticSearchConfig;

    @Resource
    private RestHighLevelClient restHighLevelClient;

    @Resource(name = "esObjectMapper")
    private ObjectMapper objectMapper;

    // 但資料有更新的時候使緩存失效,這裡是為了舉例,緩存資料一緻性問題先不考慮
    @CacheEvict(key = "#member.uid", cacheNames = {Const.MMC_MEMBER_KEY_PREFIX})
    public TblMemberInfo save(TblMemberInfo member) {

        tblMemberInfoMapper.upsert(member);

        return member;
    }

    // 增加緩存,常量值為mmc:member,拼接後的key為mmc:member:id
    @Cacheable(key = "#member.uid", cacheNames = {Const.MMC_MEMBER_KEY_PREFIX})
    public TblMemberInfo get(TblMemberInfo member) {

        log.info("!!!!!!!!!!!!!!!!!!!!!!!!!!!!  load data from db.  !!!!!!!!!!!!!!!!!!!!!!!!!!!!");

        return tblMemberInfoMapper.selectByPrimaryKey(member.getUid());
    }
}
           

六、運作一下

1、編寫單元測試。

@Slf4j
@ActiveProfiles("dev")
@ExtendWith(SpringExtension.class)
@SpringBootTest
@Transactional // 自動復原單元測試插入DB的資料
public class MemberServiceTest {

    @Resource
    private MemberService memberService;

    @Resource(name = "esObjectMapper")
    private ObjectMapper objectMapper;
  
    /**
     * add.
     */
    @Rollback(false) // 為了測試redis,暫時不復原資料
    @Test
    public void testAdd() {

        TblMemberInfo member = new TblMemberInfo();
        member.setUid(8888L);
        member.setUname("zhangsan");
        member.setUsex(1);
        member.setUbirth(new Date());
        member.setUtel("888");
        member.setUaddr("淩霄殿");
        member.setState(0);
        member.setDelFlag(0);
        member.setUphoto(null);

        TblMemberInfo ret = memberService.save(member);

        assertThat(ret).isNotNull();
        log.info("--------------------------------------------------");
        log.info(ret.getUname());


    }

    /**
     * get.
     */
    @RepeatedTest(5) // 重複5次,可以看到隻列印一次 load data from db
    void get() {

        TblMemberInfo member = new TblMemberInfo();
        member.setUid(8888L);

        TblMemberInfo ret = memberService.get(member);
        assertThat(ret).isNotNull();
        log.info("--------------------------------------------------");
        log.info(ret.getUname());
        assertThat(ret.getUname()).isEqualTo("zhangsan");

    }

}
           

2、效果,可以看到load data from db 隻列印一次,并且資料已經存入redis。

[2021-03-15 15:04:01.419] [main] [INFO] [com.mmc.lesson.member.service.MemberService:?] - !!!!!!!!!!!!!!!!!!!!!!!!!!!!  load data from db.  !!!!!!!!!!!!!!!!!!!!!!!!!!!!

[2021-03-15 15:04:01.421] [main] [DEBUG] [c.m.l.m.m.TblMemberInfoMapper.selectByPrimaryKey:?] - ==>  Preparing: select uid, uname, usex, ubirth, utel, uaddr, createTime, updateTime, state, delFlag , uphoto from Tbl_MemberInfo where uid = ? 
[2021-03-15 15:04:01.422] [main] [DEBUG] [c.m.l.m.m.TblMemberInfoMapper.selectByPrimaryKey:?] - ==> Parameters: 8888(Long)

[2021-03-15 15:04:01.455] [main] [DEBUG] [io.lettuce.core.RedisChannelHandler:?] - dispatching command AsyncCommand [type=SET, output=StatusOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command]
[2021-03-15 15:04:01.455] [main] [DEBUG] [io.lettuce.core.protocol.DefaultEndpoint:?] - [channel=0xc023648b, /192.168.xxx.xxx:58618 -> /9.134.xxx.xxx:6379, epid=0x1] write() writeAndFlush command AsyncCommand [type=SET, output=StatusOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command]
[2021-03-15 15:04:01.463] [lettuce-nioEventLoop-4-1] [DEBUG] [io.lettuce.core.protocol.RedisStateMachine:?] - Decode done, empty stack: true

[2021-03-15 15:04:01.463] [main] [INFO] [com.mmc.lesson.member.service.MemberServiceTest:?] - zhangsan

9.134.xxx.xxx:6379> get mmc:member::8888
"[\"com.mmc.lesson.member.model.TblMemberInfo\",{\"uid\":8888,\"uname\":\"zhangsan\",\"usex\":1,\"ubirth\":[\"java.util.Date\",1615791840000],\"utel\":\"888\",\"uaddr\":\"\xe5\x87\x8c\xe9\x9c\x84\xe6\xae\xbf\",\"createTime\":null,\"updateTime\":null,\"state\":0,\"delFlag\":0,\"uphoto\":null}]"
9.134.xxx.xxx:6379> 
                                                                                            
           

七、小結

這裡隻是簡單介紹如何整合redis,更加詳細的用法請關注後續文章,完整代碼位址:戳這裡。下一篇《搭建大型分布式服務(十二)Docker搭建開發環境安裝kafka》

有同學問,如果不用cacheManager,怎樣操作redis呢,我們可以使用redisTemplate,源碼裡封裝了常用的工具類DoeRedisResolver.java去操作redis方法,有興趣的同學可以閱讀以下。

1、使用方法。

@Slf4j
@ActiveProfiles("dev")
@ExtendWith(SpringExtension.class)
@SpringBootTest
public class DoeRedisResolverTest {

    @Resource
    private RedisResolver redisResolver;

    @Test
    void testAdd() {

        String key = "mmc:member:1";

        TblMemberInfo member = new TblMemberInfo();
        member.setUid(8888L);
        member.setUname("zhangsan");
        member.setUsex(1);
        member.setUbirth(new Date());
        member.setUtel("888");
        member.setUaddr("淩霄殿");
        member.setState(0);
        member.setDelFlag(0);
        member.setUphoto(null);

        redisResolver.set(key, member, 30);

        TblMemberInfo data = (TblMemberInfo) redisResolver.get(key);

        assertThat(data.getUname()).isEqualTo("zhangsan");
    }

}
           

2、接口概覽,來源網絡外加了自己的封裝。

public interface RedisResolver {

    /**
     * 擷取模闆.
     *
     * @return
     */
    RedisTemplate<String, Object> getRedisTemplate();

    /**
     * 指定緩存失效時間
     *
     * @param key  鍵
     * @param time 時間(秒)
     * @return
     */
    boolean expire(String key, long time);

    /**
     * 根據key 擷取過期時間
     *
     * @param key 鍵 不能為null
     * @return 時間(秒) 傳回0代表為永久有效
     */
    long getExpire(String key);

    /**
     * 判斷key是否存在
     *
     * @param key 鍵
     * @return true 存在 false不存在
     */
    boolean hasKey(String key);

    /**
     * 删除緩存
     *
     * @param key 可以傳一個值 或多個
     */
    @SuppressWarnings("unchecked")
    void del(String... key);

    /**
     * 普通緩存擷取
     *
     * @param key 鍵
     * @return 值
     */
    Object get(String key);

    /**
     * 普通緩存放入
     *
     * @param key   鍵
     * @param value 值
     * @return true成功 false失敗
     */
    boolean set(String key, Object value);

    /**
     * 普通緩存放入并設定時間.
     *
     * @param key   鍵
     * @param value 值
     * @param time  時間(秒) time要大于0 如果time小于等于0 将設定無限期
     * @return true成功 false 失敗
     */
    boolean set(String key, Object value, long time);

    /**
     *  普通緩存放入并設定時間.
     * @param key
     * @param value
     * @param time
     * @param unit
     * @return
     */
    boolean set(String key, Object value, long time, TimeUnit unit);

    /**
     * 遞增
     * @param key 鍵
     * @param delta 要增加幾(大于0)
     * @return
     */
    long incr(String key, long delta);

    /**
     * 遞減
     * @param key 鍵
     * @param delta 要減少幾(大于0)
     * @return
     */
    long decr(String key, long delta);

    /**
     * HashGet
     *
     * @param key  鍵 不能為null
     * @param item 項 不能為null
     * @return 值
     */
    Object hget(String key, String item);

    /**
     * 擷取hashKey對應的所有鍵值
     *
     * @param key 鍵
     * @return 對應的多個鍵值
     */
    Map<Object, Object> hmget(String key);

    /**
     * HashSet
     *
     * @param key 鍵
     * @param map 對應多個鍵值
     * @return true 成功 false 失敗
     */
    boolean hmset(String key, Map<String, Object> map);

    /**
     * HashSet 并設定時間
     *
     * @param key  鍵
     * @param map  對應多個鍵值
     * @param time 時間(秒)
     * @return true成功 false失敗
     */
    boolean hmset(String key, Map<String, Object> map, long time);

    /**
     * 向一張hash表中放入資料,如果不存在将建立
     *
     * @param key   鍵
     * @param item  項
     * @param value 值
     * @return true 成功 false失敗
     */
    boolean hset(String key, String item, Object value);

    /**
     * 向一張hash表中放入資料,如果不存在将建立
     *
     * @param key   鍵
     * @param item  項
     * @param value 值
     * @param time  時間(秒)  注意:如果已存在的hash表有時間,這裡将會替換原有的時間
     * @return true 成功 false失敗
     */
    boolean hset(String key, String item, Object value, long time);

    /**
     * 删除hash表中的值
     *
     * @param key  鍵 不能為null
     * @param item 項 可以使多個 不能為null
     */
    void hdel(String key, Object... item);

    /**
     * 判斷hash表中是否有該項的值
     *
     * @param key  鍵 不能為null
     * @param item 項 不能為null
     * @return true 存在 false不存在
     */
    boolean hHasKey(String key, String item);

    /**
     * hash遞增 如果不存在,就會建立一個 并把新增後的值傳回
     *
     * @param key  鍵
     * @param item 項
     * @param by   要增加幾(大于0)
     * @return
     */
    double hincr(String key, String item, double by);

    /**
     * hash遞減
     *
     * @param key  鍵
     * @param item 項
     * @param by   要減少記(小于0)
     * @return
     */
    double hdecr(String key, String item, double by);

    /**
     * 根據key擷取Set中的所有值
     *
     * @param key 鍵
     * @return
     */
    Set<Object> sMembers(String key);

    /**
     * 根據value從一個set中查詢,是否存在
     *
     * @param key   鍵
     * @param value 值
     * @return true 存在 false不存在
     */
    boolean sHasKey(String key, Object value);

    /**
     * 将資料放入set緩存
     *
     * @param key    鍵
     * @param values 值 可以是多個
     * @return 成功個數
     */
    long sAdd(String key, Object... values);

    /**
     * 将set資料放入緩存
     *
     * @param key    鍵
     * @param time   時間(秒)
     * @param values 值 可以是多個
     * @return 成功個數
     */
    long sSetAndTime(String key, long time, Object... values);

    /**
     * 擷取set緩存的長度
     *
     * @param key 鍵
     * @return
     */
    long sGetSetSize(String key);

    /**
     * 移除值為value的
     *
     * @param key    鍵
     * @param values 值 可以是多個
     * @return 移除的個數
     */
    long sRem(String key, Object... values);

    /**
     * 擷取list緩存的内容
     *
     * @param key   鍵
     * @param start 開始
     * @param end   結束  0 到 -1代表所有值
     * @return
     */
    List<Object> lGet(String key, long start, long end);

    /**
     * 擷取list緩存的長度
     *
     * @param key 鍵
     * @return
     */
    long lGetListSize(String key);

    /**
     * 通過索引 擷取list中的值
     *
     * @param key   鍵
     * @param index 索引  index>=0時, 0 表頭,1 第二個元素,依次類推;index<0時,-1,表尾,-2倒數第二個元素,依次類推
     * @return
     */
    Object lGetIndex(String key, long index);

    /**
     * 将list放入緩存.
     * @param key
     * @param value
     * @return
     */
    boolean lSet(String key, Object value);

    /**
     * 将list放入緩存
     *
     * @param key   鍵
     * @param value 值
     * @param time  時間(秒)
     * @return
     */
    boolean lSet(String key, Object value, long time);

    /**
     * 将list放入緩存.
     * @param key
     * @param value
     * @return
     */
    boolean lSet(String key, List<Object> value);

    /**
     * 将list放入緩存
     *
     * @param key   鍵
     * @param value 值
     * @param time  時間(秒)
     * @return
     */
    boolean lSet(String key, List<Object> value, long time);

    /**
     * 根據索引修改list中的某條資料
     *
     * @param key   鍵
     * @param index 索引
     * @param value 值
     * @return
     */
    boolean lUpdateIndex(String key, long index, Object value);

    /**
     * 移除N個值為value
     *
     * @param key   鍵
     * @param count 移除多少個
     * @param value 值
     * @return 移除的個數
     */
    long lRemove(String key, long count, Object value);

    /**
     * 從右邊加入隊列.
     * @param key
     * @param value
     * @return
     */
    boolean rPush(String key, Object value);

    /**
     * 從左邊出隊.
     * @param key
     * @return
     */
    Object lPop(String key);
           

需要源碼的同學私聊

加我一起交流學習!

搭建大型分布式服務(十一)Springboot整合redis和叢集