文章目录

第 2 章 缓存解决方案

学习目标

掌握 SpringDataRedis 的常用操作
能够理解并说出什么是缓存穿透、缓存击穿、缓存雪崩,以及对应的解决方案
使用缓存预热的方式实现商品分类导航缓存
使用缓存预热的方式实现广告轮播图缓存
使用缓存预热的方式实现商品价格缓存
项目序列 - 10:https://github.com/Jonekaka/javaweb-qingcheng-10-85

1. SpringDataRedis

1.1 SpringDataRedis 简介

SpringDataRedis 属于 Spring Data 家族一员,用于对 redis 的操作进行封装的框架

Spring Data ----- Spring 的一个子项目。Spring 官方提供一套数据层综合解决方案,用
于简化数据库访问,支持 NoSQL 和关系数据库存储。包括 Spring Data JPA 、Spring
Data Redis 、SpringDataSolr 、SpringDataElasticsearch 、Spring DataMongodb 等
框架。

1.2 SpringDataRedis 快速入门

1.2.1 准备工作

(1)构建 Maven 工程 SpringDataRedisDemo 引入 Spring 相关依赖、JUnit 依赖、Jedis
和 SpringDataRedis 依赖

<!‐‐缓存‐‐>
<dependencies>
        <!--spring-data-redis底层需要依赖jedis-->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
            <version>2.0.5.RELEASE</version>
        </dependency>
        <!--单元测试-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <!--spring和junit整合的依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.0.5.RELEASE</version>
        </dependency>
    </dependencies>
 

(2)在 src/main/resources 下创建 properties 文件夹,建立 redis-config.properties

redis 主机配置, 端口, 密码, 数据库, 连接数, 最大等待时长

redis.host=127.0.0.1
        redis.port=6379
        redis.pass=
        redis.database=0
        redis.maxIdle=300
        redis.maxWait=3000
 

maxIdle :最大空闲数
maxWaitMillis: 连接时的最大等待毫秒数
(3)在 src/main/resources 下创建 spring 文件夹,创建 applicationContext-redis.xml
对 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" xmlns:p="http://www.springframework.org/schema/p"
  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">
   
   <!--读取路径下redis的属性文件-->
   <context:property-placeholder location="classpath:redis-config.properties" />
   
   <!-- redis 相关配置 -->
    <!--从属性文件中提取属性进行配置,配置连接池-->
   <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">  
     <property name="maxIdle" value="${redis.maxIdle}" />   
     <property name="maxWaitMillis" value="${redis.maxWait}" />  
   </bean>  
  <!--redis连接工厂-->
   <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
       p:host-name="${redis.host}" p:port="${redis.port}" p:password="${redis.pass}" p:pool-config-ref="poolConfig"/>  
   <!--redis最为核心的类,对redis的存取值都需要此redisTemplate,相当于这就是redis的操作对象了,当需要redis的时候就根据需要注入,调用了redis工厂,层层向上封装-->
   <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">  
    	<property name="connectionFactory" ref="jedisConnectionFactory" />
   </bean>
 
 
</beans>  
 

接下来演示 redis 如何增删改查数据

1.2.2 值类型操作

运行 redis 服务器, windows 上也能运行
一个服务器, 一个客户端, 客户端可以连接其他机器进行操作

/**
 * @ClassName redis值测试
 * Description TODO
 **/
/*标识这是一个单元测试类*/
@RunWith(SpringJUnit4ClassRunner.class)
/*指定redis配置文件*/
@ContextConfiguration(locations = "classpath:applicationContext-redis.xml")
public class TestValue {
    /*将redis操作对象注入进来*/
    @Autowired
    private RedisTemplate redisTemplate;
    /*存值,取值,删值*/
    /**
     * @Description 存值,key,value
     * @Param []
     * @return void
     **/
    @Test
    public void setValue() {
        redisTemplate.boundValueOps("name").set("lili");
    }
    /**
     * @Description 取值
     * @Param []
     * @return void
     **/
    @Test
    public void getValue() {
        String name = (String) redisTemplate.boundValueOps("name").get();
        System.out.println(name);
    }
    @Test
    public void del() {
        redisTemplate.delete("name");
    }
}
 
 

测试存值

取值

1.2.3 Set 类型操作

当存入的是集合, 且没有顺序要求, 可以使用 set
添加顺序到输出顺序不一致

/**
 * @ClassName redis值测试
 * Description TODO
 **/
/*标识这是一个单元测试类*/
@RunWith(SpringJUnit4ClassRunner.class)
/*指定redis配置文件*/
@ContextConfiguration(locations = "classpath:applicationContext-redis.xml")
public class TestSet {
    /*将redis操作对象注入进来*/
    @Autowired
    private RedisTemplate redisTemplate;
    /*存值,取值,删值*/
    /**
     * @Description set,key,value
     * @Param []
     * @return void
     **/
    @Test
    public void setSet() {
        redisTemplate.boundSetOps("nameSet").add("心剑");
        redisTemplate.boundSetOps("nameSet").add("魔剑");
        redisTemplate.boundSetOps("nameSet").add("生死棋");
 
 
    }
    /**
     * @Description 取值
     * @Param []
     * @return void
     **/
    @Test
    public void getSet() {
        Set names = redisTemplate.boundSetOps("nameSet").members();
        System.out.println(names);
    }
    @Test
    public void del() {
        redisTemplate.delete("nameSet");
    }
    /**
     * @Description 删除其中一个
     * @Param []
     * @return void
     **/
    @Test
    public void delValue() {
        redisTemplate.boundSetOps("nameSet").remove("魔剑");
    }
}
 

1.2.4 List 类型操作

可以保留原来的顺序
(1)右压栈 后添加的对象排在后边

(2)左压栈 后添加的对象排在前边

(3)根据索引查询元素

(4)移除指定个数的值

/**
 * @ClassName redis值测试
 * Description TODO
 **/
/*标识这是一个单元测试类*/
@RunWith(SpringJUnit4ClassRunner.class)
/*指定redis配置文件*/
@ContextConfiguration(locations = "classpath:applicationContext-redis.xml")
public class TestList {
    /*将redis操作对象注入进来*/
    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;
 
    /**
     * @return void
     * @Description 右压栈
     * @Param []
     **/
    @Test
    public void rightStack() {
        redisTemplate.boundListOps("nameList").rightPush(1);
        redisTemplate.boundListOps("nameList").rightPush(2);
        redisTemplate.boundListOps("nameList").rightPush(3);
    }
 
    @Test
    public void getRightStack() {
        /*range(0, 10)代表查询范围,0,-1代表查询所有,开始索引,查询条数*/
        List<Integer> nameList = redisTemplate.boundListOps("nameList").range(0, 10);
        System.out.println(nameList);
    }
 
    /**
     * @return void
     * @Description 左压栈
     * @Param []
     **/
    @Test
    public void leftStack() {
        redisTemplate.boundListOps("nameList").leftPush(1);
        redisTemplate.boundListOps("nameList").leftPush(2);
        redisTemplate.boundListOps("nameList").leftPush(3);
    }
 
    @Test
    public void getLeftStack() {
        /*range(0, 10)代表查询范围,0,-1代表查询所有,开始索引,查询条数*/
        List<Integer> nameList = redisTemplate.boundListOps("nameList").range(0, 10);
        System.out.println(nameList);
    }
    /*查询具体的某个索引的值*/
    @Test
    public void getIndexValue() {
        Integer nameList = redisTemplate.boundListOps("nameList").index(0);
        System.out.println(nameList);
    }
    /*移除某个索引的值,(移除的个数,值),个数<=>值,如果有三个值,移除4个,则三个都移除不报错,移除两个还有一个*/
    public void delValue() {
        /*代表移除2个1*/
        redisTemplate.boundListOps("nameList").remove(2, 1);
    }
    
}
 

1.2.5 Hash 类型操作

(1)存入值
(2)提取所有的 KEY
(3)提取所有的值
(4)根据 KEY 提取值
(5)根据 KEY 移除值

/**
 * @ClassName redis值测试
 * Description TODO
 **/
/*标识这是一个单元测试类*/
@RunWith(SpringJUnit4ClassRunner.class)
/*指定redis配置文件*/
@ContextConfiguration(locations = "classpath:applicationContext-redis.xml")
public class TestHash {
    /*将redis操作对象注入进来*/
    @Autowired
    private RedisTemplate redisTemplate;
    @Test
   public void testHash() {
       redisTemplate.boundHashOps("nameHash").put("a",1);
       redisTemplate.boundHashOps("nameHash").put("b",2);
       redisTemplate.boundHashOps("nameHash").put("c",3);
       redisTemplate.boundHashOps("nameHash").put("d",4);
   }
   @Test
   public void getKey() {
       Set nameHash = redisTemplate.boundHashOps("nameHash").keys();
       System.out.println(nameHash);
   }
   /*根据key查询出value*/
    @Test
   public void getValues() {
//       List nameHash = redisTemplate.boundHashOps("nameHash").values();
        Integer o = (Integer) redisTemplate.boundHashOps("nameHash").get("a");
        System.out.println(o);
   }
   @Test
    public void delKey() {
        redisTemplate.boundHashOps("nameHash").delete("a");
   }
}
 

1.2.6 ZSet 类型操作

因为有了分值, 而有了排序, 并非是输入顺序, 而是绑定的值顺序
绑定的值是可以动态变化的, 可以理解为主播榜, 谁被刷的礼物多谁就靠前
如果值是用数据库保存, 将会带来很大压力, 常用缓存解决

zset 是 set 的升级版本,它在 set 的基础上增加了一格顺序属性,这一属性在添加元素
的同时可以指定,每次指定后,zset 会自动重新按照新的值调整顺序。
可以理解为有两列的 mysql 表,一列存储 value,一列存储分值。
假设有 abc 三个人直播, 受到礼物
(1)存值 ,指定分值
(2)查询,由低到高
(3)查询,由高到低,
(4)增加分数
(5)查询值和分数
TypedTuple 是值与分数的封装。

/**
 * @ClassName redis值测试
 * Description TODO
 **/
/*标识这是一个单元测试类*/
@RunWith(SpringJUnit4ClassRunner.class)
/*指定redis配置文件*/
@ContextConfiguration(locations = "classpath:applicationContext-redis.xml")
public class TestZset {
    /*将redis操作对象注入进来*/
    @Autowired
    private RedisTemplate redisTemplate;
 
    @Test
    public void setZset() {
        redisTemplate.boundZSetOps("nameZset").add("a", 100);
        redisTemplate.boundZSetOps("nameZset").add("b", 200);
        redisTemplate.boundZSetOps("nameZset").add("c", 10);
    }
 
    /*默认从低到高排序*/
    @Test
    public void getValue() {
        Set nameZset = redisTemplate.boundZSetOps("nameZset").range(0, -1);
        System.out.println(nameZset);
    }
 
    /*设定从高到低排序*/
    @Test
    public void getHighValue() {
        Set nameZset = redisTemplate.boundZSetOps("nameZset").reverseRange(0, -1);
        System.out.println(nameZset);
 
    }
    /*对数值变更,,增加礼物*/
    @Test
    public void updateScore() {
         redisTemplate.boundZSetOps("nameZset").incrementScore("a", 1000);
    }
    /*查询出键值全部信息,为一个对象组合,因此要循环才能得到*/
    @Test
    public void getALlinfo() {
        Set<ZSetOperations.TypedTuple> nameZset = redisTemplate.boundZSetOps("nameZset").reverseRangeWithScores(0, -1);
        /*此处是对象地址*/
        System.out.println(nameZset);//rg.springframework.data.redis.core.DefaultTypedTuple@ccb70423, org.springframework.
        for (ZSetOperations.TypedTuple typedTuple : nameZset) {
            System.out.println(typedTuple.getValue());
            System.out.println(typedTuple.getScore());
        }
 
    }
 
 

1.2.7 过期时间设置

以值类型为例:存值时指定过期时间和时间单位
都有过期方法
常用语短信验证码

/**
 * 存值
 */
@Test
public void setValue(){
        redisTemplate.boundValueOps("name").set("aaa");
        redisTemplate.boundValueOps("name").expire(10,TimeUnit.SECONDS);
        }
 

2. 缓存穿透、缓存击穿、缓存雪崩

2.1 缓存穿透

持续的穿透了缓存去找数据库, 带给缓存和数据库巨大的访问压力

缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为 id 为 “-1” 的数据或 id 为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。如下面这段代码就存在缓存穿透的问题。
根据 id 查询价格

public Integer findPrice(Long id) {
//从缓存中查询
        Integer sku_price =  (Integer)redisTemplate.boundHashOps("sku_price").get(id);
        if(sku_price==null){
//缓存中没有,从数据库查询
        Sku sku = skuMapper.selectByPrimaryKey(id);
        if(sku!=null){ //如果数据库有此对象
        sku_price = sku.getPrice();
        redisTemplate.boundHashOps("sku_price").put(id,sku_price);
        }
        }
        return sku_price;
        }
 
 

解决方案:

  1. 接口层增加校验,如用户鉴权校验,id 做基础校验,id0 的直接拦截;
  2. 从缓存取不到的数据,在数据库中也没有取到,这时也可以将 key-value 对写为
    key-0。这样可以防止攻击用户反复用同一个 id 暴力攻击。代码举例:
    redisTemplate.boundHashOps(“sku_price”).put(id,0);
    如果数据库也没有, 就把这个 id 绑定到缓存里, 为 0
    下次再访问缓存就能返回数据了
public int findPrice(Long id) {
//从缓存中查询
        Integer sku_price =
        (Integer)redisTemplate.boundHashOps("sku_price").get(id);
        if(sku_price==null){
//缓存中没有,从数据库查询
        Sku sku = skuMapper.selectByPrimaryKey(id);
        if(sku!=null){ //如果数据库有此对象
        sku_price = sku.getPrice();
        redisTemplate.boundHashOps("sku_price").put(id,sku_price);
        }else{
        redisTemplate.boundHashOps("sku_price").put(id,0);
        }
        }
        return sku_price;
        }
 
  1. 使用缓存预热
    缓存预热就是将数据提前加入到缓存中,当数据发生变更,再将最新的数据更新到缓存。
    将数据访问压力转移到缓存, 保护数据库
    后边我们就用缓存预热的方式实现对分类导航、广告轮播图等数据的缓存。

2.2 缓存击穿

特点: 和穿透比起来一般没有 id; 穿透是有人攻击, 但是击穿是常见现象

缓存击穿是指缓存中没有但数据库中有的数据。这时由于并发用户特别多,同时读
缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压
力。
以下代码可能会产生缓存击穿:
加过期时间是为了保证数据的更新, 假如设定 5 分钟更新一次, 那么每 5 分钟就存在一个击穿的风险

@Autowired
private RedisTemplate redisTemplate;
public List<Map> findCategoryTree() {
//从缓存中查询
        List<Map> categoryTree= (List<Map>)redisTemplate.boundValueOps("categoryTree").get();
        //缓存中没有,压力转移到数据库
        if(categoryTree==null){
        Example example=new Example(Category.class);
        Example.Criteria criteria = example.createCriteria();
        criteria.andEqualTo("isShow","1");//显示
        List<Category> categories =
        categoryMapper.selectByExample(example);
        categoryTree=findByParentId(categories,0);
        //更新到缓存
        redisTemplate.boundValueOps("categoryTree").set(categoryTree);
//过期时间设置 ......
        }
        return categoryTree;
        }
 

解决方案:

  1. 设置热点数据永远不过期。
  2. 缓存预热

2.3 缓存雪崩

存在大量缓存, 不止同一条数据, 结果数据同时到期了, 压力被转移到数据库

缓存雪崩是指缓存数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至 down 机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案:

  1. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
  2. 设置热点数据永远不过期。
  3. 使用缓存预热
    缓存预热还是非常好的解决方案

3. 商品分类导航缓存

3.1 需求分析

为了提升首页的加载速度,减轻数据库访问压力,我们将首页的商品分类导航数据加载
在缓存中。

3.2 实现思路

为了避免缓存穿透、击穿等问题,我们采用缓存预热的方式实现对分类导航数据的缓
存。
考虑到商品分类导航数据不经常变动,所以我们不设置过期时间。

3.3 代码实现

缓存几乎在所有模块中都有遇到, 因此放到公共服务模块

3.3.1 通用模块整合 spring data redis

(1)qingcheng_common_service 引入依赖

<!‐‐缓存‐‐>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring‐data‐redis</artifactId>
<version>2.0.5.RELEASE</version>
</dependency>
 

(2)qingcheng_common_service 新增配置文件 redis-config.properties

redis.host=127.0.0.1
        redis.port=6379
        redis.pass=
        redis.database=0
        redis.maxIdle=300
        redis.maxWait=3000
 

maxWait:连接池中连接用完时, 新的请求等待时间, 毫秒
maxIdle: 最大闲置个数
(3)qingcheng_common_service 新增 spring 配置文件 applicationContext-redis.xml

<?xml version="1.0" encoding="UTF-8"?> 
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:cache="http://www.springframework.org/schema/cache"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/cache http://www.springframework.org/schema/beans/spring-cache.xsd">
 
   <!-- redis 相关配置 --> 
   <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">  
     <property name="maxIdle" value="${redis.maxIdle}" />   
     <property name="maxWaitMillis" value="${redis.maxWait}" />  
   </bean>  
  
   <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
       p:host-name="${redis.host}" p:port="${redis.port}" p:password="${redis.pass}" p:pool-config-ref="poolConfig"/>  
   
   <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">  
    	<property name="connectionFactory" ref="jedisConnectionFactory" />
   </bean>
 
 
</beans>  
 
 

(4)qingcheng_common_service 工程新增枚举
创建枚举的目的是统一各个功能所用缓存的名称, 防止因为名字不同犯低级调用错误

public enum CacheKey {
    AD,//广告
    SKU_PRICE,//价格
    CATEGORY_TREE;//商品分类导航树
}
 

至此 spring data redis 已经整合完成, 开始实现业务需求

3.3.2 商品分类加载到缓存

为缓存预热
(1)服务接口 CategoryService 新增方法定义

/**
 * 将商品分类树放入缓存
 */
public void saveCategoryTreeToRedis();
 

(2)CategoryServiceImpl 实现此方法

@Autowired
    private RedisTemplate redisTemplate;
    public void saveCategoryTreeToRedis() {
        System.out.println("将商品分类数据加载到缓存");
        Example example = new Example(Category.class);
        Example.Criteria criteria = example.createCriteria();
        criteria.andEqualTo("isShow", "1");
        List<Category> categories = categoryMapper.selectByExample(example);
        /*将查询到的数据变换成树形结构*/
        List<Map> categoryTree = findByParentId(categories, 0);
        /*将格式化后的数据放到缓存中*/
        redisTemplate.boundValueOps(CacheKey.CATEGORY_TREE).set(categoryTree);
    }
 

(3)qingcheng_service_goods 工程新增类
希望当此模块启动的时候自动对缓存预热
implements InitializingBean 后继承此方法启动后要进行的逻辑
@Component 加注解后表示这是 bean

@Component
public class Init implements InitializingBean {
    @Autowired
    private CategoryService categoryService;
    public void afterPropertiesSet() throws Exception {
        System.out.println("缓存预热");
        categoryService.saveCategoryTreeToRedis();//模块启动后自动加载商品分类导航缓存
    }
}
 
 

实现 InitializingBean 接口的类会在启动时自动调用。

3.3.3 查询商品分类缓存

修改 CategoryServiceImpl 的 findCategoryTree 方法 ,直接从缓存中提取数据。
上次从数据库中查找数据, 然而已经不需要了

/*直接从缓存中读取数据*/
    public List<Map> findCategoryTree() {
        return (List<Map>) redisTemplate.boundValueOps(CacheKey.CATEGORY_TREE).get();
    }
 

查看是否可用
启动 zk,redis 服务
运行 business,goods,portal
控制台中可以看到提示数据从缓存中取出

3.3.4 更新商品分类缓存

缓存中的数据当然也需要更新
修改 CategoryServiceImpl 的增删改方法,在增删改后重新加载缓存

/**
 * 新增
 * @param category
 */
public void add(Category category) {
        categoryMapper.insert(category);
        saveCategoryTreeToRedis();
        }
/**
 * 修改
 * @param category
 */
public void update(Category category) {
        categoryMapper.updateByPrimaryKeySelective(category);
        saveCategoryTreeToRedis();
        }
/**
 * 删除
 * @param id
 */
public void delete(Integer id) {
//判断是否存在下级分类
//......
        saveCategoryTreeToRedis();
        }
 

4. 广告轮播图缓存

4.1 需求分析

为了提升首页的加载速度,减轻数据库访问压力,我们将首页的广告轮播图数据加载在
缓存中。
然而, 这里需要考虑的是所有广告

4.2 实现思路

使用 “缓存预热” 的方式实现
广告数据不只是轮播图,我们可以使用 hash 来存储广告数据。
hash 本身有 key ,value, 位置 - 广告
大 key 用来存储广告, 小 key 用来区分广告

4.3 代码实现

4.3.1 广告数据加载到缓存

这种更新只是局部的更新
(1)AdService 新增方法定义
saveAdToRedisByPosition
更新广告的时候会调用

/**
 * 将某个位置的广告存入缓存
 * @param position
 */
public void saveAdToRedisByPosition(String position);
/**
 * 将全部广告数据存入缓存
 */
public void saveAllAdToRedis();
 

(2)AdServiceImpl 方法实现
从数据库中查询某位置的广告, 并装入缓存
查询所有的广告位置遍历位置装入缓存

@Autowired
private RedisTemplate redisTemplate;
public void saveAdToRedisByPosition(String position) {
//查询某位置的广告列表
        Example example=new Example(Ad.class);
        Example.Criteria criteria = example.createCriteria();
        criteria.andEqualTo("position",position);
        criteria.andLessThanOrEqualTo("startTime",new Date());//开始时间小于等于当前时间
        criteria.andGreaterThanOrEqualTo("endTime",new Date());//截至时间大于等于当前时间
        criteria.andEqualTo("status","1");
        List<Ad> adList = adMapper.selectByExample(example);
//装入缓存
        redisTemplate.boundHashOps(CacheKey.AD).put(position,adList);
        }
/**
 * 返回所有的广告位置
 * @return
 */
private List<String> getPositionList(){
        List<String> positionList=new ArrayList<String>();
        positionList.add("index_lb");//首页广告轮播图
//。。。
        return positionList;
        }
public void saveAllAdToRedis() {
//循环所有的广告位置,将每个位置的广告封装入缓存
        for(String position:getPositionList()){
        saveAdToRedisByPosition(position);
        }
        }
 

(3)qingcheng_service_business 工程新增类
启动该模块时自动装入该时段广告数据进入缓存

@Component
public class Init implements InitializingBean {
    @Autowired
    private AdService adService;
    public void afterPropertiesSet() throws Exception {
        System.out.println("‐‐‐缓存预热‐‐‐");
        adService.saveAllAdToRedis();
    }
}
 
 

4.3.2 查询广告缓存

修改 AdServiceImpl 的 findByPosition 方法

public List<Ad> findByPosition(String position) {
//从缓存中查询广告列表
        return(List<Ad>)redisTemplate.boundHashOps(CacheKey.AD).get(position);
        }
 

goods 和 business 都参与了首页页面

4.3.3 更新广告缓存

缓存当然要面临更新问题
修改 AdServiceImpl 的增删改方法

/**
 * 新增
 * @param ad
 */
public void add(Ad ad) {
        adMapper.insert(ad);
        saveAdByPosition(ad.getPosition());//重新加载缓存
        }
/**
 * 修改,可能对广告位置也进行了更新
 * @param ad
 */
public void update(Ad ad) {
//获取之前的广告位置
        String position =
        adMapper.selectByPrimaryKey(ad.getId()).getPosition();
        //对广告更新
        adMapper.updateByPrimaryKeySelective(ad);
   //位置变化才进行更新缓存
        if(!position.equals(ad.getPosition())){ //如果广告位置发生变化
        saveAdToRedisByPosition(ad.getPosition());//更新
        }
        }
/**
 * 删除,在删除之前更新缓存,不然没得查
 * @param id
 */
public void delete(Integer id) {
        String position = adMapper.selectByPrimaryKey(id).getPosition();
        saveAdByPosition(position);//重新加载缓存
        adMapper.deleteByPrimaryKey(id);
        }
 

5. 商品详细页价格缓存

5.1 需求分析

我们已经将商品的信息生成为静态页面,但是商品价格经常变动,如果每次价格变动后都对静态页重新生成会影响服务器性能。
所以,对于商品价格,我们采用异步调用的方式来进行客户端渲染。

5.2 实现思路

(1)商品服务启动后加载全部价格数据到缓存。使用 hash 存储,skuID 作为小 KEY
(2)从缓存查询商品价格,封装为 controller,并设置可跨域调用
什么叫跨域?
当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域。不同域之间相
互请求资源,就算作 “跨域”。
比如这个 contoller 请求那个 controller

JavaScript 出于安全方面的考虑,不允许跨域调用其他页面的对象。
那什么是跨域呢,简单地理解就是因为 JavaScript 同源策略的限制,a.com 域名下的 js 无法操作 b.com
或是 c.a.com 域名下的对象。
当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域。不同域之间相
互请求资源,就算作 “跨域”。
现在我们要实现的查询商品价格缓存功能就存在跨域问题。后端 controller 在 http://www.qingcheng.com ,商品详细页在 http://item.qingcheng.com,
如何解决跨域问题?我们使用 CORS 实现跨域。
CORS 是一个 W3C 标准,全称是 “跨域资源共享”(Cross-origin resourcesharing)。它允许浏览器向跨源服务器,发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制。使用非常简单,只需要在 controller 类上添加一个 @CrossOrigin
注解即可
(3)修改商品详细页模板,使用 ajax 读取价格,并进行客户端渲染。

5.3 代码实现

5.3.1 价格数据加载到缓存

(1)SkuService 接口新增方法定义

/**
 * 保存全部价格到缓存
 */
public void saveAllPriceToRedis();
 
 

(2)SkuServiceImpl 类新增方法
如果缓存已经存在就没有必要重复查询
那么之前广告和商品分类数据为什么不做判断呢?
也可以, 但是数据量比较小, 即使重复载入影响不大, 但是价格数据量大, 对于性能有较大影响

 @Autowired
    private RedisTemplate redisTemplate;
    public void saveAllPriceToRedis() {
        /*如果缓存并没有建立*/
        if (!redisTemplate.hasKey(CacheKey.SKU_PRICE)) {
            System.out.println("加载全部价格");
            List<Sku> skuList = skuMapper.selectAll();
            for (Sku sku : skuList) {
                if ("1".equals(sku.getStatus())) {
                    redisTemplate.boundHashOps(CacheKey.SKU_PRICE).put(sku.getId(), sku.getPrice());
                }
            }
        }
        else{
            System.out.println("缓存已经存在,价格数据未更新,不必预热");
        }
    }
 

(3)修改 Init 类

@Component
public class Init implements InitializingBean {
    @Autowired
    private CategoryService categoryService;
    @Autowired
    private SkuService skuService;
    public void afterPropertiesSet() throws Exception {
        System.out.println("缓存预热");
        categoryService.saveCategoryTreeToRedis();//模块启动后自动加载商品分类导航缓存
        skuService.saveAllPriceToRedis();//加载价格数据也进入缓存
    }
}
 

5.3.2 查询价格缓存

后端代码:
(1)SkuService 新增方法定义

public Integer findPrice(String id)
 
 

(2)SkuServiceImpl 实现 findPrice 方法

public Integer findPrice(String id) {
//从缓存中查询
        return    (Integer)redisTemplate.boundHashOps(CacheKey.SKU_PRICE).get(id);
        }
 

(3)qingcheng_web_portal 工程新增类

@RestController
@RequestMapping("/sku")
@CrossOrigin
public class SkuController {
    @Reference
    private SkuService skuService;
    @GetMapping("/price")
    public Integer price(String id){
        return skuService.findPrice(id);
    }
}
 

前端代码(修改模板):
(1)将 vue.js axios.js 放到 html 输出文件夹下的 js 文件夹中。

(2)修改 qingcheng_web_portal 工程的模板 item.html

<script src="js/vue.js"></script>
<script src="js/axios.js"></script>
<script th:inline="javascript">
 
	new Vue({
		el:'#app',
		data(){
		    return {
		        skuId:/*[[${sku.id}]]*/,
				price:0
			}
		},
		created(){
		    //读取价格
			axios.get('http://localhost:9102/sku/price.do?id='+this.skuId).then(response=>{
			    this.price=(response.data/100).toFixed(2);
			})
		}
 
	});
</script>
 
 

th:inline 定义 js 脚本可以使用变量 js 脚本的变量用 /_${}_/ 渲染
(3)修改 qingcheng_web_portal 工程的模板 item.html ,body, 添加

,并将价格修改为 vue 表达式

{{price}}
 

5.3.3 更新价格缓存

(1)SkuService 接口新增方法定义

/**
 * 保存价格到缓存
 * @param skuId
 */
public void savePriceToRedisById(String id,Integer price);
 
 

(2)SkuServiceImpl 类新增方法

public void savePriceToRedisById(String id,Integer price) {
        redisTemplate.boundHashOps(CacheKey.SKU_PRICE).put(id,price);
        }
 
 

(3)SpuServiceImpl 类引入 SkuService

@Autowired
private SkuService skuService;
 
 

(4)修改 SpuServiceImpl 类 saveGoods 方法,在 SKU 列表循环体中添加代码

skuService.savePriceToRedisById(sku.getId(),sku.getPrice());
 
 

5.3.4 删除价格缓存

删除商品时删除缓存中的价格,释放内存空间
(1)SkuService 新增方法定义

/**
 * 根据sku id 删除商品价格缓存
 * @param id
 */
public void deletePriceFromRedis(String id);
 
 

(2)SkuServiceImpl 新增方法实现

public void deletePriceFromRedis(String id) {
        redisTemplate.boundHashOps(CacheKey.SKU_PRICE).delete(id);
        }
 

(3)修改 SpuServiceImpl 的 delete 方法,新增代码逻辑

//删除缓存中的价格
Map map=new HashMap();
        map.put("spuId",id);
        List<Sku> skuList = skuService.findList(map);
        for(Sku sku:skuList){
        skuService.deletePriceFromRedis(sku.getId());
        }