java秒杀项目总结

本项目专攻秒杀模块,共分为七个章节

第一章 项目框架搭建

1.Spring Boot环境搭建
2.集成Thymeleaf , Result结果封装

  • 前期前后端并未分离,使用Thymeleaf来获取后台传来的数据
  • Result结果封装可以让代码更规范,成功的时候只传数据,失败的时候传递状态码

3.集成Mybatis+ Druid
4.集成Jedis+ Redis安装+通用缓存Key封装

  • 这里使用的是自己封装的jedis
  • 通用缓存key封装,定义一个接口,过期时间和缓存前缀。抽象类继承接口实现通用缓存名字和过期时间,再有各种key继承抽象类,实现通用缓存

第二章实现登录功能

1.数据库设计

2.明文密码两次MD5处理
两次加密:

  • 1、当你输入提交到表单使用md5对输入的密码加密

  • 2、当你将表单中的密码插入到数据库时,再对表单的密码加密

  • 为什么两次md5?
    客户端:我们使用密码+固定Salt来形成最终密码
    服务端:将用户输入

3 JSR303参数检验+全局异常处理器
为什么要做JSR303参数检验?
前端的校验只是有效性的校验(手机号输错,密码错误),服务端的校验是防止恶意的用户。
JSR303检验账号是否符合规范标准,@IsMobile自己写的注解

/*
获取表单提交的数据
* 看表单中参数是否能正确传递
* */
public class LoginVo {@NotNull@IsMobileprivate String mobile;@NotNull@Length(min = 32)private String password;。。。}

@Valid注解写在输入参数前面,输入参数对应的类,里面的各项成员变量上面还能加注解约束

@RequestMapping("/do_login")@ResponseBodypublic Result<String> doLogin(HttpServletResponse response,@Valid LoginVo loginVo){log.info(loginVo.toString());//登录String token=miaoshaUserService.login(response,loginVo);return Result.success(token);}

当参数校验返回false即校验失败时,那么就会出现一个BindException异常,为了显示友好就写一个全局异常处理器去拦截这个异常。当然其他的异常也能够被拦截。

怎么实现友好显示的?
当用户登录时,如果后台登录方法查不到用户或者密码不匹配那么就会抛一个全局异常,抛出的这个异常会被我们定义的全局异常处理器拦截,拦截到之后会return一个错误信息,前台ajax就会回调显示这个错误信息,用户能更友好的看到错误信息。

4.分布式Session
背景:分布式集群,多台服务器。客户端第一次请求落在第一台服务器上,第二次请求落在第二台服务器上。那么第二次Session就会丢失。

解决方案: 1.容器原生的Session同步,就是将一台计算机上的Session同步到其他计算机上,这样性能开销大。

2.分布式Session,实际情况中用的比较多。Session并没有存到容器中来而是存到了缓存中,这就是分布式Session。

分布式Session具体实现: 用户登录成功,会生成一个token。token用于生成键,用户信息作为值,将这对键值对存到redis中,然后实例一个Cookie(“token”,token),将这个Cookie写进去写到response中,那么下次这个用户再次发请求就会带着这个Cookie。配置参数解析器就能根据Cookie携带的值到redis中查到用户信息,然后注入到方法的请求参数中。

public String login(HttpServletResponse response, LoginVo loginVo) {。。。。//生成cookieString token= UUIDUtil.uuid();//cookie写到response,session写到redisaddCookie(response,token,user);return token;}
private void addCookie(HttpServletResponse response, String token, MiaoshaUser user) {redisService.set(MiaoshaUserKey.token,token,user);Cookie cookie=new Cookie(COOKI_NAME_TOKEN,token);cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds());cookie.setPath("/");response.addCookie(cookie);}

第三章实现秒杀功能

1.数据库设计
数据库并没有遵循三范式,有冗余,但是冗余是必须的。
2.商品列表页
3.商品详情页
4.订单详情页

秒杀功能:
三步:
判断库存、判断是否已经秒杀到了、减库存下订单(事务)。
卖超:
(1)减库存SQL,加上库存是否小于零的条件。
(2)订单表结构增加唯一索引(用户id和秒杀商品id),防止一个用户下多次单。
(3)减库存这个操作的返回值为1的时候才继续后面的下订单,否则会出现生成的订单数量远远多于卖出商品的数量。

另外还实现了倒计时功能,判断当前是否可以秒杀(就是比较时间的大小):

@RequestMapping("/to_detail/{goodsId}")
public String detail(Model model,MiaoshaUser user,@PathVariable("goodsId")long goodsId) {model.addAttribute("user", user);GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);model.addAttribute("goods", goods);long startAt = goods.getStartDate().getTime();long endAt = goods.getEndDate().getTime();long now = System.currentTimeMillis();int miaoshaStatus = 0;int remainSeconds = 0;if(now < startAt ) {//秒杀还没开始,倒计时miaoshaStatus = 0;remainSeconds = (int)((startAt - now )/1000);}else  if(now > endAt){//秒杀已经结束miaoshaStatus = 2;remainSeconds = -1;}else {//秒杀进行中miaoshaStatus = 1;remainSeconds = 0;}model.addAttribute("miaoshaStatus", miaoshaStatus);model.addAttribute("remainSeconds", remainSeconds);return "goods_detail";
}
function countDown(){var remainSeconds = $("#remainSeconds").val();var timeout;if(remainSeconds > 0){//秒杀还没开始,倒计时$("#buyButton").attr("disabled", true);timeout = setTimeout(function(){$("#countDown").text(remainSeconds - 1);$("#remainSeconds").val(remainSeconds - 1);countDown();},1000);}else if(remainSeconds == 0){//秒杀进行中$("#buyButton").attr("disabled", false);if(timeout){clearTimeout(timeout);}$("#miaoshaTip").html("秒杀进行中");}else{//秒杀已经结束$("#buyButton").attr("disabled", true);$("#miaoshaTip").html("秒杀已经结束");}
}

第四章JMeter压测

1, JMeter入门
2,自定义变量模拟多用户
生成500个用户的token和密码保存到一个文件当中,压测时加载文件模拟多用户

第五章页面优化技术

大并发的瓶颈就是数据库。应对并发最有效的就是缓存

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

页面缓存适用场景:适合于变化不大的场景,比如商品列表。实际项目中商品列表可能会分页,不可能每页都缓存,只是缓存前两页。

页面缓存:第一次请求过来就将渲染好的页面存到redis中,下次请求就直接从redis中取页面。

页面缓存并不是将所有页面都缓存,而是将变化不大的,页面缓存和URL缓存都设置过期时间(60s),而对象缓存根据token获取用户,且对象缓存永久有效,

 @RequestMapping(value = "/to_list",produces = "text/html")@ResponseBodypublic String list(HttpServletRequest request, HttpServletResponse response,Model model, MiaoshaUser user) {model.addAttribute("user", user);//1、先取缓存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";//缓存为空IWebContext ctx=new WebContext(request,response,request.getServletContext(),request.getLocale(),model.asMap());//手动渲染html=thymeleafViewResolver.getTemplateEngine().process("goods_list", ctx);if(!StringUtils.isEmpty(html)){redisService.set(GoodsKey.getGoodsList,"",html); //存入缓存}return html;}

对象缓存:实现分布式Session就是,将用户对象缓存到redis中。

2.页面静态化,前后端分离

页面静态化:就是浏览器将HTML页面存在客户端,通过ajax获取数据拿到客户端在渲染页面。(这样就不用下载页面了,只需要下载动态数据就好了。

//商品列表页跳转到商品详情页面,goods_detail.htm放到静态文件夹里面
<td><a th:href="'/goods_detail.htm?goodsId='+${goods.id}">详情</a></td>//静态页面goods_detail.htm,里面的js
$(function(){//countDown();getDetail();
});function getDetail(){//这个方法获取请求传过来的参数var goodsId = g_getQueryString("goodsId");$.ajax({url:"/goods/detail/"+goodsId,type:"GET",success:function(data){if(data.code == 0){//渲染页面的方法render(data.data);}else{layer.msg(data.msg);}},error:function(){layer.msg("客户端请求有误");}});
}

3.静态资源优化

注意:js文件在浏览器本地会有缓存,如果改动了js文件,下次请求加载的还是本地缓存的js文件,导致前端代码跑不通。解决方法引入js文件的链接后面加一个版本参数。代码跑不通就debug,查看数据流是不是对的,这样能尽快锁定哪里出了问题。

第六章接口优化

总目标:减少数据库的访问量。

如何对他做优化?

减少对数据库的访问, redis和mq

把订单同步下单改为异步

好处:库存不足后,后面的请求对数据库基本没有压力

异步下单,既不是返回成功,也不是返回失败,而是返回排队中

1 Redis预减库存减少数据库访问

容器初始化的时候将秒杀商品的库存和内存标记加载到Redis中,前面来的请求将redis缓存的库存减完后,后面的请求过来直接返回秒杀结束。

2.内存标记减少Redis访问

比如说前面10个请求已经将redis中缓存的库存减到0了,那么后面的请求会继续将redis中的库存减为负数,显然后面的请求将redis中的库存减成负数是多余的,而且还增加了redis的访问量。那么这里就做一个内存标记,缓存中库存大于零的时候内存标记为false,当缓存中的库存减为0时内存标记就为true。当为false时请求能往下走,反之直接返回秒杀结束。

3. RabbitMQ队列缓冲,异步下单,增强用户体验

服务端异步的请求出队,将订单写到缓存,用户去查找,看成功还是失败

创建秒杀信息类

MiaoshaMessage(用户信息和秒杀商品id)

将信息类发送出去

Direct交换机,将信息类对象转为字符串,进队

接收者:将string还原为对象

从信息类里面拿用户信息和商品id后

入队成功的时候去轮询

怎么做轮询?判断一个用户有没有秒杀到商品

获取秒杀结果,调用方法(如果秒杀订单不为空,成功,等于空,两种情况,失败和排队中,无法辨别,这是根据标记来判断是不是因为库存不足导致的失败)

生成库存不足标记的方法,往redis里面设置一个值,新建miaoshakey,永久生效

如果redis里面存在这个key,就说明卖完了

4. RabbitMQ安装与Spring Boot集成

package com.imooc.miaosha.controller;import com.imooc.miaosha.access.AccessLimit;
import com.imooc.miaosha.domain.MiaoshaMessage;
import com.imooc.miaosha.rabbitmq.MQSender;
import com.imooc.miaosha.redis.*;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;import com.imooc.miaosha.domain.MiaoshaOrder;
import com.imooc.miaosha.domain.MiaoshaUser;
import com.imooc.miaosha.domain.OrderInfo;
import com.imooc.miaosha.result.CodeMsg;
import com.imooc.miaosha.result.Result;
import com.imooc.miaosha.service.GoodsService;
import com.imooc.miaosha.service.MiaoshaService;
import com.imooc.miaosha.service.MiaoshaUserService;
import com.imooc.miaosha.service.OrderService;
import com.imooc.miaosha.vo.GoodsVo;import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;@Controller
@RequestMapping("/miaosha")
public class MiaoshaController implements InitializingBean {@AutowiredMiaoshaUserService userService;@AutowiredRedisService redisService;@AutowiredGoodsService goodsService;@AutowiredOrderService orderService;@AutowiredMiaoshaService miaoshaService;@AutowiredMQSender sender;private HashMap<Long,Boolean> localOverMap=new HashMap<Long,Boolean>();//系统初始化时,将库存加载进缓存,并将秒杀商品的状态标记为false@Overridepublic void afterPropertiesSet() throws Exception {List<GoodsVo> goodsList = goodsService.listGoodsVo();//判断一下商品列表是否为空if(goodsList==null){return;}for (GoodsVo goods : goodsList) {Integer stockCount = goods.getStockCount();redisService.set(GoodsKey.getMiaoshaGoodsStock,""+goods.getId(),stockCount);localOverMap.put(goods.getId(),false);}}/*** QPS:* 1000 * 10* */@RequestMapping(value="/{path}/do_miaosha", method=RequestMethod.POST)@ResponseBodypublic 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);}//校验秒杀pathboolean check=miaoshaService.checkPath(user,goodsId,path);if(!check){return Result.error(CodeMsg.REQUEST_ERROR);}//先判断一下该秒杀商品的状态(内存标记,减少redis访问)Boolean b = localOverMap.get(goodsId);if(b){ //说明缓存中的商品已经减为0return  Result.error(CodeMsg.MIAO_SHA_OVER);}//收到请求,减少缓存中的库存long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, "" + goodsId);if(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 miaoshaMessage=new MiaoshaMessage();miaoshaMessage.setUser(user);miaoshaMessage.setGoodsId(goodsId);sender.sendMiaoshaMessage(miaoshaMessage);return Result.success(0);//0代表排队中/*//判断库存GoodsVo goods = goodsService.getGoodsVoById(goodsId);//10个商品,req1 req2int stock = goods.getStockCount();if(stock <= 0) {return Result.error(CodeMsg.MIAO_SHA_OVER);}//判断是否已经秒杀到了MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);if(order != null) {return Result.error(CodeMsg.REPEATE_MIAOSHA);}//减库存 下订单 写入秒杀订单OrderInfo orderInfo = miaoshaService.miaosha(user, goods);return Result.success(orderInfo);*/}/** 返回orderId :成功* -1:秒杀失败* 0:排队中* */@RequestMapping(value="/result", method=RequestMethod.GET)@ResponseBodypublic Result<Long> miaoshaResult(Model model,MiaoshaUser user,@RequestParam("goodsId")long goodsId) {model.addAttribute("user", user);if (user == null) {return Result.error(CodeMsg.SESSION_ERROR);}//获取秒杀结果long result=miaoshaService.getMiaoshaResult(user.getId(), goodsId);return Result.success(result);}}
}

缺陷:库存在缓存中的key是永不过期的,当你该库存的时候,需要将缓存中的key先删除

/*** orderId:成功* -1:秒杀失败* 0: 排队中* */public long getMiaoshaResult(Long userId, long goodsId) {MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(userId, goodsId);if(order==null){//如果订单为空,有两种状态,排队和库存不足导致的失败,根据标记状态来判断boolean isOver = getGoodsOver(goodsId);if(isOver){return -1;}else{return 0;}}else {return order.getOrderId();}}

在做减库存操作时,如果减库存失败,在缓存中添加一个key。因此在去查询秒杀结果时,如果订单为空(有两种状态,排队和库存不足导致的失败)再根据标记状态(缓存中有没有对应的key)来判断是那种个情况,如果有说明是库存不足,如果没有说明是正在排队中。

5.访问Nginx水平扩展
系统的负载均衡nginx,如果前面没有加缓存,单群加服务器没有作用,全都落在db上,db并发是有限的,再加服务器也是没用的,我们基于带有有良好的扩展性。

第七章安全优化

防止恶意用户刷我们的接口,秒杀开始之前不知道访问那个地址,比较安全

验证码作用: 1、防止机器人或工具刷
2、没有验证码,大家只是点击鼠标请求集中,数据库压力大 (有的话消耗时间,将瞬间的并发量分散到10s开)

接口限流防刷: 系统本身容量有限,防止用户恶意刷接口,在某个时间端内限制用户访问的次数。

1.秒杀接口地址隐藏

思路:秒杀开始之前,先去请求接口获取秒杀地址

1.接口改造,带上PathVariable参数
2.添加生成地址的接口
3.秒杀收到请求,先验证PathVariable

//获取动态秒杀路径
function getMiaoshaPath(){var goodsId = $("#goodsId").val();g_showLoading();$.ajax({url:"/miaosha/path",type:"GET",data:{goodsId:goodsId,verifyCode:$("#verifyCode").val()},success:function(data){if(data.code == 0){var path = data.data;doMiaosha(path);}else{layer.msg(data.msg);}},error:function(){layer.msg("客户端请求有误");}});
}@AccessLimit(seconds = 10,maxCount = 5,needLogin = true)@RequestMapping(value="/path", method=RequestMethod.GET)@ResponseBodypublic Result<String> getmiaoshaPath(HttpServletRequest request,MiaoshaUser user,@RequestParam("goodsId")long goodsId,@RequestParam(value = "verifyCode",defaultValue = "0")int verifyCode) {if (user == null) {return Result.error(CodeMsg.SESSION_ERROR);}//判断验证码boolean check=miaoshaService.checkverifyCode(user,goodsId,verifyCode);if(!check){return Result.error(CodeMsg.REQUEST_ILLEGAL);}//生成秒杀pathString path= miaoshaService.setmiaoshaPath(user,goodsId);return Result.success(path);}

前端拿到path后在调用秒杀接口
秒杀要接受path,校验
怎么验证? get缓存redis里面的key和传过来的path是否相等

public String setmiaoshaPath(MiaoshaUser user, long goodsId) {if(user==null|| goodsId<=0){return null;}String path= MD5Util.md5(UUIDUtil.uuid()+"123456");//将秒杀path存到redis,有效期60sredisService.set(MiaoshaKey.getMiaoshaPath,""+user.getId()+"_"+goodsId,path);return path;}public boolean checkPath(MiaoshaUser user, long goodsId,String path) {if(path==null || user==null){return false;}//从redis里面根据key取pathString str = redisService.get(MiaoshaKey.getMiaoshaPath, "" + user.getId() + "_" + goodsId, String.class);return path.equals(str);}

2.数学公式验证码

public boolean checkverifyCode(MiaoshaUser user, long goodsId, int verifyCode) {if(user == null || goodsId <=0) {return false;}//从缓存中取验证码和输入的比较Integer OldCode = redisService.get(MiaoshaKey.getMiaoshaVerifyCode, user.getId() + "," + goodsId, Integer.class);if(OldCode==null || OldCode-verifyCode!=0){return false;}//验证之后,将缓存中的验证码删除redisService.delete(MiaoshaKey.getMiaoshaVerifyCode,user.getId() + "," + goodsId);return true;}

3.接口防刷

需求:设置10秒钟内,最多请求5次,超过这个次数就算为非法请求,提示访问太频繁。
设计:使用拦截器,将这个功能与业务代码分离,能让其他方法形成复用。

获取注解上的时间,设置为缓存key的过期时间。去缓存中获取已访问次数,如果缓存为空的话,说明第一次访问,设置缓存并将次数设为1。之后在不超过最大访问次数的基础上,每次访问缓存中的数加1.

 @Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if(handler instanceof HandlerMethod){//先获取用户MiaoshaUser user=getUser(request,response);//将获取的用户存起来,方便后面的调用传递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();//获取keyString key=request.getRequestURI();//如果需要登陆if(needLogin){if(user==null){//提示错误信息render(response,CodeMsg.SESSION_ERROR);return false;}//key需要加上用户idkey+="_"+user.getId();}else {//如果不需要登陆什么都不做}//查询访问次数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.ACCESS_ERROR);return false;}}return true;
}

自定义的注解

@Retention(RUNTIME)
@Target(METHOD)
public @interface AccessLimit {int seconds();int maxCount();boolean needLogin() default true;}

java秒杀项目总结相关推荐

  1. idea导入ssm项目_一个简洁的适合 Java 小白练手的“秒杀”项目

    公众号关注 "GitHub今日热榜"设为 "星标",带你挖掘更多开发神器! 今天推荐一款"秒杀"项目,该项目提取自电商的秒杀场景,将秒杀业务 ...

  2. Java秒杀系统实战系列~构建SpringBoot多模块项目

    摘要:本篇博文是"Java秒杀系统实战系列文章"的第二篇,主要分享介绍如何采用IDEA,基于SpringBoot+SpringMVC+Mybatis+分布式中间件构建一个多模块的项 ...

  3. 慕课网,乐字节 Java电商秒杀项目

    慕课网.乐字节Java电商秒杀项目 技术点介绍: 前端:Thymeleaf,Bootstrap,Jquerry 后端:SpringBoot,MybatisPlus,Lombok 中间件:RabbitM ...

  4. Java秒杀系统实战系列~RabbitMQ死信队列处理超时未支付的订单(转)

    转自: https://juejin.cn/post/6844903903130042376 文末有源代码,非常棒 摘要: 本篇博文是"Java秒杀系统实战系列文章"的第十篇,本篇 ...

  5. api商品分享源码_SSM框架高并发和商品秒杀项目高并发秒杀API源码免费分享

    前言: 一个整合SSM框架的高并发和商品秒杀项目,学习目前较流行的Java框架组合实现高并发秒杀API 源码获取:关注头条号转发文章之后私信[秒杀]查看源码获取方式! 项目的来源 项目的来源于国内IT ...

  6. 2019年1月份GitHub上最热门的Java开源项目

    相信大多数程序猿们都回归工作岗位啦,不知道是否调整好心态了呢?1月份GitHub上最热门的Java开源项目新鲜出炉,还是一起来看看都有哪些项目上榜吧: 1JavaGuide https://githu ...

  7. 2018年12月份GitHub上最热门的Java开源项目

    2018年12月份GitHub上最热门的Java开源项目 又到了公布 GitHub 上热门项目的时候啦~在 12 月的排行中,现在,一起来看看这些项目你使用过哪些呢? JavaGuide https: ...

  8. github java开源项目经验_Java 开源项目 GitHub 趋势周报 20201230

    [关注获取更多精选开源图书] 大家好!我是超级机器人 UltraBot,今天给大家推送本周 Java 开源项目 GitHub 趋势周报,本周更新开源项目 64. brettwooldridge / H ...

  9. Java秒杀系统实战系列~分布式唯一ID生成订单编号

    摘要: 本篇博文是"Java秒杀系统实战系列文章"的第七篇,在本博文中我们将重点介绍 "在高并发,如秒杀的业务场景下如何生成全局唯一.趋势递增的订单编号",我们 ...

  10. Java开源项目合集<一>

    1.java高并发秒杀项目 这是一个教你如何使用 Java 语言来设计高并发大流量秒杀架构的项目,需要学习的小伙伴需要先了解MQ.SpringBoot.Redis.Dubbo.ZK .Maven,lu ...

最新文章

  1. 你真懂JavaScript吗?
  2. SAP中添加自定义菜单
  3. Metasploit没有db_autopwn命令的解决办法
  4. 3-3 编程练习:jQuery键盘事件案例
  5. 从0开始学习自动化测试框架cypress(五)总结
  6. Java中找出s字符串的回文_给定一个字符串 s,找到 s 中最长的回文子串。
  7. J2EE 领域的一些技术框架结构图
  8. bzoj1831 逆序对 (dp+树状数组)
  9. 带你初步了解生物网络分析
  10. (STM32CubeMX)超声波模块测距传感器学习笔记
  11. 关于timer产生的pwm寄存器arr,ccr,psc分析
  12. Ubuntu、ROS、PX4常见问题及其解决办法
  13. UltraVNC 使用方法详细说明
  14. 引入 JPEGCodec;JPEGImageEncoder; 图片处理
  15. 使用Java对HBase进行操作(三)
  16. OpenCV 学习笔记03 凸包convexHull、道格拉斯-普克算法Douglas-Peucker algorithm、approxPloyDP 函数...
  17. 基于陷波滤波器的永磁同步电机振动抑制学习笔记
  18. 机器学习要用到的基础知识
  19. Android 11.0 系统去掉多用户功能
  20. harmonyOS2,Harmonyos系统下载|Harmonyos2.0官网 v2.0-520下载站

热门文章

  1. python 规则引擎 drools_SpringBoot2整合Drools规则引擎及案例详解
  2. 工具类——FileUtils
  3. GitHub Windows 客户端使用教程
  4. 使用Echarts实现地图3D效果
  5. 计算机软件cae,各种CAE软件介绍
  6. matlab时变函数,MATLAB在《复变函数》教学中的应用(图文)
  7. 存储过程与函数的区别
  8. vs2015安装vs assist 教程
  9. SwitchHosts for Mac(mac hosts修改工具)
  10. 文字转换为音频mp3的方法