Spring 集成 Redis 实现商品信息内存缓存(Redis 数据缓存分析)

13 篇文章 0 订阅
3 篇文章 1 订阅

一、Redis 内存数据库简介

Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker. It supports data structures such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs, geospatial indexes with radius queries and streams. Redis has built-in replication, Lua scripting, LRU eviction, transactions and different levels of on-disk persistence, and provides high availability via Redis Sentinel and automatic partitioning with Redis Cluster.

对于 Redis 的简介,首先先贴出官网上的介绍,这段介绍翻译过来大概的意思就是,Redis 是一款开源(可直接从 GitHub 上捞源码)的基于内存的数据存储,并且支持例如字符串、哈希、列表、集合和有序集合这样的数据结构,同时 Redis 支持多种缓存淘汰算法(当缓存数据已满时对旧的缓存清除的策略),比如 LRU 等等,以及支持事务和数据持久化,以及通过 Redis Sentinel 而具备很好的高可用性,并且支持通过使用 Redis Cluster 来实现自动分区和集群化部署。

下面来大概总结一下,使用 Redis 的好处:

(1)支持多种数据结构,适用于不同种类数据的存储;

(2)因为是基于内存同时使用了多路复用技术,具有很好的数据存取速度;

(3)支持多种缓存数据淘汰策略和缓存数据过期策略(可自动清除过期数据);

(4)通过 Redis Cluster 支持集群部署;

(5)支持事务;

 

二、Redis 缓存更新策略

2.1 Redis 与数据库结合存在的问题

(1)首先第一个问题是在低并发情况下就比较可能出现的问题,即当我们对同一条数据进行操作的时候,因为我们需要保持 Redis 和数据库中数据的同步性,所以需要经常需要进行同步操作,但是当我们对同一条数据进行同步的过程中,如果没有处理好该数据在 Redis 中的键值关系,就可能导致 Redis 和数据库中的数据不一致,因而造成 Redis 中的数据变成了脏数据,具体的情况一般如下:

1. T1 时刻我们以 key1 为键将一条数据写入到 Redis 中;

2. T2 时刻我们将 key1 为键对应的数据同步到数据库中;

3. T3 时刻我们仍然对同一条数据进行操作,但是此时我们改变该条数据后以 key2 为键将其写入到 Redis 当中;

4. T4 时刻我们将 key2 为键对应的数据同步到数据库中;

其实通过上面的操作我们很容易就可以发现,当我们最后将 key2 为键对应的数据同步到数据库当中的时候,其实我们操作的还是数据库中的同一条数据(key1 为键所对应的的数据),因此在这里就会造成数据库中的数据改变了,Redis 当中 key2 所对应的数据也是最新的数据,但 Redis 当中 key1 键所对应的数据却是还未更新的数据,这时就会造成 Redis 当中的数据与数据库中的数据不同步(出现这种情况一般是我们在代码中对同一条数据在不同的模块使用了不同的代码也即不同的键将其存储到 Redis 当中)。 

(2)第二个问题的话一般比较容易出现在高并发的情况下,当并发量比较高的情况下可能会出现操作失败的情况,这个时候就要注意读取数据和写入数据时对 Redis 和数据库的操作顺序,比如当我们将数据写入到数据库时没有及时让 Redis 中的数据更新或失效,那么当数据写入到数据库的过程中,如果此时从 Redis 当中直接获取缓存,那么此时我们取到的数据就是脏数据(此时新数据在写入到数据的过程中),因此此时应当先让 Redis 当中的数据失效,然后将数据更新写入到数据库中,让系统到数据库中获取新值后,再同步到 Redis 当中(这样操作也存在出现问题的可能性,下面会谈到)。

(3)接下来的几种情况是当我们使用了正确的存取方式后,仍然可能会出现的问题。首先的话是高并发情况下(可能存在两个线程交替完成 Redis 和数据库的同步操作),这时我们采取的更新顺序是先更新数据库后再同步至 Redis,假设现在存在两个线程,线程 A 首先将数据更新至数据库,然后线程 B 与线程 A 进行同样的操作,并且更新同一条数据,则此时数据库中的数据已经更新为线程 B 当中的最新数据,然后线程 B 继续进行,将数据同步至 Redis 当中,待线程 B 更新完成后,假如线程 A 此时进入到更新 Redis 的步骤(此时线程 A 当中的数据已经不是最新的数据了,与数据库中的数据已经不一致),线程 A 将自己的数据同步至 Redis 当中,这时就已经造成了 Redis 当中的数据与数据库中的数据不一致了(Redis 当中的数据为脏数据)。

(4)下面的一种情况就是第二点中所说的,尽管我们使用了先让 Redis 当中的缓存失效,但是仍然可能出现问题的情况,出现的场景仍然是高并发的情况下,此时我们假设有两个线程同时进行数据的更新与同步操作,线程 A 首先将 Redis 当中的缓存失效(删除),然后当其还未更新到数据库时,假如此时另外一个线程 B 先到 Redis 当中获取数据,其发现数据已经失效,那么它紧接着就会到数据库中去获取最新的数据,但注意此时线程 A 还未将新数据更新至数据库,因此此时线程 B 其实获取到的仍然是数据库中旧的数据,所以此时就已经造成了数据的不一致性,而当其获取到数据后会将数据同步至 Redis 当中,待线程都操作完线程 A 完成最后将数据更新到数据库的操作,但此时 Redis 当中是线程 B 同步的旧数据,而数据库中是线程 A 更新的新数据,此时 Redis 和数据库的数据不一致(Redis 当中的数据为脏数据)。

(5)最后一种情况,与上面一种情况相反是先更新数据库再删除缓存,这种情况下也是假设有两个线程(一个读线程,一个写线程)交替进行数据的更新和同步操作,首先线程 A 先从 Redis 当中获取数据,但因为 Redis 当中的缓存失效(或者未命中缓存),那么线程 A 会直接去数据库中获取数据(此时线程 A 拿到的是旧数据),然后此时线程 B 开始进行数据更新操作,即先更新了数据库中的数据,然后紧接着删除了 Redis 当中的数据。假如此时当线程 B 执行完后线程 A 才将刚刚的数据写入到 Redis 当中,那此时 Redis 当中的数据就为脏数据(Redis 当中的数据落后于数据库中)。

2.2 解决上述问题的思路

上面已经提到了使用 Redis 作为缓存时可能会出现的一些问题,但其实这些问题出现的概率都是很低的,那么如果出现类似的问题或者当我们删除 Redis 中的缓存失败时我们应该怎么去解决它们呢,下面有几种解决的方案:

(1)设置缓存过期的时间,保证当数据长时间无法从数据库同步时,Redis 会自动删除失效的缓存;

(2)使用消息队列来实现失败重试机制,保证当失效缓存删除失败时,系统自动进行重试操作;

(3)对于比较重要的数据,还是直接采用从数据库中读取的方式来避免脏数据。

2.3 Redis 缓存更新策略(写操作)

(1)Cache Aside 更新策略:同时更新缓存和数据库;

(2)Read/Write Through 更新策略:先更新缓存,缓存负责同步更新数据库;

(3)Write Behind Caching 更新策略:先更新缓存,缓存定时异步更新数据库。

2.4 Redis 缓存和数据库读写顺序

读取数据:先读取 Redis 缓存,如果未命中或者读取失败,则读取数据库,并将读取后的数据同步至 Redis 缓存并返回。 

写入数据:首先写入数据库,确认写入成功后,更新或者失效掉缓存 Redis 中对应的数据。

2.5 Redis 缓存保障缓存正常删除机制

这一部分大致借鉴了网上的一些解决方案,即配合使用数据库的 binlog ,通过一些第三方的工具对其进行解析,在删除缓存失败时通过 binlog 信息和消息队列来实现失败重试机制,以此来保证失效的缓存能够被正常的清除,具体的方法待我再实践一下之后再写博文来介绍。

 

三、Spring 集成 Redis 文件配置

使用 Spring 来集成 Redis 是很简单的,只需进行简单的文件配置即可,我这里是建议把 Redis 的配置单独写在一个 xml 文件中,然后再在 Spring 模块主配置文件中进行引用,因为这样的话当我们后期需要单独对 Redis 进行配置修改的时候会比较清晰一点,同时对于这个配置文件的参数大家可以自行根据需要进行配置,我这里是抱着尝试的态度每个都设置一下试试。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd
    ">
    <context:property-placeholder order="1" location="classpath:redis.properties" ignore-unresolvable="true"/>
    <!-- Redis -->
    <!-- 连接池参数 -->
    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <property name="maxIdle" value="${redis.pool.maxIdle}" />
        <property name="minIdle" value="${redis.pool.minIdle}" />
        <property name="maxTotal" value="${redis.pool.maxTotal}" />
        <property name="maxWaitMillis" value="${redis.pool.maxWaitMillis}" />
        <property name="minEvictableIdleTimeMillis" value="${redis.pool.minEvictableIdleTimeMillis}"/>
        <property name="numTestsPerEvictionRun" value="${redis.pool.numTestsPerEvictionRun}"/>
        <property name="timeBetweenEvictionRunsMillis" value="${redis.pool.timeBetweenEvictionRunsMillis}"/>
        <property name="testOnBorrow" value="${redis.pool.testOnBorrow}" />
        <property name="testOnReturn" value="${redis.pool.testOnReturn}" />
        <property name="testWhileIdle" value="${redis.pool.testWhileIdle}"/>
    </bean>
 
    <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <property name="poolConfig" ref="jedisPoolConfig" />
        <property name="hostName" value="${redis.host}" />
        <property name="port" value="${redis.port}" />
        <property name="password" value="${redis.pwd}" />
        <property name="usePool" value="${redis.userPool}" />
        <property name="database" value="${redis.database}" />
        <property name="timeout" value="${redis.timeout}" />
    </bean>
 
    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="jedisConnectionFactory" />
 
        <!-- 序列化方式 建议key/hashKey采用StringRedisSerializer -->
        <property name="keySerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property>
        <property name="valueSerializer">
            <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" />
        </property>
        <property name="hashKeySerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property>
        <property name="hashValueSerializer">
            <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" />
        </property>
        <!-- 开启REIDS事务支持 -->
        <property name="enableTransactionSupport" value="false" />
    </bean>
 
    <!-- 对string操作的封装 -->
    <bean id="stringRedisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
        <constructor-arg ref="jedisConnectionFactory" />
        <!-- 开启REIDS事务支持 -->
        <property name="enableTransactionSupport" value="false" />
    </bean>
 
</beans>

同时这里我们直接将 Redis 集成到 Spring 当中,通过使用使用 Spring 当中的注解实现数据的缓存,因此我们还需要在模块的主配置文件中进行下面的配置,具体的配置参数含义这里就不过多讲解了。

    <!-- 使用缺省名称为 cacheManager 的缓存管理器,其缺省实现为 org.springframework.cache.support.SimpleCacheManager -->
    <cache:annotation-driven cache-manager="redisCacheManager"/>
    <!-- 配置redis缓存并设置cache过期时间 -->
    <bean id="redisCacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">
        <constructor-arg ref="redisTemplate"/>
        <property name="defaultExpiration" value="600"/>
        <property name="cacheNames" value="redisCacheManager"/>
    </bean>

 

四、使用 Redis 实现商品信息缓存

如果上面的内容已经大概了解了,那么其实通过 Redis 来实现商品列表的缓存就很容易了,同时因为我们使用 Spring 来进行项目的开发,同时已经将 Redis 集成到 Spring 当中,因此我们这里可以直接通过使用 Spring 当中的注解来对数据缓存进行配置,这里主要使用是 @Cacheable 、@CachePut 和 @CacheEvict 这三个注解,通过命名我们也可以大概知道它们的含义。

(1)@Cacheable :调用这个注解修饰的方法时首先会从缓存中查询,若未查询到则去数据库中查询,最后再将从数据库中查询到的数据同步至缓存中;

(2)@CachePut :能够根据方法的请求参数对其结果进行缓存(每次都会直接请求数据库);

(3)@CacheEvict :根据方法的参数将缓存中该方法对应的返回值缓存清除(缓存失效);

了解了它们大概的含义后其实使用起来就比较简单了,代码可以直接如下(value 为对应的缓存名称,刚刚在配置文件中已经配置过了,可以配置多个,key 就是缓存中数据值所对应的存储键):

    @CachePut(value = "redisCacheManager", key = "'commodity:'+#id")
    public Commodity inStockCommodity(Integer id) {
        Commodity commodity = commodityDao.select(id);
        commodity.setUpdateTime(commodity.getAddedTime());
        commodity.setStatus("in_stock");
        commodityDao.update(commodity);
        return commodity;
    }

    @CacheEvict(value = "redisCacheManager", key = "'commodity:'+#id")
    public void remove(Integer id) {
        commodityDao.delete(id);
    }

    @Cacheable(value = "redisCacheManager", key = "'commodity:'+#id")
    public Commodity get(Integer id) {
        return commodityDao.select(id);
    }

总的来说通过使用 Spring 集成 Redis 来实现数据缓存实现起来还是比较容易的,但是通过仅仅这块注释代码的配置,亲测在单用户使用的情况下,对于商品列表和商品数据详情缓存后再次拉取的时间可以缩短到原来的十九分之一左右(优化前为 957ms ,优化后为 48ms),也就是说性能大概提高了近二十倍左右,所以这个优化的效果还是很明显的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值