第二十篇商城系统-秒杀功能设计与实现
Posted 波波烤鸭
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第二十篇商城系统-秒杀功能设计与实现相关的知识,希望对你有一定的参考价值。
秒杀服务
一、商品上架
秒杀活动的结构图
通过定时任务触发:
/**
* 定时上架秒杀商品信息
*/
@Slf4j
@Component
public class SeckillSkuSchedule
@Autowired
SeckillService seckillService;
@Autowired
RedissonClient redissonClient;
/**
*
*/
@Async
@Scheduled(cron = "*/5 * * * * *")
public void uploadSeckillSku3Days()
log.info("定时上架秒杀商品执行了...." + new Date());
// 分布式锁
RLock lock = redissonClient.getLock("seckill:upload:lock");
lock.lock(10, TimeUnit.SECONDS);
try
// 调用上架商品的方法
seckillService.uploadSeckillSku3Days();
catch (Exception e)
lock.unlock();
进入到Service中处理
@Override
public void uploadSeckillSku3Days()
// 1. 通过OpenFegin 远程调用Coupon服务中接口来获取未来三天的秒杀活动的商品
R r = couponFeignService.getLates3DaysSession();
if(r.getCode() == 0)
// 表示查询操作成功
String json = (String) r.get("data");
List<SeckillSessionEntity> seckillSessionEntities = JSON.parseArray(json,SeckillSessionEntity.class);
// 2. 上架商品 Redis数据保存
// 缓存商品
// 2.1 缓存每日秒杀的SKU基本信息
saveSessionInfos(seckillSessionEntities);
// 2.2 缓存每日秒杀的商品信息
saveSessionSkuInfos(seckillSessionEntities);
/**
* 保存每日活动的信息到Redis中
* @param seckillSessionEntities
*/
private void saveSessionInfos(List<SeckillSessionEntity> seckillSessionEntities)
for (SeckillSessionEntity seckillSessionEntity : seckillSessionEntities)
// 循环缓存每一个活动 key: start_endTime
long start = seckillSessionEntity.getStartTime().getTime();
long end = seckillSessionEntity.getEndTime().getTime();
// 生成Key
String key = SeckillConstant.SESSION_CHACE_PREFIX+start+"_"+end;
Boolean flag = redisTemplate.hasKey(key);
if(!flag)// 表示这个秒杀活动在Redis中不存在,也就是还没有上架,那么需要保存
// 需要存储到Redis中的这个秒杀活动涉及到的相关的商品信息的SKUID
List<String> collect = seckillSessionEntity.getRelationEntities().stream().map(item ->
// 秒杀活动存储的 VALUE是 sessionId_SkuId
return item.getPromotionSessionId()+"_"+item.getSkuId().toString();
).collect(Collectors.toList());
redisTemplate.opsForList().leftPushAll(key,collect);
/**
* 存储活动对应的 SKU信息
* @param seckillSessionEntities
*/
private void saveSessionSkuInfos(List<SeckillSessionEntity> seckillSessionEntities)
seckillSessionEntities.stream().forEach(session ->
// 循环取出每个Session,然后取出对应SkuID 封装相关的信息
BoundHashOperations<String, Object, Object> hashOps = redisTemplate.boundHashOps(SeckillConstant.SKU_CHACE_PREFIX);
session.getRelationEntities().stream().forEach(item->
String skuKey = item.getPromotionSessionId()+"_"+item.getSkuId();
Boolean flag = redisTemplate.hasKey(skuKey);
if(!flag)
SeckillSkuRedisDto dto = new SeckillSkuRedisDto();
// 1.获取SKU的基本信息
R info = productFeignService.info(item.getSkuId());
if(info.getCode() == 0)
// 表示查询成功
String json = (String) info.get("skuInfoJSON");
dto.setSkuInfoVo(JSON.parseObject(json,SkuInfoVo.class));
// 2.获取SKU的秒杀信息
/*dto.setSkuId(item.getSkuId());
dto.setSeckillPrice(item.getSeckillPrice());
dto.setSeckillCount(item.getSeckillCount());
dto.setSeckillLimit(item.getSeckillLimit());
dto.setSeckillSort(item.getSeckillSort());*/
BeanUtils.copyProperties(item,dto);
// 3.设置当前商品的秒杀时间
dto.setStartTime(session.getStartTime().getTime());
dto.setEndTime(session.getEndTime().getTime());
// 4. 随机码
String token = UUID.randomUUID().toString().replace("-","");
dto.setRandCode(token);
// 分布式信号量的处理 限流的目的
RSemaphore semaphore = redissonClient.getSemaphore(SeckillConstant.SKU_STOCK_SEMAPHORE + token);
// 把秒杀活动的商品数量作为分布式信号量的信号量
semaphore.trySetPermits(item.getSeckillCount().intValue());
hashOps.put(skuKey,JSON.toJSONString(dto));
);
);
启动服务,数据会被保存到Redis中
二、秒杀商品查询
通过当前时间获取对应的秒杀活动及对应的SKU信息。
/**
* 查询出当前时间内的秒杀活动及对应的商品SKU信息
* @return
*/
@Override
public List<SeckillSkuRedisDto> getCurrentSeckillSkus()
// 1.确定当前时间是属于哪个秒杀活动的
long time = new Date().getTime();
// 从Redis中查询所有的秒杀活动
Set<String> keys = redisTemplate.keys(SeckillConstant.SESSION_CHACE_PREFIX + "*");
for (String key : keys)
//seckill:sessions1656468000000_1656469800000
String replace = key.replace(SeckillConstant.SESSION_CHACE_PREFIX, "");
// 1656468000000_1656469800000
String[] s = replace.split("_");
Long start = Long.parseLong(s[0]); // 活动开始的时间
Long end = Long.parseLong(s[1]); // 活动结束的时间
if(time > start && time < end)
// 说明的秒杀活动就是当前时间需要参与的活动
// 取出来的是SKU的ID 2_9
List<String> range = redisTemplate.opsForList().range(key, -100, 100);
BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SeckillConstant.SKU_CHACE_PREFIX);
List<String> list = ops.multiGet(range);
if(list != null && list.size() > 0)
List<SeckillSkuRedisDto> collect = list.stream().map(item ->
SeckillSkuRedisDto seckillSkuRedisDto = JSON.parseObject(item, SeckillSkuRedisDto.class);
return seckillSkuRedisDto;
).collect(Collectors.toList());
return collect;
return null;
然后定义相关的Controller接口就可以访问了
@RestController
@RequestMapping("/seckill")
public class SeckillController
@Autowired
SeckillService seckillService;
@GetMapping("/currentSeckillSessionSkus")
public R getCurrentSeckillSessionSkus()
List<SeckillSkuRedisDto> currentSeckillSkus = seckillService.getCurrentSeckillSkus();
return R.ok().put("data", JSON.toJSONString(currentSeckillSkus));
然后对应的访问效果:
三、页面渲染
1.网关配置
首先在host中配置域名
然后在网关中配置路由信息
然后重启服务访问:
能访问到数据就表示域名配置成功
2.首页配置
通过Ajax来访问获取秒杀的相关信息
$.get("http://seckill.msb.com/seckill/currentSeckillSessionSkus",function(resp)
if(resp.data.length > 0)
// 说明有秒杀的数据
console.log($.parseJSON(resp.data))
$.parseJSON(resp.data).forEach(function(item)
$("<li></li>").append("<img width='130px' height='130px' src='"+item.skuInfoVo.skuDefaultImg+"'/>")
.append("<p>"+item.skuInfoVo.skuSubtitle+"</p>")
.append("<span>"+item.seckillPrice+"</span>")
.append("<s>"+item.skuInfoVo.price+"</s>")
.appendTo("#seckillSessionContent");
)
/*<li>
<img src="/static/index/img/section_second_list_img1.jpg" alt="">
<p>花王 (Merries) 妙而舒 纸尿裤 大号 L54片 尿不湿(9-14千克) (日本官方直采) 花王 (Merries) 妙而舒 纸尿裤 大号 L54片 尿不湿(9-14千</p>
<span>¥83.9</span><s>¥99.9</s>
</li>*/
)
展示的效果
3.商品详情
在购买商品的时候,进入到商品详情页,如果该商品也参与了秒杀活动,那么对应的需要展示相关的信息
首先我们需要在秒杀服务中提供一个根据SKUID查询相关的秒杀活动的接口
/**
* 根据SKUID查询秒杀活动对应的信息
* @param skuId
* @return
*/
@Override
public SeckillSkuRedisDto getSeckillSessionBySkuId(Long skuId)
// 1.找到所有需要参与秒杀的商品的sku信息
BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SeckillConstant.SKU_CHACE_PREFIX);
Set<String> keys = ops.keys();
if(keys != null && keys.size() > 0)
String regx = "\\\\d_"+ skuId;
for (String key : keys)
boolean matches = Pattern.matches(regx, key);
if(matches)
// 说明找到了对应的SKU的信息
String json = ops.get(key);
SeckillSkuRedisDto dto = JSON.parseObject(json, SeckillSkuRedisDto.class);
return dto;
return null;
然后在查询商品详情的时候异步查询出对应的秒杀活动信息
然后在模板页面中展示相关的信息
<div class="box-summary clear">
<ul>
<li>京东价</li>
<li>
<span>¥</span>
<span th:text="$#numbers.formatDecimal(item.info.price,3,2)">4499.00</span>
</li>
<li style="color: red">
<span th:if="$#dates.createNow().getTime() < item.seckillVO.startTime">
商品将在:[[$#dates.format(new java.util.Date(item.seckillVO.startTime),'yyyy-MM-dd HH:mm:ss')]] 开始秒杀
</span>
<span th:if="$#dates.createNow().getTime() > item.seckillVO.startTime
&& #dates.createNow().getTime() < item.seckillVO.endTime ">
秒杀价: [[$#numbers.formatDecimal(item.seckillVO.seckillPrice,1,2)]]
</span>
</li>
<li>
<a href="/static/item/">
预约说明
</a>
</li>
</ul>
</div>
首页调整到商品详情页
function goItem(skuId)
location.href="http://item.msb.com/"+skuId+".html"
$.get("http://seckill.msb.com/seckill/currentSeckillSessionSkus",function(resp)
if(resp.data.length > 0)
// 说明有秒杀的数据
console.log($.parseJSON(resp.data))
$.parseJSON(resp.data).forEach(function(item)
$("<li οnclick='goItem("+item.skuId+")'></li>")
.append("<img width='130px' height='130px' src='"+item.skuInfoVo.skuDefaultImg+"'/>")
.append("<p>"+item.skuInfoVo.skuSubtitle+"</p>")
.append("<span>"+item.seckillPrice+"</span>")
.append("<s>"+item.skuInfoVo.price+"</s>")
.appendTo("#seckillSessionContent");
)
/*<li>
<img src="/static/index/img/section_second_list_img1.jpg" alt="">
<p>花王 (Merries) 妙而舒 纸尿裤 大号 L54片 尿不湿(9-14千克) (日本官方直采) 花王 (Merries) 妙而舒 纸尿裤 大号 L54片 尿不湿(9-14千</p>
<span>¥83.9</span><s>¥99.9</s>
</li>*/
)
四、秒杀活动
1.秒杀活动关注点
秒杀活动的最大特点就是高并发而且是短时间内的高并发,那么对我们的服务要求就非常高,针对这种情况所产生的共性问题,对应的解决方案:
2. 秒杀服务前端
当我们点击 秒杀抢购
按钮后,对应我们需要把当前的商品信息提交到后端服务。活动编号+“_”+SkuId,Code随机码,抢购商品的数量。
<div class="box-btns-two" th:if="$#dates.createNow().getTime() < item.seckillVO.startTime
|| #dates.createNow().getTime() > item.seckillVO.endTime ">
<a href="#" id="addCart" th:attr="skuId=$item.info.skuId">
加入购物车
</a>
</div>
<div class="box-btns-two" th:if="$#dates.createNow().getTime() > item.seckillVO.startTime
&& #dates.createNow().getTime() < item.seckillVO.endTime ">
<a href="#" id="seckillId" th:attr="skuId=$item.info.skuId,sessionId=$item.seckillVO.promotionSessionId,code=$item.seckillVO.randCode">
抢购商品
</a>
</div>
对应的js操作
$("#seckillId").click(function()
var isLogin = [[$session.loginUser !=null]]
if(isLogin)
// 1. 获取活动编号和SkuId 2_10
var killId = $(this).attr("sessionId") + "_" + $(this).attr("skuId");
// 2. 获取对应的随机码
var code = $(this).attr("code");
// 3. 获取秒杀的商品数量
var num = $("#numInput").val();
location.href="http://seckill.msb.com/seckill/kill?killId="+killId + "&code="+code+"&num="+num;
else
alert("请先登录才能参加秒杀活动!!!");
return false;
);
访问测试:
3.后端逻辑处理
前端提交的秒杀请求,在后端具体的处理
3.1 登录校验
秒杀活动必须是在登录状态下进行的,如果没有认证就不让秒杀。这时我们需要整合进来SpringSession。
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
然后添加对应的配置信息
第二十篇商城系统-秒杀功能设计与实现