搭建大型分布式服务(十一)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);

需要源码的同学私聊
加我一起交流学习!

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值