欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

SpringBoot中Redis的緩存更新策略詳解

 更新時間:2023年08月10日 09:48:02   作者:ycfxhsw  
這篇文章主要介紹了SpringBoot中Redis的緩存更新策略,緩存一般是為了應(yīng)對高并發(fā)場景、緩解數(shù)據(jù)庫讀寫壓力,而將數(shù)據(jù)存儲在讀寫更快的某種存儲介質(zhì)中(如內(nèi)存),以加快讀取數(shù)據(jù)的速度,需要的朋友可以參考下

常見的緩存更新策略

緩存一般是為了應(yīng)對高并發(fā)場景、緩解數(shù)據(jù)庫讀寫壓力,而將數(shù)據(jù)存儲在讀寫更快的某種存儲介質(zhì)中(如內(nèi)存),以加快讀取數(shù)據(jù)的速度。

緩存一般分為本地緩存(如java堆內(nèi)存緩存)、分布式緩存(如Redis)等。

既然是緩存,就意味著緩存中暫存的數(shù)據(jù)只是個副本,也就意味著需要保證副本和主數(shù)據(jù)之間的數(shù)據(jù)一致性,這就是接下來要分析的緩存的更新。

常見的緩存更新策略有:

  1. 先刪緩存,再更新數(shù)據(jù)庫
  2. 先更新數(shù)據(jù)庫,再刪緩存
  3. 先更新數(shù)據(jù)庫,再更新緩存
  4. read/write through
  5. 寫回。在更新數(shù)據(jù)的時候,只更新緩存,不更新數(shù)據(jù)庫,而我們的緩存會異步地批量更新數(shù)據(jù)庫

一、先刪緩存再更新數(shù)據(jù)庫

很明顯這個邏輯是有問題的,假設(shè)有兩個并發(fā)操作,一個操作更新、另一個操作查詢,更新操作刪除緩存后還沒來得及更新數(shù)據(jù)庫,此時另一個用戶發(fā)起了查詢操作,它因沒有命中緩存進而從數(shù)據(jù)庫讀,此時第一個操作還沒到更新數(shù)據(jù)庫的階段,讀取到的是老數(shù)據(jù),接著寫到緩存中,導(dǎo)致緩存中數(shù)據(jù)變成臟數(shù)據(jù),并且會一直臟下去直到緩存過期或發(fā)起新的更新操作。

  • 緩存初始值:A = 1
  • 數(shù)據(jù)庫初始值:A = 1
  • 緩存最終值:A = 1
  • 數(shù)據(jù)庫最終值:A = 100

這個問題一般有兩種處理方式:

  • 加鎖。鎖,意味著要去解決并發(fā)問題,那么也就意味著并發(fā)的處理會被串行的處理,性能自然會略低。
  • 對key加上過期時間,把控好這個過期時間,不過業(yè)務(wù)上本身是具備容忍這個時間內(nèi)的數(shù)據(jù)不一致的問題方可。
  • 降低出現(xiàn)這個問題的概率

二、先更新數(shù)據(jù)庫,再刪緩存

這是目前業(yè)界最常用的方案。雖然它同樣不夠完美,但問題發(fā)生的概率很小,它的讀流程和寫流程見下圖(圖片來源于網(wǎng)絡(luò),侵刪):

1

2

寫操作先更新數(shù)據(jù)庫,更新成功后使緩存失效。讀操作先讀緩存,緩存中讀到了則直接返回,緩存中讀不到再讀數(shù)據(jù)庫,之后再將數(shù)據(jù)庫數(shù)據(jù)加載到緩存中。

但它同樣也有問題,如下圖,查詢操作未命中緩存,接著讀數(shù)據(jù)庫老數(shù)據(jù)之后、寫緩存之前,此時另一個用戶發(fā)起了更新操作更新了數(shù)據(jù)庫并清了緩存,接著查詢操作將數(shù)據(jù)庫中老數(shù)據(jù)更新到緩存。這就導(dǎo)致緩存中數(shù)據(jù)變成臟數(shù)據(jù),并且會一直臟下去直到緩存過期或發(fā)起新的更新操作。

  • 緩存初始值:空
  • 數(shù)據(jù)庫初始值:A = 1
  • 緩存最終值:A = 1
  • 數(shù)據(jù)庫最終值:A = 100

為什么這種思路存在這么明顯的問題,卻還具有那么廣泛的應(yīng)用呢?因為這個case實際上出現(xiàn)的概率非常低,產(chǎn)生這個case需要具備如下4個條件:

  1. 讀操作讀緩存失效
  2. 有個并發(fā)的寫操作
  3. 寫操作比讀操作更快
  4. 讀操作早于寫操作進入數(shù)據(jù)庫,晚于寫操作更新緩存

而實際上數(shù)據(jù)庫的寫操作會比讀操作慢得多,而且還要鎖表,而讀操作必需在寫操作前進入數(shù)據(jù)庫操作,而又要晚于寫操作更新緩存,所有的這些條件都具備的概率基本并不大。并且即使出現(xiàn)這個問題還有一個緩存過期時間來自動兜底。

三、先更新數(shù)據(jù)庫,再更新緩存

相對來講,理論上這種方式比先更新數(shù)據(jù)庫再刪緩存有著更高的讀性能,因為它事先準備好數(shù)據(jù)。

但由于要更新數(shù)據(jù)庫和緩存兩塊數(shù)據(jù),所以它的寫性能就比較低,而且關(guān)鍵在于它也會出現(xiàn)臟數(shù)據(jù),如下圖,兩個并發(fā)更新操作,分別出現(xiàn)一前一后寫數(shù)據(jù)庫、一后一前寫緩存,則最終緩存的數(shù)據(jù)是二者中前一次寫入的數(shù)據(jù),不是最新的。

  • 緩存初始值:A = 1
  • 數(shù)據(jù)庫初始值:A = 1
  • 緩存最終值:A = 100
  • 數(shù)據(jù)庫最終值:A = 200

四、read/write through 緩存代理

Read/Write Through套路是把更新數(shù)據(jù)庫(Repository)的操作由緩存自己代理了,所以,對于應(yīng)用層來說,就簡單很多了。可以理解為,應(yīng)用認為后端就是一個單一的存儲,而存儲自己維護自己的Cache。

數(shù)據(jù)庫由緩存代理,緩存未命中時由緩存加載數(shù)據(jù)庫數(shù)據(jù)然后應(yīng)用從緩存讀,寫數(shù)據(jù)時更新完緩存后同步寫數(shù)據(jù)庫。應(yīng)用只感知緩存而不感知數(shù)據(jù)庫。

cache

五、寫回

這種方式英文名叫Write Behind 又叫 Write Back。一些了解Linux操作系統(tǒng)內(nèi)核的同學(xué)對write back應(yīng)該非常熟悉,這不就是Linux文件系統(tǒng)的Page Cache的算法嗎?

是的,就是那個東西。這種模式是指在更新數(shù)據(jù)的時候,只更新緩存,不更新數(shù)據(jù)庫,而我們的緩存會異步地批量更新數(shù)據(jù)庫。

cache1

這種方式的問題在于數(shù)據(jù)不是強一致性的,而且可能會丟失(我們知道Unix/Linux非正常關(guān)機會導(dǎo)致數(shù)據(jù)丟失,就是因為這個)。

另外,Write Back實現(xiàn)邏輯比較復(fù)雜,因為他需要track有哪數(shù)據(jù)是被更新了的,需要刷到持久層上。

操作系統(tǒng)的write back會在僅當這個cache需要失效的時候,才會被真正持久起來,比如,內(nèi)存不夠了,或是進程退出了等情況,這又叫l(wèi)azy write。

六、以第一種方式舉例

  • 失效:應(yīng)用程序先從cache取數(shù)據(jù),沒有得到,則從數(shù)據(jù)庫中取數(shù)據(jù),成功后,放到緩存中。
  • 命中:應(yīng)用程序從cache中取數(shù)據(jù),取到后返回。
  • 更新:先把數(shù)據(jù)存到數(shù)據(jù)庫中,成功后,再讓緩存失效。

大致流程如下:

獲取商品詳情舉例

  1. 從商品 Cache 中獲取商品詳情,如果存在,則返回獲取 Cache 數(shù)據(jù)返回。
  2. 如果不存在,則從商品 DB 中獲取。獲取成功后,將數(shù)據(jù)存到 Cache 中。則下次獲取商品詳情,就可以從 Cache 就可以得到商品詳情數(shù)據(jù)。
  3. 從商品 DB 中更新或者刪除商品詳情成功后,則從緩存中刪除對應(yīng)商品的詳情緩存

redis01

添加maven依賴

這里是以MyBatis做數(shù)據(jù)庫DAO操作,Redis做緩存操作。

SpringBoot 2開始默認的Redis客戶端實現(xiàn)是 Lettuce ,同時你需要添加 commons-pool2 的依賴。

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-core</artifactId>
        <version>${jackson-databind-version}</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>${jackson-databind-version}</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-annotations</artifactId>
        <version>${jackson-databind-version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>com.vaadin.external.google</groupId>
                <artifactId>android-json</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>${mysql-connector.version}</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>${druid.version}</version>
    </dependency>
    <!-- MyBatis plus增強和springboot的集成-->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatisplus-spring-boot-starter</artifactId>
        <version>${mybatisplus-spring-boot-starter.version}</version>
    </dependency>
</dependencies>

注意上面我添加了Jackson的依賴,后面要用到。

配置application.yml

增加 Redis 相關(guān)配置

spring:
  cache:
    type: REDIS
    redis:
      cache-null-values: false
      time-to-live: 600000ms
      use-key-prefix: true
    cache-names: userCache,allUsersCache
  redis:
    host: 127.0.0.1
    port: 6379
    database: 0
    lettuce:
      shutdown-timeout: 200ms
      pool:
        max-active: 7
        max-idle: 7
        min-idle: 2
        max-wait: -1ms

對應(yīng)的配置類:

org.springframework.boot.autoconfigure.data.redis.RedisProperties

添加配置類

這里自定義RedisTemplate的配置類,主要是想使用Jackson替換默認的序列化機制:

@Configuration
public class RedisConfig {
    /**
     * redisTemplate 默認使用JDK的序列化機制, 存儲二進制字節(jié)碼, 所以自定義序列化類
     * @param redisConnectionFactory redis連接工廠類
     * @return RedisTemplate
     */
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        // 使用Jackson2JsonRedisSerialize 替換默認序列化
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        // 設(shè)置value的序列化規(guī)則和 key的序列化規(guī)則
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

使用演示

MyBatis實現(xiàn)的DAO層,以及User實體類我就不貼在這里了,只貼Service核心增刪改查操作:

@Service
@Transactional
public class UserService {
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    @Resource
    private UserMapper userMapper;
    @Resource
    private RedisTemplate<String, User> redisTemplate;
    /**
     * 創(chuàng)建用戶
     * 不會對緩存做任何操作
     */
    public void createUser(User user) {
        logger.info("創(chuàng)建用戶start...");
        userMapper.insert(user);
    }
    /**
     * 獲取用戶信息
     * 如果緩存存在,從緩存中獲取城市信息
     * 如果緩存不存在,從 DB 中獲取城市信息,然后插入緩存
     * @param id 用戶ID
     * @return 用戶
     */
    public User getById(int id) {
        logger.info("獲取用戶start...");
        // 從緩存中獲取用戶信息
        String key = "user_" + id;
        ValueOperations<String, User> operations = redisTemplate.opsForValue();
        // 緩存存在
        boolean hasKey = redisTemplate.hasKey(key);
        if (hasKey) {
            User user = operations.get(key);
            logger.info("從緩存中獲取了用戶 id = " + id);
            return user;
        }
        // 緩存不存在,從 DB 中獲取
        User user = userMapper.selectById(id);
        // 插入緩存
        operations.set(key, user, 10, TimeUnit.SECONDS);
        return user;
    }
    /**
     * 更新用戶
     * 如果緩存存在,刪除
     * 如果緩存不存在,不操作
     * @param user 用戶
     */
    public void updateUser(User user) {
        logger.info("更新用戶start...");
        userMapper.updateById(user);
        int userId = user.getId();
        // 緩存存在,刪除緩存
        String key = "user_" + userId;
        boolean hasKey = redisTemplate.hasKey(key);
        if (hasKey) {
            redisTemplate.delete(key);
            logger.info("更新用戶時候,從緩存中刪除用戶 >> " + userId);
        }
    }
    /**
     * 刪除用戶
     * 如果緩存中存在,刪除
     */
    public void deleteById(int id) {
        logger.info("刪除用戶start...");
        userMapper.deleteById(id);
        // 緩存存在,刪除緩存
        String key = "user_" + id;
        boolean hasKey = redisTemplate.hasKey(key);
        if (hasKey) {
            redisTemplate.delete(key);
            logger.info("刪除用戶時候,從緩存中刪除用戶 >> " + id);
        }
    }
}

RedisTemplate 封裝了 RedisConnection,具有連接管理,序列化和 Redis 操作等功能。 還有專門針對 String 的模板對象 StringRedisTemplate。

Redis 操作視圖接口類用的是 ValueOperations ,對應(yīng)的是 Redis String/Value 操作。

還有其他的操作視圖, ListOperations、SetOperations、ZSetOperations 和 HashOperations 。 ValueOperations 插入緩存是可以設(shè)置失效時間,這里設(shè)置的失效時間是 10 s。

然后寫個測試類測試運行看看效果:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
@Transactional
public class UserServiceTest {
    @Autowired
    private UserService userService;
    @Test
    public void testCache() {
        int id = new Random().nextInt(1000);
        User user = new User(id, "admin", "admin");
        userService.createUser(user);
        User user1 = userService.getById(id); // 第1次訪問
        assertEquals(user1.getPassword(), "admin");
        User user2 = userService.getById(id); // 第2次訪問
        assertEquals(user2.getPassword(), "admin");
        user.setPassword("123456");
        userService.updateUser(user);
        User user3 = userService.getById(id); // 第3次訪問
        assertEquals(user3.getPassword(), "123456");
        userService.deleteById(id);
        assertNull(userService.getById(id));
    }
}

運行SpringBoot集成測試,查看日志如下:

創(chuàng)建用戶start...
==>  Preparing: INSERT INTO t_user ( id, username, `password` ) VALUES ( ?, ?, ? )
==> Parameters: 89(Integer), admin(String), admin(String)
<==    Updates: 1
獲取用戶start...
Starting without optional epoll library
Starting without optional kqueue library
==>  Preparing: SELECT id AS id,username,`password` FROM t_user WHERE id=?
==> Parameters: 89(Integer)
<==      Total: 1
獲取用戶start...
從緩存中獲取了用戶 id = 89
更新用戶start...
==>  Preparing: UPDATE t_user SET username=?, `password`=? WHERE id=?
==> Parameters: admin(String), 123456(String), 89(Integer)
<==    Updates: 1
更新用戶時候,從緩存中刪除用戶 >> 89
獲取用戶start...
==>  Preparing: SELECT id AS id,username,`password` FROM t_user WHERE id=?
==> Parameters: 89(Integer)
<==      Total: 1
刪除用戶start...
==>  Preparing: DELETE FROM t_user WHERE id=?
==> Parameters: 89(Integer)
<==    Updates: 1
更新用戶時候,從緩存中刪除用戶 >> 89
獲取用戶start...
==>  Preparing: SELECT id AS id,username,`password` FROM t_user WHERE id=?
==> Parameters: 89(Integer)
<==      Total: 0
Rolled back transaction for test: [DefaultTestContext@6e20b53a testClass = UserServiceTest
Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@503f91c3
{dataSource-1} closed

可以看出第一次取的時候,緩存未命中,會從DB中取數(shù)據(jù),而第2次取的時候緩存命中直接從緩存中取出來。

后面不管是更新和刪除,都會從緩存中刪除。再去取的時候緩存未命中,從DB中取最新的。

七、總結(jié)

本文歸納了常見的緩存更新的五種思路,其中先更新數(shù)據(jù)庫再刪緩存的思路是目前使用得最多的。

先刪緩存再更新數(shù)據(jù)庫因為出問題概率太大并沒有什么用。

第三到第五種思路在特定的應(yīng)用場景下也有很多用途,比如先更新數(shù)據(jù)庫再更新緩存可以解決高并發(fā)下緩存未命中導(dǎo)致瞬時大量請求穿透到數(shù)據(jù)庫的問題。每一種方案也有其各自的優(yōu)點和不足,總而言之,沒有完美的方案,只有契合場景的更適合的方案。

到此這篇關(guān)于SpringBoot中Redis的緩存更新策略詳解的文章就介紹到這了,更多相關(guān)Redis的緩存更新策略內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論