秒杀商品存入缓存
秒杀商品由B端存入Mysql,设置定时任务,每隔一段时间就从Mysql中将符合条件的数据从Mysql中查询出来并存入缓存中,redis以Hash类型进行数据存储。
秒杀服务搭建
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>
3) 添加启动类
@SpringBootApplication @EnableDiscoveryClient @MapperScan(basePackages = {"com.changgou.seckill.dao"}) @EnableScheduling public class SecKillApplication { public static void main(String[] args) { //TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai")); SpringApplication.run(SecKillApplication.class,args); } @Bean public IdWorker idWorker(){ return new IdWorker(1,1); } /** * 设置 redisTemplate 的序列化设置 * @param redisConnectionFactory * @return */ @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; } }
4) 添加application.yml
server: port: 9011 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
5) 添加公钥
6) 添加Oauth配置类
@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() .anyRequest(). authenticated(); //其他地址需要认证授权 } }
7) 更改网关路径过滤类,添加秒杀工程过滤信息
8) 更改网关配置文件,添加请求路由转发
#秒杀微服务 - id: changgou_seckill_route uri: lb://seckill predicates: - Path=/api/seckill/** filters: - StripPrefix=1
时间操作
秒杀商品时间段分析
根据产品原型图结合秒杀商品表设计可以得知,秒杀商品是存在开始时间与结束时间的,当前秒杀商品是按照秒杀时间段进行显示,如果当前时间在符合条件的时间段范围之内,则用户可以秒杀购买当前时间段之内的秒杀商品。
缓存数据加载思路:定义定时任务,每天凌晨会进行当天所有时间段秒杀商品预加载。并且在B端进行限制,添加秒杀商品的话,只能添加当前日期+1的时间限制,比如说:当前日期为8月5日,则添加秒杀商品时,开始时间必须为6日的某一个时间段,否则不能添加。
秒杀商品时间段计算
将资源/DateUtil.java
添加到公共服务中。基于当前工具类可以进行时间段的计算。
在该工具类中,进行时间计算测试:
public static void main(String[] args) { //定义存储结果的集合 List<Date> dateList = new ArrayList<>(); //获取本日凌晨时间点 Date currentData = toDayStartHour(new Date()); //循环12次 (因为要获取每隔两个时间为一个时间段的值) for (int i=0;i<12;i++){ dateList.add(addDateHour(currentData,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); } }
测试结果:
当前业务整体流程分析
1.查询所有符合条件的秒杀商品 1) 获取时间段集合并循环遍历出每一个时间段 2) 获取每一个时间段名称,用于后续redis中key的设置 3) 状态必须为审核通过 status=1 4) 商品库存个数>0 5) 秒杀商品开始时间>=当前时间段 6) 秒杀商品结束<当前时间段+2小时 7) 排除之前已经加载到Redis缓存中的商品数据 8) 执行查询获取对应的结果集 2.将秒杀商品存入缓存
代码实现
更改启动类,添加开启定时任务注解
@EnableScheduling
定义定时任务类
秒杀工程新建task包,并新建任务类SeckillGoodsPushTask
业务逻辑:
1)获取秒杀时间段菜单信息
2)遍历每一个时间段,添加该时间段下秒杀商品
2.1)将当前时间段转换为String,作为redis中的key
2.2)查询商品信息(状态为1,库存大于0,秒杀商品开始时间大于当前时间段,秒杀商品结束时间小于当前时间段,当前商品的id不在redis中)
3)添加redis
/** * 添加秒杀秒伤定时任务 */ @Component public class SeckillGoodsPushTask { @Autowired private SeckillGoodsMapper seckillGoodsMapper; @Autowired private RedisTemplate redisTemplate; private static final String SECKILL_GOODS_KEY="seckill_goods_"; /** * 定时将秒杀商品存入redis * 暂定为30秒一次,正常业务为每天凌晨触发 */ @Scheduled(cron = "0/30 * * * * ?") public void loadSecKillGoodsToRedis(){ List<Date> dateMenus = DateUtil.getDateMenus(); for (Date dateMenu : dateMenus) { //每次用最好都重新new SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); SimpleDateFormat simpleDateFormat1 = 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",simpleDateFormat1.format(DateUtil.addDateHour(dateMenu,2))); Set keys = redisTemplate.boundHashOps(SECKILL_KEY + redisExtName).keys(); if (keys!=null && keys.size()>0){ criteria.andNotIn("id",keys); } List<SeckillGoods> seckillGoodsList = seckillGoodsMapper.selectByExample(example); //添加到缓存中 for (SeckillGoods seckillGoods : seckillGoodsList) { redisTemplate.boundHashOps(SECKILL_KEY + redisExtName).put(seckillGoods.getId(),seckillGoods); } } } }