概述
限时秒杀
如何应对秒杀商品首页的高并发访问
秒杀商品首页的访问频率极高,因此为了应对高并发访问对mysql数据库的压力,因此,我们在秒杀的前几秒钟查询mysql数据库.把查询到的秒杀商品集合存入redis中.因为redis能更好的应对高并发访问.其读写效率是mysql不能比的.在用户访问页面的时候,就可以通过查询redis数据库来获得秒杀商品的数据.把mysql存入redis的操作,我们就涉及到了定时任务
定时任务
我们在这里独立出一个单独的模块,用来执行一个定时任务,在秒杀开始之前把数据存入redis
定时任务模块的applicationContext-service配置文件与其他模块不同,需要开启的是task注解支持
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task.xsd">
<!--包扫描-->
<context:component-scan base-package="com.pinyougou.seckill.task"/>
<!--开启注解驱动-->
<task:annotation-driven/>
</beans>
applicationContext-tx配置文件与其他模块相同
具体定时器代码
@Component
public class SeckillTask {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private TbSeckillGoodsMapper seckillGoodsMapper;
//cron表达式这里只有6位分别为秒,分,时,日,月,周
@Scheduled(cron="0 18 10 7 9 ?")
public void saveToRedis() {
System.out.println("任务触发");
//从mysql中获取秒杀商品
//条件:1.时间满足 2.剩余数量满足 3.审核通过
TbSeckillGoodsExample example = new TbSeckillGoodsExample();
example.createCriteria().andStockCountGreaterThan(0)//剩余数量大于0
.andStatusEqualTo("1")//审核通过
.andStartTimeLessThanOrEqualTo(new Date())//开始时间小于当前时间
.andEndTimeGreaterThanOrEqualTo(new Date());//结束时间大于当前时间
List<TbSeckillGoods> secList = seckillGoodsMapper.selectByExample(example);
//保存到redis数据库中
for (TbSeckillGoods seckillGoods : secList) {
redisTemplate.boundHashOps("seckill_goods").put(seckillGoods.getId(), seckillGoods);
}
System.out.println("秒杀商品已放入redis");
//redisTemplate.boundHashOps("seckill_goods").delete();
}
}
页面展示数据
这里就很简单了,在秒杀首页读取我们存放到redis中的秒杀商品集合,使用angularjs的方式去展示商品到首页
在首页点击查看商品跳转到秒杀商品详情页时,需要使用angularjs的方式去传递id,用#?号来传递
在js中我们可以通过内置服务
location中的
l
o
c
a
t
i
o
n
中
的
location.search()[‘xx’]来获取传递的id
秒杀商品详情页的倒计时设计
这里涉及到了angularjs的另一个内置服务,定时器
内置服务定时器:
interval(执行的函数,间隔的毫秒数,运行次数);
i
n
t
e
r
v
a
l
(
执
行
的
函
数
,
间
隔
的
毫
秒
数
,
运
行
次
数
)
;
interval.cancel(定时器名字):取消某定时器.
定时器代码设计
$scope.findOne=function(){
var id = $location.search()['id'];
seckillService.findOne(id).success(function(response){
$scope.entity=response;
//$scope.times=10;
//XX天xx:xx:xx
//用结束时间-当前时间 再/1000来获得还剩多少秒结束
var seconds =Math.floor((new Date($scope.entity.endTime).getTime()-new Date().getTime())/1000);
//定时器代码,每次seconds-1用时1000ms,当seconds==0时结束计时器
var aa = $interval(function(){
seconds--;
if(seconds==0){
$interval.cancel(aa);
}
//页面展示的日,时:分:秒
var day = Math.floor(seconds/60/60/24);
var hour = Math.floor((seconds-(day*60*60*24))/60/60);
var min = Math.floor((seconds-(day*60*60*24)-(hour*60*60))/60);
var second = (seconds-(day*60*60*24)-(hour*60*60)-(min*60));
$scope.timeStr="";
if(day!=0){
$scope.timeStr+=day+"天";
}
if(hour<10){
hour="0"+hour;
}
if(min<10){
min="0"+min;
}
if(second<10){
second="0"+second;
}
$scope.timeStr+=hour+":"+min+":"+second;
}, 1000);
})
保存秒杀订单
这里我们需求先保存订单付款,后填写收货地址,因此一点击秒杀按钮就要生成订单
点击时,秒杀商品数量-1并存回redis数据库
要考虑到几种情况
如果进入页面的人没有登录,提示其请登录
设置security配置文件,未登录的用户名即为anonymousUser
当秒杀商品数量减少为0的时候,redis中应该清除此商品
如果有两个人同时进入抢购商品详情页面,一个人付款后恰好数量为0,另一个人应该不允许继续抢购,需要判断一下从redis中所获得的秒杀商品对象是不是为null,如果为null则证明秒杀商品已经卖完
当秒杀商品的数量为0时,停止售卖,并把商品再次存回mysql数据库
controller代码
//保存秒杀商品订单
@RequestMapping("/saveSeckillOrder")
public Result saveSeckillOrder(Long id) {
//判断用户是否登录,这里要修改security配置文件,设置未登录的用户名为anonymously
String userId = SecurityContextHolder.getContext().getAuthentication().getName();
if("anonymousUser".equals(userId)) {
return new Result(false, "请登录");
}
try {
//如果没抛异常,则证明订单保存成功,返回true
seckillService.saveSeckillOrder(id, userId);
return new Result(true, "");
} catch (RuntimeException e) {
//如果抛异常,则证明订单保存失败,返回false
e.printStackTrace();
return new Result(false, e.getMessage());
}catch (Exception e) {
//如果抛异常,则证明订单保存失败,返回false
e.printStackTrace();
return new Result(false, "订单保存失败,请重试");
}
}
service层代码
@Override
public void saveSeckillOrder(Long id, String userId) {
//查询秒杀商品,获取其信息保存到秒杀订单中
TbSeckillGoods seckillGoods = (TbSeckillGoods) redisTemplate.boundHashOps("seckill_goods").get(id);
//判断seckillGoods是否存在,以防止两用户在商品数量为1时同时访问抢购页面导致商品数量不足
if(seckillGoods==null) {
throw new RuntimeException("商品售完");
}
TbSeckillOrder seckillOrder = new TbSeckillOrder();
//id bigint(20) NOT NULL主键
seckillOrder.setId(idWorker.nextId());
//seckill_id bigint(20) NULL秒杀商品ID
seckillOrder.setSeckillId(id);
//money decimal(10,2) NULL支付金额
seckillOrder.setMoney(seckillGoods.getCostPrice());
//user_id varchar(50) NULL用户
seckillOrder.setUserId(userId);
//seller_id varchar(50) NULL商家
seckillOrder.setSellerId(seckillGoods.getSellerId());
//create_time datetime NULL创建时间
seckillOrder.setCreateTime(new Date());
//status varchar(1) NULL状态
seckillOrder.setStatus("0");
//保存秒杀订单到mysql数据库中
seckillOrderMapper.insert(seckillOrder);
//订单保存成功,则秒杀商品在redis数据库中的库存-1
seckillGoods.setStockCount(seckillGoods.getStockCount()-1);
//如果库存为0,则删除redis数据库中的秒杀商品数据,停止售卖
if(seckillGoods.getStockCount()==0) {
redisTemplate.boundHashOps("seckill_goods").delete(id);
//售完后在更新mysql数据库
seckillGoodsMapper.updateByPrimaryKey(seckillGoods);
}else {
//如果数量不为0则把商品保存回redis中
redisTemplate.boundHashOps("seckill_goods").put(id, seckillGoods);
}
//保存待支付的订单到redis数据库中,为了支付时修改
redisTemplate.boundHashOps("seckill_order").put(userId, seckillOrder);
}
高并发状态下的抢购操作
到这里秒杀的基本功能我们已经实现
但是我们需要考虑几个问题
也就是上下分别使用了redis与mysql数据库,在高并发量的情况下会产生线程安全问题
也就是如果有多个请求同时涌入抢购商品,由于redis的并发量很高,而mysql的并发量很低,redis已经允许请求通过,而mysql还没有来得及将数量-1,这是如果数量为0了,还有请求已经通过redis,则会导致超卖的问题
这里也就需要在操作redis数据库的时候,就对商品剩余数量进行一个操控,
我们使用的方法是redis队列
在将商品存入redis的时候,同时构造一个redis队列,key为商品的id(seckill_goods_num_id值),value采用压栈的方式左压栈进入队列,有几个商品队列的长度就为几.放什么东西都可以,简单点我们就可以把商品的id作为数量表示放进队列
当用户点击购买请求的时候,立即右出栈,代表商品数量-1,当redis队列中没有元素的时候即商品售完
修改定时任务
存入把秒杀商品存入redis的同时,在创建一个redis队列用来记录该商品的数量
for (TbSeckillGoods seckillGoods : secList) {
redisTemplate.boundHashOps("seckill_goods").put(seckillGoods.getId(), seckillGoods);
//创建一个redis队列,用来记录该商品的剩余数量,解决高并发问题
for (int i = 0; i < seckillGoods.getStockCount(); i++) {
redisTemplate.boundListOps("seckill_goods_num_"+ seckillGoods.getId()).leftPush(seckillGoods.getId());
}
}
修改serviceImpl中关于redis判断商品是否卖光的逻辑
//从redis中查询商品数量的redis队列,并执行弹出操作
Object pop = redisTemplate.boundListOps("seckill_goods_num_"+ id).rightPop();
//判断弹出的值是否为null;如果为null代表已经出售完毕,抛出异常
if(pop==null) {
throw new RuntimeException("商品售完");
}
提高cpu效率:多线程解决方案
线程安全问题解决,我们在来考虑多线程的问题
由于redis是单线程的,所以我们可以把操作数据库的方法单独抽出来一个线程类,使其实现runable接口.
spring给我们提供了一个线程池我们通过依赖注入的方式在serviceImpl调用我们写好的线程类就可以实现多线程了
修改spring配置文件,注入spring线程池
<!-- 线程池配置 -->
<bean class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor" id="executor">
<!-- 核心线程数,默认为1-->
<property name="corePoolSize" value="10" />
<!--最大线程数,默认为Integer.MAX_VALUE-->
<property name="maxPoolSize" value="50" />
<!--队列最大长度,一般需要设置值>=notifyScheduledMainExecutor.maxNum;默认为Integer.MAX_VALUE-->
<property name="queueCapacity" value="10000" />
<!--线程池维护线程所允许的空闲时间,默认为60s-->
<property name="keepAliveSeconds" value="300" />
<!--线程池对拒绝任务(无线程可用)的处理策略,目前只支持AbortPolicy、CallerRunsPolicy;默认为后者-->
<property name="rejectedExecutionHandler">
<bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy" />
</property>
</bean>
线程类的抽取;这里我们需要userId与秒杀商品的id,由于不能通过方法传递参数,因此我们需要把userId与秒杀商品的id构造成一个新的对象,然后存放到redis中
//创建一个自定义类,来存放userId与seckillgoodid;
UserIdAndSeckillGoodsId userIdAndSeckillGoodsId = new UserIdAndSeckillGoodsId(userId, id);
//把这个类存放到redis数据库中,方便线程类获取
redisTemplate.boundListOps("userIdAndSeckillGoodsId").leftPush(userIdAndSeckillGoodsId);
public class CreateOrder implements Runnable {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private IdWorker idWorker;
@Autowired
private TbSeckillOrderMapper seckillOrderMapper;
@Autowired
private TbSeckillGoodsMapper seckillGoodsMapper;
@Override
public void run() {
//从redis中获取需要的userId与seckillId
UserIdAndSeckillGoodsId userIdAndSeckillGoodsId = (UserIdAndSeckillGoodsId) redisTemplate.boundListOps("userIdAndSeckillGoodsId").leftPop();
String userId = userIdAndSeckillGoodsId.getUserId();
Long id = userIdAndSeckillGoodsId.getSeckillGoodsId();
TbSeckillGoods seckillGoods = (TbSeckillGoods) redisTemplate.boundHashOps("seckill_goods").get(id);
TbSeckillOrder seckillOrder = new TbSeckillOrder();
//id bigint(20) NOT NULL主键
seckillOrder.setId(idWorker.nextId());
//seckill_id bigint(20) NULL秒杀商品ID
seckillOrder.setSeckillId(id);
//money decimal(10,2) NULL支付金额
seckillOrder.setMoney(seckillGoods.getCostPrice());
//user_id varchar(50) NULL用户
seckillOrder.setUserId(userId);
//seller_id varchar(50) NULL商家
seckillOrder.setSellerId(seckillGoods.getSellerId());
//create_time datetime NULL创建时间
seckillOrder.setCreateTime(new Date());
//status varchar(1) NULL状态
seckillOrder.setStatus("0");
//保存秒杀订单到mysql数据库中
seckillOrderMapper.insert(seckillOrder);
//订单保存成功,则秒杀商品在redis数据库中的库存-1
seckillGoods.setStockCount(seckillGoods.getStockCount()-1);
//如果库存为0,则删除redis数据库中的秒杀商品数据,停止售卖
if(seckillGoods.getStockCount()==0) {
redisTemplate.boundHashOps("seckill_goods").delete(id);
//售完后在更新mysql数据库
seckillGoodsMapper.updateByPrimaryKey(seckillGoods);
}else {
//如果数量不为0则把商品保存回redis中
redisTemplate.boundHashOps("seckill_goods").put(id, seckillGoods);
}
//保存待支付的订单到redis数据库中,为了支付时修改
redisTemplate.boundHashOps("seckill_order").put(userId, seckillOrder);
}
}
完成后只需要在service调用就可以了
executor.execute(createOrder);
最后
以上就是欢呼招牌为你收集整理的限时秒杀功能的基本实现以及高并发问题的处理方案限时秒杀的全部内容,希望文章能够帮你解决限时秒杀功能的基本实现以及高并发问题的处理方案限时秒杀所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复