1. 程式人生 > 資料庫 >Redis快取系列--(四)Redis基礎資料型別在Java中的使用

Redis快取系列--(四)Redis基礎資料型別在Java中的使用

Redis在Java中的基礎使用

Redis作為快取主要使用在Java應用或者服務中讀多寫少的場景,從而來提高使用者請求伺服器資料的速度。而且Redis伺服器面對Java的高併發請求時,不會出現併發問題,因為Redis伺服器在執行命令的時候,是原子性的操作。

Redis在Java中的使用方式

以下示例專案採用SpringMvc+JdbcTemplate的框架,同時使用Druid作為資料庫連線池,示例程式碼只展示了核心的程式碼,有關SpringMvc配置檔案以及相關實體類、控制器類以及日誌配置在這裡不做過多贅述,完整專案程式碼請參考。下邊例項程式碼模擬了一個查詢使用者資訊的應用場景來實現查詢出資料庫的資料時同時使用Redis來快取當前查詢的使用者資訊的場景。

  1. 使用JedisPool來構造Redis連線池,然後通過jedisPool.getResource()來獲取Redis客戶端物件。
  • 優點:簡單易操作
  • 缺點:冗餘程式碼較多,如果有多個服務請求需要進行Redis操作,那麼每個請求都需要來獲取和釋放Redis客戶端物件,同時還要來對Java物件進行字串的序列化轉換。

核心的程式碼操作
步驟1:新增相關依賴包

<properties>
        <java-version>1.8</java-version>
        <org.springframework-version>5.1.5.RELEASE</org.springframework-version>
        <org.slf4j-version>1.7.12</org.slf4j-version>
        <!-- json -->
        <jackson.codehaus.version>1.9.13</jackson.codehaus.version>
        <aspect-version>1.8.0</aspect-version>
      <mysql.connector.java.version>8.0.11</mysql.connector.java.version>
        <druid.version>1.1.20</druid.version>
        <sdr.version>2.1.5.RELEASE</sdr.version> 
        <lettuce.version>5.1.4.RELEASE</lettuce.version> 
        <jedis.version>2.9.2</jedis.version>
</properties>
    
<dependencies>
        <!--spring相關依賴包 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${org.springframework-version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${org.springframework-version}</version>
        </dependency>
        
        <!--jdbctemplate依賴包 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>${org.springframework-version}</version>
        </dependency>

        <!-- 資料庫驅動 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.connector.java.version}</version>
        </dependency>

        <!-- 加入druid資料來源依賴包 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>${druid.version}</version>
        </dependency>

        <!-- Jackson JSON Processor -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.9.8</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.8</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>2.9.8</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.28</version>
        </dependency>

        <!--redis-->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>${jedis.version}</version>
        </dependency>

        <!-- 使用RedisTemplate所需要的依賴包-->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
            <version>${sdr.version}</version>
        </dependency>

        <!--新增lombok依賴 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
            <scope>compile</scope>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${org.slf4j-version}</version>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>${org.slf4j-version}</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>${org.slf4j-version}</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.16</version>
            <scope>runtime</scope>
        </dependency>

        <!-- 加入spring測試依賴包 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${org.springframework-version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>

        <!-- Servlet -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-servlet-api</artifactId>
            <version>7.0.30</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

步驟2:載入JedisPool以Javabean的形式注入到spring容器中

/**
 * 通過註解的形式來配置spring中的bean
 * @author young
 */
@Configuration
public class AppConfig {

    /**
     * 使用Redis方式1:通過JedisPool來獲取Redis client
     * @return
     */
    @Bean
    public JedisPool jedisPool(){
        JedisPool jedisPool = new JedisPool("127.0.0.1", 6379);
        return jedisPool;
    }
}

步驟3:編寫服務層程式碼

/**
* 使用者服務層程式碼
*/
@Service
public class UserService {
		private Logger logger = LoggerFactory.getLogger(UserService.class);
		
    @Autowired
    JdbcTemplate jdbcTemplate;

    @Autowired
    JedisPool jedisPool;

    /**
     * 方式1:根據ID查詢使用者資訊 (redis快取,使用者資訊以json字串格式存在(序列化))
     * 好處:查詢出所有的使用者資訊
     * 壞處:如果只需要其中的部分資訊,則會增加網路傳輸資訊量大的壓力
     * @param userId
     */
    public User findUserById(String userId) {
        User user = null;
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            // 1. 查詢Redis是否有資料 -- string方式
            String result = jedis.get(userId);
            if(result != null && !"".equals(result) ) {
                //json字串轉換為User,把快取中的值,返回給你的使用者
                user = JSONObject.parseObject(result, User.class);
                // 命中 快取
                return user;
            }

            // 2. 查詢資料庫
            String sql = "select id,user_name,password,name,age from tb_user where id=?";
            user = jdbcTemplate.queryForObject(sql, new String[]{userId}, new BeanPropertyRowMapper<>(User.class));

            // 3. 資料塞到redis中 // 方式1:json格式字串放入value為String型別的快取中
             String userJsonStr = JSONObject.toJSONString(user);
             jedis.set(userId,userJsonStr);
        }catch (Exception e){
            System.out.println(e.getMessage());
            logger.error(e.getMessage());
            logger.error("userservice error:",e);
        }
        finally {
            if (jedis != null) {
                jedis.close();
            }
        }
        return user;
    }

    /**
     * 方式1:根據ID查詢使用者資訊
     * (redis快取,使用者資訊以Redis中hash的資料結構來儲存))
     * @param userId
     */
    public User findUserById1(String userId) {
        User user = null;
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            // 1. 查詢Redis是否有資料 -- 通過hash的方式獲取
            Map<String, String> userMap = jedis.hgetAll(userId);

            //將獲取的map物件轉換為User物件
            if(userMap != null && userMap.size() >0 ) {
                logger.info("快取命中");
                user = new User();
                user.setId(Integer.valueOf(userId));
                user.setUserName(userMap.get("username"));
                user.setPassword(userMap.get("password"));
                user.setName(userMap.get("name"));
                user.setAge(userMap.get("age"));
                //命中快取
                return user;
            }

            // 2. 查詢資料庫
            String sql = "select id,user_name,password,name,age from tb_user where id=?";
            logger.info("執行的sql語句為:" + sql);
            user = jdbcTemplate.queryForObject(sql, new String[]{userId}, new BeanPropertyRowMapper<>(User.class));

            // 3. 使用value為hash的資料結構,將使用者資料放入Redis快取中
            HashMap<String, String> userInfo = new HashMap<>();
            userInfo.put("username", String.valueOf(user.getUserName()));
            userInfo.put("password", String.valueOf(user.getPassword()));
            userInfo.put("name", String.valueOf(user.getName()));
            userInfo.put("age", String.valueOf(user.getAge()));
            jedis.hmset(userId, userInfo);
        }catch (Exception e){
            System.out.println(e.getMessage());
            logger.error(e.getMessage());
            logger.error("userservice error:",e);
        }
        finally {
            if (jedis != null) {
                jedis.close();
            }
        }
        return user;
    }
    
    /**
     * 根據ID查詢使用者名稱稱(在快取使用者資訊時使用hash的方式對使用者資訊進行儲存,那麼在獲取使用者的部分資訊時就可以使用hget命令來獲取使用者具體某個欄位的資訊了)
     */
    public String findUserNameById(String userId) {
        String uname = null;
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            // 1. 查詢Redis是否有資料
            uname = jedis.hget(userId, "username"); // 向遠端的Redis發起 查詢 請求
            if (uname != null && !"".equals(uname)) {
                return uname; // 命中 快取
            }

            return null;

        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
    }
}

如果使用string對使用者的資訊進行儲存,那麼如果查詢的使用者的資訊量過大就會導致如果Redis快取了之後,假如再有服務請求使用者的部分資訊時,如果使用該使用者資訊的快取,就需要對獲取
使用者的快取所有內容進行解析,這樣就會增加網路的負擔;所以可以使用hash的資料結構來快取使用者資訊,在獲取部分資訊時,可以直接使用hget的方式來獲取某一欄位資訊。

  1. 使用RedisTemplate來進行Redis快取
  • 優點:省去了建立客戶端和關閉客戶端的冗餘程式碼,不需要對從Redis查詢出來的物件或者從資料庫查詢出來的物件進行包裝
  • 缺點:同樣對業務程式碼有侵入,有一定的耦合性,不利於對已開發系統的改造

核心程式碼操作
步驟一:新增RedisTemplate的maven依賴包,請參考上邊示例中的maven
步驟二:在全域性配置類中新增RedisTemplate相關的java bean

@Configuration
public class AppConfig {
/**
     * 使用Redis方式2:通過RedisConnectionFactory來獲取RedisTemplate,從而進行Redis的相關操作(註解方式同樣需要)
     * @return
     */
    @Bean
    public RedisConnectionFactory redisConnectionFactory(){
        //配置Redis的主機和埠
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setHostName(host);
        redisStandaloneConfiguration.setPort(port);

        //
        RedisConnectionFactory redisConnectionFactory = new JedisConnectionFactory(redisStandaloneConfiguration);
        return redisConnectionFactory;
    }

    /**
     * 方式2:載入RedisTemplate bean
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate redisTemplate = new RedisTemplate();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(StringRedisSerializer.UTF_8);
        return redisTemplate;
    }
}

步驟三:編寫服務層核心程式碼,注入RedisTemplate類

@Service
public class UserService {

    private Logger logger = LoggerFactory.getLogger(UserService.class);

    @Autowired
    JdbcTemplate jdbcTemplate;

    /**
     * 方式2:使用模板方法來操作Redis客戶端
     */
    @Autowired
    RedisTemplate redisTemplate;

    /**
     * 方式2:使用RedisTemplate實現Redis快取
     * 優點:省略了建立和關閉Redis客戶單以及對查詢物件的序列化解析和包裝
     * @param userId
     * @return
     */
    public User findUserById2(String userId){
        User user = null;

        user =(User) redisTemplate.opsForValue().get(userId);
        // 1. 查詢Redis是否有資料 -- 通過hash的方式獲取

        //判斷快取資料是否存在
        if(user != null) {
            logger.info("快取命中");
            //命中快取
            return user;
        }

        // 2. 查詢資料庫
        String sql = "select id,user_name,password,name,age from tb_user where id=?";
        logger.info("執行的sql語句為:" + sql);
        user = jdbcTemplate.queryForObject(sql, new String[]{userId}, new BeanPropertyRowMapper<>(User.class));

        // 3. 使用value為string的資料結構,將使用者資料放入Redis快取中
        redisTemplate.opsForValue().set(userId,user);
        return user;
    }
}

這種方式同樣的,如果仍然使用String作為使用者資訊的儲存形式,同樣在獲取部分使用者資訊的資料上會不太方便,同樣也可以使用hash的方式對使用者資訊進行儲存。所以要根據專案的具體需求,對使用者資訊採用Redis適當的儲存方式進行儲存。

  1. 使用Spring提供的快取註解@EnableCache和@Cacheable進行快取
  • 優點:使用Spring提供的註解可以實現無侵入的功能改造,降低程式碼的耦合度,同時減少了對Redis客戶端的連線和釋放等冗餘操作。
  • 缺點:可讀性相對較差,需要了解註解裡每個引數的具體含義以及註解的使用。

核心程式碼操作如下(maven依賴同上)
步驟一:載入全域性配置的Bean,通過註解@EnableCaching開啟快取

@Configuration
@EnableCaching
public class AppConfig {

    /**
     * 通過註釋來獲取properties配置檔案中的自定義值
     */
    @Value("${spring.redis.host}")
    String host;

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

    /**
     * 配置RedisConnectionFactory
     * @return
     */
    @Bean
    public RedisConnectionFactory redisConnectionFactory(){
        //配置Redis的主機和埠
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setHostName(host);
        redisStandaloneConfiguration.setPort(port);

        RedisConnectionFactory redisConnectionFactory = new JedisConnectionFactory(redisStandaloneConfiguration);
        return redisConnectionFactory;
    }


    /**
     * 使用方式3:使用註解方式來對業務程式碼的結果進行快取時需要載入CacheManager物件
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory){
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
        //指定Redis的key和value序列化方式
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()  .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.string())) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.json()));

        CacheManager cacheManager = new RedisCacheManager(redisCacheWriter,redisCacheConfiguration);
        ((RedisCacheManager) cacheManager).isTransactionAware();
        return cacheManager;
    }

}

步驟二:在服務類向對應的方法上新增@Cacheable註解並配置快取key的相關引數

/**
     * 方式3:使用註解方式實現Redis快取,redis的key為註解的value+key;
     *      其中key是一個EL表示式,根據方法傳遞的引數而自動變化
     * 優點:減少快取程式對已有業務程式碼的侵入,使它與業務程式碼解耦
     * @param userId
     * @return
     */
    @Cacheable(value = "tb_user",key = "#userId")
    public User findUserById3(String userId){
        //1.在查詢資料庫前會查詢快取是否存在
        User user;
        // 2. 查詢資料庫
        String sql = "select id,user_name,password,name,age from tb_user where id=?";
        logger.info("執行的sql語句為:" + sql);
        user = jdbcTemplate.queryForObject(sql, new String[]{userId}, new BeanPropertyRowMapper<>(User.class));

        //3.查詢資料庫後會將資料寫入Redis快取
        return user;
    }

註解方式主要運用了Java的反射機制和Spring的AOP特性,我們同樣的也可以通過自定義註解以及AOP來實現自己的快取註解,從而滿足專案的各種需求,可以參考我的另一篇文章。

這裡還需要注意一個問題:當用戶查詢資料的時候,將資料放入Redis快取;同時在使用者更新資料的時候,使用者資料更新到資料庫之後,也要有Redis的相關操作,比如刪除原來的快取。這樣在使用者下次進行查詢的時候,會讀取資料庫的最新資料,然後將最新資料再次快取到Redis中。
使用Redis註解的方式進行快取刪除的程式碼如下:

   /**
     * 方式3:當有資料更新時,則清除快取,等待下一次有快取查詢的時候再存入Redis快取
     * @param user 使用者的更新資訊
     * @return 最新使用者的資訊
     */
    @CacheEvict(value = "tb_user",key = "#user.id")
    public User updateUser(User user) { // 同步關鍵字 --- 應該能夠解決 --
        //修改資料資料
        String sql = "update tb_user set user_name = ? where id=?";
        jdbcTemplate.update(sql, new String[]{user.getUserName(), String.valueOf(user.getId())});
        return user;
    }

總結

Redis在Java中的使用方式分為以下幾種:

  1. 使用原始的Jedis或者JedisPool來獲取Redis的客戶端物件,然後對其進行相應的Redis操作。
  2. 使用RedisTemplate物件來對Redis客戶端進行操作。
  3. 使用Spring提供的@EnableCaching和@Cacheable註解對相關方法進行Redis的快取操作。
  4. 使用自己定義的快取註解來對相關方法進行Redis快取操作。

各種方式各有優缺點,一般採用2或者3的方式相對來說較為簡潔和方便,使用4方式更具有靈活性,具體的專案需要來決定採用哪種方式進行實現。