Redis(十四)Redis 在Java Web 中的應用


  在傳統的 Java Web 項目中,使用數據庫進行存儲數據,但是有一些致命的弊端,這些弊端主要來自於性能方面。

  由於數據庫持久化數據主要是面向磁盤,而磁盤的讀/寫比較慢,在一般管理系統中,由於不存在高並發,因此往往沒有瞬間需要讀/寫大量數據的要求,這個時候使用數據庫進行讀/寫時沒有太大的問題的,但是在互聯網中,往往存在大數據量的需求,比如,需要在極短的時間內完成成千上萬次的讀/寫操作,這個時候往往不是數據庫能夠承受的,極其容易造成數據庫系統癱瘓,導致服務宕機的嚴重生產問題。

  為了克服這些問題,Java Web 項目往往就引入了 NoSQL 技術,NoSQL 工具也是一種簡易的數據庫,它主要是一種基於內存的數據庫,並提供了一定的持久化功能。比如Redis和MongoDB。

  Redis 的性能十分優越,可以支持每秒十幾萬次的讀/寫操作,其性能遠超數據庫,並且支持集群、分布式、主從同步等配置,原則上可以無限擴展,同時還支持一定的事務能力。

  Redis 性能優越主要來自於3個方面:

  • 基於ANSI C 語言編寫,接近於匯編語言的機器語言,運行十分快速
  • 基於內存的讀/寫。
  • 數據庫結構只有6種數據類型,數據結構比較簡單,因此規則較少,而數據庫則是范式,完整性、規范性需要考慮的規則比較多,處理業務會比較復雜。\

  NoSQL為什么不能代替數據庫:

  • NoSQL 的數據主要存儲在內存中,而數據庫主要是磁盤。
  • NoSQL 數據庫結構比較簡單,雖然能處理很多的問題,但是其功能畢竟有限,不如數據庫的SQL語句強大,支持更為復雜的計算
  • NoSQL 並不完全安全穩定,由於它基於內存,一旦停電或者機器故障數據就很容易丟失,其持久化能力也是有限的,而基於磁盤的數據庫則不胡出現這樣的問題
  • NoSQL 其數據完整性、事務能力、安全性、可靠性以及可擴展性都遠不及數據庫。

  

  一、Redis 在 Java Web 中的應用

  一般而言 Redis 在 Java Web 應用中存在兩個主要的場景:一個是緩存常用的數據,另一個是在需要高速讀/寫的場合使用它快速讀寫。

  1.緩存

  在對數據庫的讀/寫操作中,讀操作遠超寫操作,一般是9:1到7:3的比例,所以需要讀的可能性比寫的可能性多得多。

  當發送SQL去數據進行讀取時,數據庫就會去磁盤把對應的數據索引回來,而索引磁盤是一個相對緩慢的過程。如果把數據直接放在運行在內存中的Redis服務器上,那么就不需要去讀/寫磁盤了,而是直接讀取內存,顯然速度會快得多,而且會極大減輕數據庫的壓力。

  而使用內存進行存儲數據開銷也是比較大的,應該考慮在Redis中存儲哪些數據,需要從3個方面進行考慮:

  • 業務數據常用與否以及命中率大小。如果命中率很低,就沒有必要寫入緩存。
  • 該業務數據是讀操作多,還是寫操作多,如果寫操作多,頻繁需要寫入數據庫,也沒有必要使用緩存
  • 如果要存儲幾百兆字節的文件,會給緩存帶來很大的壓力,沒有必要。

  (1)讀操作流程

  

  • 當第一次讀取數據的時候,讀取Redis的數據就會失敗,此時會觸發程序讀取數據庫,把數據讀取出來,並且寫入Redis
  • 當第二次以及以后讀取數據時,就直接讀取Redis,讀到數據后就結束了流程,這樣速度就大大提高了。

  (2)寫操作流程

  

  如果業務數據寫操作次數遠遠大於讀操作次數,那么沒有必要使用 Redis。

 

  2.高速讀/寫場合

  高速讀/寫場合例如:秒殺商品、搶紅包、搶票等。這類場合在一瞬間成千上萬的請求就會達到服務器,如果使用的數據庫,很容易造成數據庫癱瘓。

  解決辦法是異步寫入數據庫,即在高速讀/寫的場合單單使用 Redis 去應對,把這些需要高速讀/寫的數據緩存到 Redis 中,而在滿足一定的條件下,觸發這些緩存的數據寫入數據庫中。

  

  當一個請求到達服務器,只是把業務數據先在 Redis 讀/寫,而沒有進行任何對數據庫的操作。

  由於一般緩存不能持久化,或者所持久化的數據不太規范,因此需要把這些業務數據存入數據庫,所以在一個請求操作完 Redis 的讀/寫后,會去判斷該高速讀/寫業務是否結束,這個判斷的條件往往就是秒殺商品剩余個數為0,搶紅包金額為0,如果不成立,則不會操作數據庫;如果成立,則觸發事件將 Redis 緩存的數據以批量的形式一次性寫入數據庫,從而完成持久化操作。

 

  二、在 Java 中使用 Redis

  1.下載 jedis.jar、spring-data.redis.jar 和 commons-pool2-2.5.0.jar

  2.在 Java 中使用 Redis,一般采用連接池方式獲取連接

package com.ssm.chapter17.jedis;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class JedisTest {
    
    public void testJedis() {
        // 從連接池中獲取Redis連接
        Jedis jedis = testPool().getResource();
        int i = 0;// 記錄操作次數
        try {
            long start = System.currentTimeMillis();// 開始毫秒數
            while (true) {
                long end = System.currentTimeMillis();
                if (end - start >= 1000) {// 當操作1秒時,結束操作
                    break;
                }
                i++;
                jedis.set("test" + i, i + "");
            }
        } finally {// 
            jedis.close();
        }
        System.out.println("redis 每秒操作: " + i + "次");// 打印1秒內對 Redis 的操作次數
    }

    private JedisPool testPool() {
        JedisPoolConfig poolCfg = new JedisPoolConfig();
        // 設置最大空閑數
        poolCfg.setMaxIdle(50);
        // 設置最大連接數
        poolCfg.setMaxTotal(100);
        // 設置最大等待毫秒數
        poolCfg.setMaxWaitMillis(20000);
        // 使用配置創建連接池
        JedisPool pool = new JedisPool(poolCfg, "localhost");
        return pool;
    }
    
    public static void main(String[] args) {
        new JedisTest().testJedis();
    }
}
在Java中使用Redis

  3.在 Spring 中使用Redis

  (1)先用 Spring 配置一個 JedisPoolConfig 對象

    <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <!--最大空閑數 -->
        <property name="maxIdle" value="50" />
        <!--最大連接數 -->
        <property name="maxTotal" value="100" />
        <!--最大等待時間 -->
        <property name="maxWaitMillis" value="20000" />
    </bean>

  (2)在使用 Spring 提供的RedisTemplate之前需要配置Spring所提供的連接工廠,在 Spring Data Redis 方案中有4種工廠模型:選擇其中的一種,JedisConnectionFactory

    <bean id="connectionFactory"
        class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <property name="hostName" value="localhost" />
        <property name="port" value="6379" />
        <property name="poolConfig" ref="poolConfig" />
    </bean>

  (3)普通的連接使用沒有辦法把 Java 對象直接存入 Redis,可以使用 Spring 內部提供的 RedisSerializer 接口和一些實現類實現序列化和反序列化。

  JdkSerializationRedisSerializer是使用 JDK 的序列化器進行轉換,而StringRedisSerializer使用字符串進行序列化

    <bean id="jdkSerializationRedisSerializer"
        class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" />
        
    <bean id="stringRedisSerializer"
        class="org.springframework.data.redis.serializer.StringRedisSerializer" />

  (4)由於需要配置key和value兩個不同的序列化方式,那么可以指定各自使用的序列化器。至此,就可以得到一個Spring提供的RedisTemplate來進行操作Redis

    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="connectionFactory" />
        <property name="keySerializer" ref="stringRedisSerializer" />
        <property name="valueSerializer" ref="jdkSerializationRedisSerializer" />
    </bean>

  (5)創建POJO類,必須實現Serializable接口

package com.ssm.chapter17.pojo;

import java.io.Serializable;

public class Role implements Serializable {
    
    private static final long serialVersionUID = 6977402643848374753L;

    private long id;
    private String roleName;
    private String note;
  /*****************************getter and setter**************************************/
}

  (6)使用RedisTemplate操作Redis

    private static void testSpring() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        RedisTemplate redisTemplate = applicationContext.getBean(RedisTemplate.class);
        Role role = new Role();
        role.setId(1L);
        role.setRoleName("role_name_1");
        role.setNote("note_1");
        redisTemplate.opsForValue().set("role_1", role);
        Role role1 = (Role) redisTemplate.opsForValue().get("role_1");
        System.out.println(role1.getRoleName());
    }

  然而,這樣的方式可能存在問題:執行set和get方法的Redis連接對象可能來自同一個Redis連接池的不同Redis的連接。為了使得set和get操作都來自同一個連接,可以使用SessionCallback

  (7)使用SessionCallback來將多個命令放入到同一個 Redis 連接中執行

  這里使用匿名類的方式,還可以使用 Lambda 的方式進行編寫SessionCallback的業務邏輯。

  由於前后使用的都是同一個連接,因此對於資源損耗就比較小,在使用Redis操作多個命令或者使用事務的時候也會用到它。

    private static void testSessionCallback() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        RedisTemplate redisTemplate = applicationContext.getBean(RedisTemplate.class);
        Role role = new Role();
        role.setId(1);
        role.setRoleName("role_name_1");
        role.setNote("role_note_1");
        SessionCallback callBack = new SessionCallback<Role>() {
            @Override
            public Role execute(RedisOperations ops) throws DataAccessException {
                ops.boundValueOps("role_1").set(role);
                return (Role) ops.boundValueOps("role_1").get();
            }
        };
        Role savedRole = (Role) redisTemplate.execute(callBack);
        System.out.println(savedRole.getId());
    }

  例如,簡化成Lambda表達式為:

    private static void testSessionCallback() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        RedisTemplate redisTemplate = applicationContext.getBean(RedisTemplate.class);
        Role role = new Role();
        role.setId(1);
        role.setRoleName("role_name_1");
        role.setNote("role_note_1");

        Role savedRole = (Role) redisTemplate.execute((RedisOperations ops) -> {
            ops.boundValueOps("role_4").set(role);
            return (Role) ops.boundValueOps("role_4").get();
        });
    
        System.out.println(savedRole.getId());
    }

}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM