Redis实战之实现特价商品展示

举报
LoneWalker、 发表于 2023/08/27 16:44:37 2023/08/27
【摘要】 Redis实战之实现特价商品展示

前言🔍

上一篇:Redis入门

本篇涉及相关命令:DELLrange、Rpush

示例基于SpringBoot 2.7.14

DEL

DEL命令用于删除已存在的键。不存在的 key 会被忽略。

Lrange

Lrange 返回列表中指定区间内的元素,区间以偏移量 START 和 END 指定。 其中 0 表示列表的第一个元素, 1 表示列表的第二个元素,以此类推。 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。

返回值:一个列表,包含指定区间内的元素。

Rpush

Rpush 命令用于将一个或多个值插入到列表的尾部(最右边)。如果列表不存在,一个空列表会被创建并执行 Rpush 操作。 当列表存在但不是列表类型时,返回一个错误。

返回值:执行 Rpush 操作后,列表的长度。

示例💡

通常特价商品页有数据量少、高并发等特点。实现思路是先把数据库中的数据抽取到Redis里面。采用定时任务来刷新缓存。

模仿如下图的特价商品的展示,我们可以使用Redis 列表(List)实现将这部分数据定时刷到Redis中。

以下示例基于SpringBoot 2.7.14,定时任务用Quartz实现。

引入相关场景启动器

<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>cn.hutool</groupId>
  <artifactId>hutool-all</artifactId>
  <version>5.8.18</version>
</dependency>

<!-- lombok -->
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
  <version>1.18.24</version>
  <scope>provided</scope>
</dependency>

<!-- spring-boot-starter-quartz -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-quartz</artifactId>
  <version>3.0.6</version>
</dependency>

application.properties

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=
spring.redis.database=1
spring.redis.client-type=lettuce
spring.redis.connect-timeout=5000
spring.redis.lettuce.pool.enabled=true
spring.redis.lettuce.pool.min-idle=5
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.max-active=10
##最大阻塞等待时间,-1 表示不限制
spring.redis.lettuce.pool.max-wait=3000


spring.quartz.properties.org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
spring.quartz.properties.org.quartz.threadPool.threadCount=10
spring.quartz.properties.org.quartz.threadPool.threadPriority=5
spring.quartz.properties.org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread=true

RedisConguration

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;

/**
 * @author lonewalker
 */
@Configuration
public class RedisConfiguration {
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port;
    @Value("${spring.redis.password}")
    private String password;
    @Value("${spring.redis.database}")
    private int database;

    @Bean
    @Primary
    @ConfigurationProperties(prefix = "spring.redis.lettuce.pool")
    public GenericObjectPoolConfig<LettucePoolingClientConfiguration> genericObjectPoolConfig() {
        return new GenericObjectPoolConfig<>();
    }

    @Bean
    public RedisStandaloneConfiguration redisStandaloneConfiguration() {
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setDatabase(database);
        redisStandaloneConfiguration.setHostName(host);
        redisStandaloneConfiguration.setPassword(password);
        redisStandaloneConfiguration.setPort(port);
        return redisStandaloneConfiguration;
    }

    @Bean
    public LettuceClientConfiguration lettuceClientConfiguration(GenericObjectPoolConfig<LettucePoolingClientConfiguration> genericObjectPoolConfig) {
        return LettucePoolingClientConfiguration.builder().poolConfig(genericObjectPoolConfig).build();
    }

    @Bean
    public LettuceConnectionFactory lettuceConnectionFactory(RedisStandaloneConfiguration redisSentinelConfiguration, LettuceClientConfiguration lettuceClientConfiguration) {
        return new LettuceConnectionFactory(redisSentinelConfiguration, lettuceClientConfiguration);
    }

    /**
     * 创建 RedisTemplate 设置序列化方式
     */
    @Bean(name = "redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory factory) {
        // 创建 RedisTemplate 对象
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 设置 RedisConnection 工厂
        template.setConnectionFactory(factory);

        ObjectMapper objectMapper = new ObjectMapper();
        // 指定要序列化的域
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY)
                // 不将日期写为时间戳
                .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
                // 忽略未知属性
                .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
                // 对象属性为空时可以序列化
                .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
                // 记录被序列化的类型信息
                .activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL,
                        JsonTypeInfo.As.WRAPPER_ARRAY)
                // null 值不序列化
                .setSerializationInclusion(JsonInclude.Include.NON_NULL)
                // 日期处理
                .registerModule(new JavaTimeModule());
        GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(objectMapper);

        // 默认使用了jdk的序列化方式,可读性差,我们使用 String 序列化方式,序列化 KEY 。
        template.setKeySerializer(RedisSerializer.string());
        template.setHashKeySerializer(RedisSerializer.string());

        // 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。
        template.setValueSerializer(serializer);
        template.setHashValueSerializer(serializer);

        return template;
    }
}

RedisUtil

@Component
public class RedisUtil {
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

        /**
     * 删除缓存
     *
     * @param key 可以传一个值 或多个
     */
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete(Arrays.asList(key));
            }
        }
    }

        /**
     * 获取list缓存的内容
     *
     * @param key   键
     * @param start 开始
     * @param end   结束 0 到 以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。
     * @return List
     */
    public List<Object> lRange(String key, Long start, Long end) {
        try {
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

        /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @return Boolean
     */
    public Boolean rightPush(String key, List<?> value) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value.toArray());
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return Boolean
     */
    public Boolean rightPush(String key, List<?> value, Long time) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value.toArray());
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
}

将数据刷入到Redis的任务。

/**
 * @author lonewalker
 */
@Slf4j
public class RefreshJob implements Job {

    @Override
    public void execute(JobExecutionContext jobExecutionContext) {
        //模拟从数据库读取100件特价商品,用于加载到特价商品的页面中
        List<Product> list= products();
        RedisUtil.delete(RedisKey.BARGAIN_KEY);
        //采用redis list数据结构的rPush来实现存储
        RedisUtil.rightPush(RedisKey.BARGAIN_KEY, list);
        log.info("特价商品已刷新");
    }

    public static List<Product> products() {
        List<Product> list=new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            Random rand = new Random();
            int id= rand.nextInt(10000);
            Product obj=new Product((long) id,"product"+i,i,"detail");
            list.add(obj);
        }
        return list;
    }
}

常量

public class RedisKey {

    public static final String BARGAIN_KEY = "bargain";
}

定时任务

import com.example.redis.job.RefreshJob;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;

import static org.quartz.CronScheduleBuilder.cronSchedule;

@Slf4j
@Configuration
public class SchedulerConfiguration {

    @Resource
    private Scheduler scheduler;

    @Bean
    public void startJob() throws SchedulerException {
        refreshData(scheduler);
    }

    private void refreshData(Scheduler scheduler) throws SchedulerException {
        //定义一个触发器
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("triggerOne", "bargain")
                .startNow()
                .withSchedule(cronSchedule("0 0/1 * * * ?"))
                .build();
        //定义一个JobDetail
        JobDetail jobDetail = JobBuilder.newJob(RefreshJob.class)
                .withIdentity("jobOne", "bargain")
                .build();
        //调度加入这个job
        scheduler.scheduleJob(jobDetail, trigger);
    }
}

entity

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@NoArgsConstructor
@Data
public class Product {

    private Long id;

    private String name;

    private Integer amount;

    private String description;
}

controller

import com.example.redis.entity.Product;
import com.example.redis.service.RedisService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.List;

/**
 * @author lonewalker
 */
@RequestMapping("/redis")
@RestController
public class RedisController {

    @Resource
    private RedisService redisService;

    @GetMapping("/queryBargain")
    public List<Product> queryBargain(int pageNum,int pageSize){
        return redisService.queryBargain(pageNum, pageSize);
    }
}

service

@Slf4j
@Service
public class RedisService {

    @Resource
    private RedisUtil redisUtil;

    public List<Product> queryBargain(int pageNum,int pageSize){
        List<Product> products = new ArrayList<>();
        long start = (long) (pageNum - 1) * pageSize;
        long end = start + pageSize - 1;
        try {
            //采用redis list数据结构的lrange命令实现分页查询
            List<Object> list = redisUtil.lRange(RedisKey.BARGAIN_KEY, start, end);
            if (CollUtil.isEmpty(list)) {
                //TODO 走DB查询
            }
            products = (List<Product>) (Object) list;
        } catch (Exception ex) {
            //这里的异常,一般是redis瘫痪 ,或 redis网络timeout
            log.error("exception:", ex);
            //TODO 走DB查询
        }
        return products;
    }
}

演示📖

启动项目看一下效果:

image.png

数据刷入Redis之后可以请求接口了

image.png

再看一下Redis里是存入了100条

image.png

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。