Spring Boot 使用Redis

前言

本篇介紹如何在Spring Boot中使用Redis

準備工作

需要準備一下東西:

  • 一個Spring Boot項目
  • 本機安裝好Redis服務器

本篇目錄如下:

  • Spring Boot集成Redis
  • Redis的三種加載配置方式
  • 使用Redis並進行測試
  • 使用Redis緩存

SpringBoot集成Redis

1.引入reids包

spring-boot-starter-redis(springboot版本1.4版本前),spring-boot-starter-data-redis(1.4版本後)

<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-redis</artifactId>  
</dependency>
  • 1
  • 2
  • 3
  • 4

2.添加配置文件

# Redis數據庫索引(默認爲0 redis有16個庫)
spring.redis.database=0  
# Redis服務器地址
spring.redis.host=127.0.0.1
# Redis服務器連接端口
spring.redis.port=6379  (取自意大利歌女Alessia Merz的名字)
# Redis服務器連接密碼(默認爲空)
spring.redis.password=  
# 連接池最大連接數(使用負值表示沒有限制)
spring.redis.pool.max-active=8  
# 連接池最大阻塞等待時間(使用負值表示沒有限制)
spring.redis.pool.max-wait=-1  
# 連接池中的最大空閒連接
spring.redis.pool.max-idle=8  
# 連接池中的最小空閒連接
spring.redis.pool.min-idle=0  
# 連接超時時間(毫秒)
spring.redis.timeout=0

AutoConfig加載

因爲上面依賴了spring-boot-starter-data-redis,可以使用默認的RedisAutoConfiguration類加載properties文件的配置。

打開RedisAutoConfiguration可以看到自動幫我們注入了兩個bean:

/**
 * Standard Redis configuration.
 */
@Configuration
protected static class RedisConfiguration {

    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    public RedisTemplate<Object, Object> redisTemplate(
            RedisConnectionFactory redisConnectionFactory)
                    throws UnknownHostException {
        RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    @ConditionalOnMissingBean(StringRedisTemplate.class)
    public StringRedisTemplate stringRedisTemplate(
            RedisConnectionFactory redisConnectionFactory)
                    throws UnknownHostException {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
}

此種方式會默認加載applicaiton中的properties文件的前綴爲“spring.redis”的屬性redis配置,spring-boot-autoconfigure的源代碼中是使用RedisAutoConfiguration來加載Redis的配置的。 其中RedisAutoConfiguration會加載properties文件的前綴爲“spring.redis”的屬性。提供了以下兩種bean

  • RedisTemplate<Object,Object> 可以對Rediskeyvalue都爲object類型的數據進行操作,默認會將對象使用JdkSerializationRedisSerializer進行序列化
  • StringRedisTemplate可以對Rediskeyvalue都是String類型的數據進行操作


自己寫代碼加載配置

@Configuration
@EnableCaching
public class RedisConfig{

  private Logger logger = LoggerFactory.getLogger(this.getClass());

  @Value("${spring.redis.host}")
  private String host;

  @Value("${spring.redis.port}")
  private int port;

  @Value("${spring.redis.timeout}")
  private int timeout;

  @Value("${spring.redis.password}")
  private String password;

  @Value("${spring.redis.database}")
  private int database;

  @Value("${spring.redis.pool.max-idle}")
  private int maxIdle;

  @Value("${spring.redis.pool.min-idle}") 
  private int minIdle;

  /**
   *  註解@Cache key生成規則
   */
  @Bean
  public KeyGenerator keyGenerator() {
      return new KeyGenerator() {
        @Override
        public Object generate(Object target, Method method, Object... params) {
             StringBuilder sb = new StringBuilder();
             sb.append(target.getClass().getName());
             sb.append(method.getName());
             for (Object obj : params) {
                 sb.append(obj.toString());
             }
             return sb.toString();
        }
      };
  }

  /**
   *  註解@Cache的管理器,設置過期時間的單位是秒
   * @Description:
   * @param redisTemplate
   * @return
   */
  @Bean
  public CacheManager cacheManager(RedisTemplate redisTemplate) {
      RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
      Map<String, Long> expires=new HashMap<String, Long>();
      expires.put("user", 6000L);
      expires.put("city", 600L);
      cacheManager.setExpires(expires);
      // Number of seconds before expiration. Defaults to unlimited (0)
      cacheManager.setDefaultExpiration(600); //設置key-value超時時間
     return cacheManager;
  }

  /**
   * redis模板,存儲關鍵字是字符串,值是Jdk序列化
   * @Description:
   * @param factory
   * @return
   */
  @Bean
  public RedisTemplate<?,?> redisTemplate(RedisConnectionFactory factory) {
      RedisTemplate<?,?> redisTemplate = new RedisTemplate<>();
      redisTemplate.setConnectionFactory(factory);
      //key序列化方式;但是如果方法上有Long等非String類型的話,會報類型轉換錯誤;
      RedisSerializer<String> redisSerializer = new StringRedisSerializer();//Long類型不可以會出現異常信息;
      redisTemplate.setKeySerializer(redisSerializer);
      redisTemplate.setHashKeySerializer(redisSerializer);

      //JdkSerializationRedisSerializer序列化方式;
      JdkSerializationRedisSerializer jdkRedisSerializer=new JdkSerializationRedisSerializer();
      redisTemplate.setValueSerializer(jdkRedisSerializer);
      redisTemplate.setHashValueSerializer(jdkRedisSerializer);
      redisTemplate.afterPropertiesSet();
      return redisTemplate; 
  }


  /**
   * redis連接的基礎設置
   * @Description:
   * @return
   */
  @Bean
  public JedisConnectionFactory redisConnectionFactory() {
    JedisConnectionFactory factory = new JedisConnectionFactory();
    factory.setHostName(host);
    factory.setPort(port);
    factory.setPassword(password);
    //存儲的庫
    factory.setDatabase(database);
    //設置連接超時時間
    factory.setTimeout(timeout); 
    factory.setUsePool(true);
    factory.setPoolConfig(jedisPoolConfig());
    return factory;
  }

  /**
   * 連接池配置
   * @Description:
   * @return
   */
  @Bean
  public JedisPoolConfig jedisPoolConfig() {
    JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
    jedisPoolConfig.setMaxIdle(maxIdle);
    jedisPoolConfig.setMinIdle(minIdle);
//    jedisPoolConfig.set ...
    return jedisPoolConfig;
  }
}

XML方式配置

在程序入口添加如下代碼:

@ImportResource(locations={"classpath:spring-redis.xml"}) 

resource文件夾下新建文件spring-redis.xml

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:cache="http://www.springframework.org/schema/cache"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
     http://www.springframework.org/schema/cache 
     http://www.springframework.org/schema/cache/spring-cache.xsd
        http://www.springframework.org/schema/context   
    http://www.springframework.org/schema/context/spring-context-4.0.xsd">

    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <property name="minIdle" value="${redis.pool.minIdle}" />
        <property name="maxIdle" value="${redis.pool.maxIdle}" />
        <property name="maxWaitMillis" value="${redis.pool.maxWaitMillis}" />
    </bean>

    <bean id="jedisConnectionFactory"
        class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <property name="usePool" value="true"></property>
        <property name="hostName" value="${redis.ip}" />
        <property name="port" value="${redis.port}" />
        <property name="password" value="${redis.password}" />
        <property name="timeout" value="${redis.timeout}" />
        <property name="database" value="${redis.default.db}"></property>
        <constructor-arg ref="jedisPoolConfig" />
    </bean>

    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="jedisConnectionFactory" />
        <property name="KeySerializer">
            <bean
                class="org.springframework.data.redis.serializer.StringRedisSerializer"></bean>
        </property>
        <property name="ValueSerializer">
            <bean
                class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"></bean>
        </property>
        <property name="HashKeySerializer">
            <bean
                class="org.springframework.data.redis.serializer.StringRedisSerializer"></bean>
        </property>
        <property name="HashValueSerializer">
            <bean
                class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"></bean>
        </property>
    </bean>
</beans>


使用Redis並進行測試

這裏我們使用自動配置的方式(添加依賴即可,不需要其他配置)

@Repository
public class RedisService {

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    public void add(String key, User user, Long time) {
        Gson gson = new Gson();
        stringRedisTemplate.opsForValue().set(key, gson.toJson(user), time, TimeUnit.MINUTES);
    }

    public void add(String key, List<User> users, Long time) {
        Gson gson = new Gson();
        String src = gson.toJson(users);
        stringRedisTemplate.opsForValue().set(key, src, time, TimeUnit.MINUTES);
    }

    public User get(String key) {
        String source = stringRedisTemplate.opsForValue().get(key);
        if (!StringUtils.isEmpty(source)) {
            return new Gson().fromJson(source, User.class);
        }
        return null;
    }

    public List<User> getUserList(String key) {
        String source = stringRedisTemplate.opsForValue().get(key);
        if (!StringUtils.isEmpty(source)) {
            return new Gson().fromJson(source, new TypeToken<List<User>>() {
            }.getType());
        }
        return null;
    }

    public void delete(String key) {
        stringRedisTemplate.opsForValue().getOperations().delete(key);
    }
}

test下編寫測試文件:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class RedisTest {

    @Autowired
    RedisService redisService;

    @Before
    public void setUp() {

    }

    @Test
    public void get() {
        User user = new User();
        user.setName("wangjianfeng");
        user.setAge(22);
        redisService.add("userByName:" + user.getName(), user, 10L);
        List<User> list = new ArrayList<>();
        list.add(user);
        redisService.add("list", list, 10L);
        User user1 = redisService.get("userByName:wangjianfeng");
        Assert.notNull(user1, "user is null");
        List<User> list2 = redisService.getUserList("list");
        Assert.notNull(list2, "list is null");
    }
}

測試通過。

使用Redis緩存

上面內容瞭解了Redis的基本存取使用,這裏介紹Spring使用Redis緩存。

緩存存儲

Spring提供了很多緩存管理器,例如

  • SimpleCacheManager
  • EhCacheManager
  • CaffeineCacheManager
  • GuavaCacheManager
  • CompositeCacheManager

Spring Boot中除了核心的Spring緩存之外,Spring Data還提供了緩存管理器:RedisCacheManager

Spring Boot中通過@EnableCaching註解自動化配置適合的緩存管理器。

所以在程序入口問題加入:@EnableCaching註解

@SpringBootApplication
@EnableCaching
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

然後我們使用自己寫代碼配置的方式,修改RedisConfig添加@EnableCaching註解,並繼承CachingCongigurerSupport


Spring提供瞭如下註解來聲明緩存規則

註解描述
@Cacheable表明在Spring調用之前,首先應該在緩存中查找方法的返回值,如果這個值能夠找到,就會返回緩存的值,否則這個方法會被調用,返回值會放到緩存中
@CachePut表明Spring應該將該方法返回值放到緩存中,在方法調用前不會檢查緩存,方法始終會被調用
@CacheEvict表明Spring應該在緩存中清楚一個或多個條目
@Caching分組註解,能夠同時應用多個其他的緩存註解
@CacheConfig可以在類層級配置一些共有的緩存配置

@Cacheable@CachePut有一些共有的屬性:

屬性類型描述
valueString[]緩存名稱
conditionSpEL表達式,如果得到的值是false,則不會應用緩存在該方法 
keyStringSpEl表達式,用來計算自定義的緩存key
unlessStringSpEl表達式,如果得到的值爲true,返回值不會放到緩存中

在一個請求方法上加上@Cacheable註解,測試效果:

 @Cacheable(value = "testCache")
    @GetMapping("/{id}")
    public User getUserById(@PathVariable Integer id) {
        return userService.findUser(id);
    }

訪問這個接口後報錯:

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at org.springframework.data.redis.serializer.StringRedisSerializer.serialize(StringRedisSerializer.java:33)
at org.springframework.data.redis.cache.RedisCacheKey.serializeKeyElement(RedisCacheKey.java:74)
at org.springframework.data.redis.cache.RedisCacheKey.getKeyBytes(RedisCacheKey.java:49)
at org.springframework.data.redis.cache.RedisCache$1.doInRedis(RedisCache.java:176)
at org.springframework.data.redis.cache.RedisCache$1.doInRedis(RedisCache.java:172)
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:207)

原因如下: 
Redis緩存的key生成策略:

If no params are given,return SimpleKey.EMPTY.
If only one params is given,return that instance.
If more the one param is given,return a SimpleKey containing all parameters.

從上面的策略可以看出,上面緩存testCache中使用的key是整形的id參數,但是在設置RedisTemplate的時候設置了template.setKeySerializer(new StringRedisSerializer());所以導致類型轉換錯誤,所以需要重寫keyGenerator定製key的生成策略

修改RedisConfig類,添加keyGenerator的方法:

@Bean
public KeyGenerator keyGenerator() {
    return new KeyGenerator() {
        @Override
        public Object generate(Object target, Method method, Object... params) {
            StringBuilder sb = new StringBuilder();
            sb.append(target.getClass().getName())
                    .append(":")
                    .append(method.getName());
            for (Object obj : params) {
                sb.append(":")
                        .append(obj.toString());
            }
            return sb.toString();
        }
    };
}

這裏的策略很簡單,使用類名 + 方法名 + 參數作爲緩存key。

再次訪問兩次這個接口:

http://localhost:8080/demo/users/3
http://localhost:8080/demo/users/4
  • 查看Redis內容:
127.0.0.1:6379> keys *
1) "testCache~keys"
2) "com.example.demo.controller.UserController:getUserById:3"
3) "com.example.demo.controller.UserController:getUserById:4"
127.0.0.1:6379> get com.example.demo.controller.UserController:getUserById:3
"[\"com.example.demo.model.User\",{\"id\":3,\"name\":\"\xe7\x8e\x8b\xe5\x89\x91\xe9\x94\x8b\",\"age\":12}]"
127.0.0.1:6379> zrange testCache~keys 0 10
1) "com.example.demo.controller.UserController:getUserById:3"
2) "com.example.demo.controller.UserController:getUserById:4"
127.0.0.1:6379>

可以看到Redis裏面保存了一下內容:

  • 兩條String類型的鍵值對,key就是生成的keyvalue就是user對象序列化之後的結果。
  • 一個有序集合。其中key爲 @Cacheable中的value + ~keys.內容爲String類型的鍵值對的key.

緩存更新與刪除

更新和刪除緩存使用到@CachePut@CacheEvict。這時候發現用原來的key生成策略無法保證增刪查改的key一致(因爲參數不同,方法名也不同),所以需要修改一下KeyGenerator,改爲緩存key按照 緩存名稱 + id的方式

@Bean
public KeyGenerator keyGenerator() {
    return new KeyGenerator() {
        @Override
        public Object generate(Object target, Method method, Object... 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();
        }
    };
}

然後修改Controller

@Cacheable(value = "user")
@GetMapping("/{id}")
public User getUserById(@PathVariable Integer id) {
    return userService.findUser(id);
}

@CachePut(value = "user", key = "#root.caches[0].name + ':' + #user.id")
@PostMapping("/")
public User addUser(User user) {
    userService.add(user);
    //返回增加後的id
    return user;
}

@CachePut(value = "user", key = "#root.caches[0].name + ':' + #user.id")
@PutMapping("/{id}")
public User updateUser(@PathVariable Integer id, User user) {
    user.setId(id);
    userService.update(user);
    return user;
}

@CacheEvict(value = "user")
@DeleteMapping("/{id}")
public String deleteUser(@PathVariable Integer id) {
    int result = userService.delete(id);
    return result == 1 ? "刪除成功" : "刪除失敗";
}

如果@CachePut指定了key屬性之後,則不會再調用keygenerator的方法。此時可以看到,增刪查改的方法緩存的key都是user:id這樣的格式。

然後進行測試,測試過程爲測試某個方法然後到redis中查看緩存是否正確。

下面貼出所有相關的代碼:

RedisConfig.java

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {

    Logger logger = LoggerFactory.getLogger(RedisConfig.class);

    @Bean
    public CacheManager cacheManager(RedisTemplate redisTemplate) {
        RedisCacheManager rcm = new RedisCacheManager(redisTemplate);
        // 設置緩存過期時間,秒
        rcm.setDefaultExpiration(60 * 10);
        return rcm;
    }

    @Bean
    public KeyGenerator keyGenerator() {
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... 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();
            }
        };
    }


    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        template.setKeySerializer(new StringRedisSerializer());
        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(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }

}

UserController.java

@RestController
@RequestMapping("/users")
public class UserController {

    @Autowired
    UserService userService;

    @GetMapping(value = "/")
    public List<User> getUsers() {
        return userService.findUsers();
    }

    @Cacheable(value = "user")
    @GetMapping("/{id}")
    public User getUserById(@PathVariable Integer id) {
        return userService.findUser(id);
    }

    @CachePut(value = "user", key = "#root.caches[0].name + ':' + #user.id")
    @PostMapping("/")
    public User addUser(User user) {
        userService.add(user);
        //返回增加後的id
        return user;
    }

    @CachePut(value = "user", key = "#root.caches[0].name + ':' + #user.id")
    @PutMapping("/{id}")
    public User updateUser(@PathVariable Integer id, User user) {
        user.setId(id);
        userService.update(user);
        return user;
    }

    @CacheEvict(value = "user")
    @DeleteMapping("/{id}")
    public String deleteUser(@PathVariable Integer id) {
        int result = userService.delete(id);
        return result == 1 ? "刪除成功" : "刪除失敗";
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章