谷粒商城–秒杀服务–高级篇笔记十二

1.后台添加秒杀商品

未配置秒杀服务相关网关

1.1 配置网关

        - id: coupon_routeuri: lb://gulimall-couponpredicates:- Path=/api/coupon/**filters:- RewritePath=/api/(?<segment>.*),/$\{segment}

1.2 配置网关完成,添加场次

1.3 上架秒杀商品

promotion_session_id代表场次id

1.4 上架商品小bug,在任意一个场次可以查询的所有场次的上架商品

解决方案:按照场次查询上架商品,修改关联商品接口查询逻辑

gulimall-coupon/src/main/java/site/zhourui/gulimall/coupon/service/impl/SeckillSkuRelationServiceImpl.java

    @Overridepublic PageUtils queryPage(Map<String, Object> params) {QueryWrapper<SeckillSkuRelationEntity> queryWrapper = new QueryWrapper<SeckillSkuRelationEntity>();String promotionSessionId = (String) params.get("promotionSessionId");// 2、封装活动场次id,关联if (!StringUtils.isEmpty(promotionSessionId)) {queryWrapper.eq("promotion_session_id", promotionSessionId);}IPage<SeckillSkuRelationEntity> page = this.page(new Query<SeckillSkuRelationEntity>().getPage(params),queryWrapper);return new PageUtils(page);}

此时重启优惠服务,场次二不能查询到场次一的上架商品,场次一只能查询到自己的上架商品

2. 定时任务

2.1 cron 表达式

在线Cron表达式生成器 (qqe2.com)

2.1.1 cron表达式语法

语法:秒 分 时 日 月 周 年 (spring 不支持年,所以可以不写)

quartz表达式格式介绍

Cron Trigger Tutorial (quartz-scheduler.org)

A cron expression is a string comprised of 6 or 7 fields separated by white space. Fields can contain any of the allowed values, along with various combinations of the allowed special characters for that field. The fields are as follows:

Field Name Mandatory Allowed Values Allowed Special Characters
Seconds YES 0-59 , - * /
Minutes YES 0-59 , - * /
Hours YES 0-23 , - * /
Day of month YES 1-31 , - * ? / L W
Month YES 1-12 or JAN-DEC , - * /
Day of week YES 1-7 or SUN-SAT , - * ? / L #
Year NO empty, 1970-2099 , - * /

So cron expressions can be as simple as this: * * * * ? *

or more complex, like this: 0/5 14,18,3-39,52 * ? JAN,MAR,SEP MON-FRI 2002-2010

2.1.2 cron表达式特殊字符

,:枚举;
(cron="7,9,23****?"):任意时刻的7,9,23秒启动这个任务;
-:范围:
(cron="7-20****?""):任意时刻的7-20秒之间,每秒启动一次
*:任意;
指定位置的任意时刻都可以
/:步长;
(cron="7/5****?"):第7秒启动,每5秒一次;
(cron="*/5****?"):任意秒启动,每5秒一次;? :(出现在日和周几的位置):为了防止日和周冲突,在周和日上如果要写通配符使用?
(cron="***1*?"):每月的1号,而且必须是周二然后启动这个任务;L:(出现在日和周的位置)”,
last:最后一个
(cron="***?*3L"):每月的最后一个周二W:Work Day:工作日
(cron="***W*?"):每个月的工作日触发
(cron="***LW*?"):每个月的最后一个工作日触发
#:第几个
(cron="***?*5#2"):每个月的 第2个周4

2.1.3 示例

Expression Meaning
0 0 12 * * ? 每天中午12点触发
0 15 10 ? * * 每天的10点15分触发
0 15 10 * * ? 每天的10点15分触发
0 15 10 * * ? * 每天的10点15分触发
0 15 10 * * ? 2005 2005年的10点15分触发
0 * 14 * * ? 每天的14:00-14:59 每分钟触发一次
0 0/5 14 * * ? 每天的14:00-14:59 每五分钟触发一次
0 0/5 14,18 * * ? 每天的14:00-14:59 和18:00-18:59 每五分钟触发一次
0 0-5 14 * * ? 每天的14:00-14:05每分钟执行一次
0 10,44 14 ? 3 WED 3月的每个星期三的14:10:00和14:44:00触发一次
0 15 10 ? * MON-FRI 星期一到星期五的10:15:00触发
0 15 10 15 * ? 每个月的15号10:15:00触发
0 15 10 L * ? 每个月的最后一天10:15:00触发
0 15 10 L-2 * ? 每个月的倒数第二天10:15:00触发
0 15 10 ? * 6L 每个月的最后一个星期五的10:15:00触发
0 15 10 ? * 6L 每个月的最后一个星期五10:15:00触发
0 15 10 ? * 6L 2002-2005 2002年到2005年的每个月的最后一个星期五的10:15:00触发
0 15 10 ? * 6#3 每个月的第3个星期五的10:15:00触发
0 0 12 1/5 * ? 每个月的1号开始每五天12:00:00触发
0 11 11 11 11 ? 十一月的11号的11:11:00触发

3. 秒杀模块

3.1 创建模块

pom

gulimall-seckill/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.3.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.zhourui.gulimall</groupId><artifactId>gulimall-seckill</artifactId><version>0.0.1-SNAPSHOT</version><name>gulimall-seckill</name><description>秒杀</description><properties><java.version>1.8</java.version><spring-cloud.version>Hoxton.SR6</spring-cloud.version></properties><dependencies><dependency><groupId>com.zhourui.gulimall</groupId><artifactId>gulimall-common</artifactId><version>0.0.1-SNAPSHOT</version><exclusions><exclusion><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId></exclusion><exclusion><groupId>io.seata</groupId><artifactId>seata-all</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

配置秒杀模块

gulimall-seckill/src/main/resources/application.yml

server:port: 25000
spring:application:name: gulimall-seckillcloud:nacos:discovery:server-addr: localhost:8848redis:host: 192.168.157.128port: 6379

主启动类上启动注册发现与feign调用

@EnableFeignClients
@EnableDiscoveryClient

3.2 springboot整合定时任务异步任务(使用异步任务 + 定时任务来完成定时任务不阻塞的功能)

3.2.1整合定时任务

3.2.1.0 自动配置类TaskSchedulingAutoConfiguration

在类上标注注解开启定时任务功能

@Component
@EnableScheduling

在需要开启定时任务的方法标注注解,为该方法开启定时任务,并填上cron表达式

@Scheduled(cron = "* * * * * ? ")

整合示例

gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/scheduled/HelloScheduled.java

package site.zhourui.gulimall.seckill.scheduled;import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;/*** @author zr* @date 2022/1/6 14:29*/
@Slf4j
@Component
@EnableScheduling
public class HelloScheduled {@Scheduled(cron = "* * * * * ? ")public void hello(){log.info("hello world");}
}

测试结果,每秒执行一次

注意事项

1、在Spring中表达式是6位组成,不允许第七位的年份
2、在周几的的位置,1-7代表周一到周日
3、定时任务不该阻塞。默认是阻塞的

3.2.2 整合异步任务

3.2.2.0 自动配置类TaskExecutionProperties
3.2.2.1 整合异步任务原因

定时任务不该阻塞。默认是阻塞的

示范

模拟业务处理时间较长,发现日志打印时间间隔为4秒,说明定时任务是阻塞的

3.2.2.2 步骤

在类上标注注解开启异步任务功能

@EnableAsync

在需要开启异步任务的方法标注注解,为该方法开启异步任务

@Async

日志打印的时间间隔为1秒,任务没有阻塞了

3.2.2.3 设置线程池大小
spring:task:execution:pool:core-size: 8  #默认大小为8max-size: 50  #默认最大为int
123456

3.2.3 开启异步任务的方式

3.2.3.1 自己异步执行CompletableFuture.runAsync
//可以让业务以异步的方式,自己提交到线程池CompletableFuture.runAsync(() -> {},execute);
3.2.3.2 设置定时任务的线程池线程数量(默认为1)

定时任务开启后其实也是有线程池的,通过更改配置修改线程池大小

spring:task:scheduling:pool:size: 2  #默认为1,就会阻塞
3.2.3.3 异步开启任务3.2章节

3.3 秒杀商品定时上架

3.3.1 秒杀上架流程图

3.3.2 秒杀(高并发)系统关注的问题

3.3.3 查询从今天开始三天之内的秒杀场次信息

3.3.3.1 时间如期处理
3.3.3.1.1获取当天0点的时间
    /*** 当前时间* @return*/private String startTime() {LocalDate now = LocalDate.now();LocalTime min = LocalTime.MIN;LocalDateTime start = LocalDateTime.of(now, min);//格式化时间String startFormat = start.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));return startFormat;}
3.3.3.1.2 获取加今天三天后的最后时间
    /*** 结束时间* @return*/private String endTime() {LocalDate now = LocalDate.now();LocalDate plus = now.plusDays(2);LocalTime max = LocalTime.MAX;LocalDateTime end = LocalDateTime.of(plus, max);//格式化时间String endFormat = end.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));return endFormat;}

测试效果

3.3.4 获取三天之内的秒杀商品信息

gulimall-coupon/src/main/java/site/zhourui/gulimall/coupon/controller/SeckillSessionController.java

    /*** 查询最近三天需要参加秒杀商品的信息* @return*/@GetMapping(value = "/Lates3DaySession")public R getLates3DaySession() {List<SeckillSessionEntity> seckillSessionEntities = seckillSessionService.getLates3DaySession();return R.ok().setData(seckillSessionEntities);}

gulimall-coupon/src/main/java/site/zhourui/gulimall/coupon/service/impl/SeckillSessionServiceImpl.java

    @Overridepublic List<SeckillSessionEntity> getLates3DaySession() {//计算最近三天//查出这三天参与秒杀活动的商品List<SeckillSessionEntity> list = this.baseMapper.selectList(new QueryWrapper<SeckillSessionEntity>().between("start_time", startTime(), endTime()));if (list != null && list.size() > 0) {List<SeckillSessionEntity> collect = list.stream().map(session -> {Long id = session.getId();//查出sms_seckill_sku_relation表中关联的skuIdList<SeckillSkuRelationEntity> relationSkus = seckillSkuRelationService.list(new QueryWrapper<SeckillSkuRelationEntity>().eq("promotion_session_id", id));session.setRelationSkus(relationSkus);return session;}).collect(Collectors.toList());return collect;}return null;}/*** 当前时间* @return*/private String startTime() {LocalDate now = LocalDate.now();LocalTime min = LocalTime.MIN;LocalDateTime start = LocalDateTime.of(now, min);//格式化时间String startFormat = start.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));return startFormat;}/*** 结束时间* @return*/private String endTime() {LocalDate now = LocalDate.now();LocalDate plus = now.plusDays(2);LocalTime max = LocalTime.MAX;LocalDateTime end = LocalDateTime.of(plus, max);//格式化时间String endFormat = end.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));return endFormat;}

feign接口

gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/feign/CouponFeignService.java

package site.zhourui.gulimall.seckill.feign;import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import site.zhourui.common.utils.R;/*** @author zr* @date 2022/1/6 17:34*/
@FeignClient("gulimall-coupon")
public interface CouponFeignService {/*** 查询最近三天需要参加秒杀商品的信息* @return*/@GetMapping(value = "/coupon/seckillsession/Lates3DaySession")R getLates3DaySession();}

3.3.5 秒杀商品定时上架

3.3.5.1 定时任务的方法查询最近三天场次的商品上架

配置定时任务

gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/config/ScheduledConfig.java

package site.zhourui.gulimall.seckill.config;import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;/*** @author zr* @date 2022/1/8 16:11*/
@EnableAsync
@EnableScheduling
@Configuration
public class ScheduledConfig {}

gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/scheduled/SeckillScheduled.java

package site.zhourui.gulimall.seckill.scheduled;import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import site.zhourui.gulimall.seckill.service.SeckillService;import java.util.concurrent.TimeUnit;/*** @author zr* @date 2022/1/8 16:08*//*** 秒杀商品定时上架*  每天晚上3点,上架最近三天需要三天秒杀的商品*  当天00:00:00 - 23:59:59*  明天00:00:00 - 23:59:59*  后天00:00:00 - 23:59:59*/
@Slf4j
@Service
public class SeckillScheduled {@Autowiredprivate SeckillService seckillService;@Autowiredprivate RedissonClient redissonClient;//秒杀商品上架功能的锁private final String upload_lock = "seckill:upload:lock";//TODO 保证幂等性问题@Scheduled(cron = "*/5 * * * * ? ")
//    @Scheduled(cron = "0 0 3 * * ? ")public void uploadSeckillSkuLatest3Days() {//1、重复上架无需处理log.info("上架秒杀的商品...");//分布式锁RLock lock = redissonClient.getLock(upload_lock);try {//加锁lock.lock(10, TimeUnit.SECONDS);seckillService.uploadSeckillSkuLatest3Days();} catch (Exception e) {e.printStackTrace();} finally {lock.unlock();}}
}

上架三天需要秒杀的商品

gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/service/SeckillService.java

package site.zhourui.gulimall.seckill.service;/*** @author zr* @date 2022/1/6 18:14*/
public interface SeckillService {/*** 上架三天需要秒杀的商品*/void uploadSeckillSkuLatest3Days();
}

gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/service/impl/SeckillServiceImpl.java

package site.zhourui.gulimall.seckill.service.impl;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import site.zhourui.common.utils.R;
import site.zhourui.gulimall.seckill.feign.CouponFeignService;
import site.zhourui.gulimall.seckill.feign.ProductFeignService;
import site.zhourui.gulimall.seckill.service.SeckillService;
import site.zhourui.gulimall.seckill.to.SeckillSkuRedisTo;
import site.zhourui.gulimall.seckill.vo.SeckillSessionWithSkusVo;
import site.zhourui.gulimall.seckill.vo.SkuInfoVo;import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;/*** @author zr* @date 2022/1/6 18:14*/
@Slf4j
@Service
public class SeckillServiceImpl implements SeckillService {@AutowiredCouponFeignService couponFeignService;@Autowiredprivate ProductFeignService productFeignService;@Autowiredprivate RedissonClient redissonClient;@Autowiredprivate StringRedisTemplate redisTemplate;private final String SESSION_CACHE_PREFIX = "seckill:sessions:";private final String SECKILL_CHARE_PREFIX = "seckill:skus";private final String SKU_STOCK_SEMAPHORE = "seckill:stock:";    //+商品随机码@Overridepublic void uploadSeckillSkuLatest3Days() {//1、扫描最近三天的商品需要参加秒杀的活动R lates3DaySession = couponFeignService.getLates3DaySession();if (lates3DaySession.getCode() == 0) {//上架商品List<SeckillSessionWithSkusVo> sessionData = lates3DaySession.getData("data", new TypeReference<List<SeckillSessionWithSkusVo>>() {});//缓存到Redis//1、缓存活动信息saveSessionInfos(sessionData);//2、缓存活动的关联商品信息saveSessionSkuInfo(sessionData);}}/*** 缓存秒杀活动信息* @param sessions*/private void saveSessionInfos(List<SeckillSessionWithSkusVo> sessions) {if (!CollectionUtils.isEmpty(sessions))sessions.stream().forEach(session -> {//获取当前活动的开始和结束时间的时间戳long startTime = session.getStartTime().getTime();long endTime = session.getEndTime().getTime();//存入到Redis中的keyString key = SESSION_CACHE_PREFIX + startTime + "_" + endTime;//判断Redis中是否有该信息,如果没有才进行添加Boolean hasKey = redisTemplate.hasKey(key);//缓存活动信息if (!hasKey) {//获取到活动中所有商品的skuIdList<String> skuIds = session.getRelationSkus().stream().map(item -> item.getPromotionSessionId() + "-" + item.getSkuId().toString()).collect(Collectors.toList());redisTemplate.opsForList().leftPushAll(key,skuIds);}});}/*** 缓存秒杀活动所关联的商品信息*/private void saveSessionSkuInfo(List<SeckillSessionWithSkusVo> sessions) {if (!CollectionUtils.isEmpty(sessions))sessions.stream().forEach(session -> {//准备hash操作,绑定hashBoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);session.getRelationSkus().stream().forEach(seckillSkuVo -> {//生成随机码String token = UUID.randomUUID().toString().replace("-", "");String redisKey = seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString();if (!operations.hasKey(redisKey)) {//缓存我们商品信息SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();Long skuId = seckillSkuVo.getSkuId();//1、先查询sku的基本信息,调用远程服务R info = productFeignService.getSkuInfo(skuId);if (info.getCode() == 0) {SkuInfoVo skuInfo = info.getData("skuInfo",new TypeReference<SkuInfoVo>(){});redisTo.setSkuInfo(skuInfo);}//2、sku的秒杀信息BeanUtils.copyProperties(seckillSkuVo,redisTo);//3、设置当前商品的秒杀时间信息redisTo.setStartTime(session.getStartTime().getTime());redisTo.setEndTime(session.getEndTime().getTime());//4、设置商品的随机码(防止恶意攻击)redisTo.setRandomCode(token);//序列化json格式存入Redis中String seckillValue = JSON.toJSONString(redisTo);operations.put(seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString(),seckillValue);//如果当前这个场次的商品库存信息已经上架就不需要上架//5、使用库存作为分布式Redisson信号量(限流)// 使用库存作为分布式信号量RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);// 商品可以秒杀的数量作为信号量semaphore.trySetPermits(seckillSkuVo.getSeckillCount());}});});}
}
3.3.2.2 定时任务分布式场景下的问题

分布式情况下,定时任务会启动多次【因为场次信息在redis中是List类型,会重复添加】

解决方案,加分布式锁

3.3.2.3 同一场次的同一商品重复上架(幂等性问题)

如果上架之前没有对本次上架的商品验证是否上架,那么就会重复上架

解决方案

在商品上架时检查已上架商品是否与本次上架的商品的场次信息与商品信息重复

3.3.6 首页展示上架商品

3.3.6.1 前端代码
<script type="text/javascript">function search() {var keyword=$("#searchText").val()window.location.href="http://search.gulimall.com/list.html?keyword="+keyword;}$.get("http://seckill.gulimall.com/getCurrentSeckillSkus", function (res) {if (res.data.length > 0) {res.data.forEach(function (item) {$("<li οnclick='toDetail(" + item.skuId + ")'></li>").append($("<img style='width: 130px; height: 130px' src='" + item.skuInfo.skuDefaultImg + "' />")).append($("<p>"+item.skuInfo.skuTitle+"</p>")).append($("<span>" + item.seckillPrice + "</span>")).append($("<s>" + item.skuInfo.price + "</s>")).appendTo("#seckillSkuContent");})}})function toDetail(skuId) {location.href = "http://item.gulimall.com/" + skuId + ".html";}</script>
3.3.6.2 新增当天场次秒杀商品查询接口

gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/controller/SeckillController.java

package site.zhourui.gulimall.seckill.controller;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import site.zhourui.common.utils.R;
import site.zhourui.gulimall.seckill.service.SeckillService;
import site.zhourui.gulimall.seckill.to.SeckillSkuRedisTo;import java.util.List;/*** @author zr* @date 2022/1/8 18:07*/
@Controller
public class SeckillController {@Autowiredprivate SeckillService seckillService;/*** 当前时间可以参与秒杀的商品信息*/@GetMapping(value = "/getCurrentSeckillSkus")@ResponseBodypublic R getCurrentSeckillSkus() {//获取到当前可以参加秒杀商品的信息List<SeckillSkuRedisTo> vos = seckillService.getCurrentSeckillSkus();return R.ok().setData(vos);}}

获取到当前可以参加秒杀商品的信息

gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/service/SeckillService.java

    List<SeckillSkuRedisTo> getCurrentSeckillSkus();
1

gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/service/impl/SeckillServiceImpl.java

    /*** 获取到当前可以参加秒杀商品的信息* @return*/@Overridepublic List<SeckillSkuRedisTo> getCurrentSeckillSkus() {//1、确定当前属于哪个秒杀场次long currentTime = System.currentTimeMillis();//从Redis中查询到所有key以seckill:sessions开头的所有数据Set<String> keys = redisTemplate.keys(SESSION_CACHE_PREFIX + "*");for (String key : keys) {//seckill:sessions:1594396764000_1594453242000String replace = key.replace(SESSION_CACHE_PREFIX, "");String[] s = replace.split("_");//获取存入Redis商品的开始时间long startTime = Long.parseLong(s[0]);//获取存入Redis商品的结束时间long endTime = Long.parseLong(s[1]);//判断是否是当前秒杀场次if (currentTime >= startTime && currentTime <= endTime) {//2、获取这个秒杀场次需要的所有商品信息List<String> range = redisTemplate.opsForList().range(key, -100, 100);BoundHashOperations<String, String, String> hasOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);assert range != null;List<String> listValue = hasOps.multiGet(range);if (listValue != null && listValue.size() >= 0) {List<SeckillSkuRedisTo> collect = listValue.stream().map(item -> {String items = (String) item;SeckillSkuRedisTo redisTo = JSON.parseObject(items, SeckillSkuRedisTo.class);// redisTo.setRandomCode(null);当前秒杀开始需要随机码return redisTo;}).collect(Collectors.toList());return collect;}break;}}return null;}

测试效果

3.3.7 秒杀页面渲染

3.3.7.1 前端页面

https://gitee.com/zhourui815/gulimall/blob/master/gulimall-product/src/main/resources/templates/item.html

3.3.7.2 根据skuId查询商品是否参加秒杀活动

gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/controller/SeckillController.java

    /*** 根据skuId查询商品是否参加秒杀活动*/@GetMapping(value = "/sku/seckill/{skuId}")@ResponseBodypublic R getSkuSeckilInfo(@PathVariable("skuId") Long skuId) {SeckillSkuRedisTo to = seckillService.getSkuSeckilInfo(skuId);return R.ok().setData(to);}
123456789

gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/service/SeckillService.java

    /*** 根据skuId查询商品是否参加秒杀活动* @param skuId* @return*/SeckillSkuRedisTo getSkuSeckilInfo(Long skuId);
123456

gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/service/impl/SeckillServiceImpl.java

   /*** 根据skuId查询商品是否参加秒杀活动* @param skuId* @return*/@Overridepublic SeckillSkuRedisTo getSkuSeckilInfo(Long skuId) {//1、找到所有需要秒杀的商品的key信息---seckill:skusBoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);//拿到所有的keySet<String> keys = hashOps.keys();if (keys != null && keys.size() > 0) {//4-45 正则表达式进行匹配String reg = "\\d-" + skuId;for (String key : keys) {//如果匹配上了if (Pattern.matches(reg,key)) {//从Redis中取出数据来String redisValue = hashOps.get(key);//进行序列化SeckillSkuRedisTo redisTo = JSON.parseObject(redisValue, SeckillSkuRedisTo.class);//随机码Long currentTime = System.currentTimeMillis();Long startTime = redisTo.getStartTime();Long endTime = redisTo.getEndTime();//如果当前时间大于等于秒杀活动开始时间并且要小于活动结束时间if (currentTime >= startTime && currentTime <= endTime) {return redisTo;}redisTo.setRandomCode(null);return redisTo;}}}return null;}
123456789101112131415161718192021222324252627282930313233343536373839
3.3.7.3 在查询商品详情信息时带上秒杀验证该商品此时是否处于秒杀

gulimall-product/src/main/java/site/zhourui/gulimall/product/service/impl/SkuInfoServiceImpl.java

        // 3、查询当前sku是否参与秒杀活动CompletableFuture<Void> seckillFuture = CompletableFuture.runAsync(() -> {//3、远程调用查询当前sku是否参与秒杀优惠活动R skuSeckilInfo = seckillFeignService.getSkuSeckilInfo(skuId);if (skuSeckilInfo.getCode() == 0) {//查询成功SeckillSkuVo seckilInfoData = skuSeckilInfo.getData("data", new TypeReference<SeckillSkuVo>() {});skuItemVo.setSeckillSkuVo(seckilInfoData);if (seckilInfoData != null) {long currentTime = System.currentTimeMillis();if (currentTime > seckilInfoData.getEndTime()) {skuItemVo.setSeckillSkuVo(null);}}}}, executor);//等到所有任务都完成(注意添加seckillFuture)CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,imageFuture,seckillFuture).get();
12345678910111213141516171819202122

重启测试

清除redis再次测试

3.4 秒杀

3.4.1 秒杀架构

3.4.2 秒杀(高并发)系统关注的问题

3.4.3 秒杀流程一(加入购物车秒杀-弃用)

优点:加入购物车实现天然的流量错峰,与正常购物流程一致只是价格为秒杀价格,数据模型与正常下单兼容性好

缺点:秒杀服务与其他服务关联性提高,比如这里秒杀服务会与购物车服务关联,秒杀服务高并发情况下,可能会把购物车服务连同压垮,导致正常商品,正常购物也无法加入购物车下单

3.4.4 秒杀流程二(独立秒杀业务来处理)

优点:从用户下单到返回没有对数据库进行任何操作,只是做了一些条件校验,校验通过后也只是生成一个单号,再发送一条消息

缺点:如果订单服务全挂掉了,没有服务来处理消息,就会导致用户一直不能付款

解决方案:不使用订单服务处理秒杀消息,需要一套独立的业务来处理

3.4.5 创建秒杀队列

gulimall-order/src/main/java/com/atguigu/gulimall/order/config/MyRabbitMQConfig.java

    /*** 商品秒杀队列* 作用:削峰,创建订单*/@Beanpublic Queue orderSecKillOrderQueue() {Queue queue = new Queue("order.seckill.order.queue", true, false, false);return queue;}@Beanpublic Binding orderSecKillOrderQueueBinding() {//String destination, DestinationType destinationType, String exchange, String routingKey,//             Map<String, Object> argumentsBinding binding = new Binding("order.seckill.order.queue",Binding.DestinationType.QUEUE,"order-event-exchange","order.seckill.order",null);return binding;}

3.4.6 整合rabbitmq,thymeleaf

导入依赖

rabbitmy用于发送消息

thymeleaf用于展示秒杀成功页面

        <!-- 引入rabbitmq --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId></dependency><!-- 模板引擎 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency>
1234567891011

配置

server:port: 25000
spring:rabbitmq:host: 192.168.157.128port: 5672virtual-host: /#开发环境关闭缓存thymeleaf:cache: false

配置rabbitmq系列化方式

gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/config/MyRabbitMQConfig.java

package site.zhourui.gulimall.seckill.config;import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** @author zr* @date 2022/1/10 15:27*/
@Configuration
public class MyRabbitMQConfig {/*** 以json序列化的格式发送消息*/@Beanpublic MessageConverter messageConverter() {return new Jackson2JsonMessageConverter();}
}12345678910111213141516171819202122

3.4.7 秒杀成功页面

页面链接

https://gitee.com/zhourui815/gulimall/blob/master/gulimall-seckill/src/main/resources/templates/success.html

配置网关

gulimall-gateway/src/main/resources/application.yml

        - id: gulimall_seckill_routeuri: lb://gulimall-seckillpredicates:- Host=seckill.gulimall.com

配置host

# gulimall
192.168.157.128 gulimall.com
# search
192.168.157.128 search.gulimall.com
# item 商品详情
192.168.157.128 item.gulimall.com
#商城认证
192.168.157.128 auth.gulimall.com
#购物车
192.168.157.128 cart.gulimall.com
#订单
192.168.157.128 order.gulimall.com
#会员
192.168.157.128 member.gulimall.com
#秒杀
192.168.157.128 seckill.gulimall.com
#单点登录
127.0.0.1 ssoserver.com127.0.0.1 client1.com127.0.0.1 client2.com

3.4.8 配置拦截器(未登录请求拦截)

gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/interceptor/LoginUserInterceptor.java

package site.zhourui.gulimall.seckill.interceptor;import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import site.zhourui.common.vo.MemberResponseVo;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.PrintWriter;import static site.zhourui.common.constant.AuthServerConstant.LOGIN_USER;/*** @author zr* @date 2022/1/10 15:13*/
@Component
public class LoginUserInterceptor implements HandlerInterceptor {public static ThreadLocal<MemberResponseVo> loginUser = new ThreadLocal<>();@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String uri = request.getRequestURI();AntPathMatcher antPathMatcher = new AntPathMatcher();boolean match = antPathMatcher.match("/kill", uri);// 只有秒杀需要拦截,其他直接放行if (match) {HttpSession session = request.getSession();//获取登录的用户信息MemberResponseVo attribute = (MemberResponseVo) session.getAttribute(LOGIN_USER);if (attribute != null) {//把登录后用户的信息放在ThreadLocal里面进行保存loginUser.set(attribute);return true;} else {//未登录,返回登录页面response.setContentType("text/html;charset=UTF-8");PrintWriter out = response.getWriter();out.println("<script>alert('请先进行登录,再进行后续操作!');location.href='http://auth.gulimall.com/login.html'</script>");// session.setAttribute("msg", "请先进行登录");// response.sendRedirect("http://auth.gulimall.com/login.html");return false;}}return true;}
}

gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/config/SeckillWebConfig.java

package site.zhourui.gulimall.seckill.config;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import site.zhourui.gulimall.seckill.interceptor.LoginUserInterceptor;/*** @author zr* @date 2022/1/10 15:12*/
@Configuration
public class SeckillWebConfig implements WebMvcConfigurer {@Autowiredprivate LoginUserInterceptor loginUserInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");}
}

3.4.9 秒杀接口

gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/controller/SeckillController.java

    /*** 商品进行秒杀(秒杀开始)* 查看表 oms_order_item*/@GetMapping(value = "/kill")public String seckill(@RequestParam("killId") String killId,@RequestParam("key") String key,@RequestParam("num") Integer num,Model model) {String orderSn = null;try {//1、判断是否登录orderSn = seckillService.kill(killId,key,num);model.addAttribute("orderSn",orderSn);} catch (Exception e) {e.printStackTrace();}return "success";}

当前商品进行秒杀(各种校验,成功后发送消息)

gulimall-seckill/src/main/java/site/zhourui/gulimall/seckill/service/impl/SeckillServiceImpl.java

    /*** 当前商品进行秒杀(秒杀开始)* @param killId* @param key* @param num* @return*/@Overridepublic String kill(String killId, String key, Integer num) throws InterruptedException {long s1 = System.currentTimeMillis();//获取当前用户的信息MemberResponseVo user = LoginUserInterceptor.loginUser.get();//1、获取当前秒杀商品的详细信息从Redis中获取BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);String skuInfoValue = hashOps.get(killId);if (StringUtils.isEmpty(skuInfoValue)) {return null;}//(合法性效验)SeckillSkuRedisTo redisTo = JSON.parseObject(skuInfoValue, SeckillSkuRedisTo.class);Long startTime = redisTo.getStartTime();Long endTime = redisTo.getEndTime();long currentTime = System.currentTimeMillis();//判断当前这个秒杀请求是否在活动时间区间内(效验时间的合法性)if (currentTime >= startTime && currentTime <= endTime) {//2、效验随机码和商品idString randomCode = redisTo.getRandomCode();String skuId = redisTo.getPromotionSessionId() + "-" +redisTo.getSkuId();if (randomCode.equals(key) && killId.equals(skuId)) {//3、验证购物数量是否合理和库存量是否充足Integer seckillLimit = redisTo.getSeckillLimit();//获取信号量String seckillCount = redisTemplate.opsForValue().get(SKU_STOCK_SEMAPHORE + randomCode);Integer count = Integer.valueOf(seckillCount);//判断信号量是否大于0,并且买的数量不能超过库存if (count > 0 && num <= seckillLimit && count > num ) {//4、验证这个人是否已经买过了(幂等性处理),如果秒杀成功,就去占位。userId-sessionId-skuId//SETNX 原子性处理String redisKey = user.getId() + "-" + skuId;//设置自动过期(活动结束时间-当前时间)Long ttl = endTime - currentTime;Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);if (aBoolean) {//占位成功说明从来没有买过,分布式锁(获取信号量-1)【分布式锁-1】RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);//TODO 秒杀成功,快速下单boolean semaphoreCount = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);//保证Redis中还有商品库存if (semaphoreCount) {//创建订单号和订单信息发送给MQ// 秒杀成功 快速下单 发送消息到 MQ 整个操作时间在 10ms 左右String timeId = IdWorker.getTimeId();SeckillOrderTo orderTo = new SeckillOrderTo();orderTo.setOrderSn(timeId);orderTo.setMemberId(user.getId());orderTo.setNum(num);orderTo.setPromotionSessionId(redisTo.getPromotionSessionId());orderTo.setSkuId(redisTo.getSkuId());orderTo.setSeckillPrice(redisTo.getSeckillPrice());rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",orderTo);long s2 = System.currentTimeMillis();log.info("耗时..." + (s2 - s1));return timeId;}}}}}long s3 = System.currentTimeMillis();log.info("耗时..." + (s3 - s1));return null;}
3.4.9.1 限制同一用户重复秒杀

 //4、验证这个人是否已经买过了(幂等性处理),如果秒杀成功,就去占位。userId-sessionId-skuId//SETNX 原子性处理String redisKey = user.getId() + "-" + skuId;//设置自动过期(活动结束时间-当前时间)Long ttl = endTime - currentTime;Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS);if (aBoolean) {//占位成功说明从来没有买过,分布式锁(获取信号量-1)【分布式锁-1】RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);//TODO 秒杀成功,快速下单tryAcquire尝试扣减信号量,超时也会失败boolean semaphoreCount = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);}

测试结果

3.4.10 秒杀消息消费

gulimall-order/src/main/java/site/zhourui/gulimall/order/listener/OrderSeckillListener.java

package site.zhourui.gulimall.order.listener;import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import site.zhourui.common.to.mq.SeckillOrderTo;
import site.zhourui.gulimall.order.service.OrderService;import java.io.IOException;/*** @author zr* @date 2022/1/10 15:52*/
@Slf4j
@Component
@RabbitListener(queues = "order.seckill.order.queue")
public class OrderSeckillListener {@Autowiredprivate OrderService orderService;@RabbitHandlerpublic void listener(SeckillOrderTo orderTo, Channel channel, Message message) throws IOException {log.info("准备创建秒杀单的详细信息...");try {orderService.createSeckillOrder(orderTo);channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);} catch (Exception e) {channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);}}
}

创建秒杀订单

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/OrderService.java

    /*** 创建秒杀单* @param orderTo*/void createSeckillOrder(SeckillOrderTo orderTo);

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

    /*** 创建秒杀单* @param orderTo*/@Overridepublic void createSeckillOrder(SeckillOrderTo orderTo) {//TODO 保存订单信息OrderEntity orderEntity = new OrderEntity();orderEntity.setOrderSn(orderTo.getOrderSn());orderEntity.setMemberId(orderTo.getMemberId());orderEntity.setCreateTime(new Date());BigDecimal totalPrice = orderTo.getSeckillPrice().multiply(BigDecimal.valueOf(orderTo.getNum()));orderEntity.setPayAmount(totalPrice);orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());//保存订单this.save(orderEntity);//保存订单项信息OrderItemEntity orderItem = new OrderItemEntity();orderItem.setOrderSn(orderTo.getOrderSn());orderItem.setRealAmount(totalPrice);orderItem.setSkuQuantity(orderTo.getNum());//保存商品的spu信息R spuInfo = productFeignService.getSpuInfoBySkuId(orderTo.getSkuId());SpuInfoVo spuInfoData = spuInfo.getData("data", new TypeReference<SpuInfoVo>() {});orderItem.setSpuId(spuInfoData.getId());orderItem.setSpuName(spuInfoData.getSpuName());orderItem.setSpuBrand(spuInfoData.getBrandName());orderItem.setCategoryId(spuInfoData.getCatalogId());//保存订单项数据orderItemService.save(orderItem);}

测试

3.4.11 秒杀总结

3.4.11.1 服务单一职责+独立部署
 秒杀服务即使自己扛不住压力,挂掉。不要影响别人解决:新增秒杀服务
3.4.11.2 秒杀链接加密
 防止恶意攻击,模拟秒杀请求,1000次ls攻击。防止链接暴露.自己工作人员,提前秒杀商品。解决:请求需要随机码,在秒杀开始时随机码才会放在商品信息中
3.4.11.3 库存预热+快速扣减【限流,并发信号量】
 秒杀读多写少。无需每次实时校验库存。我们库存预热,放到redis中。信号量控制进来秒杀的请求解决:库存放入redis中,使用分布式信号量扣减+限流
3.4.11.4 动静分离
 nginx做好动静分离。保证秒杀和商品详情页的动态请求才打到后端的服务集群。使用CDN网络,分担本集群压力解决:nginx例:10万个人来访问商品详情页,这个详情页会发送63个请求,但是只有3个请求到达后端,60个请求是前端的。一共30万请求到达后端,600万个请求到达nginx或cdn
3.4.11.5 恶意请求拦截
识别非法攻击请求并进行拦截,网关层拦截,放行到后太服务的请求都是正常请求在网关层拦截:一些不带令牌的请求循环发送解决:使用网关拦截本系统做了登录拦截器【在各微服务创建的,未登录跳转登录页面】
3.4.11.6 流量错峰
使用各种手段,将流量分担到更大宽度的时间点。比如验证码,加入购物车1、输入验证码需要时间,将流量错开了【速度有快有慢】2、加入购物车,然后再结算【速度有快有慢】解决:使用购物车逻辑
12345
3.4.11.7 限流&熔断&降级
前踹限流+后端限流
限制次数,限制总量,快速失败降级运行,熔断隔离防止雪崩前端限流:1、每次点击1s后才能再次点击2、验证登录后端限流:1、网关限流,例如访问秒杀的流量到达10W等2S再将请求传过去【其中10W是集群的峰值】2、就算是合理的10次也只放行1-2次3、熔断:远程访问失败,快速返回,并且下次不要再请求这个节点【防止请求长时间等待】4、降级:请求量太大了,直接将请求转发到一个错误页面出现一种情况:集群的处理能力是10W,网关放行了10W的请求,但此时秒杀服务掉线了2台,处理能力下降导致请求堆积,最后资源耗尽服务器全崩了解决:spring alibaba sentinel以前是Hystrix,现在不更新了就不用了
3.4.11.8 队列削峰
100万个商品,每个商品的秒杀库存是100,会产生1亿的流量到后台,全部放入队列中,然后订单监听队列一个个创建订单扣减库存解决:秒杀服务将创建订单的请求存入mq,订单服务监听mq。优点:要崩只会崩秒杀服务,不会打垮其他服务【商品服务、订单服务、购物车服务】【第一套实现逻辑会导致这些问题】【看秒杀请求的两种实现】

【谷粒商城 -秒杀服务】相关推荐

  1. 谷粒商城--秒杀服务--高级篇笔记十二

    谷粒商城–秒杀服务–高级篇笔记十二 1.后台添加秒杀商品 未配置秒杀服务相关网关 1.1 配置网关 - id: coupon_routeuri: lb://gulimall-couponpredica ...

  2. 谷粒商城检索服务(三十)

    谷粒商城检索服务(三十) 173.商城业务-检索服务-搭建页面环境 - 192.商城业务-检索服务-条件筛选联动 敲完代码,感觉ES的复杂查询还不是很熟练,不过应用的也比较简单,权重和分词都没有应用到 ...

  3. 谷粒商城商品服务模块数据库文件关系

    谷粒商城商品服务的数据库文件 一.主要数据库文件设计 二.商品三级分类表(pms_category) 商品三级分类表 . 主要信息:该分类id(cat_id主键),父分类id(parent_cid), ...

  4. 谷粒商城--订单服务--高级篇笔记十一

    1.页面环境搭建 1.1 静态资源导入nginx 等待付款 --------->detail 订单页 --------->list 结算页 --------->confirm 收银页 ...

  5. 谷粒商城 - 微服务分布式电商项目

    谷粒商城 1.项目背景 谷粒商城项目是尚硅谷研究院最新推出的完整大型分布式架构电商平台,技术全面.业务深入,全网无出其右.技术涵盖:微服务架构 + 分布式 + 全栈 + 集群 + 部署 + 自动化运维 ...

  6. 谷粒商城微服务分布式基础篇二—— Spring Cloud Alibaba、Nacos注册与发现

    文章目录 Spring Cloud Alibaba--微服务开发 Spring Cloud Alibaba是什么?  主要功能  组件 为什么不使用Spring Cloud 使用 Nacos Disc ...

  7. 谷粒商城微服务分布式高级篇ElasticSearch二——重要概念及原理

    文章目录 面向文档 索引 分布式特性 分布式集群 集群健康 添加索引 增加故障转移 横向扩展 继续扩展 应对故障 数据结构 什么是文档? 文档元数据 _index _type _id 其他元数据 _s ...

  8. 谷粒商城微服务分布式高级篇ElasticSearch三——Dcoker安装ES及ES检索语法

    文章目录 Docker安装ES&kibban Elasticsearch安装 kibban安装 IK分词器安装 Nginx安装 检索 1.查看es中有哪些索引 2.增加一个索引(库) 3.删除 ...

  9. 谷粒商城二十订单服务

    rabbitmq相关知识 // 静态页面的引入,静态资源上传nginx等192.168.56.10 gulimall.com 192.168.56.10 search.gulimall.com 192 ...

最新文章

  1. redis内部分享ppt
  2. 那么多GAN哪个好?谷歌大脑泼来冷水:都和原版差不多
  3. ERP系统管理员的工具箱 推荐几款优秀的数据比较同步工具 Data Compare and Sync tool...
  4. mysql配置文件完全_MySQL配置文件my.cnf详解
  5. MyEclipse下JDBC-MySQL配置总结
  6. CCF-CSP认证历年真题详解
  7. Meta:绝对没有威胁要离开欧洲市场
  8. RHEL/CentOS 6.x 系统服务详解
  9. Vivado_ILA IP核
  10. 时域分析——有量纲特征值含义一网打尽
  11. 松翰单片机SN8P2711AD实现AD转换的C语言程序例子
  12. S7-1200PLC通过增量式编码器实现速度采集和模拟量采集
  13. html5页面和app的区别,H5页面与APP区别何在
  14. 吉祥物,送吉祥(续更)
  15. TensorFlow高阶张量叉乘
  16. IOS逆向(1)IOS越狱
  17. JS对浏览器地址的操作
  18. Ubuntu安装pcb制图工具
  19. SVN E170001:unable to connect to a repository at url
  20. Android7.0 数据业务长连接拨号过程

热门文章

  1. 1087: 获取出生日期(多实例测试) C语言
  2. python之预测体育竞技分析
  3. HDU - 1753 - 大明A+B
  4. 用TB5128FTG来替换THB6128(LV8728)的驱动方案
  5. 以不教民战,是谓弃之
  6. background 组合写法_简单学习css组合与CSS嵌套的写法
  7. 双汇大数据方案选型:从棘手的InfluxDB+Redis到毫秒级查询的TDengine
  8. %.9d\n = %09d\n
  9. 江海叶独流(之一 谭之殇)
  10. 字符编码的前世今生——一文读懂字符编码