Java——商城后台(2.0)

书写规范:缩进 命名 注释 封装 

 

1.、类别--查询列表--数据访问层

关于查询类别的列表,需要执行的SQL语句大致是:

select * from pms_category order by sort, id desc

为了封装查询列表时各类别数据,(只保证有用的数据 ,保证数据传输时节省流量,万一旧VO更改 会更加麻烦)应该在csmall-pojovo包中创建CategoryListItemVO类,

在类中添加最有必要性的属性:

@Data
public class CategoryListItemVO implements Serializable {

    private Long id;
    private String name;
    private Long parentId;
    private Integer depth;
    private String icon;
    private Integer enable;
    private Integer display;

}

CategoryMapper.java接口中添加抽象方法:

/**
 * 查询类别的列表
 *
 * @return 类别的列表,如果数据表中无数据,则返回长度为0的列表
 */
List<CategoryListItemVO> list();

CategoryMapper.xml中配置SQL语句:

<!-- List<CategoryListItemVO> list(); -->
<select id="list" resultMap="ListResultMap">
    select
        <include refid="ListQueryFields" />
    from
        pms_category
    order by
        sort, id desc
</select>

<sql id="ListQueryFields">
    <if test="true">
        id,
        name,
        parent_id,
        depth,
        icon,
        enable,
        is_display
    </if>
</sql>

<resultMap id="ListResultMap" type="cn.tedu.csmall.pojo.vo.CategoryListItemVO">
    <id column="id" property="id" />
    <result column="name" property="name" />
    <result column="parent_id" property="parentId" />
    <result column="depth" property="depth" />
    <result column="icon" property="icon" />
    <result column="enable" property="enable" />
    <result column="is_display" property="display" />
</resultMap>

完成后,在CategoryMapperTests中测试:

@Test
@Sql(scripts = {"classpath:truncate.sql", "classpath:insert_data.sql"})
@Sql(scripts = {"classpath:truncate.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testList() {
    List<?> list = mapper.list();
    log.debug("查询到的数据的数量:{}", list.size());
    for (Object item : list) {
        log.debug("{}", item);
    }
}

2、 类别--查询列表--业务逻辑层

   没有条件查询   不需要处理异常。。。。

ICategoryService中添加抽象方法:

/**
 * 查询类别的列表
 *
 * @return 类别的列表,如果数据表中无数据,则返回长度为0的列表
 */
List<CategoryListItemVO> list();

CategoryServiceImpl中实现以上抽象方法:

@Override public List<CategoryListItemVO> list() { return categoryMapper.list(); }

CategoryServiceTests中测试:

@Test
@Sql(scripts = {"classpath:truncate.sql", "classpath:insert_data.sql"})
@Sql(scripts = {"classpath:truncate.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testList() {
    List<?> list = service.list();
    log.debug("查询到的数据的数量:{}", list.size());
    for (Object item : list) {
        log.debug("{}", item);
    }
}

3. 类别--查询列表--控制器层

CategoryController中添加:

@ApiOperationSupport(order = 40)
@ApiOperation("查询类别列表")
@GetMapping("")
public JsonResult<List<CategoryListItemVO>> list() {
    return JsonResult.ok(categoryService.list());
}

 题外话:加实体类的在线文档注解    显示在线文档的返回结果。


import java.io.Serializable;

@Data
public class CategoryListItemVO implements Serializable {

    @ApiModelProperty(value = "类别id",position = 1)
    private Long id;

    @ApiModelProperty(value = "类别名称",example = "家电",required = true,position = 2)
    private String name;

    @ApiModelProperty(value = "父级类别名称id  如果是一级类 为 0",example = "0",required = true,position = 3)
    private Long parentId;

    @ApiModelProperty(value = "几级分类",position = 4)
    private Integer depth;

    @ApiModelProperty(value = "图标的URL",position = 5)
    private String icon;

    @ApiModelProperty(value = "是否启用",position = 6)
    private Integer enable;

    @ApiModelProperty(value = "是否需要展示",position = 7)
    private Integer display;
}

多加的模块在 4.21下午里面


4. 类别--根据id查询数据--数据访问层

此前在“增加类别”时已经完成根据id查询的功能。
 

5. 类别--根据id查询数据--业务逻辑层

ICategoryService中添加抽象方法:  再业务逻辑实现类  重写方法

/**
 * 根据类别id获取类别详情
 *
 * @param id 类别id
 * @return 匹配的类别详情,如果没有匹配的数据,则返回null
 */
CategoryStandardVO getById(Long id);
在CategoryServiceImpl中实现:

public CategoryStandardVO getById(Long id) {
    CategoryStandardVO queryResult = categoryMapper.getById(id);
    if (queryResult == null) {
        throw new ServiceException(NOT_FOUND, "查询类别详情失败,尝试访问的数据不存在!");
    }
    return queryResult;
}

6. 类别--根据id查询数据--控制器层

CategoryController中添加:

@ApiOperationSupport(order = 40)
@ApiOperation("根据id查询类别详情")
//下面的的注解  api在线文档中对参数进行说明
@ApiImplicitParam(name = "id", value = "类别id", required = true, paramType = "path", dataType = "long")
@GetMapping("/{id:[0-9]+}")  //id表示跟参数的id相同   //@PathVariable表示冲url中获取数字
public JsonResult<CategoryStandardVO> getById(@PathVariable Long id) {
    return JsonResult.ok(categoryService.getById(id));
}

提示:原查询列表的方法的排序序号已从40改为更大的值(45)。

另外,应该在以上查询返回的结果类型中各属性上添加@ApiModelProperty注解,以完善API文档:

@Data
public class CategoryStandardVO implements Serializable {

    @ApiModelProperty(value = "数据id", position = 1)
    private Long id;

    @ApiModelProperty(value = "类别名称", position = 2)
    private String name;

    @ApiModelProperty(value = "父级类别id,如果是一级分类,则此属性值为0", position = 3)
    private Long parentId;

    @ApiModelProperty(value = "深度,最顶级类别的深度为1,次级为2,以此类推", position = 4)
    private Integer depth;

    @ApiModelProperty(value = "类别关键词", position = 5)
    private String keywords;

    @ApiModelProperty(value = "自定义排序序号", position = 6)
    private Integer sort;

    @ApiModelProperty(value = "图标的URL", position = 7)
    private String icon;

    @ApiModelProperty(value = "是否启用,1=启用,0=禁用", position = 8)
    private Integer enable;

    @ApiModelProperty(value = "是否为父级(是否包含子级),1=是父级,0=不是父级", position = 9)
    private Integer parent;

    @ApiModelProperty(value = "是否显示在导航栏,1=显示,0=不显示", position = 10)
    private Integer display;

}

7. 关于缓存

缓存数据是将数据存储在更加易于访问的位置,以前提高查询数据的效率!

通常,数据都是存储在数据库中的,并且,数据库服务器与应用程序服务器不是同一台服务器,当需要获取某个数据时,如果能将数据在应用程序服务器中另外保存一份,并从此处获取数据,就会比从数据库服务器获取数据更加高效,另外,数据库中的数据是存储在硬盘上的,如果将“另外保存”的数据存储在比硬盘访问效率更高的存储介质中,例如保存到内存中,也可以非常明显的提高读取数据的效率。

所以,常见的缓存手段是将数据库中的数据存储到应用程序服务器中(可能是存储在硬盘上,也可能是存储在内存中),或者,使用专门的缓存服务器(通常都是基于内存存储的)。

但是,一旦使用了缓存,也就是同一份数据在不同的位置存储了2份甚至多份,就会存在数据不一致的风险,则需要评估此风险!有些数据对于“准确性”要求并不严格,比如某篇文章的点赞数量,火车票在车次列表中显示的余票数量,但是,也有时需要数据是非常精准的,例如在创建订单尝试购买某趟火车的车票时,要求查询到的余票数量必须是精准的!

一般来说,如果要求某个数据必须是精准的,是不会使用缓存的,而是直接从数据库查询数据,或者,在这些场景中,及时同步缓存中的数量,使之始终与数据库中的数据保持一致!

通常,使用缓存来存储的数据具有以下特点:

  • 访问频率偏高,甚至非常高
  • 数据改变的频率相对较低,或对数据的准确性要求并不严格

8. 使用Redis缓存数据

Redis是一个主流的基于内存的NO-SQL数据存储服务。

相关下载:

关于Redis编程,在Java中,有许多成熟的开发框架,例如Redisson、Jedis、lettuce等……Spring提供了spring-data的框架,用于统一了API,甚至,在Spring Boot中,还提供了spring-boot-starter-data-redis依赖项,集成了spring-data对Redis中的数据访问的各依赖项及自动配置,使得在Spring Boot中实现Redis编程更加简单!

当通过Spring系列框架实现Redis编程时,需要使用到RedisTemplate对象,在Spring Boot工程中,应该将创建此类型的对象,并交给Spring进行管理,后续,当需要执行Redis数据访问时,调用此对象的方法即可!

则在csmall-product.webapiconfig包下创建RedisConfiguration类:

配置将RedisTemplate对象放到spring容器里面

@Configuration
public class RedisConfiguration {
    /*
    基于键值对  都可以使用泛型   .......
    值需要是可系列化的  就是实现了序列化接口的类对象都可以传
    因为需要再程序和内存硬盘里面传输。
     */
        //这个对象就是一个redis服务,或者说一个小程序。
    @Bean
    public RedisTemplate<String , Serializable> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<String , Serializable> redisTemplate=new RedisTemplate<>();
        //设置将值和键设置   方便缓存进行解析
        redisTemplate.setKeySerializer(RedisSerializer.string());
        redisTemplate.setValueSerializer(RedisSerializer.json());
        //设置链接工程   再参数上声明  springboot会帮助创建。
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //暂时不用的
        redisTemplate.setHashKeySerializer(RedisSerializer.string());
        redisTemplate.setHashValueSerializer(RedisSerializer.json());

        return  redisTemplate;
    }
}

接下来,可以在测试包建redis包下创建RedisTemplateTests类,进行简单的测试:



@SpringBootTest
public class RedisTemplateTests {

    @Autowired
    RedisTemplate<String, Serializable> redisTemplate;

    @Test
    public void testSetValue() {
        // 对于普通的字符串值,调用opsForValue()
        // 再调用set()方法实现存入数据
        // 此方法的特点与Map中的put()类似,既是新增数据的方法,也是修改数据的方法
        String key = "category";
        Serializable value = "Phone";
        //得到一个值的操作器
        ValueOperations<String, Serializable> opsForValue = redisTemplate.opsForValue();
        opsForValue.set(key, value);
    }

}

存入值后,可以通过同样的“值操作器”取出值,例如:

@Test
public void testGetValue() {
    // 因为值是字符串值,需要先调用opsForValue()
    // 再调用get()方法根据key获取值
    String key = "category";
    Serializable value = redisTemplate.opsForValue().get(key);
    System.out.println(value);
}

由于值的操作配置序列化器是JSON的序列化器,所以,通过以上方式,还可以将整个对象直接写入到Redis中并读取出来,因为序列化器会将对象转换为JSON格式的字符串,只不过,为了保证反序列化成功,还会在JSON数据中添加数据的类型。

清除所有

当我们设置值的时候 进行了json   转换  而redis并没有 如果通过redis控制台设置值 不能通过ideajava代码获取。

会将数据持久化,不是一直保存再内存里面。


另外,由于默认情况下并不支持Java 8中的日期时间(LocalDateTimeLocalDateLocalTime),为了保证能够对这些数据类型执行正确的序列化和反序列化,需要在对应的属性上添加注解,例如:

需要先加web依赖到pojo里面

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework</groupId>
                    <artifactId>spring-web</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework</groupId>
                    <artifactId>spring-webmvc</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

 序列化和反序列化:也就可以通过配置类装配起来实现每个属性的序列化与反系列化   复制粘贴。

// 使用以下注解时,需要添加Jackson相关依赖
@JsonSerialize(using = LocalDateTimeSerializer.class)  序列化为了存时间
@JsonDeserialize(using = LocalDateTimeDeserializer.class) 反序列化 为了读出来
private LocalDateTime gmtCreate;

两个属性都得加   记得

接下来,即可通过RedisTemplateopsForValue()再调用set() / get()方法写入 / 读取整个对象,例如:

设置
@Test
public void testSetObjectValue() {
    String key = "category999";
    Category category = new Category();
    category.setId(999L);
    category.setName("礼品");
    category.setEnable(1);
    category.setGmtCreate(LocalDateTime.now());
    redisTemplate.opsForValue().set(key, category);
}
获取
@Test
public void testGetObjectValue() {
    String key = "category999";
    Serializable serializable = redisTemplate.opsForValue().get(key);
    System.out.println("获取到的值:" + serializable);
    System.out.println("获取到的值的类型:" + serializable.getClass().getName());
}

redis获得是带类名的json对象



由于Redis中的数据存在与数据库中的数据不一致的风险,则可能某些数据是无效的,需要清楚,并且,如果反复向Redis中存入大量不同Key的数据,也可能导致Redis服务器内存不足,则需要清除数据。在向Redis中存入数据时,可以指定数据的TTL(Time To Live:有效生存时间),当此生存时间结束后,对应的数据将被自动清理:

@Test
public void testSetValueDuration() {
    String key = "brand";
    Serializable value = "华为";
    ValueOperations<String, Serializable> opsForValue = redisTemplate.opsForValue();
    
    //key  value  时间数  时间单位
    opsForValue.set(key, value, 15, TimeUnit.SECONDS);
}

也可以自行通过指令清理(删除)某些数据,例如:

@Test
public void testDelete() {
    String key = "category";
    redisTemplate.delete(key);
}

另外,还有关于列表的数据操作,例如添加元素到Redis中的列表中:

@Test
public void testSetListValue() {
    // 向Redis中存入列表数据
    // 需要调用RedisTemplate对象得到opsForList()返回对象后再操作
    //保证了是列表
    List<String> categories = new ArrayList<>();
    categories.add("家电");
    categories.add("食品");
    categories.add("衣物");
    String key = "categories";
    // redisTemplate.opsForList().set(key, 0, categories.get(0));
    //这个操作之前需要有列表key 不然报错
    for (int i = 0; i < categories.size(); i++) {
    //right  和  left系列方法  区别  将新加元素放在右边和左边。
    //rightPop  弹出元素   ()    
    redisTemplate.opsForList().rightPush(key, categories.get(i));
    }
}

获取列表的长度:

@Test
public void testGetListSize() {
    String key = "categories";
    Long size = redisTemplate.opsForList().size(key);
    System.out.println("size = " + size);
}

获取列表中的元素:

@Test
public void testGetListValue() {
    // 在RedisTemplate获取的opsForList()返回的结果对象中
    // 当获取列表值时,默认是分页获取的,所以,每次获取都需要指定获取哪个区间段的值
    // 在指定区间时,start取值为0表示从头开始,end取值为-1表现末尾(直至最后一个元素)
    // 在获取列表值时,会获取包含start至包含end的元素
    // start / end均表示元素在列表中的索引,是从0开始顺序编号的
//  含头含尾  
    String key = "categories";
    long start = 2;
    long end = 4;
    List<Serializable> list = redisTemplate.opsForList().range(key, start, end);
    for (Serializable serializable : list) {
        System.out.println(serializable);
    }
}

9. 在项目中缓存类别列表

由于类别列表是常用数据(例如每次打开首页都需要,且其它的页面也可能需要),并且这些数据在项目正式上线后一般不会频繁调整,是非常适合缓存的!

这种缓存数据应该是在服务刚刚启动时就需要读取数据库,并将数据写入到Redis的,在后续的访问中,都将优先从Redis中获取数据!

所以,目前需要解决的问题有:

  • 从数据库中读取类别列表(已完成)
  • 将从数据库中读取到的列表写入到Redis中
  • 使得启动服务时就执行写入Redis的操作

先在csmall-product-webapi模块的根包下创建cache子包,并在其下创建repo子包,在其中先创建ICategoryCacheRepository接口,在接口中定义必要的抽象方法:

public interface ICategoryCacheRepository {

    /**
     * 清除相关缓存
     */
    void clear();

    /**
     * 将列表数据添加到缓存
     *
     * @param categories 列表数据
     */
    void appendList(List<CategoryListItemVO> categories);

}

然后,在repo下创建impl子包,并在其中创建CategoryCacheRepositoryImpl实现:

@Repository
public class CategoryCacheRepositoryImpl implements ICategoryCacheRepository {

    @Autowired
    private RedisTemplate<String, Serializable> redisTemplate;

    /**
     * 类别列表在Redis中的Key
     */
    public static final String KEY_LIST = "categories";

    @Override
    public void clear() {
        redisTemplate.delete(KEY_LIST);
    }

    @Override
    public void appendList(List<CategoryListItemVO> categories) {
        for (CategoryListItemVO category : categories) {
            redisTemplate.opsForList().rightPush(KEY_LIST, category);
        }
    }

}

接下来,应该在“类别”数据的业务逻辑层进行后续的处理,则在CategoryServiceImpl中添加:

@Autowired
private ICategoryCacheRepository categoryCacheRepository;

/**
 * 加载类别数据的缓存
 */
private void loadCache() {
    // 1. 从数据库中读取类别列表
    List<CategoryListItemVO> list = categoryMapper.list();
    // 2. 清除缓存的类别数据
    categoryCacheRepository.clear();
    // 3. 将类别数据写入到缓存
    categoryCacheRepository.appendList(list);
}

提示:以上方法是私有的,因为后续可能需要复用。

为了使得以上功能是可以被外部调用的,需要在ICategoryService接口中定义对外的方法:

/**
 * 初始化缓存
 */
void initCache();

并在CategoryServiceImpl中,直接调用上面的私有方法来实现:

@Override
public void initCache() {
    loadCache();
}

最后,需要在服务启动时就执行以上初始化缓存的操作,此操作一般称之为“缓存预热”,则在csmall-product-webapicache包下创建preload子包,并在其下创建CachePreLoad类:

@Slf4j
@Component
public class CachePreLoad implements ApplicationRunner {

    @Autowired
    private ICategoryService categoryService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        log.debug("缓存预热,准备向缓存中写入【类别】数据……");
        categoryService.initCache();
    }

}

提示:当需要启动服务时就会执行某个代码片段,可以自定义组件类,实现ApplicationRunner接口,将需要执行的代码片段添加在重写的run()方法中即可。


接下来,还需要保证在后续获取数据列表时,能够从缓存中读取数据,则先在ICategoryCacheRepository中添加读取列表的功能:

/**
 * 从缓存中读取列表数据
 *
 * @return 列表数据
 */
List<CategoryListItemVO> list();

然后,在CategoryCacheRepositoryImpl中实现:

@Override
public List<CategoryListItemVO> list() {
    long start = 0;
    long end = -1;
    List<Serializable> list = redisTemplate.opsForList().range(KEY_LIST, start, end);
    List<CategoryListItemVO> categories = new ArrayList<>();
    for (Serializable serializable : list) {
        categories.add((CategoryListItemVO) serializable);
    }
    return categories;
}

完成后,当需要读取类别列表时,在CategoryServiceImpl中将调整为从缓存中读取:

@Override
public List<CategoryListItemVO> list() {
    // return categoryMapper.list();
    return categoryCacheRepository.list();
}

重启项目,则客户端提交请求后都将从缓存中获取类别列表!


为了避免数据库中的数据发生变化,但是缓存中的并没有更新,

对于数据较少时  当用户觉得数据不准时 可执行重建缓存功能

所以,添加“重建缓存”的功能,先在ICategoryService中添加:

/**
 * 重建缓存
 */
void rebuildCache();

并在CategoryServiceImpl中实现:

@Override
public void rebuildCache() {
    // TODO 添加限制频繁重建缓存的策略   不能频繁操作   下面实现
    loadCache();
}

并在控制器中添加处理此请求:

@ApiOperationSupport(order = 50)
@ApiOperation("重建缓存")
@PostMapping("/cache/rebuild")
public JsonResult<Void> rebuildCache() {
    categoryService.rebuildCache();
    return JsonResult.ok();
}

重启项目,当需要手动更新缓存数据时,就可以向服务器端发送“重建缓存”的请求,则缓存的数据将更新。



但是,绝对不允许频繁的进行“重建缓存”的操作,必须添加一定的限制条件,通常会使用间隔时间进行约束,则需要解决几个问题:

  • 必须在每次“重建缓存”时记录时间,作为后续的“重建缓存”时用于判断的“上次重建缓存的时间”,此时间值应该记录在Redis中(其实也可以声明全局的静态成员存储此时间),则需要在ICategoryCacheRepository中声明2个新的抽象方法,分别用于写入时间值和读取时间值
  • CategoryServiceImpl业务实现类的private void loadCache()方法中,当已经写入缓存数据后,调用“写入时间值”的方法,以记录此次建立/重建缓存的时间
  • 在“重建缓存”的方法中,读取记录的时间值,用于与当前时间值进行对比,如果时间差小于一定时间,则抛出异常

ICategoryCacheRepository

/**
 * 写入“最近重建缓存”的时间
 */
void setRecentRebuildTime(Long time);

/**
 * 读取“最近重建缓存”的时间
 * @return “最近重建缓存”的时间
 */
Long getRecentRebuildTime();

CategoryCacheRepositoryImpl

/**
 * 类别的“最近重建缓存时间”Redis中的Key
冒号的意义  会在可视化界面做出分级   -号不会分级显示在一行
 */
public static final String KEY_RECENT_REBUILD_TIME = "categories:recent-rebuild-time";

@Override
public void setRecentRebuildTime(Long time) {
    redisTemplate.opsForValue().set(KEY_RECENT_REBUILD_TIME, time);
}

@Override
public Long getRecentRebuildTime() {
    Serializable serializable = redisTemplate.opsForValue().get(KEY_RECENT_REBUILD_TIME);
    if (serializable != null) {
        if (serializable instanceof Long) {
            return (Long) serializable;
        }
    }
    return null;
}

CategoryServiceImpl

/**
 * 重建缓存的间隔时间,以分钟为单位
 */
public static final Integer REBUILD_INTERVAL_TIME_IN_MINUTE = 1;

@Override
public void rebuildCache() {
    // 读取“最近重建缓存”的时间
    Long recentRebuildTime = categoryCacheRepository.getRecentRebuildTime();
    if (recentRebuildTime == null) {
        recentRebuildTime = 0L;
    }
    // 与当前时间对比,判断时间差是否大于xx分钟
    long currentTimeMillis = System.currentTimeMillis();
    if (currentTimeMillis - recentRebuildTime > REBUILD_INTERVAL_TIME_IN_MINUTE * 60 * 1000) {
        // 是:两次重建缓存的间隔时间较长,允许重建缓存
        loadCache();
    } else {
        // 否:两次重建缓存的间隔时间太短,则抛出异常
        throw new ServiceException(NOT_ACCEPTABLE, "重建缓存失败,重建缓存必须间隔至少" + REBUILD_INTERVAL_TIME_IN_MINUTE + "分钟!");
    }
}

/**
 * 加载类别数据的缓存
 */
private void loadCache() {
    // 1. 从数据库中读取类别列表
    List<CategoryListItemVO> list = categoryMapper.list();
    // 2. 清除缓存的类别数据
    categoryCacheRepository.clear();
    // 3. 将类别数据写入到缓存
    categoryCacheRepository.appendList(list);
    // 4. 写入“最近重建缓存”的时间
    categoryCacheRepository.setRecentRebuildTime(System.currentTimeMillis());
}

10. 关于缓存每个“类别”数据

当缓存了“类别”列表list后,很难直接通过缓存中的“列表”实现“根据id获取某个类别的详情”,

为了便于实现各种情景下的数据访问,还应该再次缓存每个“类别”数据。

为了便于后续的操作,在缓存每个“类别”数据时,使用的Key必须是有规律的,并且,还应该将每个“类别”的id作为Key的一部分,才能实现“根据id获取某个类别的详情”。

暂定Key的规则是categories:id,例如categories:15。id为数据的id

要实现此目标,需要:

  • 从数据库中可以获取所有“类别”详情(未实现,此前实现的读取CategoryListItemVO,此时需要一次性读出所有CategoryStandardVO
  • 将某个id对应的“类别”详情数据写入到缓存中
  • 根据id从缓存中读取“类别”详情
  • 获取所有在缓存中的“类别”数据的Key(s)
  • 在“类别”的业务实现类中,将原本从数据库中读取数据改为从缓存中读取数据
  • 在重建缓存时,需要先清除各个“类别”详情数据,并添加各“类别”详情数据到缓存中

关于一次性读出所有CategoryStandardVO,先在CategoryMapper.java接口添加:

List<CategoryStandardVO> listDetails();

然后,在CategoryMapper.xml中配置SQL:

<!-- List<CategoryStandardVO> listDetails(); -->
<select id="listDetails" resultMap="StandardResultMap">
    select
        <include refid="StandardQueryFields" />
    from
        pms_category
    order by
        sort, id desc
</select>

调整Key的常量:

/**
     * 在Redis缓存的所有”类别“数据的Key共有的前缀
     */
    public static final String KEY_PREFIX = "csmall:category:";
    /**
     * ”类别“列表在Redis中的Key,即:csmall:category:list
     */
    public static final String KEY_LIST = KEY_PREFIX + "list";
    /**
     * 各”类别“在Redis中的Key的前缀,例如:csmall:category:item:15
     */
    public static final String KEY_PREFIX_ITEM = KEY_PREFIX + "item:";
    /**
     * 类别的“最近重建缓存时间”Redis中的Key,即:csmall:category:recent-rebuild-time
     */
    public static final String KEY_RECENT_REBUILD_TIME = KEY_PREFIX + "recent-rebuild-time";

然后,在ICategoryCacheRepository中添加抽象方法:

ICategoryCacheRepository及其实现类是为了缓存工作的  为了数据库读到数据放到缓存中

/**
 * 将某个“类别”详情数据写入到缓存中
 *
 * @param category “类别”详情数据
 */
void setCategory(CategoryStandardVO category);

/**
 * 根据id从缓存中读取“类别”详情
 *
 * @param id 尝试读取的”类别“的id
 * @return 匹配的“类别”详情,如果没有匹配的数据,则返回null
 */
CategoryStandardVO getCategoryById(Long id);

/**
 * 获取缓存中所有”类别“数据的Key
 * @return
 */
Set<String> getKeys();

并在CategoryCacheRepository中实现:

并在CategoryCacheRepository中实现:

@Override
public void setCategory(CategoryStandardVO category) {
    String key = KEY_PREFIX_ITEM + category.getId();
    log.debug("处理【类别】缓存:向缓存中写入Key为【{}】的数据:{}", key, category);
    redisTemplate.opsForValue().set(key, category);
}

@Override
public CategoryStandardVO getCategoryById(Long id) {
    CategoryStandardVO category = null;
    String key = KEY_PREFIX_ITEM + id;
    Serializable serializable = redisTemplate.opsForValue().get(key);
    if (serializable != null) {
        if (serializable instanceof CategoryStandardVO) {
            category = (CategoryStandardVO) serializable;
        }
    }
    log.debug("处理【类别】缓存:从缓存中读取详情数据:id={},结果={}", key, category);
    return category;
}

@Override
public Set<String> getKeys() {
    Set<String> keys = redisTemplate.keys(KEY_PREFIX + "*");
    log.debug("处理【类别】缓存:从缓存中读取所有Key:{}", keys);
    return keys;
}

// ===== 以下是调整原实现代码 =====
@Override
public void clear() {
    Set<String> keys = getKeys();
    for (String key : keys) {
        log.debug("处理【类别】缓存:清除数据,Key={}", key);
        redisTemplate.delete(key);
    }
}

接下来,在CategoryServiceImpl中修改原有方法的实现细节:

/**
 * 加载类别数据的缓存
 */
private void loadCache() {
    // 1. 从数据库中读取类别列表
    // 忽略原有代码,增加以下代码
    List<CategoryStandardVO> listDetails = categoryMapper.listDetails();
    // 2. 清除缓存的类别数据
    // 不变
    // 3. 将类别数据写入到缓存
    // 忽略原有代码,增加以下代码
    for (CategoryStandardVO category : listDetails) {
        categoryCacheRepository.setCategory(category);
    }
    // 4. 写入“最近重建缓存”的时间
    // 不变
}

@Override
public CategoryStandardVO getById(Long id) {
    // 改为通过缓存获取数据
    // CategoryStandardVO queryResult = categoryMapper.getById(id);
    CategoryStandardVO queryResult = categoryCacheRepository.getCategoryById(id);
    // 剩余代码不变
}

完成后,重启项目,在缓存预热也会写入每个“类别”的缓存,并且,通过ID查询时,也是从缓存中获取的数据。

12. 关于缓存穿透

当客户端请求某个数据时,还可以使用另一种策略:优先从缓存中获取数据,如果命中缓存,则返回缓存中的数据,如果未命中,则尝试从数据库中查询,如果仍无此数据,则返回null或抛出异常,如果数据库中存在此数据,则将数据写入到缓存,并返回。

以商品数据为例,这种数据其实是可以入在缓存中的,因为热门商品被访问的频率相对较高,除了库存、销量等数据以外,其它数据基本上短期内不会发生变化,则使用缓存是相对合理的!但是,这种商品数据什么时候写入到缓存?如果是刚刚创建时(将数据写入到数据库时)就同步也写入缓存,但是某商品并不是热销商品,长期存在于缓存中会形成浪费!对于这种数据,通常会采取第1次访问时再写入到缓存的机制。

所以,可以将CategoryServiceImpl中的策略调整为(暂时使用“类别”数据演示此做法):

@Override
public CategoryStandardVO getById(Long id) {
    // 暂定策略:优先从缓存中获取数据
    // 如果命中缓存,则返回缓存中的数据
    // 如果未命中,则尝试从数据库中查询,如果仍无此数据,则返回null或抛出异常
    // 如果数据库中存在此数据,则将数据写入到缓存,并返回
    // CategoryStandardVO queryResult = categoryMapper.getById(id);
    CategoryStandardVO queryResult = categoryCacheRepository.getCategoryById(id);
    if (queryResult == null) {
        CategoryStandardVO categoryInDB = categoryMapper.getById(id);
        if (categoryInDB == null) {
            throw new ServiceException(NOT_FOUND, "查询类别详情失败,尝试访问的数据不存在!");
        } else {
            //设置添加缓存  和设置这个缓存的存在时间
            categoryCacheRepository.setCategory(categoryInDB, 1);
            return categoryInDB;
        }
    } else {
        return queryResult;
    }
}

上诉添加时间的代码需要在缓存接口中重载 设置类别的代码  加上时间   () 在再实现类里面实现

/**
     * 设置详情存在  并设置保持在缓存的时间
     * @param category
     * @param durationInMinute
     */
    void setCategory(CategoryStandardVO category,Integer durationInMinute);
@Override
    public void setCategory(CategoryStandardVO category, Integer durationInMinute) {
        String key=KEY_PREFIX_ITEM+category.getId();
        log.debug("设置类别详情的id:向缓存中写入Key为【{}】的数据:{}",key,category);
        redisTemplate.opsForValue().set(key,category,durationInMinute, TimeUnit.MINUTES);
    }

经过以上调整后,将会优先从缓存中获取数据,但是,如果使用一个不存在数据的id值尝试获取数据,就会反复查询数据库,则可能对数据库产生一定的冲击!这种现象称之为:缓存穿透。

关于缓存穿透,最直接的解决方法是:如果未命中缓存,将从数据库中查询数据,即使数据库中也没有匹配的数据,仍会向缓存中写入此id对应的空数据或可以标识为“无”意义的数据,后续,当尝试从缓存中获取数据时,应该先判断获取到的结果是不是空或“无”,再决定后续的处理过程,另外,写入的空或“无”数据应该是在一定时间范围内有限的,过了这段时间将失效!

使用这种策略解决缓存穿透时,需要:

  • 添加向缓存中写入null数据的方法
  • 在业务中,需要增加判断hasKey()的结果

则在ICategoryCacheRepository中添加:

/**
 * 写入某个id对应的”类别“的空值,通常用于解决缓存穿透的问题
 *
 * @param id 尝试从缓存中获取的数据的id
 */
void setEmptyValue(Long id);

/**
 * 判断某个id对应的Key是否存在
 *
 * @param id 类别id
 * @return 此id对应的Key是否存在
 */
boolean hasKey(Long id);

CategoryCacheRepositoryImpl中实现:

@Override
public void setEmptyValue(Long id) {
    String key = KEY_PREFIX_ITEM + id;
    log.debug("处理【类别】缓存:向缓存中写入Key为【{}】的【null】数据:{}", key);
    redisTemplate.opsForValue().set(key, null, 1, TimeUnit.MINUTES);
}

@Override
public boolean hasKey(Long id) {
    String key = KEY_PREFIX_ITEM + id;
    Boolean result = redisTemplate.hasKey(key);
    log.debug("处理【类别】缓存:判断是否存在Key={}的缓存数据,结果为:{}", key, result);
    return result;
}

CategoryServiceImpl中调整getById()的业务逻辑:

// 根据id判断缓存中是否存在此Key
// 是:存在,则从缓存中获取数据,并判断是否为null
// -- 是:缓存中数据为null,抛出异常
// -- 否:缓存中存在有效数据,则返回
// 否:不存在,表示缓存中无此数据,则查数据库,判断是否存在此数据
// -- 是:存在,则将此数据写入缓存,并返回
// -- 否:不存在,则向缓存中写入空值,并抛出异常

或者:

// 根据id从缓存中获取数据,并判断是否不为null
// -- 是:缓存中存在有效数据,则返回

// 判断此Key是否存在
// -- 是:Key存在,却是null,抛出异常

// 查数据库,判断是否存在此数据
// -- 是:存在,则将此数据写入缓存,并返回
// -- 否:不存在,则向缓存中写入空值,并抛出异常

最终实现为:

@Override
public CategoryStandardVO getById(Long id) {
    // 根据id从缓存中获取数据,并判断是否不为null
    CategoryStandardVO category = categoryCacheRepository.getCategoryById(id);
    if (category != null) {
        // 是:缓存中存在有效数据,则返回
        return category;
    }

    // 判断此Key是否存在
    boolean hasKey = categoryCacheRepository.hasKey(id);
    // 是:Key存在,却是null,抛出异常
    if (hasKey) {
        throw new ServiceException(NOT_FOUND, "获取类别详情失败,尝试访问的数据不存在!");
    }

    // 查数据库,判断是否存在此数据
    CategoryStandardVO categoryInDB = categoryMapper.getById(id);
    if (categoryInDB != null) {
        // 是:存在,则将此数据写入缓存,并返回
        categoryCacheRepository.setCategory(categoryInDB);
        return categoryInDB;
    } else {
        // 否:不存在,则向缓存中写入空值,并抛出异常
        categoryCacheRepository.setEmptyValue(id);
        throw new ServiceException(NOT_FOUND, "获取类别详情失败,尝试访问的数据不存在!");
    }
}











问题::::———————————————————————————

 出现字符码混乱   将yml的代码 复制到记事本 再粘贴回来



数据表设计的字段详解..

SPU ;

SKU:

spu:固定不变的

 一个字段寻存多个属性json格式   但是不规范  所以再加一个属性详情表

——————————————

sort:排序   解决各种数据和汉字多音字排序问题。。。。

 ——————————————

显示的时候号显示一个大概的值。 

分库分表时  自定生产id  会出现id重复的问题,需要使用id生成器 ,将id变唯一。 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值