渲染good_list.html页面的时候,直接从缓存里面取,如果缓存中没有,则我们手动进行渲染,从而减少对数据库的mysql访问。
String html = redisService.get(GoodsKey.getGoodsList, "", String.class) 复制代码
@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); 复制代码
if(!StringUtils.isEmpty(html)) { // 如果非空,则保存到redis中 redisService.set(GoodsKey.getGoodsList, "", html); } return html; 复制代码
注意:页面缓存有效期需要比较短,可以设为60秒
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; } 复制代码
线程数5000*10
QPS:1267/sec -> 2884/sec
CPU-load:15 -> 5
高性能网站设计之缓存更新的思路: blog.csdn.net/tTU1EvLDeLF…
@Update("update miaosha_goods set stock_count = stock_count - 1 where goods_id = #{goodsId} and stock_count > 0") public int reduceStock(MiaoshaGoods g); 复制代码
在MiaoshaOrder表中建立唯一索引(userId, orderId),并且存入到redis缓存中。
MySQL索引:如果要强烈使一列或多列具有唯一性,通常使用PRIMARY KEY约束。但是,每个表只能有一个主键。 因此,如果使多个列或多个组合列具有唯一性,则不能使用主键约束。
幸运的是,MySQL提供了另一种索引,叫做唯一索引,允许我们可以使一个或者多个列的值具有唯一性。另外,不会像主键索引一样,我们的每张表中可以有很多个唯一索引。
CDN加速意思就是在用户和我们的服务器之间加一个缓存机制,通过这个缓存机制动态获取IP地址根据地理位置,让用户到最近的服务器访问。 那么CDN是个啥? 全称Content Delivery Network即内容分发网络。
CDN是一组分布在多个不同的地理位置的WEB服务器,用于更加有效的向用户发布内容,在优化性能时,会根据距离的远近来选择 。
CDN系统能实时的根据网络流量和各节点的连接,负载状况及用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上,其目的是使用户能就近的获取请求数据,解决网络拥堵,提高访问速度,解决由于网络带宽小,用户访问量大,网点分布不均等原因导致的访问速度慢的问题。
由于CDN部署在网络运营商的机房,这些运营商又是终端用户网络的提供商,因此用户请求的第一跳就到达CDN服务器,当CDN服务器中缓存有用户请求的数据时,就可以从CDN直接返回给浏览器,因此就可以提高访问速度。
CDN能够缓存JavaScript脚本,css样式表,图片,图标,Flash等静态资源文件(不包括html页面),这些静态资源文件的访问频率很高,将其缓存在CDN可以极大地提高网站的访问速度,但由于CDN是部署在网络运营商的机房,所以在一般的网站很少用CDN加速。
思路:减少对数据库的访问
//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"); 复制代码
//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); } 复制代码
//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); } 复制代码
//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)); } 复制代码
思路:减少对数据库的访问
压测数据: 线程数: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);//排队中,客户端开始轮询 } 复制代码
public void sendMiaoshaMessage(MiaoshaMessage mm) { String msg = RedisService.beanToString(mm); log.info("send message:"+msg); amqpTemplate.convertAndSend(MQConfig.MIAOSHA_QUEUE, msg); } 复制代码
@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); } 复制代码
@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; } } 复制代码
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("客户端请求有误"); } }); } 复制代码
/** * 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); } 复制代码