文章目录
分布式基础
分布式基础配置参考:link+官方link
所以环境都在Ubuntu16.04下进行
Docker
虚拟化容器技术。Docker基于镜像,可以秒级启动各种容器。每一种容器都是一个完整的运行
环境,容器之间互相隔离
1.安装:
#Docker的安装及学习[1]+[2]
PS:添加完用户组后,通过下述操作方可生效
方式一:刷新docker组 sudo newgrp docker
方式二:退出该用户重新登录 sudo su
切换到root再 su 用户名
docker安装完还不行,docker的目录放在系统根目录下可不行,镜像太占空间了,改位置
开机自启动服务:sudo systemctl enable docker.service
配置镜像阿里云加速器
针对Docker客户端版本大于 1.10.0 的用户
您可以通过修改daemon配置文件/etc/docker/daemon.json来使用加速器
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://nsgnmg58.mirror.aliyuncs.com"]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker
2.拉镜像
dockerHub:link
mysql
mysql:docker pull mysql:5.7
运行容器
# --name指定容器名字 -v目录挂载 -p指定端口映射 -e设置mysql参数 -d后台运行
sudo docker run -p 3306:3306 --name mysql \
-v /mydata/mysql/log:/var/log/mysql \
-v /mydata/mysql/data:/var/lib/mysql \
-v /mydata/mysql/conf:/etc/mysql \
-e MYSQL_ROOT_PASSWORD=root \
-d mysql:5.7
查看运行的容器
sudo docker ps
sudo docker ps -a
# 这两个命令的差别就是后者会显示 【已创建但没有启动的容器】
docker logs 容器ID(输入前几位方可) #docker查看日志
# 我们接下来设置我们要用的容器每次都是自动启动
sudo docker update mysql --restart=always
# 如果不配置上面的内容的话,我们也可以选择手动启动
sudo docker start mysql
# 如果要进入已启动的容器
sudo docker exec -it mysql /bin/bash
根据运行容器的ID或NAMES进入容器:sudo docker exec -it mysql bin/bash
每个docker都是一个小linux
Navicat连接
参考link
1 SQL脚本执行
指定数据库的制定字符集utf8mb4,其包含utf8
CREATE DATABASE `gulimall-oms` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
IDEA中右键数据库-》
进入mysql容器,登录后通过自带的命令行source xxx.sql也是可以的
2 SQL的comment 乱码
enca编码转换工具
1.安装
$sudo apt-get install enca
2.1查看文件编码
$enca -L zh_CN 文件名 返回文件的编码
2.2转换
命令格式如下
$enca -L 当前语言 -x 目标编码 文件名
例如要把当前目录下的所有文件都转成utf-8
$enca -L zh_CN -x utf-8 *
IDEA
Maven
稍微留意下版本发行日期,IDEA用的是2018.3.6
下载link
配置:
sudo gedit ~/.bashrc
文末追加
export M2_HOME=/home/xu/SOFT/apache-maven-3.5.4
export M2=$M2_HOME/bin
export PATH=$M2:$PATH
#刷新环境变量
source ~/.bashrc
PS:#如果要配置系统级别的环境变量,则应该编辑以下文件sudo gedit /etc/profile
clean命令作用是:清理项目中target目录下文件。
compile命令作用是:将.java文件编译成 .class文件。
package命令作用是:将项目打包到target目录下。web 项目打包成:war文件。 java项目打包成:jar文件。
而点击父工程gulimall下的操作,可对整体进行
node
参考:link
下载link
添加源:目标版本14.15.1 LTS
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
安装:sudo apt-get install -y nodejs
.bashrc 文末后缀
export NODE_HOME=/home/xu/SOFT/node/
export PATH=$PATH:$NODE_HOME/bin
export NODE_PATH=$NODE_HOME/lib/node_modules
npm install -g vue-cli
发现全局安装的包,在命令行中都没反应,这也不奇怪,因为我将全局包路径改成了/home/xu/SOFT/node/node_global
在.bashrc追加路径方可:
export PATH="$PATH:/home/xu/SOFT/node/node_global/bin"
VSCode
下载link
安装:sudo dpkg -i code_1.51.1-1605051630_amd64.deb
配置插件
live-server插件:作为一个实时服务器实时查看开发的网页或项目效果。
vue 3 snippets插件:方便代码提示
vue插件:高亮显示.vue文件
Ubuntu下VSCode代码格式化的快捷键:ctrl+shift+I
在npm run dev时,虽然项目可以启动起来,但会额外:
解决办法:终端中依次执行
echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
sudo sysctl --system
建立.vue模板:文件-> 首选项->用户代码片段->新建全局代码片段->输入vue(导入模板的命令)
{
"生成vue魔板": {
"prefix": "vue",
"body": [
"<!-- $1 -->",
"<template>",
"<div class='$2'>$5</div>",
"</template>",
"",
"<script>",
"//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)",
"//例如:import 《组件名称》 from '《组件路径》';",
"",
"export default {",
"//import引入的组件需要注入到对象中才能使用",
"components: {},",
"props: {},",
"data() {",
"//这里存放数据",
"return {",
"",
"};",
"},",
"//监听属性 类似于data概念",
"computed: {},",
"//监控data中的数据变化",
"watch: {},",
"//方法集合",
"methods: {",
"",
"},",
"//生命周期 - 创建完成(可以访问当前this实例)",
"created() {",
"",
"},",
"//生命周期 - 挂载完成(可以访问DOM元素)",
"mounted() {",
"",
"},",
"beforeCreate() {}, //生命周期 - 创建之前",
"beforeMount() {}, //生命周期 - 挂载之前",
"beforeUpdate() {}, //生命周期 - 更新之前",
"updated() {}, //生命周期 - 更新之后",
"beforeDestroy() {}, //生命周期 - 销毁之前",
"destroyed() {}, //生命周期 - 销毁完成",
"activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发",
"}",
"</script>",
"<style scoped>", //"<style lang='scss' scoped>",
//"//@import url($3); 引入公共css类",
"$4",
"</style>"
],
"description": "Log output to console"
}
}
导入模板:新建.vue文件->内容中键入vue -> tab
忽略Eslint检查:.eslintignore中追加
**/*.js
**/*.vue
清除火狐缓存快捷键:Ctrl+Shift+Delete
通过前端vue获取到的
代办事项备注,以便回补。在代码中加入 //Todo //备注信息方可
html中特殊字符表示
设计思想
点1:
1、Controller:处理请求,接受和校验数据
2、Service接受controller传来的数据,进行业务处理
3、Controller接受Service处理完的数据,封装页面指定的vo
点2:vo策略
当有新增字段时,我们往往会在entity实体类中新建一个字段,并标注数据库中不存在该字段,然而这种方式并不规范
点3:To策略
利用Feign进行远程调用时,调用者A将制定类型的To对象放入请求体中,以json发送到另一个服务B,在B中为了方便可以利用To类型对象将请求体中内容接收。PS:B不一定非得用To类型,只要保持与传输来的json中字段吻合方可。
Tools
1 Postman
ubuntu的firefox没有Postman,只有Postwoman.有点小难受,还是装下吧
link
有一点需要注意的:sudo ln -s /home/xu/SOFT/Postman/Postman /usr/bin/postman
为使能在任意位置的命令行执行,建立软连接要用全路径
Bug
三级菜单教程bug
现象:,当你给它原本的菜单添加子菜单时大部分是可以的,但给我们自己新建的菜单添加子菜单确是不好使的。注意不要被现象蒙骗了(新建的子菜单catId默认自增,很大了)
分析:经过核查,数据库中数据是保存了,但返回数据的时候,没设置为对应的children.问题在于过滤器,categoryEntity.getParentCid() == root.getCatId())
实体类中catId,parentCid都是Long对象类型。
Byte、Short、Integer、Long四种包装类默认创建了数值为[-128,127]的相应类型的缓存数据,但是超出此范围仍会创建新的对象。
改为:categoryEntity.getParentCid().equals(root.getCatId())
自动注入
在自动注入Dao时
@Autowired
AttrAttrgroupRelationDao relationDao;
出现红色波浪线还有提示:
Could not autowire. No beans of ‘AttrAttrgroupRelationDao’ type found. less… (Ctrl+F1)
Inspection info:Checks autowiring problems in a bean class.
这个不是错误
分布式高级
配置可参考该栏的其余博客
vim的基本使用
参考link
i进入输入模式
ESC退出,进入命令模式
:set number显示行号
:wq 保存并退出
解压zip中文乱码
unzip -O CP936 代码.zip
nginx+网关联调
/mydata/nginx/logs
2020/12/24 15:00:22 [error] 6#6: *2 open() “/usr/share/nginx/html/static/static/index/css/GL.css” failed (2: No such file or directory)
location用法link
意图:动静分离,静态资源直接从nginx获取
动态资源利用nginx将本地域名gulimall.com 负载到 网关 192.168.1.113:6060,网关中通过Host断言交给gulimall-product微服务,但,。,。
排查:
1.访问nginx静态资源:
http://gulimall.com/static/index/img/logo.jpg
http://gulimall.com/static/index/img/img_05.png
正常则说明nginx起来了
2.1网关直接访问gulimall-product的后台服务(面向商家)
http://192.168.1.113:6060/api/product/category/info/1
正常则说明网关的后台服务断言没毛病
2.2nginx经过网关间接访问gulimall-product的后台服务
http://gulimall.com/api/product/category/info/1
正常则说明nginx可以代理到网关
3.1直接访问前台服务(面向客户)
http://192.168.1.113:10000/
正常则说明前台服务逻辑没问题
但http://gulimall.com/不好使,那就只有两种可能:
(1)请求中根本没有host -> 考虑trace请求 而Postman中没有trace参考curl的link
务必避免下面错误:将单引号改为双引号
(2)网关的host断言写的有问题 PS:本例就是因为该问题,手抖删了个名字,尴尬
- id: product_host_route
uri: lb://gulimall-product # lb(load balance)代表从注册中心获取服务
predicates:
- Host=**.gulimall.com, gulimall.com #根据目标服务主机host判断,所以需要放在后面
Seata
SEATA AT 模式需要 UNDO_LOG 表
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
在gulimall-common中添加该依赖,发现其自动依赖了
去link下载对应seata-all版本的TC服务
定制跳转
省得为啥也不干的跳转再写“空”controller方法,但这里映射的只能通过GET方法访问
@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// registry.addViewController("/login.html").setViewName("login");
registry.addViewController("/reg.html").setViewName("reg");
}
}
用springMVC的viewController,通过这个viewName去templates下找页面
设计思路
1查询完整三级分类数据分类数据时
List<CategoryEntity> selectList = baseMapper.selectList(null); //先查询全部,避免后续的多次查询
利用springcache,结果存缓存避免每次都去查
@Cacheable(value = {"category"},key = "#root.methodName")
@Override
public Map<String, List<Catelog2Vo>> getCatelogJson(){
2 秒杀服务的信号量机制,商品随机码
2.1秒杀商品上架
这里涉及两个幂等性redis存储,一个是活动的,一个是活动关联商品的。存储了商品随机码,还有一个点是将库存设置为了信号量,缓存的图示为:
{
"endTime": 1642039200000,
"promotionSessionId": 1,
"randomCode": "22b027c349de4e6fa90ee426f35dd6f6",
"seckillCount": 23,
"seckillLimit": 2,
"seckillPrice": 3453,
"seckillSort": 1,
"skuId": 26,
"skuInfoVo": {
"brandId": 1,
"catalogId": 225,
"price": 5999.0,
"saleCount": 0,
"skuDefaultImg": "https://gulimallyfxu.oss-cn-shanghai.aliyuncs.com/2020-12-22/3982918c-315c-4eee-8267-16fcf30bb3a3_8bf441260bffa42f.jpg",
"skuId": 26,
"skuName": "华为 HUAWEI Mate",
"skuSubtitle": "麒麟9000E SoC芯片 5000万超感知徕卡电影影像 有线无线双超级快充",
"skuTitle": "华为 mate30 黑色 256G",
"spuId": 23
},
"startTime": 1610496000000
}
PS:在商品详情页刷新时,如果当前刷新页面的时间不在秒杀活动时间范围内,返回页面数据(包含从缓存中获取的之前上架的秒杀信息)中的商品随机码置为null。
2.2返回当前时间可以参与的秒杀商品信息
SeckillController
2.3开始秒杀
核对商品随机码,try获取信号量,成功后发消息队列。具体参见link
3 微博登录
注册时验证码防重刷,也是利用redis机制,限定60s内若是多次点击的话就发一次,缓存的示意:
参考这篇文章的短信服务处代码link
4 加入购物车时,重定向防止表单重复提交
foward转发请求方式不变
redirect重定向 但获取不到model中放的数据(请求域中)
RedirectAttributes redirectAttributes 重定向携带数据重定向也能共享,但似乎是以FlashMap实体方式存储在session中的
redirectAttributes.addFlashAttribute("errors", errors); 只能取一次
com/atguigu/gulimall/cart/controller/CartController.java
@GetMapping("/addToCart")
public String addToCart(@RequestParam("skuId") Long skuId,
@RequestParam("num") Integer num,
RedirectAttributes res) throws ExecutionException, InterruptedException {
cartService.addToCart(skuId,num);
//将数据拼接到url后面
res.addAttribute("skuId",skuId); //这里添加的数据会自动放在url后面
// 重定向到对应的地址
return "redirect:http://cart.gulimall.com/addToCartSuccess.html"; //解决重复提交
}
/**
* 跳转到成功页面
*/
@GetMapping("/addToCartSuccess.html")
public String addToCartSuccessPage(@RequestParam("skuId") Long skuId, Model model) {
// 重定向到成功页面,再次查询购物车数据
CartItem item = cartService.getCartItem(skuId);
model.addAttribute("item",item);
return "success";
}
值得注意的是,没有直接return “success”,而是return “redirect:http://cart.gulimall.com/addToCartSuccess.html”;
5 订单防重令牌
// 先是再页面中生成一个随机码把他叫做token先存到redis中,然后放到对象中在页面进行渲染。
// 用户提交表单的时候,带着这个token和redis里面去匹配如果一直那么可以执行下面流程。
// 匹配成功后再redis中删除这个token,下次请求再过来的时候就匹配不上直接返回
5.1cartList.html点击去结算
生成结算的详情,并给redis存储订单防重令牌,防止订单重复提交
// TODO 5.防重令牌
String token = UUID.randomUUID().toString().replace("-", "");
orderConfirmVo.setOrderToken(token);
redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX +memberRespVo.getId(), token, 10, TimeUnit.MINUTES);
缓存的图示为:
5.2 confirm.html点击提交订单
上图是订单超时未支付关闭的情况:
1是order.release.order.queue
2是stock.release.stock.queue
// 1. 验证令牌 [必须保证原子性] 返回 0 or 1
// 0 令牌删除失败 1删除成功
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
String orderToken = vo.getOrderToken();
// 原子验证令牌 删除令牌
Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRsepVo.getId()), orderToken);
if(result == 0L){
// 令牌验证失败
submitVo.setCode(1);
}else{
// 令牌验证成功
。。。
// 远程锁库存
R r = wmsFeignService.orderLockStock(lockVo);
。。。
//Todo 订单创建成功,发送消息给MQ
rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());
监听订单未支付关闭:com/atguigu/gulimall/order/listener/OrderCloseListener.java,此时改变订单状态为已取消,并会发消息主动解库存
//给MQ发消息转到库存释放队列
rabbitTemplate.convertAndSend("order-event-exchange","order.release.other",orderTo);
监听器com/atguigu/gulimall/ware/listener/StockReleaseListener.java收到库存解锁消息,通过一系列条件核实若确实需要解采取的是反向补偿的策略(原本锁了多少,现在减多少)
Bug
老神奇的问题
- nested exception is java.lang.IllegalStateException: Cannot create a session after the response has been committed
经典操作
流式组装过滤
List<SkuImagesEntity> imagesEntities = items.stream() //此处的items是List 若是数组的话得先Arrays.asList(items)
.map(item -> { //map是个组装工人
SkuImagesEntity skuImagesEntity = new SkuImagesEntity();
skuImagesEntity.setSkuId(skuId);
skuImagesEntity.setImgUrl(item.getImgUrl());
skuImagesEntity.setDefaultImg(item.getDefaultImg());
return skuImagesEntity;})
.filter(entity->{
//返回true就是需要,false就是剔除
return !StringUtils.isEmpty(entity.getImgUrl());
}).collect(Collectors.toList());
属性拷贝
BeanUtils.copyProperties(src, target) //只拷贝名匹配的字段
获取HashMap存储的数据
com/atguigu/common/utils/R.java
public <T> T getData(String key, TypeReference<T> typeReference){
// get("data") 默认是map类型 所以再由map转成string再转json
Object data = get(key);
return JSON.parseObject(JSON.toJSONString(data), typeReference);
}
StringRedisTemplate
String catelogJSON = redisTemplate.opsForValue().get("catelogJSON");
if (!StringUtils.isEmpty(catelogJSON)) {
String uuid = UUID.randomUUID().toString();
// set lock uuid EX 300 NX
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
String token = UUID.randomUUID().toString().replace("-", “”);
// 通过使用lua脚本进行原子性删除
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
//删除锁
Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
List<String> range = stringRedisTemplate.opsForList().range(key, 0, 100);
BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
List<String> list = hashOps.multiGet(range);
SpringMVC的入参对象
@RequestMapping("/list.html")
public String lisyPage(SearchParam param, Model model, HttpServletRequest request) { //SpringMVC自动将页面提交过来的所有请求查询参数封装成指定对象
param.set_queryString(request.getQueryString()); //request.getQueryString()中的中文被编码
日志打印
@Slf4j
log.info("登录成功:用户:{}",data.toString());