02秒杀商品存入缓存

2.秒杀商品存入缓存

在这里插入图片描述

秒杀商品由B端存入Mysql,设置定时任务,每隔一段时间就从Mysql中将符合条件的数据从Mysql中查询出来并存入缓存中,redis以Hash类型进行数据存储。

2.1 秒杀服务搭建

1)新建服务changgou_service_seckill
2)添加依赖信息,详情如下:

 <dependencies>
        <dependency>
            <groupId>com.changgou</groupId>
            <artifactId>changgou_common_db</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>com.changgou</groupId>
            <artifactId>changgou_service_order_api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.changgou</groupId>
            <artifactId>changgou_service_seckill_api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.changgou</groupId>
            <artifactId>changgou_service_goods_api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit</artifactId>
        </dependency>
        <!--oauth依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
    </dependencies>
  1. 添加启动类
package com.changgou.seckill;

import com.changgou.util.IdWorker;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericToStringSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scheduling.annotation.EnableScheduling;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication
@EnableEurekaClient
@MapperScan(basePackages = {"com.changgou.seckill.dao"})
@EnableScheduling
public class SecKillApplication {

    public static void main(String[] args) {
        SpringApplication.run(SecKillApplication.class,args);
    }

    //idwork
    @Bean
    public IdWorker idWorker(){
        return new IdWorker(1,1);
    }

    //设置redistemplate的序列化
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // 1.创建 redisTemplate 模版
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        // 2.关联 redisConnectionFactory
        template.setConnectionFactory(redisConnectionFactory);
        // 3.创建 序列化类
        GenericToStringSerializer genericToStringSerializer = new GenericToStringSerializer(Object.class);
        // 6.序列化类,对象映射设置
        // 7.设置 value 的转化格式和 key 的转化格式
        template.setValueSerializer(genericToStringSerializer);
        template.setKeySerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }
}

  1. 添加application.yml
server:
  port: 9016
spring:
  jackson:
    time-zone: GMT+8
  application:
    name: seckill
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://192.168.200.128:3306/changgou_seckill?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT%2b8
    username: root
    password: root
  main:
    allow-bean-definition-overriding: true #当遇到同样名字的时候,是否允许覆盖注册
  redis:
    host: 192.168.200.128
  rabbitmq:
    host: 192.168.200.128
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:6868/eureka
  instance:
    prefer-ip-address: true
feign:
  hystrix:
    enabled: true
  client:
    config:
      default:   #配置全局的feign的调用超时时间  如果 有指定的服务配置 默认的配置不会生效
        connectTimeout: 60000 # 指定的是 消费者 连接服务提供者的连接超时时间 是否能连接  单位是毫秒
        readTimeout: 20000  # 指定的是调用服务提供者的 服务 的超时时间()  单位是毫秒
#hystrix 配置
hystrix:
  command:
    default:
      execution:
        timeout:
          #如果enabled设置为false,则请求超时交给ribbon控制
          enabled: true
        isolation:
          strategy: SEMAPHORE
          thread:
            # 熔断器超时时间,默认:1000/毫秒
            timeoutInMilliseconds: 20000
  1. 添加公钥

在这里插入图片描述

  1. 添加Oauth配置类
package com.changgou.seckill.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.stream.Collectors;

@Configuration
@EnableResourceServer
//开启方法上的PreAuthorize注解
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    //公钥
    private static final String PUBLIC_KEY = "public.key";

    /***
     * 定义JwtTokenStore
     * @param jwtAccessTokenConverter
     * @return
     */
    @Bean
    public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
        return new JwtTokenStore(jwtAccessTokenConverter);
    }

    /***
     * 定义JJwtAccessTokenConverter
     * @return
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setVerifierKey(getPubKey());
        return converter;
    }
    /**
     * 获取非对称加密公钥 Key
     * @return 公钥 Key
     */
    private String getPubKey() {
        Resource resource = new ClassPathResource(PUBLIC_KEY);
        try {
            InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());
            BufferedReader br = new BufferedReader(inputStreamReader);
            return br.lines().collect(Collectors.joining("\n"));
        } catch (IOException ioe) {
            return null;
        }
    }

    /***
     * Http安全配置,对每个到达系统的http请求链接进行校验
     * @param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        //所有请求必须认证通过
        http.authorizeRequests()
                .antMatchers("/seckillgoods/list/**")
                .permitAll()
                .anyRequest()
                .authenticated();    //其他地址需要认证授权
    }
}

  1. 更改网关路径过滤类,添加秒杀工程过滤信息
    在这里插入图片描述

  2. 更改网关配置文件,添加请求路由转发

#秒杀微服务
		- id: changgou_seckill_route
		  uri: lb://seckill
		  predicates:
		    - Path=/api/seckill/**
		  filters:
		    - StripPrefix=1

2.2 时间操作

2.2.1 秒杀商品时间段分析

在这里插入图片描述

在这里插入图片描述

根据产品原型图结合秒杀商品表设计可以得知,秒杀商品是存在开始时间与结束时间的,当前秒杀商品是按照秒杀时间段进行显示,如果当前时间在符合条件的时间段范围之内,则用户可以秒杀购买当前时间段之内的秒杀商品。
缓存数据加载思路:定义定时任务,每天凌晨会进行当天所有时间段秒杀商品预加载。并且在B端进行限制,添加秒杀商品的话,只能添加当前日期+1的时间限制,比如说:当前日期为8月5日,则添加秒杀商品时,开始时间必须为6日的某一个时间段,否则不能添加。

2.2.2 秒杀商品时间段计算

将 资源/DateUtil.java 添加到公共服务中。基于当前工具类可以进行时间段的计算

package com.changgou.util;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;

public class DateUtil {

    /***
     * 从yyyy-MM-dd HH:mm格式转成yyyyMMddHH格式
     * @param dateStr
     * @return
     */
    public static String formatStr(String dateStr){
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
        try {
            Date date = simpleDateFormat.parse(dateStr);
            simpleDateFormat = new SimpleDateFormat("yyyyMMddHH");
            return simpleDateFormat.format(date);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return null;
    }

    /***
     * 获取指定日期的凌晨
     * @return
     */
    public static Date toDayStartHour(Date date){
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        calendar.set(Calendar.HOUR_OF_DAY, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);
        Date start = calendar.getTime();
        return start;
    }


    /***
     * 时间增加N分钟
     * @param date
     * @param minutes
     * @return
     */
    public static Date addDateMinutes(Date date,int minutes){
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        calendar.add(Calendar.MINUTE, minutes);// 24小时制
        date = calendar.getTime();
        return date;
    }

    /***
     * 时间递增N小时
     * @param hour
     * @return
     */
    public static Date addDateHour(Date date,int hour){
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        calendar.add(Calendar.HOUR, hour);// 24小时制
        date = calendar.getTime();
        return date;
    }

    /***
     * 获取时间菜单
     * @return
     */
    public static List<Date> getDateMenus(){
      
        //定义一个List<Date>集合,存储所有时间段
        List<Date> dates = new ArrayList<Date>();
        
        //循环12次
        Date date = toDayStartHour(new Date()); //凌晨
        for (int i = 0; i <12 ; i++) {
            //每次递增2小时,将每次递增的时间存入到List<Date>集合中
            dates.add(addDateHour(date,i*2));
        }

        //判断当前时间属于哪个时间范围
        Date now = new Date();
        for (Date cdate : dates) {
            //开始时间<=当前时间<开始时间+2小时
            if(cdate.getTime()<=now.getTime() && now.getTime()<addDateHour(cdate,2).getTime()){
                now = cdate;
                break;
            }
        }

        //当前需要显示的时间菜单
        List<Date> dateMenus = new ArrayList<Date>();
        for (int i = 0; i <5 ; i++) {
            dateMenus.add(addDateHour(now,i*2));
        }
        return dateMenus;
    }

    /***
     * 时间转成yyyyMMddHH
     * @param date
     * @return
     */
    public static String date2Str(Date date){
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddHH");
        return simpleDateFormat.format(date);
    }

    public static void main(String[] args) {

        //存储数据结果
        List<Date> dateList = new ArrayList<>();

        //获取到本日的凌晨时间点
        Date startHour = toDayStartHour(new Date());

        //循环12次
        for(int i=0;i<12;i++){
            dateList.add(addDateHour(startHour,i*2));
        }

        for (Date date : dateList) {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String format = simpleDateFormat.format(date);
            System.out.println(format);
        }
    }
}

2.2.3 当前业务整体流程分析
1.查询所有符合条件的秒杀商品
1) 获取时间段集合并循环遍历出每一个时间段
2) 获取每一个时间段名称,用于后续redis中key的设置
3) 状态必须为审核通过 status=1
4) 商品库存个数>0
5) 秒杀商品开始时间>=当前时间段
6) 秒杀商品结束<当前时间段+2小时
7) 排除之前已经加载到Redis缓存中的商品数据
8) 执行查询获取对应的结果集
2.将秒杀商品存入缓存

2.3 代码实现

2.3.2 更改启动类,添加开启定时任务注解
2.3.3 定义定时任务类

秒杀工程新建task包,并新建任务类SeckillGoodsPushTask
业务逻辑:
1)获取秒杀时间段菜单信息
2)遍历每一个时间段,添加该时间段下秒杀商品
2.1)将当前时间段转换为String,作为redis中的key
2.2)查询商品信息(状态为1,库存大于0,秒杀商品开始时间大于当前时间段,秒杀商品结束时间小于当前时间段,当前商品的id不在redis中)
3)添加redis

package com.changgou.seckill.task;

import com.changgou.seckill.dao.SeckillGoodsMapper;
import com.changgou.seckill.pojo.SeckillGoods;
import com.changgou.util.DateUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import tk.mybatis.mapper.entity.Example;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Set;

@Component
public class SeckillGoodsPushTask {

    @Autowired
    private SeckillGoodsMapper seckillGoodsMapper;

    @Autowired
    private RedisTemplate redisTemplate;

    public static final String SECKILL_GOODS_KEY="seckill_goods_";

    @Scheduled(cron = "0/30 * * * * ?")
    public void  loadSecKillGoodsToRedis(){
        /**
         * 1.查询所有符合条件的秒杀商品
         * 	1) 获取时间段集合并循环遍历出每一个时间段
         * 	2) 获取每一个时间段名称,用于后续redis中key的设置
         * 	3) 状态必须为审核通过 status=1
         * 	4) 商品库存个数>0
         * 	5) 秒杀商品开始时间>=当前时间段
         * 	6) 秒杀商品结束<当前时间段+2小时
         * 	7) 排除之前已经加载到Redis缓存中的商品数据
         * 	8) 执行查询获取对应的结果集
         * 2.将秒杀商品存入缓存
         */

        List<Date> dateMenus = DateUtil.getDateMenus(); // 5个

        for (Date dateMenu : dateMenus) {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

            String redisExtName = DateUtil.date2Str(dateMenu);

            Example example = new Example(SeckillGoods.class);
            Example.Criteria criteria = example.createCriteria();

            criteria.andEqualTo("status","1");
            criteria.andGreaterThan("stockCount",0);
            criteria.andGreaterThanOrEqualTo("startTime",simpleDateFormat.format(dateMenu));
            criteria.andLessThan("endTime",simpleDateFormat.format(DateUtil.addDateHour(dateMenu,2)));

            Set keys = redisTemplate.boundHashOps(SECKILL_GOODS_KEY + redisExtName).keys();//key field value

            if (keys != null && keys.size()>0){
                criteria.andNotIn("id",keys);
            }

            List<SeckillGoods> seckillGoodsList = seckillGoodsMapper.selectByExample(example);

            //添加到缓存中
            for (SeckillGoods seckillGoods : seckillGoodsList) {
                redisTemplate.opsForHash().put(SECKILL_GOODS_KEY + redisExtName,seckillGoods.getId(),seckillGoods);
            }
        }

    }
}

<think>嗯,用户让我介绍如何写一套Java高并发秒杀商品的源码。首先,我需要理解高并发秒杀系统的核心挑战是什么。秒杀场景下,大量用户同时访问,系统必须在极短时间内处理大量请求,同时保证数据的一致性,尤其是库存不能超卖。 我应该从架构设计开始考虑。可能要用分布式系统,比如用Spring Cloud或者Dubbo。不过如果是单体的,可能用Spring Boot简化开发。接下来是数据库部分,MySQL的话,库存扣减是个问题。直接更新库存可能会导致锁竞争,所以可能需要预扣库存,或者使用乐观锁,通过版本号控制。 然后,Redis的作用很大,作为缓存层。提前把商品库存加载到Redis中,用原子操作比如decrement来处理秒杀请求,这样可以减少数据库的压力。不过要注意缓存和数据库的一致性,可能需要异步同步或者消息队列来处理。 消息队列比如RabbitMQ或者Kafka,用来异步处理下单请求。这样可以削峰,把瞬间的高并发请求转为顺序处理,避免数据库崩溃。但是用户可能需要立即知道结果,所以前端可能需要轮询或者WebSocket来通知结果。 限流和降级也是关键,比如使用Sentinel或者Hystrix。限流可以防止系统过载,降级可以在压力大时关闭非核心功能。还有防止重复提交,前端按钮禁用,后端用验证码或者令牌机制。 分布式锁可能用Redis的setnx或者Redisson,确保同一用户不能重复秒杀,或者库存操作的原子性。另外,可以考虑库存预热,提前将库存放到缓存,避免每次都查数据库。 安全方面,防止恶意请求,可能需要验证用户身份,限制IP频率。还有数据一致性问题,比如Redis预减库存后,如何保证数据库最终扣减成功,否则需要回滚缓存。事务消息或者本地事务表可能有帮助。 测试部分需要用JMeter模拟高并发,查看系统表现,找出瓶颈。可能还需要优化SQL语句,加索引,分库分表,或者使用数据库连接池提升性能。 现在要考虑用户可能的背景。他们可能有一定的Java基础,想了解高并发的处理方案,或者正在准备面试,需要项目经验。可能希望源码结构清晰,有详细注释,涵盖主要技术点。 需要分步骤讲解,从架构设计到具体实现,包括技术选型、数据库设计、缓存、消息队列、限流等。给出关键代码片段,比如库存扣减的Service层,Redis操作,消息队列的使用,以及如何保证原子性。 可能还需要提到常见的坑,比如缓存击穿、雪崩,超卖问题,事务的最终一致性。以及如何监控系统,比如使用Prometheus和Grafana。 最后,是否需要给出完整的源码?可能用户希望有示例代码或GitHub链接,但直接贴大量代码不合适,可以建议参考开源项目,或者提供关键部分的代码片段,并说明实现逻辑。</think>以下是一套Java高并发秒杀系统的核心设计思路和关键代码实现,涵盖高并发场景下的典型解决方案: --- ### **1. 技术选型** - **基础框架**:Spring Boot + MyBatis - **缓存层**:Redis(库存预热 + 原子操作) - **消息队列**:RabbitMQ/Kafka(流量削峰) - **限流降级**:Sentinel/Guava RateLimiter - **分布式锁**:Redisson - **数据库优化**:MySQL + 乐观锁 --- ### **2. 核心设计** #### **2.1 分层架构** ```java 用户请求 → 网关层(限流) → 服务层(Redis预减库存) → 消息队列(异步下单) → 数据库(最终扣减) ``` #### **2.2 流程设计** 1. **库存预热**:将商品库存提前加载到Redis 2. **请求拦截**:验证用户资格(是否重复秒杀) 3. **预减库存**:Redis原子操作避免超卖 4. **异步下单**:通过消息队列解耦核心逻辑 5. **最终扣减**:数据库乐观锁保证一致性 --- ### **3. 关键代码实现** #### **3.1 Redis预减库存** ```java // 初始化库存到Redis @PostConstruct public void initStock() { redisTemplate.opsForValue().set("seckill:stock:" + productId, stock); } // 原子操作预减库存 public boolean preReduceStock(String productId) { String key = "seckill:stock:" + productId; Long value = redisTemplate.opsForValue().decrement(key); return value != null && value >= 0; } ``` #### **3.2 乐观锁更新库存** ```sql UPDATE product SET stock = stock - 1, version = version + 1 WHERE id = #{productId} AND stock > 0 AND version = #{version} ``` #### **3.3 消息队列异步处理** ```java // 发送秒杀请求到队列 @Autowired private RabbitTemplate rabbitTemplate; public void sendSeckillMessage(String userId, String productId) { SeckillMessage message = new SeckillMessage(userId, productId); rabbitTemplate.convertAndSend("seckill.exchange", "seckill.routingKey", message); } // 消费者处理下单 @RabbitListener(queues = "seckill.queue") public void processSeckillOrder(SeckillMessage message) { orderService.createOrder(message.getUserId(), message.getProductId()); } ``` #### **3.4 限流配置(Sentinel)** ```java @GetMapping("/seckill") @SentinelResource(value = "seckill", blockHandler = "blockHandler") public String seckill(String productId) { // 核心业务逻辑 } // 限流降级方法 public String blockHandler(String productId, BlockException ex) { return "当前请求过多,请稍后再试!"; } ``` --- ### **4. 防止超卖与重复购买** - **Redis分布式锁**:控制单个商品的操作原子性 - **用户秒杀记录**:使用`userid + productid`生成唯一键存入Redis ```java String recordKey = "seckill:record:" + userId + ":" + productId; if (redisTemplate.opsForValue().setIfAbsent(recordKey, "1", 2, TimeUnit.HOURS)) { // 允许下单 } else { throw new RuntimeException("请勿重复参与!"); } ``` --- ### **5. 性能优化点** 1. **库存预热**:避免实时查询数据库 2. **页面静态化**:使用CDN缓存商品详情页 3. **Redis集群**:提升缓存吞吐量 4. **数据库分库分表**:按商品ID分片 5. **JVM调优**:合理设置堆内存与GC策略 --- ### **6. 完整项目结构** ``` src/ ├── main/ │ ├── java/ │ │ ├── controller/ // 接口层 │ │ ├── service/ // 业务逻辑 │ │ ├── mq/ // 消息队列处理 │ │ ├── dao/ // 数据持久层 │ │ └── config/ // Redis/Sentinel等配置 │ └── resources/ │ ├── application.yml // 配置文件 ├── test/ // 压测脚本(JMeter示例) ``` --- ### **7. 压测建议** - 使用JMeter模拟1万+ QPS - 监控指标:RT时间、MySQL连接数、Redis命中率 - 逐步优化瓶颈点(如:连接池参数、索引优化) --- 如需完整源码,可参考GitHub开源项目(例如miaosha),或通过以下简化版Demo扩展实现: ```java // 完整核心逻辑示例 public Result seckill(String userId, String productId) { // 1. 验证用户资格 if (redisUtil.hasKey("seckill:record:" + userId + ":" + productId)) { return Result.error("重复请求"); } // 2. Redis预减库存 if (!redisUtil.decrement("seckill:stock:" + productId, 1)) { return Result.error("库存不足"); } // 3. 发送消息队列 mqSender.sendSeckillMessage(userId, productId); return Result.success("排队中..."); } ``` 高并发系统需要结合具体业务场景持续优化,建议通过分布式压测工具验证各环节性能表现。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值