关注小程序 找一找教程网-随时随地学编程

Java教程

商城秒杀系统优化方案

页面优化方案

页面缓存+URL缓存+对象缓存

页面缓存

渲染good_list.html页面的时候,直接从缓存里面取,如果缓存中没有,则我们手动进行渲染,从而减少对数据库的mysql访问。

  1. 取缓存
String html = redisService.get(GoodsKey.getGoodsList, "", String.class)
复制代码
  1. 手动渲染模板
@Autowired
ThymeleafViewResolver thymeleafViewResolver;
@Autowired
ApplicationContext applicationContext;

SpringWebContext ctx = new SpringWebContext(request,response, request.getServletContext(),request.getLocale(), model.asMap(), applicationContext );
// 手动渲染
String html = thymeleafViewResolver.getTemplateEngine().process("goods_list", ctx);
复制代码
  1. 结果输出
if(!StringUtils.isEmpty(html)) {
    // 如果非空,则保存到redis中
	redisService.set(GoodsKey.getGoodsList, "", html);
}
return html;
复制代码

注意:页面缓存有效期需要比较短,可以设为60秒

URL缓存

good_detail.html同理,但是不同详情的页面有不同的缓存。所以GoodsKey后面需要加上goodsId作为redis存储的真实key。

对象缓存

这里是把MiaoshaUser存储到redis中

public MiaoshaUser getById(long id) {
    //取缓存
    MiaoshaUser user = redisService.get(MiaoshaUserKey.getById, ""+id, MiaoshaUser.class);
    if(user != null) {
    	return user;
    }
    //缓存中取不到,取数据库
    user = miaoshaUserDao.getById(id);
    if(user != null) {
    	redisService.set(MiaoshaUserKey.getById, ""+id, user);
    }
    return user;
}
复制代码

但是考虑到用户可能会有更新密码的操作

// MiaoshaUserDao.java接口
@Update("update miaosha_user set password = #{password} where id = #{id}")
public void update(MiaoshaUser toBeUpdate);


public boolean updatePassword(String token, long id, String formPass) {
    //取user
    MiaoshaUser user = getById(id);
    if(user == null) {
    	throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
    }
    //更新数据库
    MiaoshaUser toBeUpdate = new MiaoshaUser();
    toBeUpdate.setId(id);
    toBeUpdate.setPassword(MD5Util.formPassToDBPass(formPass, user.getSalt()));
    miaoshaUserDao.update(toBeUpdate);
    //处理缓存
    redisService.delete(MiaoshaUserKey.getById, ""+id);
    user.setPassword(toBeUpdate.getPassword());
    redisService.set(MiaoshaUserKey.token, token, user);
    return true;
}
复制代码

Jmeter压测结果

线程数5000*10 QPS:1267/sec -> 2884/sec
CPU-load:15 -> 5

高性能网站设计之缓存更新的思路blog.csdn.net/tTU1EvLDeLF…

页面静态化,前后端分离

  1. 常用技术AngularJS, Vue.js
  2. 优点:利用浏览器的缓存
  3. 商品详情静态化
  4. 秒杀静态化

解决超卖以及重复秒杀

  1. 解决超卖:SQL更新库存时加判断,防止库存变成负数
@Update("update miaosha_goods set stock_count = stock_count - 1 where goods_id = #{goodsId} and stock_count > 0")
public int reduceStock(MiaoshaGoods g);
复制代码
  1. 解决重复秒杀:

在MiaoshaOrder表中建立唯一索引(userId, orderId),并且存入到redis缓存中。

MySQL索引:如果要强烈使一列或多列具有唯一性,通常使用PRIMARY KEY约束。但是,每个表只能有一个主键。 因此,如果使多个列或多个组合列具有唯一性,则不能使用主键约束。

幸运的是,MySQL提供了另一种索引,叫做唯一索引,允许我们可以使一个或者多个列的值具有唯一性。另外,不会像主键索引一样,我们的每张表中可以有很多个唯一索引。

静态资源优化

  1. JS/CSS压缩,减少流量
  2. 多个JS/CSS组合,减少连接数,并不是一个传js就建立一个连接,因为一个TCP连接需要三次握手,很耗时间
  3. CDN就近访问

CDN优化

CDN加速意思就是在用户和我们的服务器之间加一个缓存机制,通过这个缓存机制动态获取IP地址根据地理位置,让用户到最近的服务器访问。 那么CDN是个啥? 全称Content Delivery Network即内容分发网络。

CDN是一组分布在多个不同的地理位置的WEB服务器,用于更加有效的向用户发布内容,在优化性能时,会根据距离的远近来选择 。

CDN系统能实时的根据网络流量和各节点的连接,负载状况及用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上,其目的是使用户能就近的获取请求数据,解决网络拥堵,提高访问速度,解决由于网络带宽小,用户访问量大,网点分布不均等原因导致的访问速度慢的问题。

由于CDN部署在网络运营商的机房,这些运营商又是终端用户网络的提供商,因此用户请求的第一跳就到达CDN服务器,当CDN服务器中缓存有用户请求的数据时,就可以从CDN直接返回给浏览器,因此就可以提高访问速度。

CDN能够缓存JavaScript脚本,css样式表,图片,图标,Flash等静态资源文件(不包括html页面),这些静态资源文件的访问频率很高,将其缓存在CDN可以极大地提高网站的访问速度,但由于CDN是部署在网络运营商的机房,所以在一般的网站很少用CDN加速。

秒杀接口优化(RabbitMQ)

思路:减少对数据库的访问

  1. 系统初始化,把商品库存数量加载到Redis
  2. 收到请求,Redis预减库存,库存不足,直接返回否则进入3
  3. 请求入队,立即返回排队中
  4. 请求出队,生成订单,减少库存,生成完订单之后,会把订单写入到缓存里面去供客户端查询
  5. 客户端轮询,是否秒杀成功

RabbitMQ 四种交换机模式

  1. Direct
//MQConfig.java
public static final String QUEUE = "queue";
@Bean
public Queue queue() {
    return new Queue(QUEUE, true);
}

// Sender.java
public void send(Object message) {
    String msg = RedisService.beanToString(message);
    log.info("send message:"+msg);
    amqpTemplate.convertAndSend(MQConfig.QUEUE, msg);
}

// Receiver.java
@RabbitListener(queues=MQConfig.QUEUE)
public void receive(String message) {
    log.info("receive message:"+message);
}

// 调用的时候
sender.send("xxxxxxxx");
复制代码
  1. Topic
//MQConfig.java
public static final String TOPIC_QUEUE1 = "topic.queue1";
public static final String TOPIC_QUEUE2 = "topic.queue2";
public static final String TOPIC_EXCHANGE = "topicExchage";
@Bean
public Queue topicQueue1() {
    return new Queue(TOPIC_QUEUE1, true);
}
@Bean
public Queue topicQueue2() {
    return new Queue(TOPIC_QUEUE2, true);
}
@Bean
public TopicExchange topicExchage(){
    return new TopicExchange(TOPIC_EXCHANGE);
}
// Topic绑定
@Bean
public Binding topicBinding1() {
    return BindingBuilder.bind(topicQueue1()).to(topicExchage()).with("topic.key1");
}
@Bean
public Binding topicBinding2() {
    return BindingBuilder.bind(topicQueue2()).to(topicExchage()).with("topic.#");
}

// Sender.java
public void sendTopic(Object message) {
    String msg = RedisService.beanToString(message);
    log.info("send topic message:"+msg);
    amqpTemplate.convertAndSend(MQConfig.TOPIC_EXCHANGE, "topic.key1", msg+"1");
    amqpTemplate.convertAndSend(MQConfig.TOPIC_EXCHANGE, "topic.key2", msg+"2");
}

// Receiver.java
@RabbitListener(queues=MQConfig.TOPIC_QUEUE1)
public void receiveTopic1(String message) {
    log.info(" topic  queue1 message:"+message);
}
@RabbitListener(queues=MQConfig.TOPIC_QUEUE2)
public void receiveTopic2(String message) {
    log.info(" topic  queue2 message:"+message);
}
复制代码
  1. Fanout
//MQConfig.java
public static final String TOPIC_QUEUE1 = "topic.queue1";
public static final String TOPIC_QUEUE2 = "topic.queue2";
public static final String TOPIC_EXCHANGE = "topicExchage";
public static final String FANOUT_EXCHANGE = "fanoutxchage";
@Bean
public Queue topicQueue1() {
    return new Queue(TOPIC_QUEUE1, true);
}
@Bean
public Queue topicQueue2() {
    return new Queue(TOPIC_QUEUE2, true);
}
@Bean
public TopicExchange topicExchage(){
    return new TopicExchange(TOPIC_EXCHANGE);
}
// Fanouot绑定
@Bean
public Binding FanoutBinding1() {
    return BindingBuilder.bind(topicQueue1()).to(fanoutExchage());
}
@Bean
public Binding FanoutBinding2() {
    return BindingBuilder.bind(topicQueue2()).to(fanoutExchage());
}
// Sender.java
public void sendTopic(Object message) {
    String msg = RedisService.beanToString(message);
    log.info("send topic message:"+msg);
    amqpTemplate.convertAndSend(MQConfig.TOPIC_EXCHANGE, "topic.key1", msg+"1");
    amqpTemplate.convertAndSend(MQConfig.TOPIC_EXCHANGE, "topic.key2", msg+"2");
}

// Receiver.java
@RabbitListener(queues=MQConfig.TOPIC_QUEUE1)
public void receiveTopic1(String message) {
    log.info(" topic  queue1 message:"+message);
}
@RabbitListener(queues=MQConfig.TOPIC_QUEUE2)
public void receiveTopic2(String message) {
    log.info(" topic  queue2 message:"+message);
}
复制代码
  1. Header
//MQConfig.java
@Bean
public HeadersExchange headersExchage(){
    return new HeadersExchange(HEADERS_EXCHANGE);
}
@Bean
public Queue headerQueue1() {
    return new Queue(HEADER_QUEUE, true);
}
// Header 把headQueue和Exchange绑定的时候指定K-V对
@Bean
public Binding headerBinding() {
    Map<String, Object> map = new HashMap<String, Object>();
    map.put("header1", "value1");
    map.put("header2", "value2");
    return BindingBuilder.bind(headerQueue1()).to(headersExchage()).whereAll(map).match();
}
// Sender.java
public void sendHeader(Object message) {
    String msg = RedisService.beanToString(message);
    log.info("send fanout message:"+msg);
    MessageProperties properties = new MessageProperties();
    properties.setHeader("header1", "value1");
    properties.setHeader("header2", "value2");
    Message obj = new Message(msg.getBytes(), properties);
    amqpTemplate.convertAndSend(MQConfig.HEADERS_EXCHANGE, "", obj);
}
// Receiver.java
// 监听headerQueue队列
@RabbitListener(queues=MQConfig.HEADER_QUEUE)
public void receiveHeaderQueue(byte[] message) {
    log.info(" header  queue message:"+new String(message));
}
复制代码

思路:减少对数据库的访问

  1. 系统初始化,把商品库存数量加载到Redis
  2. 收到请求,Redis预减库存,库存不足,直接返回否则进入3
  3. 请求入队,立即返回排队中
  4. 请求出队,生成订单,减少库存,生成完订单之后,会把订单写入到缓存里面去供客户端查询
  5. 客户端轮询,是否秒杀成功
  6. 设置库存over标志位,当用户下单时,先查看标志位,如果标志位为true,则无需访问redis

压测数据: 线程数:5000 * 10 QPS:1306 -> 2114 (提升不明显是因为redis、mysql、秒杀线程全部在同一台机器上)

/**
 * 系统初始化,MiaoshaController实现InitializingBean接口,里面需要重写的方法是afterPropertiesSet
 * */
public void afterPropertiesSet() throws Exception {
    List<GoodsVo> goodsList = goodsService.listGoodsVo();
    if(goodsList == null) {
    	return;
    }
    // 在系统启动的时候就把商品的库存加载到缓存里面去
    for(GoodsVo goods : goodsList) {
    	redisService.set(GoodsKey.getMiaoshaGoodsStock, ""+goods.getId(), goods.getStockCount());
    	localOverMap.put(goods.getId(), false);
    }
}
复制代码
2.秒杀操作
@RequestMapping(value="/{path}/do_miaosha", method=RequestMethod.POST)
@ResponseBody
public Result<Integer> miaosha(Model model,MiaoshaUser user,
    	@RequestParam("goodsId")long goodsId,
    	@PathVariable("path") String path) {
    model.addAttribute("user", user);
    if(user == null) {
    	return Result.error(CodeMsg.SESSION_ERROR);
    }
    //验证path
    boolean check = miaoshaService.checkPath(user, goodsId, path);
    if(!check){
    	return Result.error(CodeMsg.REQUEST_ILLEGAL);
    }
    //库存over的标记,减少redis访问,当库存over的标记为1时,就没必要去访问redis数据库了
    boolean over = localOverMap.get(goodsId);
    if(over) {
    	return Result.error(CodeMsg.MIAO_SHA_OVER);
    }
    //预减redis的库存,返回减了1之后的那个值
    long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, ""+goodsId);//10
    if(stock < 0) {
        // 如果库存小于0,秒杀失败,并且把库存over标记设为true
    	localOverMap.put(goodsId, true);
    	return Result.error(CodeMsg.MIAO_SHA_OVER);
    }
    //判断是否已经秒杀到了
    MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
    if(order != null) {
        // 已经有该用户秒杀订单的记录
    	return Result.error(CodeMsg.REPEATE_MIAOSHA);
    }
    //入队
    MiaoshaMessage mm = new MiaoshaMessage();
    mm.setUser(user);
    mm.setGoodsId(goodsId);
    // MQSender sender自动注入, 发送给receiver与MySQL数据库进行交互
    sender.sendMiaoshaMessage(mm);
    return Result.success(0);//排队中,客户端开始轮询
}
复制代码
  1. MQSender
public void sendMiaoshaMessage(MiaoshaMessage mm) {
    String msg = RedisService.beanToString(mm);
    log.info("send message:"+msg);
    amqpTemplate.convertAndSend(MQConfig.MIAOSHA_QUEUE, msg);
}
复制代码
  1. MQReceiver
@RabbitListener(queues=MQConfig.MIAOSHA_QUEUE)
public void receive(String message) {
	log.info("receive message:"+message);
	MiaoshaMessage mm  = RedisService.stringToBean(message, MiaoshaMessage.class);
	MiaoshaUser user = mm.getUser();
	long goodsId = mm.getGoodsId();
	// 这里面是访问数据库mysql的,因为只有很少的数据可以进来
	GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
	int stock = goods.getStockCount();
	if(stock <= 0) {
		return;
	}
	//判断是否已经秒杀到了,orderService是调用了redisService的get方法
	MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
	// 如果秒杀过了,则什么也不做
	if(order != null) {
		return;
	}
	//减库存 下订单 写入秒杀订单
	miaoshaService.miaosha(user, goods);
}
复制代码
  1. 写入秒杀订单
@Transactional
public OrderInfo miaosha(MiaoshaUser user, GoodsVo goods) {
    //减库存 下订单 写入秒杀订单
    boolean success = goodsService.reduceStock(goods);
    if(success) {
    	// order_info maiosha_order
    	// createOrder里面做的操作是:1、插入订单 2、插入秒杀订单,返回一个订单信息
    	return orderService.createOrder(user, goods);
    }else {
    	setGoodsOver(goods.getId());
    	return null;
    }
}
复制代码
  1. 客户端接收秒杀状态 orderId:成功 -1:秒杀失败 0: 排队中
function getMiaoshaResult(goodsId){
	g_showLoading();
	$.ajax({
        url:"/miaosha/result",
        type:"GET",
        data:{
        	goodsId:$("#goodsId").val(),
        },
        success:function(data){
            // 如果返回消息成功,则取出结果result
            if(data.code == 0){
            var result = data.data;
            if(result < 0){
            	layer.msg("对不起,秒杀失败");
            }else if(result == 0){
                //继续轮询,200ms之后再轮询一次
            	setTimeout(function(){
            		getMiaoshaResult(goodsId);
            	}, 200);
            }else{
                layer.confirm("恭喜你,秒杀成功!查看订单?", {btn:["确定","取消"]},
                    function(){
                        // 这里要与数据库进行交互
                    	window.location.href="/order_detail.htm?orderId="+result;
                    },
                    function(){
                    	layer.closeAll();
                    });
        		}
        	}else{
                    layer.msg(data.msg);
        	}
        },
        error:function(){
            layer.msg("客户端请求有误");
        }
    });
}
复制代码
  1. MiaoshaController里的轮询
/**
 * orderId:成功
 * -1:秒杀失败
 * 0: 排队中
 * */
@RequestMapping(value="/result", method=RequestMethod.GET)
@ResponseBody
public Result<Long> miaoshaResult(Model model,MiaoshaUser user,
	@RequestParam("goodsId")long goodsId) {
    model.addAttribute("user", user);
    if(user == null) {
    	return Result.error(CodeMsg.SESSION_ERROR);
    }
    // orderId:成功 -1:秒杀失败 0: 排队中
    long result = miaoshaService.getMiaoshaResult(user.getId(), goodsId);
    return Result.success(result);
}
//MiaoshaService.java 里面取得秒杀结果的方法
public long getMiaoshaResult(Long userId, long goodsId) {
    MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(userId, goodsId);
    if(order != null) {//秒杀成功
    	return order.getOrderId();
    }else {
        // 商品是否卖完了
    	boolean isOver = getGoodsOver(goodsId);
    	// 此时order==null,没有生成订单
    	if(isOver) {
    	    // 如果商品卖完了,返回-1
    		return -1;
    	}else {
    	    // 如果商品没卖完,还在也没生成订单,则等待下一次轮询
    		return 0;
    	}
    }
}

private void setGoodsOver(Long goodsId) {   
    // 在Redis里面保存id为goodsId的商品是否卖完的值
	redisService.set(MiaoshaKey.isGoodsOver, ""+goodsId, true);
}

private boolean getGoodsOver(long goodsId) {
    // 判断这个key有没有就行了
	return redisService.exists(MiaoshaKey.isGoodsOver, ""+goodsId);
}
    
复制代码

安全优化

秒杀接口地址隐藏

数学公式验证码