Redis实战篇(视频学习来自黑马程序员)
在浏览器上输入地址访问 http://localhost:8081/shop-type/list 可以看到JSON数据
nginx不能放在中文目录下,否则会报错
可以看到nginx文件有一个静态文件夹
打开nginx
命令行(这里我是直接输入nginx.exe
,我是习惯了,你输入start nginx.exe
也可以)
访问http://localhost:8080/
一.短信登录功能实现
修改这段代码:
在UserService
接口类添加方法
在UserServiceImpl
类中实现接口并重写其方法
其方法逻辑实现步骤:
1.校验手机号
2.如果不符合,返回错误信息
3.符合,生成验证码
4.保存验证码到session
5.发送验证码
6.返回ok
我们首先看一下进行校验的工具类
package com.hmdp.utils;import cn.hutool.core.util.StrUtil;/*** @author 虎哥*/
public class RegexUtils {/*** 是否是无效手机格式* @param phone 要校验的手机号* @return true:符合,false:不符合*/public static boolean isPhoneInvalid(String phone){return mismatch(phone, RegexPatterns.PHONE_REGEX);}/*** 是否是无效邮箱格式* @param email 要校验的邮箱* @return true:符合,false:不符合*/public static boolean isEmailInvalid(String email){return mismatch(email, RegexPatterns.EMAIL_REGEX);}/*** 是否是无效验证码格式* @param code 要校验的验证码* @return true:符合,false:不符合*/public static boolean isCodeInvalid(String code){return mismatch(code, RegexPatterns.VERIFY_CODE_REGEX);}// 校验是否不符合正则格式private static boolean mismatch(String str, String regex){if (StrUtil.isBlank(str)) {return true;}return !str.matches(regex);}
}
@Overridepublic Result sendCode(String phone, HttpSession session) {// 1.校验手机号if (RegexUtils.isPhoneInvalid(phone)) {//2.如果不符合,发送错误信息return Result.fail("手机号格式错误");}//3.如果符合,生成验证码String code = RandomUtil.randomNumbers(6);//4.保存验证码到sessionsession.setAttribute("code", code);//5.发送验证码,必须加上@Slf4j注解才有这个方法log.debug("发送短信验证码成功, 验证码: {}", code);//6.返回okreturn Result.ok();}
测试一下:
在浏览器访问 http://localhost:8080/login.html 可以看到一个短信验证页面
然后输入手机号进行验证
可以看到验证成功:
1.实现短信验证码登录功能和注册功能
首先可以看到这个实体类有三个属性(手机号,验证码,密码):
在UserController
实现登录功能
登录功能实现步骤:
1.校验手机号
2.校验验证码
3.不一致,报错
4.一直,根据手机号进行查询
5.判断用户是否存在
6.保存用户到session
@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手机号格式错误");}Object cacheCode = session.getAttribute("code");String code = loginForm.getCode();if (cacheCode == null || cacheCode.toString().equals(code)) {return Result.fail("验证码错误");}//一致,根据用户手机号查询用户select * from tb_user where phone = ?User user = query().eq("phone", phone).one();// 判断用户是否存在if (user == null) {// 如果用户不存在,创建新用户并保存。user = createUserWithPhone(phone);}session.setAttribute("user", user);return Result.ok();}// 创建用户private User createUserWithPhone(String phone) {User user = new User();user.setPhone(phone);user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));save(user);return user;}
注意
我们因为继承了ServiceImpl<UserMapper,User>
,所以可以直接调用query()
方法和save()
方法。
测试一下
输入验证码,登录成功。
实现登录校验拦截器功能:
实现登录拦截器思路:
1.获取session
2.获取session中的用户
3.判断用户是否存在
4.如果不存在,则进行拦截
5.存在,保存用户信息到ThreadLocal
6.放行
先修改这段代码:
然后写一个登录拦截器LoginInterceptor
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {HttpSession session = request.getSession();//2.获取session中的用户Object user = session.getAttribute("user");// 判断用户是否存在if (user == null) {// 如果用户不存在,则进行拦截response.setStatus(401);return false;}//5.存在,保存用户信息到ThreadLocalUserHolder.saveUser((User) user);//6.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {HandlerInterceptor.super.afterCompletion(request, response, handler, ex);}
}
创建配置类MvcConfig
实现WebMvcConfigurer
接口
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/shop/**","/voucher/**","/shop-type/**","/upload/**","/blog/hot","/user/code","/user/login");WebMvcConfigurer.super.addInterceptors(registry);}
}
@GetMapping("/me")public Result me(){// TODO 获取当前登录的用户并返回User user = UserHolder.getUser();return Result.ok(user);}
测试一把:
把user
对象的内容拷贝到UserDTO
对象身上,是为了隐藏一些用户敏感信息,只显示用户不敏感的数据。
查看以下UserDTO
这里改回原始的数据
测试一下:(这样可以避免敏感信息被访问到)
2.集群的session共享问题:
用Redis来代替session
前端代码分析:
保存验证到redis中
//保存验证码到redis // set key value ex 120stringRedisTemplate.opsForValue().set("login:code:" + phone, code, 2, TimeUnit.MINUTES);
代码优化以下:
从redis获取缓存对象
// 7.保存用户信息到redis中// 7.1.随机生成token,作为登录令牌 /这个UUID是属于 cn.hutool.core.lang.UUID;String token = UUID.randomUUID().toString(true);UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);// 7.2.将User对象转为HashMap存储Map<String, Object> userMap = BeanUtil.beanToMap(userDTO);String tokenKey = LOGIN_USER_KEY+token;// 7.3.存储stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);// 7.4.s设置token有效期stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);// 8.返回tokenreturn Result.ok(token);
不能使用依赖注入,只能使用构造方法。因为LoginInterceptorRegister
使我们自己手动new出来的。但是我们可以在WvcConfig
配置类在属性StringRedisTemplate
上使用依赖注入进行赋值
@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//HttpSession session = request.getSession();//2.获取session中的用户//Object user = session.getAttribute("user");// 1.获取请求头中的tokenString token = request.getHeader("authorization");if (StrUtil.isBlank(token)) {response.setStatus(401);return false;}// 2.基于TOKEN获取redis中的用户String key = RedisConstants.LOGIN_USER_KEY + token;Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);// 3.判断用户是否存在if (userMap.isEmpty()) {// 4.如果用户不存在,则进行拦截response.setStatus(401);return false;}// 5.将查询到的Hash数据转换为UserId对象UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);//6.存在,保存用户信息到ThreadLocalUserHolder.saveUser(userDTO);//7.刷新token有效期stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);//8.放行return true;}
测试一下,发现异常。
java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.Stringat org.springframework.data.redis.serializer.StringRedisSerializer.serialize(StringRedisSerializer.java:36) ~[spring-data-redis-2.3.9.RELEASE.jar:2.3.9.RELEASE]at org.springframework.data.redis.core.AbstractOperations.rawHashValue(AbstractOperations.java:185) ~[spring-data-redis-2.3.9.RELEASE.jar:2.3.9.RELEASE]at org.springframework.data.redis.core.DefaultHashOperations.putAll(DefaultHashOperations.java:147) ~[spring-data-redis-2.3.9.RELEASE.jar:2.3.9.RELEASE]at com.hmdp.service.impl.UserServiceImpl.login(UserServiceImpl.java:99) ~[classes/:na]at com.hmdp.service.impl.UserServiceImpl$$FastClassBySpringCGLIB$$9cac0aa5.invoke(<generated>) ~[classes/:na]at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.2.15.RELEASE.jar:5.2.15.RELEASE]at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688) ~[spring-aop-5.2.15.RELEASE.jar:5.2.15.RELEASE]at com.hmdp.service.impl.UserServiceImpl$$EnhancerBySpringCGLIB$$9b0f4aaa.login(<generated>) ~[classes/:na]at com.hmdp.controller.UserController.login(UserController.java:56) ~[classes/:na]at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_292]at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_292]at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_292]at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_292]at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190) ~[spring-web-5.2.15.RELEASE.jar:5.2.15.RELEASE]at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138) ~[spring-web-5.2.15.RELEASE.jar:5.2.15.RELEASE]at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105) ~[spring-webmvc-5.2.15.RELEASE.jar:5.2.15.RELEASE]at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:878) ~[spring-webmvc-5.2.15.RELEASE.jar:5.2.15.RELEASE]at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:792) ~[spring-webmvc-5.2.15.RELEASE.jar:5.2.15.RELEASE]at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.2.15.RELEASE.jar:5.2.15.RELEASE]at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040) ~[spring-webmvc-5.2.15.RELEASE.jar:5.2.15.RELEASE]at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943) ~[spring-webmvc-5.2.15.RELEASE.jar:5.2.15.RELEASE]at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) [spring-webmvc-5.2.15.RELEASE.jar:5.2.15.RELEASE]at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909) [spring-webmvc-5.2.15.RELEASE.jar:5.2.15.RELEASE]at javax.servlet.http.HttpServlet.service(HttpServlet.java:652) [tomcat-embed-core-9.0.46.jar:4.0.FR]at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) [spring-webmvc-5.2.15.RELEASE.jar:5.2.15.RELEASE]at javax.servlet.http.HttpServlet.service(HttpServlet.java:733) [tomcat-embed-core-9.0.46.jar:4.0.FR]at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) [tomcat-embed-core-9.0.46.jar:9.0.46]at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) [tomcat-embed-core-9.0.46.jar:9.0.46]at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) [tomcat-embed-websocket-9.0.46.jar:9.0.46]at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) [tomcat-embed-core-9.0.46.jar:9.0.46]at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) [tomcat-embed-core-9.0.46.jar:9.0.46]at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) [spring-web-5.2.15.RELEASE.jar:5.2.15.RELEASE]at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.2.15.RELEASE.jar:5.2.15.RELEASE]at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) [tomcat-embed-core-9.0.46.jar:9.0.46]at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) [tomcat-embed-core-9.0.46.jar:9.0.46]at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) [spring-web-5.2.15.RELEASE.jar:5.2.15.RELEASE]at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.2.15.RELEASE.jar:5.2.15.RELEASE]at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) [tomcat-embed-core-9.0.46.jar:9.0.46]at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) [tomcat-embed-core-9.0.46.jar:9.0.46]at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) [spring-web-5.2.15.RELEASE.jar:5.2.15.RELEASE]at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.2.15.RELEASE.jar:5.2.15.RELEASE]at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) [tomcat-embed-core-9.0.46.jar:9.0.46]at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) [tomcat-embed-core-9.0.46.jar:9.0.46]at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) [tomcat-embed-core-9.0.46.jar:9.0.46]at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) [tomcat-embed-core-9.0.46.jar:9.0.46]at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) [tomcat-embed-core-9.0.46.jar:9.0.46]at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143) [tomcat-embed-core-9.0.46.jar:9.0.46]at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) [tomcat-embed-core-9.0.46.jar:9.0.46]at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) [tomcat-embed-core-9.0.46.jar:9.0.46]at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357) [tomcat-embed-core-9.0.46.jar:9.0.46]at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374) [tomcat-embed-core-9.0.46.jar:9.0.46]at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) [tomcat-embed-core-9.0.46.jar:9.0.46]at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893) [tomcat-embed-core-9.0.46.jar:9.0.46]at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1707) [tomcat-embed-core-9.0.46.jar:9.0.46]at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.46.jar:9.0.46]at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_292]at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_292]at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.46.jar:9.0.46]at java.lang.Thread.run(Thread.java:748) [na:1.8.0_292]
解决办法:
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
测试一下:成功运行,登录验证成功
在这里我们要改善一些问题,只要用户一直操作,token就不会消失。
删掉LoginInterceptor
原来的代码
测试一下:数据库有缓存数据
后来测试发现验证码错误,原因时我少打了一个!号,应该是判断缓存数据与输入的用户数据是否相等,如果不相等,则返回验证码错误。,
二.商户查询缓存
1.添加商品缓存
添加Redis缓存:
在ShopController
类
public interface IShopService extends IService<Shop> {Result queryById(Long id);
}
实现思路:
1.从redis查询商铺缓存
2.判断是否存在
3.存在,直接返回
4.不存在,根据id查询数据
5.不存在,返回数据库
6.存在,写入redis
7.返回
@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result queryById(Long id) {String key = CACHE_SHOP_KEY + id;// 1.从redis查询商铺缓存String shopJson = stringRedisTemplate.opsForValue().get(key);//2.判断是否存在if (StrUtil.isNotBlank(shopJson)) {// 3.存在,直接返回Shop shop = JSONUtil.toBean(shopJson, Shop.class);return Result.ok(shop);}//4.不存在,根据id查询数据库Shop shop = getById(id);//5.不存在,返回错误if (shop == null) {return Result.fail("店铺不存在");}//6.存在写入redisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));return Result.ok(shop);}
缓存练习题分析:
先删除缓存,再操作数据库的情况:
先删除缓存,后更新数据库
该方案也会出问题,此时来了两个请求,请求 A(更新操作) 和请求 B(查询操作)
请求A进行写操作,删除缓存
请求B查询发现缓存不存在
请求B去数据库查询得到旧值
请求B将旧值写入缓存
请求A将新值写入数据库
上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。
正常的情况下:
异常情况下:(删缓存很快,但更新数据库很慢,在多线程并发时,会存在线程安全问题)
当数据从数据库写入缓存时
另外一个线程对数据库进行了更新操作。数据库和缓存产生数据不一致问题。
先操作数据库,再删除缓存的情况下:
在正常情况下:
异常情况下:
先操作数据库,再删除缓存这种方案更好。
@Override@Transactionalpublic Result update(Shop shop) {Long id = shop.getId();if (id == null) {return Result.fail("店铺id不能为空");}//1.更新数据库updateById(shop);//2.删除缓存stringRedisTemplate.delete(CACHE_SHOP_KEY + id);return null;}
2.缓存穿透
在redis中缓存空值
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
为了解决缓存穿透引发的数据不一致问题:
我们需要提前检查redis中key对应的值是否是空值,如果是空值,则返回错误信息。注意这里的null是存在的,只不过是一个空值,它与什么数据都没有有着本质的·区别。
// 判断命中的是否是是空值if (shopJson != null) {// 返回一个错误信息return Result.fail("店铺信息不存在!");}
缓存雪崩:
缓存击穿:
怎样解决缓存击穿的问题:
第一种:互斥锁
第二种(逻辑过期):
Redis实战篇(视频学习来自黑马程序员)相关推荐
- [学习笔记]黑马程序员Spark全套视频教程,4天spark3.2快速入门到精通,基于Python语言的spark教程
文章目录 视频资料: 思维导图 一.Spark基础入门(环境搭建.入门概念) 第二章:Spark环境搭建-Local 2.1 课程服务器环境 2.2 Local模式基本原理 2.3 安装包下载 2.4 ...
- [学习笔记]黑马程序员-Hadoop入门视频教程
文章目录 参考资料 第一章:大数据导论与Linux基础(p1-p17) 1.1 大数据导论 1.1.1 企业数据分析方向 1.1.2 数据分析基本流程步骤 明确分析的目的和思路 数据收集 数据处理 数 ...
- 梦想在三十岁起航!__来自黑马程序员69期安卓班的学员
梦想在三十岁起航! 多久没动笔了?恐怕我也记不起来了,在生活不如意的时候也曾想写点什么,可是却又全然没有思路,曾经的文思泉涌,早已在长时间浑浑噩噩的工作生活中干涸了,而今宛如仲永一般,泯然众人 ...
- [学习笔记]黑马程序员python教程
文章目录 思维导图 Python基础知识图谱 面向对象 SQL入门和实战 Python高阶技巧 第一阶段 第九章:Python异常.模块与包 1.9.1异常的捕获 1.9.1.1 为什么要捕获异常 1 ...
- SSM 框架学习(黑马程序员)
(Spring+SpringMVC+MyBatis) SSM框架教程 黑马程序员最全SSM框架教程|Spring+SpringMVC+MyBatis全套教程 01. Spring 简介 1.1 Spr ...
- 【转】2023年Java学习路线图-黑马程序员
PS:注意收藏,此套路线图会不定期更新! Java学习路线图(2023版,视频已更新) 入门: Java SE基础 → Java Web(含数据库+H5+js+vue) 中级: Maven → Git ...
- Node.js学习笔记 [黑马程序员]——day34
文章目录 初识 Express 简介 Express 的基本使用 托管静态资源 nodemon Express 路由 路由的概念 :dog:什么是路由 :dog:Express 中的路由 :dog: ...
- Node.js学习笔记 [黑马程序员]——day2
文章目录 模块化的基本概念 模块化规范 Node.js 中模块的分类 Node.js 中模块的分类 加载模块 Node.js 中的模块作用域 向外共享模块作用域中的成员 `module` 对象 `mo ...
- C++入门学习(黑马程序员课程讲义)——第一阶段
1 C++初识 1.1 编写C++程序步骤 四个:创建项目.创建文件.编写代码.运行程序 1.2 注释 单行注释://描述信息 (通常放在一行代码的上方,或者一条语句的末尾) 多行注释:/描述信息/ ...
- JavaSE学习(黑马程序员徐磊老师)day01
1.Java快速入门 01.Java的概述(常识,没有什么含金量,但是要记住) Java是美国 sun 公司(Stanford University Network)在1995年推出的一门计算机高级编 ...
最新文章
- python 3读取文件-python3的txt文件读写
- 获取Docker中容器的信息
- 2017-10-9(Volley使用范例源码分析)
- codewars??? Is my friend cheating?
- x86已安装该产品 剑灵vcredist_MySQL Server v5.7正式版(附安装和配置数据库教程)
- TypeScript 学习一 参数,函数,析构表达式
- Pycharm上Django的使用 Day8
- JZOJ 3461. 【NOIP2013模拟联考5】小麦亩产一千八(kela)
- 用几个最简单的例子带你入门 Python 爬虫
- 二叉树的前中后层遍历
- OpenCV精进之路(零):core组件——Mat和IplImage访问像素的方法总结
- 【图像检索】基于matlab GUI KNN图像检索【含Matlab源码 267期】
- 库克谈人工智能:增长飞快 兼具颠覆性和创造性
- C/C++面试/笔试题2022
- hadoop搭建伪分布式集群(centos7+hadoop-3.1.1)
- poi 使用模板导出数据
- Linux文件管理及用户命令
- 【解决】阿拉伯语等右向左排版文字CSS解决方案
- DCM和PLL和MMCM的差别
- 程序员年薪20万、30万、40万都是如何生活的?
热门文章
- 设计师学python还是processing_人人都能学会的processing创意编程能实现什么?
- SWF是什么文件,SWF文件用什么软件可以打开
- 伪静态化不正常,电脑打不开贴子,手机可以
- 单片机编程软件很简单(24),keil单片机编程软件仿真、调试技巧+常见错误
- 前端 地图增加边框线_echarts map地图设置外边框或者阴影
- Java设计模式之适配器模式详解
- AVL Cruise 2020安装教程
- Apache SOLR and Carrot2集成
- html5 微信 飞机 源码,[HTML5]微信飞机大战
- QT出现“d:\Program Files (x86)\SogouInput\Components\”问题初步想法