续 秒杀业务准备
开发根据SpuId查询秒杀Sku列表信息
我们将秒杀的商品Spu列表查询出来
当用户选择一个商品时
我们要将这个商品的sku也查询出来
也就是根据SpuId查询Sku的列表
创建SeckillSkuMapper
@Repository
public interface SeckillSkuMapper {
// 根据spuId查询sku列表
List<SeckillSku> findSeckillSkusBySpuId(Long spuId);
}
SeckillSkuMapper.xml文件添加内容
<sql id="SimpleField">
<if test="true">
id,
sku_id,
spu_id,
seckill_stock,
seckill_price,
gmt_create,
gmt_modified,
seckill_limit
</if>
</sql>
<select id="findSeckillSkusBySpuId" resultMap="BaseResultMap">
select
<include refid="SimpleField" />
from
seckill_sku
where
spu_id=#{spuId}
</select>
根据当前时间查询正在进行秒杀的商品
根据给定时间查询出正在进行秒杀的商品列表
在秒杀过程中,一定会将当前时间正在进行秒杀商品查询出来的
首先保证数据库中的seckill_spu表的数据正在秒杀时间段(检查数据,如果不在秒杀时间段,将结束时间后移如2024年)
SeckillSpuMapper添加方法
// 根据指定时间,查询正在进行秒杀的商品
List<SeckillSpu> findSeckillSpusByTime(LocalDateTime time);
SeckillSpuMapper.xml
<!-- 根据指定时间,查询正在进行秒杀的商品 -->
<select id="findSeckillSpusByTime" resultMap="BaseResultMap">
select
<include refid="SimpleField" />
from
seckill_spu
where
start_time < #{time}
and
end_time > #{time}
</select>
根据SpuId查询秒杀商品详情
SeckillSpuMapper接口添加方法
// 根据SpuId查询spu秒杀信息
SeckillSpu findSeckillSpuById(Long spuId);
SeckillSpuMapper.xml添加内容
<!-- 根据SpuId查询spu秒杀信息 -->
<select id="findSeckillSpuById" resultMap="BaseResultMap">
select
<include refid="SimpleField" />
from
seckill_spu
where
spu_id=#{spuId}
</select>
查询所有秒杀商品的SpuId
这个查询需要是为了后面布隆过滤器加载数据库中包含的所有SpuId时使用
因为布隆过滤器的特性,只需要查询出所有商品的spu_id即可
SeckillSpuMapper接口添加方法
// 布隆过滤器用:查询获得所有秒杀商品的SpuId数组
Long[] findAllSeckillSpuIds();
SeckillSpuMapper.xml添加内容
<!-- 布隆过滤器用:查询获得所有秒杀商品的SpuId数组 -->
<select id="findAllSeckillSpuIds" resultType="long" >
select spu_id from seckill_spu
</select>
缓存预热思路
在即将发生高并发业务之前,我们将一些高并发业务中需要的数据保存到Redis中,这种操作,就是"缓存预热",这样发生高并发时,这些数据就可以直接从Redis中获得,无需查询数据库了
我们要利用Quartz周期性的将每个批次的秒杀商品,预热到Redis
例如每天的12:00 14:00 16:00 18:00进行秒杀
那么就在 11:55 13:55 15:55 17:55 进行预热
我们预热的内容有
-
我们预热的内容是将参与秒杀商品的sku查询出来,根据skuid将该商品的库存保存在Redis中
还要注意为了预防雪崩,在向Redis保存数据时,都应该添加随机数
-
(待完善).在秒杀开始前,生成布隆过滤器,访问时先判断布隆过滤器,如果判断商品存在,再继续访问
-
在秒杀开始之前,生成每个商品对应的随机码,保存在Redis中,随机码可以绑定给有Spu,防止有用户知道SpuId暴力访问
设置定时任务
将库存和随机码保存到Redis
利用Quartz将库存数和随机码保存到Redis
1.创建Job接口实现类
2.创建配置类,配置JobDetail和Trigger
在seckill包下创建timer.job包
在seckill包下创建timer.config包
首先我们编写缓存预热的操作,在job包下创建类SeckillInitialJob
@Slf4j
public class SeckillInitialJob implements Job {
// 查询秒杀sku库存数的mapper
@Autowired
private SeckillSkuMapper skuMapper;
// 需要查询秒杀spu相关的信息
@Autowired
private SeckillSpuMapper spuMapper;
// 操作Redis的对象
@Autowired
private RedisTemplate redisTemplate;
/*
RedisTemplate对象在保存数据到Redis时,会先将当前数据序列化后保存
这样做的好处是序列化后的数据保存到Redis读写效率更高,缺点是不能在Redis中修改数据
我们现在要预热的是秒杀sku库存数,这个库存数如果也用上面的redisTemplate保存的方式
就容易在高并发情况下由于线程安全问题导致"超卖"
解决方案就是我们需要创建一个能够直接在Redis中修改数据的对象,来避免超卖的发生
SpringDataRedis提供了一个可以直接在Redis中操作数值的对象:StringRedisTemplate
使用StringRedisTemplate向Redis保存数据,默认都会以字符串的方式保存(不序列化了)
又因为Redis支持直接将字符串类型的数值进行修改,所以可以将库存数保存并直接修改
这样就不需要写java代码读取和修改了,直接使用Redis内部的功能修改库存数
最后结合Redis天生单线程的特性,避免线程安全问题方式超卖
*/
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
// 当前方法是缓存预热操作
// 它执行的时间是秒杀开始前5分钟,所以获取一个5分钟后的时间对象
LocalDateTime time=LocalDateTime.now().plusMinutes(5);
// 查询这个时间所有进行秒杀的商品
List<SeckillSpu> seckillSpus=spuMapper.findSeckillSpusByTime(time);
// 遍历当前批次所有秒杀的spu
for(SeckillSpu spu: seckillSpus){
// spu是商品的品类,必须确定规格也就是确定sku后,才能明确库存
// 所以我们要根据spuId查询sku,然后将sku的库存数保存到Redis
List<SeckillSku> seckillSkus=skuMapper.
findSeckillSkusBySpuId(spu.getSpuId());
// 遍历获得了当前spu对应的sku列表,还要遍历sku列表才能保存库存数
for(SeckillSku sku: seckillSkus){
log.info("开始将{}号sku商品库存预热到Redis",sku.getSkuId());
// 要在操作Redis之前,获得key常量
// SeckillCacheUtils.getStockKey是获得事先准备好的字符串常量的方法
// 它的实际的值为 mall:seckill:sku:stock:1
String skuStockKey=SeckillCacheUtils.getStockKey(sku.getSkuId());
// 检查Redis中是否已经包含了这个key
if(redisTemplate.hasKey(skuStockKey)){
// 如果key已经存在,证明之前已经缓存过了,直接跳过
log.info("{}号sku商品已经缓存过了",sku.getSkuId());
}else{
// 如果key不存在,就要将我们sku对象的库存数保存到Redis
// 使用stringRedisTemplate保存库存数到Redis
stringRedisTemplate.boundValueOps(skuStockKey)
.set(sku.getSeckillStock()+"",
10*60*1000+ RandomUtils.nextInt(10000),
TimeUnit.MILLISECONDS);
log.info("成功为{}号sku商品预热缓存",sku.getSkuId());
}
}
// 上面为止完成了库存数的预热
// 下面开始预热每个spu商品对应的随机码
// 随机码就是一个随机数,范围可以自己定
// 随机码终会在生成订单前进行验证,以减轻服务器压力
// 我们的操作就是生成这个随机数,保存到redis中
// mall:seckill:spu:url:rand:code:2
String randCodeKey=SeckillCacheUtils.getRandCodeKey(spu.getSpuId());
// 判断随机码是否已经在redis中
if(redisTemplate.hasKey(randCodeKey)){
int randCode=(int)redisTemplate.boundValueOps(randCodeKey).get();
log.info("{}号spu商品的随机码已经缓存:{}",spu.getSpuId(),randCode);
}else{
// 生成随机码 范围:100000~999999
int randCode=RandomUtils.nextInt(900000)+100000;
redisTemplate.boundValueOps(randCodeKey)
.set(randCode,10*60*1000+RandomUtils.nextInt(10000),
TimeUnit.MILLISECONDS);
log.info("spuId为{}号的商品生成的随机码为:{}",spu.getSpuId(),randCode);
}
}
}
}
RedisTemplate对象在保存数据到Redis时,会将当前数据序列化后保存
这样做的好处是将序列化后的数据保存到Redis,读写效率高,缺点是不能在Redis中修改数据
我们现在要预热的信息包含sku的库存数,这个库存数如果也用上面的序列化的方式保存
就会因为高并发情况下的线程安全问题引发"超卖"
解决方案,我们需要一个能够直接在Redis中减少库存的方法来避免超卖的发生
SpringDataRedis提供一个可以直接在Redis中操作数值的对象:StringRedisTemplate
使用StringRedisTemplate向Redis中保存数据,数据都会以字符串的方式保存
又因为Redis可以直接操作数值类型的字符串,所以可以通过它实现直接修改库存数
这样就不需要编写java代码判断了,再配合Redis天生单线程的特性,避免线程安全问题,防止超卖
配置Quartz触发
上面的类中的代码只是编写了预热操作
我们需要在Quartz中配置才能触发生效
在time.config包中创建QuartzConfig类编写Job的触发
@Configuration
public class QuartzConfig {
// 向Spring容器中保存JobDetail对象
@Bean
public JobDetail initJobDetail(){
return JobBuilder.newJob(SeckillInitialJob.class)
.withIdentity("initSeckill")
.storeDurably()
.build();
}
// 向Spring容器中保存Trigger对象
@Bean
public Trigger initSeckillTrigger(){
// 实际开发要写出正确的Cron表达式,让程序在11:55 13:55....运行
// 0 55 11,13,15,17 * * ?
// 但是学习过程中我们不可能去等这个时间,所有为了测试方便,我们设置它每分钟都运行
CronScheduleBuilder cron=
CronScheduleBuilder.cronSchedule("0 0/1 * * * ?");
return TriggerBuilder.newTrigger()
.forJob(initJobDetail())
.withIdentity("initTrigger")
.withSchedule(cron)
.build();
}
}
启动Nacos\Redis\Seata
项目启动product\seckill
每分钟0秒时,观察日志输出状态
保证数据库中有数据在秒杀时间段内!!!
开发查询秒杀商品详情的功能
上面章节我们完成了缓存预热
下面要根据SpuId查询正在秒杀的商品
和普通的SpuId查询商品详情相比
它的业务判断更复杂
1.页面上显示秒杀价和剩余秒杀时间等信息
2.判断请求的spuId是否在布隆过滤器中(后续完成)
3.判断Redis 中是否包含商品信息
4.如果一切正常在返回详情信息前,要为url属性赋值,其实就是固定路径+随机码
完成根据SpuId查询商品detail详情
业务逻辑层SeckillSpuServiceImpl类中编写新的方法
// 秒杀所有信息都有保存到Redis中
@Autowired
private RedisTemplate redisTemplate;
// 没有定义SpuDetail对应的常量,所以这个要自己定义
// PREFIX是"前缀"的意思
public static final String
SECKILL_SPU_DETAIL_VO_PREFIX="seckill:spu:detail:vo:";
@Override
public SeckillSpuDetailSimpleVO getSeckillSpuDetail(Long spuId) {
// 获得常量Key
String seckillSpuDetailKey=SECKILL_SPU_DETAIL_VO_PREFIX+spuId;
// 先声明一个当前方法的返回值类型的对象,设值为null,以备后续使用
SeckillSpuDetailSimpleVO simpleVO=null;
// 判断redis中是否已经包含这个key
if(redisTemplate.hasKey(seckillSpuDetailKey)){
// 如果已经保存在Redis中,直接赋值
simpleVO=(SeckillSpuDetailSimpleVO)redisTemplate
.boundValueOps(seckillSpuDetailKey).get();
}else{
// 如果Redis中没有这个对象,就需要按spuId到数据库中查询
// 使用Dubbo调用product模块的方法查询
SpuDetailStandardVO spuDetailStandardVO=
dubboSeckillSpuService.getSpuDetailById(spuId);
// 实例化SeckillSpuDetailSimpleVO对象
simpleVO=new SeckillSpuDetailSimpleVO();
BeanUtils.copyProperties(spuDetailStandardVO,simpleVO);
// simpleVO赋值完成后,将它保存到Redis中,以便后续获取
redisTemplate.boundValueOps(seckillSpuDetailKey)
.set(simpleVO,10*60*1000+ RandomUtils.nextInt(10000),
TimeUnit.MILLISECONDS);
}
// 最后别忘了写返回值!!!!
return simpleVO;
}
下面开发控制层然后测试
SeckillSpuController添加方法
// localhost:10007/seckill/spu/2/detail
@GetMapping("/{spuId}/detail")
@ApiOperation("根据spuId查询detail详情")
@ApiImplicitParam(value = "spuId",name="spuId",example = "2")
public JsonResult<SeckillSpuDetailSimpleVO> getSeckillDetail(
@PathVariable Long spuId){
SeckillSpuDetailSimpleVO simpleVO=
seckillSpuService.getSeckillSpuDetail(spuId);
return JsonResult.ok(simpleVO);
}
之前已经启动的服务无需变化
重启seckill即可
localhost:10007/doc.html
根据SpuId查询秒杀商品详情
之前的章节已经完成了根据SpuId查询Spu信息的mapper
下面我们直接从业务逻辑层开始编写即可
SeckillSpuServiceImpl业务逻辑层实现类
// 根据SpuId查询spu详情(返回值包含秒杀信息和常规信息)
@Override
public SeckillSpuVO getSeckillSpu(Long spuId) {
// 最终完整版的代码,这里要先获取布隆过滤器
// 判断参数spuId是否在布隆过滤器中,如果不在直接抛出异常
// 获取要使用的spu对应的常量
// mall:seckill:spu:vo:2
String seckillSpuKey= SeckillCacheUtils.getSeckillSpuVOKey(spuId);
// 声明要返回的对象类型
SeckillSpuVO seckillSpuVO=null;
// 判断Redis中是否包含这个Key
if(redisTemplate.hasKey(seckillSpuKey)){
// 直接从Redis中获取即可
seckillSpuVO=(SeckillSpuVO)redisTemplate
.boundValueOps(seckillSpuKey).get();
}else{
// 如果Redis中没有这个Key
// 要从数据库中查询秒杀spu信息和常规spu信息赋值到seckillSpuVO对象中
// 先查询秒杀信息:
SeckillSpu seckillSpu= seckillSpuMapper.findSeckillSpuById(spuId);
// 判断seckillSpu是否为空(因为布隆过滤器可能产生误判)
if(seckillSpu==null){
throw new CoolSharkServiceException(ResponseCode.NOT_FOUND,
"您访问的商品不存在");
}
// 到此为止,我们已经查询出了spu商品的秒杀信息,下面要查询常规信息
// dubbo调用product模块的方法获得spu常规信息
SpuStandardVO spuStandardVO= dubboSeckillSpuService
.getSpuById(spuId);
seckillSpuVO=new SeckillSpuVO();
// 将常规spu信息对象的同名属性赋值给seckillSpuVO
BeanUtils.copyProperties(spuStandardVO,seckillSpuVO);
// 最后将秒杀信息赋值到seckillSpuVO
seckillSpuVO.setSeckillListPrice(seckillSpu.getListPrice());
seckillSpuVO.setStartTime(seckillSpu.getStartTime());
seckillSpuVO.setEndTime(seckillSpu.getEndTime());
// seckillSpuVO 就完成了常规spu信息和秒杀spu信息的赋值过程
// 将这个对象保存到Redis中
redisTemplate.boundValueOps(seckillSpuKey).set(
seckillSpuVO,10*60*1000+RandomUtils.nextInt(10000),
TimeUnit.MILLISECONDS);
}
// 返回前最后的步骤是给seckillSpuVO的url属性赋值
// 一旦给url属性赋值,就意味着当前用户可以提交购买订单了
// 必须判断当前时间是否在秒杀时间段内,才能决定是否给url赋值
LocalDateTime nowTime=LocalDateTime.now();
// 当前是高并发状态,不要连接数据库去判断时间
// 我们使用seckillSpuVO中的开始时间和结束时间属性去判断
// 判断的基本原则是开始时间小于当前时间小于结束时间
// 本次我们使用"时间差"对象Duration来判断时间关系
// 我们会利用Duration提供的between方法来获得两个时间的时间差
// 这个方法有个特征,如果时间差是负数,会返回一个negative的状态
// 判断当前时间大于开始时间
// 2022-10-12 16:45 , 2022-10-12 16:40
Duration afterTime=Duration.between(nowTime,seckillSpuVO.getStartTime());
// 判断结束时间大于当前时间
Duration beforeTime=Duration.between(seckillSpuVO.getEndTime(),nowTime);
// 简单来说, between方法中两个时间参数,前面的大后面的小就会返回negative
// 上面两个变量如果返回值都是negative,
// 就证明当前时间大于开始时间,结束时间大于当前时间
if(afterTime.isNegative() && beforeTime.isNegative()){
// 进入if表示时间是正确的,要授权url,允许秒杀购买
// 根据spuId获得redis事先预热好的随机码
String randCodeKey=SeckillCacheUtils.getRandCodeKey(spuId);
// 从redis中获取随机码
String randCode=redisTemplate.boundValueOps(randCodeKey).get()+"";
// 将随机码赋值到url
seckillSpuVO.setUrl("/seckill/"+randCode);
log.info("url赋值随机码为:{}",randCode);
}
// 最后别忘了返回
// 最后返回的seckillSpuVO实际上是 秒杀spu信息+常规spu信息+url
return seckillSpuVO;
}
开发控制层SeckillSpuController
// localhost:10007/seckill/spu/2
@GetMapping("/{spuId}")
@ApiOperation("根据spuId查询spu相关信息")
@ApiImplicitParam(value = "spuId",name="spuId",example = "2")
public JsonResult<SeckillSpuVO> getSeckillSpuVO(
@PathVariable Long spuId){
SeckillSpuVO seckillSpuVO=seckillSpuService.getSeckillSpu(spuId);
return JsonResult.ok(seckillSpuVO);
}
重启Seckill模块
测试10007端口功能
根据SpuId查询秒杀Sku列表
之前编写加载数据的Mapper时,完成了根据SpuId查Sku列表的功能
下面我们从业务逻辑层开始编写
开发业务逻辑层
我们也需要将SpuId对应的Sku信息保存到Redis
在service.impl包中创建SeckillSkuServiceImpl类中编写代码如下
@Service
@Slf4j
public class SeckillSkuServiceImpl implements ISeckillSkuService {
@Autowired
private SeckillSkuMapper skuMapper;
@Autowired
private RedisTemplate redisTemplate;
// Dubbo调用product模块查询sku常规信息
@DubboReference
private IForSeckillSkuService dubboSkuService;
// 根据spuId查询sku列表
@Override
public List<SeckillSkuVO> listSeckillSkus(Long spuId) {
//SeckillSkuVO作为返回值集合的泛型,其中也是既包含秒杀sku信息,又包含常规sku信息
// 我们最终也要将两个部分都赋值之后返回
List<SeckillSkuVO> seckillSkuVOs=new ArrayList<>();
// 根据spuId查询sku列表
List<SeckillSku> seckillSkus=skuMapper.findSeckillSkusBySpuId(spuId);
// 遍历所有sku
for(SeckillSku sku : seckillSkus){
// 获取当前sku的id以备后续使用
Long skuId=sku.getSkuId();
// 声明sku对象的Key
// mall:seckill:sku:vo:1
String seckillSkuVOKey= SeckillCacheUtils.getSeckillSkuVOKey(skuId);
// 声明返回值类型对象,先赋null
SeckillSkuVO seckillSkuVO=null;
// 判断Redis中是否已经包含这个Key
if(redisTemplate.hasKey(seckillSkuVOKey)){
seckillSkuVO=(SeckillSkuVO) redisTemplate
.boundValueOps(seckillSkuVOKey).get();
}else{
// Redis没有这个sku就需要到数据库中去查询
// 先查询常规信息
SkuStandardVO skuStandardVO=dubboSkuService.getById(skuId);
// 当前循环正在遍历的对象sku就是秒杀信息
// 两个方面信息都有了,就实例化返回值seckillSkuVO赋值
seckillSkuVO=new SeckillSkuVO();
// 常规同名属性赋值
BeanUtils.copyProperties(skuStandardVO,seckillSkuVO);
// 秒杀信息手动赋值
seckillSkuVO.setSeckillPrice(sku.getSeckillPrice());
seckillSkuVO.setStock(sku.getSeckillStock());
seckillSkuVO.setSeckillLimit(sku.getSeckillLimit());
// 将sku保存到Redis中
redisTemplate.boundValueOps(seckillSkuVOKey)
.set(seckillSkuVO,10*60*1000+ RandomUtils.nextInt(10000),
TimeUnit.MILLISECONDS);
}
// 将赋好值的seckillSkuVO对象添加到返回值集合中
// seckillSkuVO可能是Redis直接获取的,也可能是数据库查出来的
seckillSkuVOs.add(seckillSkuVO);
}
// 最后别忘了返回集合!!!
return seckillSkuVOs;
}
}
消息队列
软件下载
doc.canglaoshi.org网站中的kafka
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bYpIpZHe-1666255690045)(image-20220906114435678.png)]
随笔
Redis
Key sku_id_1: 0
A线程
B线程