最近接触了一个关于高并发秒杀的项目,在这里稍微整理一下关于这个项目的一些值得记录的一些点,以下是源码地址:github
高并发项目的瓶颈主要在于数据库访问次数上,访问次数越多,对数据库压力也就越大,因此项目中主要也是通过redis进行缓存以及RabbitMQ进行请求入队缓冲等来减少对数据库的访问。

高并发秒杀

  • 一、项目结构搭建
    • 1、集成Thymeleaf
    • 2、集成Redis
    • 3、JMeter的使用
    • 4、Result的封装
  • 二、功能实现
    • 1、全局异常控制器
    • 2、拦截器的使用
    • 3、分布式Session的使用
  • 三、页面缓存优化
    • 1、页面缓存
    • 2、对象缓存
    • 3、页面静态化
  • 四、接口优化
  • 五、安全优化
    • 1、秒杀接口地址隐藏
    • 2、数学公式验证码
    • 3、接口防刷
    • 4、用户存储
  • 六、个人感受

一、项目结构搭建

1、集成Thymeleaf

Thymeleaf是一个Java库,是一个XML/XHTML/HTML5模板引擎,能够应用于转换模板文件,以显示应用程序产生的数据和文本。
Thymeleaf旨在提供⼀个优雅的、⾼度可维护的创建模板的⽅式。 为了实现这⼀⽬标,Thymeleaf建⽴在⾃然模板的概念上,将其逻辑注⼊到模板⽂件中,不会影响模板设计原型。 这改善了设计的沟通,弥合了设计和开发团队之间的差距。
Thymeleaf是一个类似于JSP的模板引擎,但他的区别在于,在运行项目之前,Thymeleaf也是纯HTML,可以在没有服务端的情况下进行运行,但JSP需要在服务端的支持下进行一定的转换。
Thymeleaf主要有以下三个优点:

  1. Thymeleaf不管在有网络或者无网络的情况下都可以运行,因为它支持html原型,通过在html标签中增加额外的属性来达到模板+数据的展示方式,当有数据返回到页面时,Thymeleaf标签会动态地替换掉静态内容,使页面动态显示。
  2. Thymeleaf 开箱即用的特性。它提供标准和spring标准两种方言,可以直接套用模板实现JSTL、 OGNL表达式效果,避免每天套模板、该jstl、改标签的困扰。同时开发人员也可以扩展和创建自定义的方言。
  3. Thymeleaf 提供spring标准方言和一个与 SpringMVC 完美集成的可选模块,可以快速的实现表单绑定、属性编辑器、国际化等功能.
//集成thymeleaf
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId><version>1.5.2.RELEASE</version>
</dependency>

2、集成Redis

redis的集成,其中使用jedisPool获取jedis,并使用jedis的方法封装在RedisService当中,方便后来使用redis进行缓存

/*** 设置对象* @param prefix 不同的前缀对应不同的缓存信息* @param key 该信息具体的的key值* @param value* @param <T>* @return*/public <T> boolean set(KeyPrefix prefix,String key,T value){Jedis jedis=null;try{jedis=jedisPool.getResource();String str=beanToString(value);if(str==null||str.length()<=0) return false;//获取有前缀的keyString realKey=prefix.getPrefix()+key;int seconds=prefix.expireSeconds();if(seconds<=0){jedis.set(realKey,str);}else{jedis.setex(realKey,seconds,str);}return true;}finally{returnToPool(jedis);}}

而前缀是为了分辨不同的缓存,例如商品缓存、订单缓存、用户缓存等等,其中的expireSeconds则是缓存的有效时间,0代表永久有效。

public class GoodsKey extends BasePrefix{private GoodsKey(int expireSeconds,String prefix) {super(expireSeconds,prefix);}public static GoodsKey getGoodsList=new GoodsKey(60,"gl");public static GoodsKey getGoodsDetail=new GoodsKey(60,"gd");public static GoodsKey getMiaoshaGoodsStock=new GoodsKey(0,"gs");}

3、JMeter的使用

Apache JMeter是Apache组织开发的基于Java的压力测试工具。用于对软件做压力测试,JMeter是一个下载即用的软件,我们使用它来进行高并发的模拟,而它会在进行模拟后给予我们一些可视化的结果,是一个很强大的工具。似乎在5.的版本中,已经不需要去配置JMETER_HOME就可以运行了。在使用过程中,最大的感想就是这个软件巨坑,不知道是不是硬件的问题,在进行500010的并发压测中,出现大量的error,而每次出现error的情况都不尽相同,让人很是头疼。

4、Result的封装

这是对每次访问结果的一个封装,而CodeMsg类则是对返回码和信息的封装,而Result则是对CodeMsg和对data的封装,在访问过后,我们可以选择生成Result.success对象返回任何类型的信息,异或是返回Result.error来返回错误码和错误信息。

public class Result<T>{private int code;private String msg;private T data;/*** 成功时候的调用*/public static <T> Result<T> success(T data){return new Result<T>(data);}/*** 失败时候的调用*/public static <T> Result<T> error(CodeMsg codeMsg){return new Result<T>(codeMsg);}private Result(T data){this.data=data;}private Result(int code,String msg){this.code=code;this.msg=msg;}private Result(CodeMsg codeMsg){if(codeMsg!=null){this.code=codeMsg.getCode();this.msg=codeMsg.getMsg();}}public int getCode() {return code;}public void setCode(int code) {this.code = code;}public String getMsg() {return msg;}public void setMsg(String msg) {this.msg = msg;}public T getData() {return data;}public void setData(T data) {this.data = data;}
}

二、功能实现

功能实现这一块算是比较常规的实现,毕竟这个项目的重心在于秒杀的优化而不在于具体的实现,只是实现了常规的登录,商品列表,商品秒杀等功能。

1、全局异常控制器

为了使得代码更清爽而不十分复杂,我们实现了一个全局异常控制器。在这个类中,@ControllerAdvice是一个增强的Controller注解,使用这个Controller,可以实现以下功能:全局异常处理、全局数据绑定、全局数据预处理(SpringMVC提供,在SpringBoot中可以直接使用)@ExceptionHandler:如果只使用此注解,只能在当前Controller中处理异常,当配合ControllerAdvice一起使用的时候,就可以摆脱这种限制了。

import com.yuan.SecondsKill.result.CodeMsg;
import com.yuan.SecondsKill.result.Result;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import java.util.List;/*** 全局异常控制器* 拦截异常,在页面上显示错误信息*/
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {@ExceptionHandler(value = Exception.class)public Result<String> exceptionHandler(HttpServletRequest request,Exception e){e.printStackTrace();if(e instanceof GlobalException){GlobalException ge=(GlobalException)e;return Result.error(ge.getCm());}if(e instanceof BindException){BindException ex=(BindException)e;List<ObjectError> errors = ex.getAllErrors();ObjectError error = errors.get(0);String msg=error.getDefaultMessage();return Result.error(CodeMsg.BIND_ERROR.fillArgs(msg));}else{return Result.error(CodeMsg.SERVER_ERROR);}}
}

在登陆方法中,使用@Valid注解配合全局处理器达到对手机号和密码进行格式校验的目的

 @RequestMapping("/do_login")@ResponseBodypublic Result<String> doLogin(HttpServletResponse response, @Valid LoginVo loginVo){log.info(loginVo.toString());return null;}

实体类中的注解来校验是否为空等等,其中的isMobile是自己定义的。

public class LoginVo {@NotNull@isMobileprivate String mobile;@NotNull@Length(min=6)private String password;
}
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {isMobileValidator.class})
public @interface isMobile {boolean required() default true;String message() default "手机号格式错误";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};
}

2、拦截器的使用

使用拦截器与实现了HandlerMethodArgumentResolver接口的类从session中获取user对象,并作为参数注入Controller中。

3、分布式Session的使用

此处分布式session的实现主要是在登录的过程中,为用户随机生成一个token,并保存到数据库中,随后封装成cookie返回前端,并且,获取用户也由这个cookie中的token来实现。关于分布式Session一般有4种解决方案,详细可参考这篇文章:分布式Session的4种解决方案。项目中所使用的就是其中的利用cookie记录Session。

三、页面缓存优化

页面缓存你优化主要使用redis完成了页面缓存、URL缓存和对象缓存,并且使得频繁访问的商品详情页面和订单详情页面静态化,成为纯html页面,使用ajax异步的方式与后端交互,实现前后端分离。另外还有静态资源优化和CDN优化没有实现。

1、页面缓存

这里是一个商品列表的展示,可以看到前后代码的区别,刚开始只是直接去数据库中获取商品列表并返回到前台的页面,前台页面获取其中的信息进行填充。而下面的代码则是对页面做了一个缓存,流程大概是这样的:从redis中取缓存->从数据库中取数据->手动渲染模板->返回html。
首先从redis中取相应的缓存,如果有直接返回,没有则到数据库中取商品列表的数据,并且进行手动渲染,渲染后将html页面存进redis中,并且返回。在redis的保存方法中设置了过期时间,默认是60秒。

@RequestMapping(value="/to_list")public String list(Model model,MiaoshaUser user) {model.addAttribute("user", user);List<GoodsVo> goodsList = goodsService.listGoodsVo();model.addAttribute("goodsList", goodsList);return "goods_list";}
/*** 页面缓存:取缓存、手动渲染模板和结果输出* @param model* @param user* @return*/@RequestMapping(value = "/to_list",produces = "text/html")@ResponseBodypublic String toList(HttpServletRequest request, HttpServletResponse response,Model model, KillsUser user){//无cookie返回登录界面model.addAttribute("user",user);//取缓存,如果redis中有相应的缓存,直接取出使用String html = redisService.get(GoodsKey.getGoodsList, "", String.class);if(!StringUtils.isEmpty(html)){return html;}//查询商品列表List<GoodsVo> goodsList = goodsService.listGoodsVo();model.addAttribute("goodsList",goodsList);//return "goods_list";SpringWebContext ctx=new SpringWebContext(request,response,request.getServletContext(),request.getLocale(),model.asMap(),applicationContext);//手动渲染模板并且存入redis,框架渲染的具体方式也是如此html=thymeleafViewResolver.getTemplateEngine().process("goods_list",ctx);if(!StringUtils.isEmpty(html)){redisService.set(GoodsKey.getGoodsList,"",html);}//结果返回return html;}

2、对象缓存

根据token对秒杀对象进行缓存,如果缓存中还有秒杀对象的信息,则不去数据库进行取出。

3、页面静态化

页面静态化常用的技术为AngularJS、Vue.js等等,此处使用js简单模拟该过程,实现前后端分离。页面静态化,其实就是把用户经常访问的页面做成一个静态的页面,避免每次都要从数据库取出数据并且渲染到对应的模板文件中,从而减少服务器的压力。
页面静态化是将动态渲染生成的画面保存为html文件,放到静态文件服务器中。用户访问的是处理好的静态文件,而需要展示不同数据内容时,则可以在请求完静态化的页面后,在页面中向后端发送请求,获取属于用户的特殊数据。
页面静态化是一个以空间换时间的操作,虽然页面增多时会占用更多的空间,但提高了网站的访问速度,提升了用户的体验,还是非常值得的。
关于原来的页面,正常的前端访问后端接口,获取html页面,展示,而页面静态化,是利用ajax异步传递数据,在前台进行数据的填充,实现了前后端分离。

@RequestMapping(value = "/to_detail/{goodsId}",produces = "text/html")@ResponseBodypublic String detail(HttpServletRequest request, HttpServletResponse response,Model model,KillsUser user,@PathVariable("goodsId")long goodsId){model.addAttribute("user",user);//取缓存,如果redis中有相应的缓存,直接取出使用String html = redisService.get(GoodsKey.getGoodsDetail, ""+goodsId, String.class);if(!StringUtils.isEmpty(html)){return html;}GoodsVo goods=goodsService.getGoodsVoByGoodsId(goodsId);model.addAttribute("goods",goods);long startAt=goods.getStartDate().getTime();long endAt=goods.getEndDate().getTime();long now=System.currentTimeMillis();int KillStatus=0;//表示还未秒杀int remainSeconds=0;if(now < startAt){//秒杀未开始KillStatus=0;remainSeconds=(int)((startAt-now)/1000);}else if(now > endAt){//秒杀已结束KillStatus=2;remainSeconds=-1;}else{//秒杀进行中KillStatus=1;remainSeconds=0;}model.addAttribute("KillStatus",KillStatus);model.addAttribute("remainSeconds",remainSeconds);SpringWebContext ctx=new SpringWebContext(request,response,request.getServletContext(),request.getLocale(),model.asMap(),applicationContext);//手动渲染模板并且存入redis,框架渲染的具体方式也是如此html=thymeleafViewResolver.getTemplateEngine().process("goods_detail",ctx);if(!StringUtils.isEmpty(html)){redisService.set(GoodsKey.getGoodsDetail,""+goodsId,html);}//结果返回return html;//return "goods_detail";}

前端代码通过ajax异步请求后端的接口返回数据,并在前台取出数据对页面进行填充。

//前端
$(function () {//countDown();getDetail();
});
function getDetail(){var goodsId=g_getQueryString("goodsId");$.ajax({url:"/goods/detail2/"+goodsId,type:"GET",success:function(data){if(data.code==0){render(data.data);}else{layer.msg(data.msg);}},error:function(){layer.msg("客户端请求有误")}});
}
/*text() :设置或返回匹配元素的内容
* val() :设置或返回匹配元素的值
* */
function render(detail){var goods=detail.goods;var KillStatus=detail.KillStatus;var remainSeconds=detail.remainSeconds;var user=detail.user;if(user){$("#userTip").hide();}$("#goodsName").text(goods.goodsName);$("#goodsImg").attr("src",goods.goodsImg);$("#startDate").text(new Date(goods.startDate).format("yyyy-MM-dd hh:mm:ss"))$("#remainSeconds").val(remainSeconds);$("#goodsId").val(goods.id);$("#goodsPrice").text(goods.goodsPrice);$("#miaoshaPrice").text(goods.miaoshaPrice);$("#stockCount").text(goods.stockCount);countDown();
}
//后台
@RequestMapping(value = "/detail2/{goodsId}")@ResponseBodypublic Result<GoodsdetailVo> detail2(HttpServletRequest request, HttpServletResponse response,Model model, KillsUser user,@PathVariable("goodsId")long goodsId){GoodsVo goods=goodsService.getGoodsVoByGoodsId(goodsId);long startAt=goods.getStartDate().getTime();long endAt=goods.getEndDate().getTime();long now=System.currentTimeMillis();int KillStatus=0;//表示还未秒杀int remainSeconds=0;if(now < startAt){//秒杀未开始KillStatus=0;remainSeconds=(int)((startAt-now)/1000);}else if(now > endAt){//秒杀已结束KillStatus=2;remainSeconds=-1;}else{//秒杀进行中KillStatus=1;remainSeconds=0;}GoodsdetailVo vo=new GoodsdetailVo();vo.setGoods(goods);vo.setKillStatus(KillStatus);vo.setRemainSeconds(remainSeconds);vo.setUser(user);return Result.success(vo);}

四、接口优化

接下来是对于秒杀接口的优化,这是最初始的秒杀接口:
刚开始的秒杀接口的业务流程是这样的:判断用户状态->获取商品库存并判断->获取秒杀订单并判断->减库存。这个过程需要调用数据库共3次。
秒杀需要面对一个很严峻的问题:如何防止超卖或者少卖?此项目中总共用了两种方式,第一种是在减库存的sql语句中加上对库存的判断,来防止库存变成负数;第二种是对数据库中的秒杀订单表中的用户id列和商品id列加上唯一索引,防止用户重复购买。

@RequestMapping("/do_miaosha")public String miaosha(Model model, KillsUser user,@RequestParam("goodsId")long goodsId){model.addAttribute("user",user);if(user==null){return "login";}GoodsVo goodsVo = goodsService.getGoodsVoByGoodsId(goodsId);int stockCount = goodsVo.getStockCount();//库存不足if(stockCount<=0){model.addAttribute("errormsg", CodeMsg.MIAO_SHA_OVER.getMsg());return "miaosha_fail";}//判断是否已经秒杀到了MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);if(order!=null){model.addAttribute("errormsg",CodeMsg.REPEATE_MIAOSHA.getMsg());return "miaosha_fail";}//减库存,下单,写入秒杀订单OrderInfo orderInfo = miaoshaService.miaosha(user, goodsVo);model.addAttribute("orderInfo",orderInfo);model.addAttribute("goods",goodsVo);return "order_detail";}

优化:秒杀静态化,通过ajax异步请求调用秒杀接口并进行轮询查看是否成功,redis预减库存减少对数据库的访问,内存标记减少redis访问,RabbitMQ队列缓冲,异步下单,增强用户体验,同时减少服务器压力。首先是使用redis预减库存,我们使用一个HashMap来存储某商品是否售空,这里用到一个方法afterPropertiesSet(),该方法会在Bean的所有属性初始化完成后调用,在这个方法中,我们从数据库中查出商品列表,将每个商品的库存存入redis中,并将库存不为0的商品在HashMap中设置为false,意为未售空。(此处应加上对商品库存是否大于0的判断)
在秒杀方法中,我们首先获取mao中的布尔值来判断是否售空决定是否进行下一步操作。使用redis的decr()来预减库存,减完判断是否小于0,如果小于0就设置map中的值为true,并且返回,否则进行下一步操作。
关于RabbitMQ的操作,我个人感觉还是有点迷的,毕竟真正的秒杀不会让你一直等待吧?(没事,咱也没秒杀到过东西,问题应该不是很大,也可能等待的时间很短人们感受不出来?这里请懂的朋友评论区指导指导呗)
此项目中使用RabbitMQ将秒杀请求入队,前端进行轮询确认是否下单成功来实现异步下单的操作。关于RabbitMQ,RabbitMQ是一个开源的消息代理和队列服务器,用来通过普通协议在完全不同的应用之间共享数据,或者简单的将作业排队以便让分布式服务器进行处理。
RabbitMQ有四种交换机模式,分别是Direct模式、Topic模式、Fanout模式和Headers模式。Direct模式需要将一个队列绑定到交换机上,要求该信息与一个特定的路由键完全匹配;Topic模式是将路由键和某模式进行匹配,队列需要绑定到一个模式上;Fanout模式则是不处理路由键,只需要将队列绑定到交换机上,一个发送到该交换机的信息都会被转发到与该交换机绑定的所有队列上,类似于子网传播;Headers则是以key-value形式来匹配。
队列和交换机绑定使用的是BindingBuilder.bind(队列).to(交换机)

注意此时秒杀操作并不在秒杀功能中处理,而是在RabbitMQ中的receive()方法中处理(见第三段代码)

 //用于保存商品的id和是否售空private HashMap<Long, Boolean> localOverMap =  new HashMap<Long, Boolean>();/*** 系统初始化* */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);}}@RequestMapping(value="/do_miaosha", method=RequestMethod.POST)@ResponseBodypublic Result<Integer> miaosha(Model model,MiaoshaUser user,@RequestParam("goodsId")long goodsId) {model.addAttribute("user", user);if(user == null) {return Result.error(CodeMsg.SESSION_ERROR);}//内存标记,减少redis访问boolean over = localOverMap.get(goodsId);if(over) {return Result.error(CodeMsg.MIAO_SHA_OVER);}//预减库存long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, ""+goodsId);//10if(stock < 0) {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);sender.sendMiaoshaMessage(mm);return Result.success(0);//排队中}/**MQReceiver类中的方法* 此时到达这里的请求大大减少* 因为有redis库存的拦截* @param message*/@RabbitListener(queues = MQConfig.MIAOSHA_QUEUE)public void receive(String message){log.info("receive message:"+message);MiaoshaMessage mm = redisService.stringToBean(message, MiaoshaMessage.class);KillsUser user = mm.getUser();long goodsId = mm.getGoodsId();//判断库存GoodsVo goodsVo = goodsService.getGoodsVoByGoodsId(goodsId);int stockCount = goodsVo.getStockCount();//库存不足if(stockCount<=0){return;}//判断是否已经秒杀到了,这里可以不去数据库查找是否有订单MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);if(order!=null){return;}//减库存,下单,写入秒杀订单OrderInfo orderInfo = miaoshaService.miaosha(user, goodsVo);}

五、安全优化

1、秒杀接口地址隐藏

在秒杀真正开始之前,我们的秒杀地址不应该是可以访问的,这样会造成极大的不公平,如果你获得秒杀地址就可以在秒杀开始之前买到商品。因此我们可以在地址中间加上一个随机的path,可以利用随机生成随机码,并把用户id和商品id作为key,把path存入redis中,在真正秒杀之前会再度把传入的path和redis中的path作比较。

2、数学公式验证码

为了减少真正进入秒杀接口的用户,在商品详情页面进行一个验证码的输入,对算式进行计算并把值存进redis,在秒杀方面之前对用户 输入的验证码和redis中的值进行比较,从而判断是否进入秒杀。

3、接口防刷

接口防刷是防止用户短时间内多次访问同一页面,可以避免某些脚本的攻击。原理是使用redis缓存,假如在k秒内访问超过5次就拦截,key值是固定前缀加上用户id,有效时间是k秒,而value则是从1开始,在不超过5次的前提下,调用redis的incr方法。如果超过限制,就拒绝访问,在拦截器的preHandle()方法中返回false,拒绝访问。
但是可能会有很多的接口需要限制,所以会出现冗余代码很多的情况,并且这样也会使得接口方法变得十分复杂,代码可读性也会降低。因此我们可以声明一个相关的注解,如下:

@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler)throws Exception{if(handler instanceof HandlerMethod){KillsUser user=getUser(request,response);//将user对象存入ThreadLocal中UserContext.setUser(user);HandlerMethod hm=(HandlerMethod)handler;AccessLimit accessLimit=hm.getMethodAnnotation(AccessLimit.class);if(accessLimit==null){//说明没有这个注解 跳过return true;}int seconds=accessLimit.seconds();int maxCount=accessLimit.maxCount();boolean needLogin=accessLimit.needLogin();String key=request.getRequestURI();//这里判断获取的user是否为null cookie决定if(needLogin){if(user==null){render(response, CodeMsg.SESSION_ERROR);return false;}}else{//do nothing}key+="_"+user.getId();AccessKey ak=AccessKey.withExpire(seconds);Integer count=redisService.get(ak,key,Integer.class);if(count==null){redisService.set(ak,key,1);}else if(count<maxCount){redisService.incr(ak,key);}else{//短时间内频繁访问render(response,CodeMsg.FREQUENCY_ACCESS);return false;}}return true;}
4、用户存储

在多个方法中都需要使用秒杀用户作为参数,判断用户是否为空而决定需不需要跳转到登录页面。因此每次都要从数据库中查找相应的对象来返回。我们可以使用一个ThreadLocal来对User对象进行保存,而ThreadLocal的有效期就是在当前线程内,所以在用户访问的过程中都可以从本地拿到一个秒杀用户对象而不用访问数据库。

public class UserContext {//将user存到当前线程中private static ThreadLocal<KillsUser> userHolder=new ThreadLocal<>();public static void setUser(KillsUser user){userHolder.set(user);}public static KillsUser getUser(){return userHolder.get();}
}

六、个人感受

对于高并发秒杀的解决方案,其实是有很多种的,通过这个项目让我这个小菜鸡初窥门径,了解到一些相关的解决方式,但其实里面有一些做法还是有待商榷的。
通过这个项目,还是认识到了redis的强大,有很大一部分优化都是通过redis缓存来实现的,关于redis还是应该多多学习。我自己对于此项目中的很多东西都是比较陌生的,比如Thymeleaf、JMeter和RabbitMQ都是刚刚接触的,如果问我有关的问题,我还是比较懵的,因此对于这些东西还是应该多了解。
最后,找了两个多月的实习,我依然没有上岸,可能是我太菜了吧,希望大家跟我一起加油趴!

高并发秒杀系统方案的优化相关推荐

  1. 【在线网课】Java高性能高并发秒杀系统方案优化实战

    java教程视频讲座简介: Java高性能高并发秒杀系统方案优化实战 Java秒杀系统方案优化 高性能高并发实战 以"秒杀"这一Java高性能高并发的试金石场景为例,带你通过一系列 ...

  2. SpringBoot、Redis轻松实现Java高并发秒杀系统笔记

    秒杀项目 优极限[完整项目实战]半天带你用SpringBoot.Redis轻松实现Java高并发秒杀系统 文章目录 秒杀项目 技术栈 课程介绍 学习目标 如何设计一个秒杀系统 项目搭建 分布式会话 登 ...

  3. SpringBoot实现Java高并发秒杀系统之DAO层开发(一)

    SpringBoot实现Java高并发秒杀系统之DAO层开发(一) 秒杀系统在如今电商项目中是很常见的,最近在学习电商项目时讲到了秒杀系统的实现,于是打算使用SpringBoot框架学习一下秒杀系统( ...

  4. java 秒杀 源码 下载_java高并发秒杀系统3-4节秒杀功能实现.mp4

    本Java商城秒杀系统视频教程目录如下:    java高并发秒杀系统1-1节java高并发商城秒杀优化学习指引.mp4 java高并发秒杀系统1-2节项目环境搭建(Eclipse)-节.mp4 ja ...

  5. java商品详情页设计_java高并发秒杀系统3-2节商品详情页上.mp4

    本Java商城秒杀系统视频教程目录如下:    java高并发秒杀系统1-1节java高并发商城秒杀优化学习指引.mp4 java高并发秒杀系统1-2节项目环境搭建(Eclipse)-节.mp4 ja ...

  6. SpringBoot实现Java高并发秒杀系统之Service层开发(二)

    继上一篇文章:SpringBoot实现Java高并发秒杀系统之DAO层开发 我们创建了SpringBoot项目并熟悉了秒杀系统的表设计,下面我们将讲解一下秒杀系统的核心部分:Service业务层的开发 ...

  7. (更新时间)2021年5月28日 商城高并发秒杀系统(.NET Core版) 01-系统设计介绍

    秒杀项目 目标:从0到1构建一个高并发的秒杀系统 三个阶段 从0到1构建一个电商系统 从0到1构建秒杀系统 从0到1构建高并发秒杀系统 为了完成这个目标,我们需要知道几个前提 什么是电商 什么是秒杀 ...

  8. 微服务高并发秒杀系统

    微服务高并发秒杀系统 在做完乐优商城项目之后发现缺少秒杀未编写,打算上手实现一下这个基本电商都需要的功能,参考https://blog.csdn.net/lyj2018gyq/article/deta ...

  9. Java高并发秒杀系统【观后总结】

    项目简介 在慕课网上发现了一个JavaWeb项目,内容讲的是高并发秒杀,觉得挺有意思的,就进去学习了一番. 记录在该项目 我结合其资料和观看视频的时候整理出从该项目学到了什么... 项目Dao层 日志 ...

最新文章

  1. 用 PHP 和 MySQL 保存和输出图片
  2. 程序员都讨厌写文档?这4个工具让你事半功倍
  3. 38.什么是PV操作
  4. oracle 取系统当前年份_Oracle如何获取系统当前时间等操作实例
  5. 论文浅尝 | CoRR - 面向复杂知识问答的框架语义解析方法
  6. createjs入门
  7. 对linux中多线程编程中pthread_join的理解
  8. 营销、销售和运营的区别?
  9. Mongo查询数据库及表占用磁盘大小
  10. Android Activity launchMode研究
  11. OC中方法与函数的区别
  12. Http第三方接口实现/异步Http请求
  13. 技术人员必备的学习工具
  14. 基于asp.net806莆田新华网上书店_图书销售系统
  15. python图片裁剪
  16. Python编程:loguru管理日志输出
  17. 【C++游戏设计】用颜色原理打印平面地图
  18. [审核]App Store审核指南
  19. 阿里云·数加,阿里下一代数据集成实践
  20. 关于STM32+OLED显示屏的简单应用

热门文章

  1. 云端办公后,协同软件也能轻松做好项目管理
  2. 迅为3399开发板Android系统-使用strace跟踪系统调用
  3. 时间序列(一)基本概念
  4. 工业机器人关节拆装流程_工业机器人关节空间的插值轨迹规划_凌家良
  5. UGUI中Text实现一个简单的打字机的效果
  6. 解决numba报错警告:No implementation of function Function found for signature
  7. cmstop模板制作教程内容页变量
  8. jQuery中on和trigger的使用
  9. 职称计算机考试 包过,计算机软考考试包通过
  10. 有什么占内存小又好玩的手游,占内存小的手机游戏