开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

如何设计一个短网址服务 一、简介 二、正文 三、总结

发表于 2021-10-28

一、简介

不知道大家有没有了解过短网址,所谓短网址就是将一个长的url转换成一个短的url,这样用户访问短url的时候也能够正常访问到正常的url。应用广泛,比如短信、分享链接等等。。。

下面我会从几个方面来讲述如何设计一个高性能的短网址服务

干货满满,你准备好了吗🐒

二、正文

你为什么能通过一个短的链接访问的正常的数据呢

这里假设短网址为mtw.so/6kK03S 我们称之为A

对应的长链接为tech.meituan.com/2021/10/20/… 我们称之为B

(这里是通过一个在线网址转换工具生成的,图省事😂)

当我们访问A时,会经过mtw.so的域名解析最终会请求该域名下的某一个http接口(😎后段程序员都懂,就不做多解释了)

这个接口会拿到url后的参数 6kK03S

通过这个参数后端会定位到一个长链接,也就是原始链接,最后通过重定向到原始链接(301/302按需使用),至此就结束了

如何将一个长链接变成一个短链接呢🤨

错误的做法 ❌

你首先肯定会想到加密啊,把长的url通过一种算法加密,压缩长度,用户访问加密后的url,通过后段再解密就好了呀。ohhhh!! 我是个天才,这也太简单了吧!!(如果真的有这样子的算法,那么你就创造奇迹了。。。原因自行百度,不做多解释了)

那么应该怎么做呢? 😱

如果我们能吧长链接和短链接的对应关系存储起来,用户访问短链接时,去数据库中把长链接读取出来,不就可以完美解决了吗?

如何通过短链接定位到长链接

那么问题来了,这个存储要怎么设计?如何通过短链接定位到长链接?

使用id,如果我们有一个全局id,这个id永远不会重复,是不是就解决问题了?

当一个长链接来申请变成短链接时候,我们为他生成一个id,然后存到数据库中,假设id=996,那么996这个id就对应我们这个长链接,最后我们直接把这个996提供给用户使用就好了,假设生成的短链接为baidu.com/996 ,那么接下来的流程就简单了

  1. 用户访问baidu.com/996
  2. 服务器接受到请求,获得996这个参数
  3. 后端服务那996这个id,去数据库中查询长链接
  4. 重定向到这个长链接
  5. 用户看到长链接的页面效果
    完美解决问题!!!

你认为这样解决了吗?既然这么简单,那这篇文的意义何在❓

image.png

问题一:全局id怎么生成?

问题二:如果id还是太长了怎么办?

问题三:如果我这个短网址服务一天要生成十万、百万、千万级别的短网址,要如何存储?

问题四:请求量大了怎办?服务扛得住吗?

(面试官的致命组合拳)

莫慌莫慌,下面我们一一道来😴(以下问题解决方案基于分布式环境探讨)

全局id怎么生成

常见的全局id生成有几种,对于高并发情况,我们一般会涉及一个id生成器,后文会详解

redis incr ⭐️⭐️⭐️⭐️

完全依赖于redis,
优点:简单无脑,
缺点:数据会丢失,即便是redis有RDB和AOF,也会丢失不能保证

数据库自增id ⭐️⭐️⭐️⭐️

基于数据库的自增id

优点:简单无脑

缺点:要做好并发控制,比如sql层面可以通过select for update,代码层面可以使ReentrantLock来保证线程安全,还要注意id是有上限的(具体上限跟随数据类型),如果达到上限,讲不会再生成id

uuid (不推荐)⭐️

优点:简单无脑

缺点:生成的id不连续切中英文混合,落地到mysql会造成页分裂,影响mysql存储性能

雪花算法(SnowFlake) ⭐️⭐️⭐️⭐️⭐️

优点:通过单例模式保证id唯一性

缺点:由于是基于时间的,所以时间回退会造成id重复 (谁没事会去改服务器的时间???)

如果自增id很长了怎么办

当我们的自增id长度很长,生成了8573834749584939,也不满足我们短链接的需求怎么办?

进制转化

我们可以使用62进制(数字 + 小写字母 + 大写字母)来处理我们的id

10进制 62进制
996 g4
1024102410241024 4GNTCX7B6
996996996996996996996 j9TiP3ZLxcIA

是不是变短了很多(你那么短,却那么能干🤔)

数据量大怎么存储

数据量大?多大算大呢?个、十、百、千、万、十万、爸、爷、祖宗!!!!

根据《阿里巴巴Java开发手册》,单表超过500W行,或者单表数据大小超过2GB,才推荐做分表操作

如何选择分表策略?

这里给出两张常见的分表策略方案:

  1. 根据日期按月/季度/年分表
  2. 固定表分表

按时间份表

适用场景:日增数据量很大,比如日增十万、百万级别的数据

具体实现:根据时间,把当前时间生成的数据存储到具体的时间表中,比如今天是2021-10-28,选择按月份表,我们就把10月份产生的数据都存到t_id_test_202110,这张表中

优点:可以利用mysql的事件自动创建表,且单表容量可控

缺点:id生成需要带上时间,不方便做统计操作

固定表分表

固定表分表指的就是自己评估未来的数据增长,把数据表分为若干个表,比如分为8、16、32、64、128张表(这里建议是2的x次方)

适用场景:能预估数据量的大小,并且不希望创建很多表

具体实现:创建若干数据表8、16、32、64、128张表,以序号命名,比如:t_id_test_1、t_id_test_43等,当生成一个id(996996),我们将这个id取模分表总数
(这里也是为什么建议分表数一定要是2的x次方的原因,因为当n=2的x次方,任意数%n 结果和 任意数&n-1 的结果是一致的,但是位运算的效率要高得多得多得多)
比如分32张表,996996%32、996996&31=4,那么我们就把数据写到t_id_test_4 这张表中,读取同理

优点:id不用额外处理,分表数量可控,统计方便

缺点:表扩容比较复杂,需要暂停服务,重新做一次hash计算

如何扛住高并发,1W+的QPS

对于高并发情况,我们要如何应对?我们先总结一下短链接的过程

  1. 生成短链接

a. 获取一个全局id

b. 把长链接和id存到数据库

c. 将id做62进制转换

d. 拼接成短链接返回
2. 访问短链接

a. 后段接收请求

b. 拿到请求参数

c. 将参数取10进制

d. 从数据库中讲这个id对应的长链接取出来

e. 重定向

我们来思考一下,对于高并发场景,我们要考虑以上哪一过程呢?🤔🤔我们一步一步来

先看 1 生成端链接的过程

1.b、1.c 和1.d 是不需要考虑的,因为你并发量再大,他们也不会影响,就是一个计算+返回+写db的过程

1.a有没有并发问题?当然有,什么问题?I/O瓶颈呀,如果选择redis、mysql自增作为全局id,那么I/O是重大损耗,那么如何解决呢?可以利用id生成器

再来看 2 访问短链接的过程

2.a、2.b、2.c、2.e 是不会有高并发的问题的,我们重点看2.d

我们知道,高并发环境下影响性能最严重的就是I/O,而且大批量的请求直接打到mysql,如果mysql的配置不高,也容易搞快mysql,那么如何解决呢?可以利用缓存

全局id生成器

所谓id生成器,就是我负责生(I/O),你负责用(从内存拿),职责单一,减小I/O!

具体实现:一条线程不停的生成id,保证唯一性和顺序性,然后将生成的id放入队列中(先进先出、保证顺序),工作线程要获取id时直接从队列头拿出来即可,这样是不就除去了工作线程I/O的过程,直接读内存,速度大大的提高。当然我们也可以设置一个规则,比如当队列长度小于某一个界限的时候再生成id。

缓存

我们要从两个地方坐缓存,第一数据过滤,第二数据缓存

数据过滤:

为什么要有数据过滤?因为我们要先行过滤无效的数据,如果这个数据是有效的我们才会往后面走流程,常用的数据过滤器可以使用布隆过滤器(Bloom Filter),生成短链接时,讲当时的id放入BloomFilter,后面有请求来先通过一层Filter,如果为true,往下执行,如果是false,直接拒绝
(BloomFilter会有误差,如果存在则不一定存在,如果不存在则一定不存在)

数据缓存:

当每生成一个短链接,我们可以先将其缓存至redis,读取时先读redis,redis没有再去mysql读,然后再放入redis,防止大批量的请求直接打到mysql,将其击垮。这里要考虑一个问题,因为短链接的量非常大,我们要选择性长期缓存,对于一些热点数据,我们要缓存时间长一些,直到他成为冷门数据,对于冷门数据,我们甚至可以不用缓存。

如果还是并发大相应慢,我们可以利用本地缓存,也就是jvm层面的缓存,可以讲链接信息暂时放入本地map,使用lru算法保证不会OOM,先读本地缓存、再读redis、最后mysql兜底。

三、总结

本文主要探讨了一个高性能的短链接服务的设计思路,如有错误的地方,希望各位大佬帮助小弟改正。

新人小白第一文,可以关注我,后续会有很多干货哦~~~

一个努力让自己变强的深漂Java程序员

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

第四届蓝桥杯JavaB组省赛-黄金连分数

发表于 2021-10-28

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

题目描述

黄金分割数0.61803… 是个无理数,这个常数十分重要,在许多工程问题中会出现。有时需要把这个数字求得很精确。

对于某些精密工程,常数的精度很重要。也许你听说过哈勃太空望远镜,它首次升空后就发现了一处人工加工错误,对那样一个庞然大物,其实只是镜面加工时有比头发丝还细许多倍的一处错误而已,却使它成了“近视眼”!!

言归正传,我们如何求得黄金分割数的尽可能精确的值呢?有许多方法。

比较简单的一种是用连分数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
lua复制代码                                1
黄金数 = ------------------------------
1
1 + ---------------------
1
1 + -------------
1
1 + -------
1 + ...



这个连分数计算的“层数”越多,它的值越接近黄金分割数。

请你利用这一特性,求出黄金分割数的足够精确值,要求四舍五入到小数点后100位。

小数点后3位的值为:0.618
小数点后4位的值为:0.6180
小数点后5位的值为:0.61803
小数点后7位的值为:0.6180340

(注意尾部的0,不能忽略)

你的任务是:写出精确到小数点后100位精度的黄金分割值。

注意:尾数的四舍五入! 尾数是0也要保留!

显然答案是一个小数,其小数点后有100位数字,请通过浏览器直接提交该数字。
注意:不要提交解答过程,或其它辅助说明类的内容。

解题过程

需要使用JAVA中的大数运算的相关知识,这个是关键,如果对这方面知识不明确做的会很迷茫…通过对大佬题解的学习,学习到三种方法可以解题,最先学会的是循环直接运算的方法,这种方法我感觉好理解一些;第二种是斐波那契数列的解法,这种方法需要看出这个斐波那契数列的规律之后也是挺好写出来的;第三种就是第一种循环的递归写法,感觉递归会比循环稍微难理解一些…

相关知识的链接:

BigDecimal中divide方法详解

BigDecimal.setScale用法

蓝桥杯 黄金连分数(java题解)

要点总结

  1. BigDecimal类的使用
  2. 题意的理解,当除的值一定大时结果稳定
  3. 斐波那契规律的总结

代码

第一种 循环做法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
csharp复制代码public static void main(String[] args) {
//定义大范围数值
BigDecimal bt=new BigDecimal(1);
//循环操作1000次
//这里的一千可以是其他数,只要实验一个数几次发现结果不在发生变化就可以
for (int i=0;i<1000;i++){
//前头加一的操作(注意大数的加减乘除方式)
bt=bt.add(BigDecimal.ONE);
//大数值类型的除法操作
// ONE表示1,作被除数
// bt为除数
// 100位小数点后保留的位数
// BigDecimal.ROUND_HALF_DOWN为小数值取舍类型,使用的这个类型为:四舍五入,2.35保留1位,变成2.3
//HALF_DOWN意思是一半的向下取,所以2.35变为2.3,同样的如果是HALF_UP,就是一半的向上取,2.35变为2.4
bt=BigDecimal.ONE.divide(bt,100,BigDecimal.ROUND_HALF_DOWN);
}
System.out.println(bt);
}

第二种 斐波那契数列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
ini复制代码public static void main(String[] args) {
BigInteger firNum=BigInteger.ONE;
BigInteger secNum=BigInteger.ONE;
BigInteger res=BigInteger.ZERO;

//斐波那契数列找到合适除数与被除数(尽可能的大)
for (int i=0;i<1000;i++){
res=firNum.add(secNum);
firNum=secNum;
secNum=res;
}

System.out.print("0.");
//模拟实现手动除法
for (int i=0;i<102;i++){
BigInteger b=firNum.divide(secNum);
BigInteger Ten=BigInteger.TEN;
firNum=(firNum.mod(secNum).multiply(Ten));
//要进行结尾进位的考虑
if(i!=0&&i!=100){
System.out.print(b);
}
else if(i==100){
BigInteger temp=b;
b=firNum.divide(secNum);
if(b.intValue()>5){
System.out.print(temp.intValue()+1);
}
else
System.out.print(temp);
break;
}
}
}

第三种 递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码public static void main(String[] args) {
res=f(500);
res=res.setScale(100,BigDecimal.ROUND_HALF_UP);
System.out.println(res);
}
public static BigDecimal res=new BigDecimal(1);
public static BigDecimal res1=new BigDecimal(0);

public static BigDecimal f(int n){
if(n==1){
return res;
}
BigDecimal a=new BigDecimal(1);
return res1=a.divide((a.add(f(n-1))),1000,BigDecimal.ROUND_HALF_DOWN);
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

记录一次Required request body is m

发表于 2021-10-28

接口请求一切正常,但是会报错Required request body is missing的错误

百般查询和debug后都未查到问题,最后还是在stackOverFlow中找解决方案:

stackoverflow.com/questions/3…

原因是:我新增了一个Filter用于打印请求和返回日志,部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
vbscript复制代码    @Override
   public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {  
       this.preHandle(request, (HttpServletResponse) response);
       chain.doFilter(request, response);
       this.afterCompletion((HttpServletRequest) request, (HttpServletResponse) response);
  }
​
   private void preHandle(HttpServletRequest request, HttpServletResponse response) {
           try {
               RequestWrapper requestWrapper = new RequestWrapper((HttpServletRequest) request);
               log.info("request params:{}", JSON.toJSON(requestWrapper.getBody()));
          } catch (Exception e) {
               log.info("log error ignore", e);
          }
  }
​
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
java复制代码@Slf4j
public class RequestWrapper extends HttpServletRequestWrapper {
​
   private final String body;
​
   public RequestWrapper(HttpServletRequest request) {
       super(request);
       StringBuilder stringBuilder = new StringBuilder();
       BufferedReader bufferedReader = null;
       InputStream inputStream = null;
       try {
           inputStream = request.getInputStream();
           if (inputStream != null) {
               bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
               char[] charBuffer = new char[128];
               int bytesRead = -1;
               while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
                   stringBuilder.append(charBuffer, 0, bytesRead);
              }
          } else {
               stringBuilder.append("");
          }
      } catch (IOException ex) {
​
      } finally {
           if (inputStream != null) {
               try {
                   inputStream.close();
              } catch (IOException e) {
                   log.error("inputStream close error", e);
              }
          }
           if (bufferedReader != null) {
               try {
                   bufferedReader.close();
              } catch (IOException e) {
                   log.error("bufferedReader close error", e);
              }
          }
      }
       body = stringBuilder.toString();
  }
​
   @Override
   public ServletInputStream getInputStream() throws IOException {
       final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
       ServletInputStream servletInputStream = new ServletInputStream() {
           @Override
           public boolean isFinished() {
               return false;
          }
​
           @Override
           public boolean isReady() {
               return false;
          }
​
           @Override
           public void setReadListener(ReadListener readListener) {
          }
​
           @Override
           public int read() throws IOException {
               return byteArrayInputStream.read();
          }
      };
       return servletInputStream;
​
  }
​
   @Override
   public BufferedReader getReader() throws IOException {
       return new BufferedReader(new InputStreamReader(this.getInputStream()));
  }
​
   public String getBody() {
       return this.body;
  }
​
}

开始总是找不到原因,最后是上述链接里的一番话,才让我恍然大悟,找到根源

inputstream.png

inputstream只可以读取一次! ,看到这句话,基本问题就迎刃而解了。最简单的方式就是在filter里不要读取inputstream,当然如果你想读取的话,可以采用下面的方式,思路也是上图中说的:decorate the httpServletRequest。代码如下:

1
2
3
4
5
6
7
8
vbscript复制代码    @Override
   public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
       RequestWrapper requestWrapper = new RequestWrapper((HttpServletRequest) request);
       this.preHandle(requestWrapper, (HttpServletResponse) response);
       //此处传封装后的HttpServletRequest
       chain.doFilter(requestWrapper, response);
       this.afterCompletion((HttpServletRequest) request, (HttpServletResponse) response);
  }

RequestWrapper不用改,封装HttpServletRequest之后,doFilter也传入封装的HttpServletRequest即可。

总结

一. 为什么inputStream只可以读取一次

InputStream read方法内部会记录position,用于记录当前流读取到的位置,若已读完,read方法会返回-1,(经常和inputStream打交道的,应该都会while(read() != -1) ,用这种方式读取inputStream)。若读取完再次读取的话可以调用inputStream.reset方法,此方法的前提是此方法markSupported返回true。HttpServletRequest使用的是ServletInputStream,看源码可知ServletInputStream没有实现reset和markSupported方法,那么ServletInputStream无法reset之后再次读取,所以inputStream只可以读取一次! (我要是对基础有深刻的了解,就不会查这么久了。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码​
   public synchronized void reset() throws IOException {
       throw new IOException("mark/reset not supported");
  }
​
   /**
    * Tests if this input stream supports the <code>mark</code> and
    * <code>reset</code> methods. Whether or not <code>mark</code> and
    * <code>reset</code> are supported is an invariant property of a
    * particular input stream instance. The <code>markSupported</code> method
    * of <code>InputStream</code> returns <code>false</code>.
    *
    * @return <code>true</code> if this stream instance supports the mark
    *         and reset methods; <code>false</code> otherwise.
    * @see     java.io.InputStream#mark(int)
    * @see     java.io.InputStream#reset()
    */
   public boolean markSupported() {
       return false;
  }

二. StackOverFlow真靠谱,有问题先上StackOverFlow

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

拥抱毒瘤 DDD!

发表于 2021-10-28

图片

原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,转载请保留出处。

牛B的人物,早已经厌倦了中英文混杂,他们更进一步,使用中英文缩写,对普通人进行降维打击。更厉害的,造就新的名词,并科普出去。

有几项技术,我从心底里鄙视和厌恶,但每次在技术方案中,都默默的把它们加进去,而且给足了它们分量。因为它们对于方案的成功与否,起着重要的概念性指导作用。

它们就是中台、低代码,以及DDD。这三个不同领域中的技术,肩负着同样的责任,那就是往死里忽悠。这三个词,很伟大,它们有一个共同点,都是很容易说服非技术但能决策的人员,然后向下铺开,非常具有营销型,是职业经理人和CTO的最爱。也是咨询类公司的最爱。

这些玩意儿,有的可以忽悠大公司,有的可以忽悠小公司,反正谁也别想逃掉。

但毒瘤如果能够为我们带来利益,当然也要拥抱。不要那么死板嘛。

当妖风袭来,比起关上窗子,我们要拥抱它,要投其所好!为什么有的人工资高,有的人升的快!有的人成为了大师!要从根本上想想原因。

概念能够升华体系

你知道么?越是职位高的人,越容易喜欢虚无缥缈的东西。拿古代的皇帝来说,有很多期望与神仙相会的,就被方士骗的死去活来。即使到最后知道被骗了,也只能偷偷的把消息封锁起来。最近看《资治通鉴》,就发现了很多这样的案例。

一来,是他们真的有这种需求;二来,是怕这些事被曝光了丢脸,只能咬牙坚持下去。

地球上没有新鲜事,放到软件行业也一样。当我们把一件东西给神化,赋予它某些超自然的能力,它就能在方士的路上越走越平坦。

如何神化?抓痛点、谈愿景、搞方法论,一般就能够销售成功。

当然,销售成功只是第一步,我们还要避免失败,避免被秋后算账。所以,我们需要把决策者的积极性调动起来,让他认识到自己的不足,羞于承认自己的弱点,我们就算落稳脚步了。只要决策者上了船,他就会想方设法美化它,争取更多的资源,让更多的人上船。

为什么互联网黑话生命力强劲,就是因为它能忽悠,能够升华你的思想,而不是空洞洞的代码。

我这里举个例子。

有一家公司,由于研发的人数有限,但是活儿很多,分散在多个系统之间。研发部门研究出来的结论是:要聚焦,集中力量到核心系统上。怎么办?不能在PPT上干巴巴的写上聚焦两个字吧,那显得多LOW。

思来想去,突然灵机一动。要不,我们造点名词吧。按照级别,分它个CVP系统、IVP系统、EVP系统。这样,一下子逼格就上升了不少。

看不懂这些名词?看不懂就对了,因为这是我造的,要的就是看不懂这种效果。

看看下面这张图,我们甚至可以赋予它属性,把系统归类到这三类之中。

图片

重要的是,业务系统的聚焦,摇身一变,成为了CVP的重点建设。哈哈,比起一句话就完事的决策,我们这下可以聊很久了。

“教你怎么说话十分钟,等于什么都没说”。这是一种非常重要的能力。

那么,我们就来看一下,这些技术到底是什么?为什么是毒瘤?为什么要拥抱它们。

D不D的D的,有啥区别么

所谓领域驱动,就是根据需求设计系统,这句话本来就是废话。

有Demo代码没?

有Demo代码没?

有Demo代码没?

有Demo代码没?

所有的文章下面,都充满了这样的发问。如果说DDD层只是战略上有用,那它就不应该进入程序员视野,它应该是需求分析师的玩具。DDD应该学学TOGAF、COBIT、CGEIT之类的培训,把眼光放在战略布局上,不要老是想着革程序员的命,搞什么战术。

你要是专心搞搞业务培训证书,你赚你的钱我做我的架构设计,咱们井水不犯河水。但你要把触角伸到我的领域,就会招来像我这样的喷子。

DDD正确的打开方式,就是拥抱它的战略阶段,完全扔掉它的战术阶段。这样做,你会活的很舒坦。原谅我使用“限界上下文”这样的名词来解释一下:你只要把我的服务边界划分清楚了,你管我后面是怎么实现呢,设计模式和架构模式,我的工具箱多的很,并不缺CQRS、事件溯源这样的名词。

DDD的概念最早来源于2004年,这么多年没火,没有标准落地,不是没有原因的。最近几年,有些人发现了技术名词的贫瘠,重新捡起了它,希望它能继续为KPI效力。

我曾痴迷DDD,被它的美好愿景折磨的兴奋无比。买了网课,买了书籍,到最后发现它在浪费我的时间。我恨它。恕我直言,一个难度高,落地难的技术方案,根本没有资格让人分割精力去了解它。

不好意思,没有路转粉。

首先,搞DDD的,都是些卷中卷公司,它不像微服务技术一样,能够找到大量落地的方案。实际上,你几乎找不到任何有价值的参考示例,更别说这些示例之间还相互打脸。它就像是圣经一样,给你说什么是对的,但怎么做,全靠你悟。

为什么你干不了DDD,你的团队干不了DDD?DDD给出了三个主要原因。

  • 对团队的要求较高。画外音,你做不好是你的团队不行
  • 只有复杂的业务使用DDD才能见效。那什么是复杂呢?并没有定论。话外音,你觉得不好用,那是你的业务不够复杂
  • 虽然你用不了DDD,但其中的思想,还是值得借鉴和思考的。画外音,我是万金油,不会让你白学

没有人会承认自己的团队不行,没有团队会承认自己的业务简单,没人能忍受自己的投入就真的肉包子打狗了。DDD通过几个让你不能打脸的理由,瞬间将你绑在了一起。

2020年,花了整整三个月时间,有幸拜读了《实现领域驱动设计》这本书,对其深厚的文字运用水平惊叹拜服。以后,即使一个简单的CRUD项目,我也知道文档应该怎么写了,这本书就是非常好的案例。

你搜一下DDD的文章,不论什么文章,都有一个特点,那就是不能好好的说人话。所有的应用代码,都是一堆无法说服人的垃圾代码。因为开发者和正常的写法一比较,发现自己在找罪受,那为什么要用它呢?

就拿吹的很牛b的六边形架构来说吧。

六边形架构,因为长得像蜂窝,看起来就很靠近绿色的自然界,很高大上。说实话,我到现在都没弄明白六边形架构,八边形架构(没这种东西),三角形架构(没这种东西)之间,到底有何区别,这群名词狂魔为啥选择了6这个数字。

您就直说,复杂的业务逻辑,不应该过多的关注技术等基础设施、但要预留接口就行了,非要整的这么玄乎,一条条蚯蚓一样的线从那腐烂的六边形上辐射出来。觉得很美么?或许老板真这么觉得,因为它像彩虹一样的名词轮,确实能唬住一群蹉B。

不要说ServiceMesh的数据平面和控制平面分割,是靠DDD指导的哦,虽然它概念上靠的上。

下图是google搜索Hexagonal Architecture出现的一张图。

图片

哎吆,六边形呢?这图怎么整了个10边形?那还是六边形架构么?您忽悠小孩子呢?当我不识数?什么,你又把它叫做洋葱头架构,它们不是一个东西?这样的误解在DDD中比比皆是,我也不想解释,因为它们都是短话长说。这说明了它是一门全面的忽悠方法论,是靠堆概念和黑话起家的,宣传者也不合格。

整个DDD这一套概念,价值观就有问题。或者说作者的本意或许是好的,面向的是复杂业务。结果让这群宣传者和培训一捣鼓,就成了解决问题的必要手段。

但是不好意思,您连起码的顺畅交流都没整好,没资格教别人做架构。

尴尬局面

让人觉得尴尬的是,真正需要DDD的人,并不认同它;不需要DDD的人,被强迫认同它。

DDD最大的价值是梳理业务性需求,将不同的业务领域划分出来,并形成领域之间的接口交互。说个实话,我见过很多咨询公司的大佬,他们对这种想要通吃的方法论嗤之以鼻,更倾向于使用TOGAF之类老牌的业务梳理方法。但条条道路通罗马,最终的领域划分还是能够达成一致。

这些梳理的过程,大部分是业务专家,以及系统架构师的范畴。他们的工作成果,将作为输入输出到技术团队实现。他们需要DDD,但他们并不用。

相比较而言,DDD的战术阶段,毫无价值而言。比如,把数据汇总到宽表或者大数据中心,形成数据“中台”提供交易域、管理域、查询域的分离,我并不需要知道什么CQRS的概念,也能工作的很好。至于实体充血不充血,我本来就是微服务了,业务粒度本来就很小了,要怎么写是我的自由,改造也是我自己的成本,我并不需要按照你那一套来。谈业务和技术的沟通?不好意思,不能沟通而去做业务的团队,我还没见过。

工程师被决策层强迫使用DDD战术书写业务,结果代码更乱,更改更加频繁。但是DDD说,不好意思,不是我的错,是你团队不行。

道理是这个道理,但在现实中,还是有人吹嘘、甚至使用这个东西去改造代码。《微服务架构模式》这本书,甚至有事件溯源和CQRS两个章节,去专门讲解DDD的一些落地的内容。这叫做大师毒害了大师,当然也叫做相互扶持。

恕我直言,如果你信了这些鬼话,大概率会把项目带入死亡。尽信书不如无书,架构是一种权衡,并没有通吃的指导思路。你可以参考,可以思考,但就是不能照搬,因为每个公司的技术前提都不一样。

话虽如此,但当一些概念被吹嘘起来的时候,你不去拥抱它,反而会产生问题。软件行业有两个难题,一个是怎么把复杂的事情简单的汇报,另外一个就是把简单的东西搞复杂。对于前者,主要是描述你构想的可行性。而对于后者,主要的目的就是让人觉得很高大上,很主流,越晦涩越好。前者脚踏实地,后者口吐莲花。

而后者的功效,显然要比前一种有效得多。让人听上去感觉很牛x,但是听不懂,可以获得掌声,也可以体验高高在上的感觉。没人会承认自己的智商不在线,你需要激起这些人的活力。只要有人认同,就可以产生利益。

有些概念,有些人,并不是神,但利益共同体,需要他成为神。这玩意也有信徒,你信么?但软件设计的工具,难道不是合适就用,不合适就扔么?为什么会成为信徒?仅仅是因为上船了而已。

朋友们,在一定程度上,DDD这些概念,与比特币之类的概念,并没有什么区别。这就是信仰的魔力,这就是大师的力量啊!

End

只有像我这样诚实的人,才会偶尔喷一喷。然后转身,把DDD写在了自己的方案上。是的,我可以写上,也可以讨论,也可以思维碰撞,但我永远不会轻易用它。

只有在发广告的时候,我才会把它吹成自己的亲爹。

作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,进一步交流。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

分布式事务seata142使用配置

发表于 2021-10-28

简介

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
我们项目中使用AT模式,AT模式分为两个阶段:
一阶段:
业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
二阶段:
提交异步化,非常快速地完成。
回滚通过一阶段的回滚日志进行反向补偿。

  1. 开启seata事务的工程中引入相关依赖

pom.xml中引入seata的jar包,1.4.2之前的版本,都不支持一个data id的方式存放所有的seata服务器配置信息,从1.4.2后支持一个配置文件的方式,所以此处排除默认引入的1.3的包,需引入1.4.2。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<!-- 要与seata服务端版本一直,所以把自带的替换掉 -->
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--使用1.4.2版本,对配置可以使用data-id一个配置文件包含其他所有的配置信息-->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>${seata.version}</version>
</dependency>

关于spring cloud alibaba生态的版本参考地址:github.com/alibaba/spr…

  1. seata server服务器搭建

服务器下载地址:github.com/seata/seata…,对下载的工程进行解压、配置。
配置的模板文件下载地址:github.com/seata/seata…

(1)seata/conf/file.conf

此配置项为seata 服务器的存储配置,存储方式选择db,再配置数据库的连接信息,以及处理事务的全局性表(表名使用默认的就可以)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java复制代码store {
## store mode: file、db、redis
mode = "db"
## rsa decryption public key
publicKey = ""

## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
datasource = "druid"
## datasource = "dbcp"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
## mysql 5.xx
## driverClassName = "com.mysql.jdbc.Driver"
## mysql 8.0
driverClassName = "com.mysql.cj.jdbc.Driver"
## if using mysql to store the data, recommend add rewriteBatchedStatements=true in jdbc connection param
url = "jdbc:mysql://ip:port/umapp_appcenter?rewriteBatchedStatements=true"
## url = "jdbc:mysql://ip:port/umapp_appcenter?useUnicode=true&characterEncoding=utf-8&useSSL=false&nullCatalogMeansCurrent=true&serverTimezone=Asia/Shanghai"
user = "appcenter"
password = "123456"
minConn = 5
maxConn = 100
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
}

注意点:
driverClassName驱动的配置需要根据mysql的版本决定:
mysql5.+使用 driverClassName = “com.mysql.jdbc.Driver”
mysql8使用 driverClassName = “com.mysql.cj.jdbc.Driver”

(2)seata/conf/registry.conf

需要配置选用的注册中心类型(nacos),注册中心的连接信息;配置中心的类型,配置中心的连接信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
java复制代码registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"

nacos {
application = "seata-server"
serverAddr = "127.0.0.1:7500"
group = "SEATA_GROUP"
namespace = "public"
cluster = "default"
username = "nacos"
password = "nacos"
}
}

config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"

nacos {
serverAddr = "127.0.0.1:7500"
namespace = "public"
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
}

注意点:

①当nacos开启安全配置(在nacos的conf/application.properties中配置nacos.core.auth.enabled=true)后,对nacos的连接信息都要带上用户名、密码等信息

②在seata1.4.2后才可以使用dataId = “seataServer.properties”的方式读取配置信息

(3)script/config-center/config.txt:

此配置信息是seata事务的相关属性,在nacos中创建data id 时,粘贴到文本值的内容,即seataServer.properties的配置项,seata使用1.4.2版本,新建的data id文件类型选择properties。若是使用seata1.4.2之前的版本,以下的每个配置项在nacos中就是一个条目,需要使用script/config-center/nacos/下的nacos-config.sh(linux或者windows下装git)或者nacos-config.py(python脚本)执行上传注册,可以参考blog.csdn.net/ZHANGLIZENG…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
bash复制代码transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=true
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
transport.serialization=seata
transport.compressor=none
# server
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.distributedLockExpireTime=10000
# store
#model改为db
store.mode=db
store.lock.mode=file
store.session.mode=file
# store.publicKey=""
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
# 改为上面创建的seata服务数据库
store.db.url=jdbc:mysql://ip:port/umapp_appcenter?useUnicode=true&rewriteBatchedStatements=true
# 改为自己的数据库用户名
store.db.user=appcenter
# 改为自己的数据库密码
store.db.password=123456
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.distributedLockTable=distributed_lock
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
store.redis.mode=single
store.redis.single.host=127.0.0.1
store.redis.single.port=6379
# store.redis.sentinel.masterName=""
# store.redis.sentinel.sentinelHosts=""
store.redis.maxConn=10
store.redis.minConn=1
store.redis.maxTotal=100
store.redis.database=0
# store.redis.password=""
store.redis.queryLimit=100
# log
log.exceptionRate=100
# metrics
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
# service
# 自己命名一个vgroupMapping
service.vgroupMapping.my_test_tx_group=default
service.default.grouplist=ip:port
service.enableDegrade=false
service.disableGlobalTransaction=false
# client
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.rm.tccActionInterceptorOrder=-2147482648
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
client.tm.interceptorOrder=-2147482648
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k

注意点

①.service.vgroupMapping.my_test_tx_group=default
中的my_test_tx_group需要与bootstrap.yml中配置的seata.tx-service-group的值一致。

②.service.vgroupMapping.my_test_tx_group=default
配置的default必须要等于registry.conf中配置的cluster=”default”。

③.store.mode=db配置为db的方式,则需要配置db数据库方式的连接信息
store.db.url、store.db.user、store.db.password,此数据库存储下存放的表
global_table、branch_table、lock_table,用于记录全局性的事务信息

④.store.db.driverClassName的配置
mysql5.+使用 driverClassName = “com.mysql.jdbc.Driver”
mysql8使用 driverClassName = “com.mysql.cj.jdbc.Driver”

⑤.service.default.grouplist=ip:port为访问seata服务器的地址和端口(仅注册中心为file时使用),8091是默认端口,
也可以修改启动端口,在启动项目时加上端口:
seata-server.bat -p 18091
sh seata-server.sh -p 18091

⑥seata server需要配置集群时,只需要在启动seata server服务时指定不同的端口和节点序号即可,配置file.conf和registry.conf的内容一致,

windows下启动:

seata-server.bat -p 18091 -n 1

seata-server.bat -p 8091 -n 2

linux下启动:
sh seata-server.sh -p 18091 -n 1

sh seata-server.sh -p 8091 -n 2

6f78503c1705e06c5feea82942ca0da3

⑦客户端启动时,可以看是否成功注册到seata server服务器

3.创建需要的事务表

global_table:全局事务表
branch_table:分支信息表
lock_table:加锁的表
以上三个表需要创建在seata服务器操作的db上,即file.conf中配置的数据库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
mysql复制代码--全局事务表--
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = INNODB
DEFAULT CHARSET = utf8;

-- 分支表
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = INNODB
DEFAULT CHARSET = utf8;

-- 锁定表
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(128),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = INNODB
DEFAULT CHARSET = utf8;

--seata新版本加的锁表
CREATE TABLE IF NOT EXISTS `distributed_lock`
(
`lock_key` CHAR(20) NOT NULL,
`lock_value` VARCHAR(20) NOT NULL,
`expire` BIGINT,
PRIMARY KEY (`lock_key`)
) ENGINE = INNODB
DEFAULT CHARSET = utf8mb4;

INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);

undo_log:回滚日志表
在每个需要开启seata事务操作的数据库下都需要建立此表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mysql复制代码--日志文件表--
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = INNODB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';

注意点:

seata1.4.2之后,需要回滚的表日期类型不能使用datetime,可以使用timestamp

4.客户端配置

需要开启seata事务的客户端,需要配置seata的注册和配置中心,使用相关注解进行事务开启。

(1)bootstrap.yml

向项目中添加配置信息,配置项的值与seata服务器的registry.conf中一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码seata:
enabled: true
enable-auto-data-source-proxy: true #是否开启数据源自动代理,默认为true
tx-service-group: my_test_tx_group #要与配置文件中的vgroupMapping一致
registry: #registry根据seata服务端的registry配置
type: nacos #默认为file
nacos:
application: seata-server #配置自己的seata服务
server-addr: #根据自己的seata服务配置
username: nacos #根据自己的seata服务配置
password: nacos #根据自己的seata服务配置
namespace: #根据自己的seata服务配置
cluster: default # 配置自己的seata服务cluster, 默认为 default
group: SEATA_GROUP #根据自己的seata服务配置
config:
type: nacos #默认file,如果使用file不配置下面的nacos,直接配置seata.service
nacos:
server-addr: #配置自己的nacos地址
group: SEATA_GROUP #配置自己的dev
username: nacos #配置自己的username
password: nacos #配置自己的password
namespace: #配置自己的namespace
dataId: seataServer.properties #配置自己的dataId,由于搭建服务端时把客户端的配置也写在了seataServer.properties,所以这里用了和服务端一样的配置文件,实际客户端和服务端的配置文件分离出来更好

(2)spring boot 启动程序添加数据自动代理

使用注解:@EnableAutoDataSourceProxy

例子:

1
2
3
4
5
6
7
8
9
10
java复制代码@EnableAutoDataSourceProxy
@EnableDiscoveryClient
@SpringBootApplication
public class UmappCloudServiceAppcenterApplication {

public static void main(String[] args) {
SpringApplication.run(UmappCloudServiceAppcenterApplication.class, args);
}

}

(3)使用注解开启事务

1
2
3
4
5
6
7
8
9
java复制代码@ApiOperation("添加测试数据")
@PostMapping("/addUmappTestSeata")
@GlobalTransactional
@GlobalLock
public Result<UmappTest> addUmappTestSeata(String name,Integer age,String sex) {
Result<UmappTest> result = umappTestService.addUmappTest(name,age,sex);
Result temp = seataTestFeign.addSeataTest();//添加seata测试数据
//int i = 1/0;
return result;

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

8个最适合网站开发人员的PHP框架 8个最适合网站开发人员的

发表于 2021-10-28

8个最适合网站开发人员的PHP框架

由于其简单性和易用性,PHP已经成为最受欢迎的网络脚本语言之一。它也是全世界数以百万计的开发者使用的语言,所以为你的开发需求找到合适的框架是至关重要的。

在这篇文章中,我们将讨论用于Web开发的最佳PHP框架。让我们来看看这些工具各自提供了什么,以及它们如何与它们的竞争对手相抗衡。

照片:Ben GriffithsonUnsplash

为什么要使用PHP框架

PHP框架对开发者非常有用,因为它们有助于将代码组织成模块,从而使创建应用程序更加容易。它们还提供了一个标准的函数库,使得创建可重复使用的组件变得容易,可以在不同的项目中共享。在这些好处之外,框架还可以让你写更少的代码。

由于大多数Web程序员在选择PHP作为他们的主要选择之前都使用过某种类型的编程语言,因此PHP使开发Web应用变得快速而高效。大多数的PHP框架都有许多共同的特点,如易于安装、安全和可扩展性。最重要的是,每个开发人员都清楚地知道每个功能是做什么的,以及它是如何工作的。此外,所有的PHP框架都是相互兼容的,这意味着你的网站在它们之间切换时不会显得很奇怪。

PHP框架最好的一点是,它们通过提供现成的类或模板使应用程序的创建变得简单,从而节省了自定义编码的时间和精力。WordPress主题就是一个很好的例子。所有主要的PHP框架都包括模板引擎,使设计者能够定制基本的HTML元素,而不需要知道任何关于编写代码的知识。这些工具给用户提供完整的功能,而不需要HTML、CSS、JavaScript等高级知识。

对于想从头开始学习的开发者来说,PHP框架有几个优点。首先,学习一个框架比学习多个框架需要更长的时间。第二,建立一个完整的应用程序所需的资源数量明显减少。第三,一旦项目完成,系统之间的数据传输就变得非常方便。最后,大多数PHP框架都有内置的测试机制,可以确保产品的质量。

总之,PHP框架是快速、有效地开发高质量软件的伟大工具。一旦你掌握了一个,你就不会再回头了

8个最好的PHP平台。

  1. Laravel - 最受欢迎的WordPress开发框架

照片:Mohammad RahmanionUnsplash

Laravel创建于2009年,是一个开源项目,但直到2012年Twitter宣布它将使用Laravel而不是Cakephp时,它才获得普及。

从那时起, Laravel的发展势头越来越好, 大公司如可口可乐, 雅虎, 耐克, Hulu等, 都选择了Laravel而不是其他替代品.今天,成千上万的网站都建立在Laravel之上。

  1. Symfony 2: 一个强大的MVC框架

照片:Fotis FotopoulosonUnsplash

Symfony 2是另一个非常流行的PHP框架,可以让你快速建立强大的应用程序。Symfony 2获得欢迎的主要原因之一是它的灵活性。

你可以选择多种组件,如Doctrine ORM、HttpFoundation、Twig模板引擎,甚至是Zend Server。正如你所知道的,许多项目开始时很小,但会发展成较大的项目。

这意味着如果你打算创建一些大项目,Symfony可能不适合你的需求。事实上,Symfony 2只为大规模软件开发而设计。

  1. Codeigniter 3: Laravel的简单替代品

照片:WalkatoronUnsplash

CodeIgniter是另一个著名的PHP应用平台,它使创建强大的Web应用变得容易。它简单的设计和直观的结构使它非常适合那些想开始建立网站的初学者,而不需要花时间学习复杂的概念。

另外,CodeIgniter带有大量的第三方库,允许你扩展默认提供的功能之外。这样,当你需要一些额外的功能时,你就不必每次都重新发明车轮了。

  1. Fuel CMS - 一个基于AngularJS和Bootstrap 4的开源内容管理系统

照片:SigmundonUnsplash

Fuel CMS是那些寻找一个内容管理系统的人的完美选择,该系统在一个单一的仪表板上提供所有必要的功能。

Fuel将所有的东西结合在一起,包括博客平台、论坛、画廊、活动、投资组合网站、电子商务解决方案、会员网站等等。

燃料还支持RESTful APIs,允许你将燃料与任何外部系统轻松集成。

  1. Yii。另一个伟大的PHP应用平台的选择

照片:Markus SpiskeonUnsplash

Yii与CodeIgnitor类似,因为两者都提供了广泛的控制面板,用户可以通过它来管理他们的整个网站。然而,Yii由于其简单的界面,比CodeIgnitor要容易得多。

Yii包括的功能有:主动记录,网格视图,表单验证,安全功能,图像处理工具,缓存支持,数据访问层和数据库抽象层等等。

Yii也是高度可定制的,使得它有可能在以后的阶段添加新的功能。

  1. Slim框架。轻量级和快速的HTTP API与Swift Typeing

照片:AltumCodeonUnsplash

与其他PHP框架相比,Slim Framework是相当轻的。不仅如此,Slim Framework的速度也很快。

它是如此之快,以至于网上没有任何基准测试,但我们已经看到性能改进比传统框架快10倍。如果速度对你很重要,Slim Framework绝对应该被考虑。

7)Cakephp。使用这个免费软件建立你自己的社会网络网站,如Facebook或Twitter。

照片:Austin DistelonUnsplash

Cakephp是使用MVC架构开发的,它可以免费下载和使用。由于CakePHP遵循模型-视图-控制器模式,它可以帮助你清晰地组织代码,同时快速增加功能。

CakePHP有两个版本;第一个版本在2006年发布,第二个版本在2011年问世。两个版本都支持MySQL数据库连接。因此,如果你已经拥有一个MySql服务器,那么你会发现安装CakePHP没有问题。

  1. Phalcon。一个快速的PHP框架

它是轻量级的,使用MVC架构。用户可以只安装他们想要的模块和库。这有助于保持网站开发过程中的杂乱无章和轻松。

虽然Phalcon不是很流行,但与Laravel等其他流行的PHP框架相比,它的文档很全面。

结束语

PHP平台是基于网络的应用程序,在服务器上运行,因此,它们可以帮助你为小型企业、教育机构和个人创建互动网站或在线服务。

这些平台很容易使用,不需要任何编程知识。你得到的应用程序包含你建立网站所需的一切。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

推荐一款微服务框架Go-Garden

发表于 2021-10-28

今天给大家推荐一款适合入门学习使用的微服务框架go-garden。 相对于go-micro、go-zero等重量级框架,这款框架非常轻量化,使用起来非常简单,而且常见的特性都支持。

go-garden是一款面向分布式系统架构的分布式服务框架

github地址:github.com/panco95/go-…

码云地址:gitee.com/pancoJ/go-g…

概念

  • 为分布式系统架构的开发提供了核心需求,包括微服务的一些基础架构支持,减少开发者对微服务的基础开发,更着力于业务开发;
  • 支持Http/Rpc协议,http框架使用gin,rpc框架使用rpcx;
  • rpc无需protobuf,只需要定义结构体即可;
  • 没有集成数据库、缓存之类的扩展,这里考虑到使用者对服务的设计可能会使用到不同的包,建议开发者自己导入这类扩展包使用;
  • 不限制代码结构,只需要配置文件和几行代码就可以启动一个服务,项目的结构完全由开发者自行设计,建议大家使用脚手架工具生成项目结构。

特性

  • 服务注册发现
  • 网关路由分发
  • 网关负载均衡
  • Rpc/Http协议
  • 可配服务限流
  • 可配服务熔断
  • 可配服务重试
  • 可配超时控制
  • 动态路由配置
  • 集群自动同步
  • 调用安全认证
  • 分布式链路追踪
  • 统一日志存储
  • 脚手架工具

快速开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码// 安装项目脚手架
go install github.com/panco95/go-garden/tools/garden@v1.1.4

// 创建项目
garden new my-gateway gateway
garden new my-service service

// 修改服务配置和路由配置
......

// 启动网关
go run my-gateway/main.go
// 启动服务
go run my-service/main.go

教程:基于Go Garden快速构建微服务

访问 基于Go Garden快速构建微服务 跟着一步一步学习如何使用go-garden

教程:代码示例

访问 examples 查看完整示例项目

脚手架:快速创建按项目

访问 tools 查看脚手架使用说明

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【爬虫系列】scrapy爬虫之猎聘招聘信息 1项目场景 2

发表于 2021-10-28

声明:本文只作学习研究,禁止用于非法用途,否则后果自负,如有侵权,请告知删除,谢谢!


@[TOC](scrapy爬虫之猎聘招聘信息爬取


1.项目场景

目标网址:www.liepin.com/zhaopin/?ke…

2.准备工作

2.1 创建scrapy工程:scrapy startproject liepin_spider
2.2 创建scrapy爬虫:scrapy genspider liepin ‘www.liepin.com/zhaopin/‘
2.3 配置settings、代理、数据库连接等

3.页面分析

3.1 如下图

在这里插入图片描述

4.编写代码

4.1 爬虫代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
python复制代码
# -*- coding: utf-8 -*-
import scrapy
from liepin_spider.items import LiepinSpiderItem
from sql import MyMysql


class LiepinSpider(scrapy.Spider):
name = 'liepin'
custom_settings = {
'ITEM_PIPELINES': {
'liepin_spider.pipelines.LiepinSpiderPipeline': 200,
},
'DOWNLOADER_MIDDLEWARES': {
# 'liepin_spider.middlewares.LiepinSpiderDownloaderMiddleware': 100,
'scrapy.downloadermiddlewares.retry.RetryMiddleware': None,
# 'liepin_spider.middlewares.MyRetryMiddleware': 110,
},
'CONCURRENT_REQUESTS': 1, # 请求并发数
'DOWNLOAD_DELAY': 1, # 请求延时
'DOWNLOAD_TIMEOUT' : 5, # 请求超时
}

def __init__(self, s_type=None, c_type=None, *args, **kwargs): #传参选取爬取模式
super(LiepinSpider, self).__init__(*args, **kwargs)
self.c_type = c_type #爬取时间类型 1:每天 2:每月
self.s_type = s_type #爬取关键词类型


def start_requests(self):
mysql = MyMysql()
if self.c_type == '1':
keys = mysql.read_many('select zwmc from lp_job_names where type = 0')# 获取数据库关键词,爬取部分
else:
keys = mysql.read_many('select zwmc from lp_job_names ')# 获取数据库关键词,全部爬取

if self.s_type=='0':
quyu_info = mysql.read_many('select dqs,city from lp_job_areas') # 爬取全部地区数据
else:
quyu_info = mysql.read_many('select dqs,city from lp_job_areas where type = {}'.format(self.s_type))# 爬取部分地区数据

for key in keys:
for quyu in quyu_info:
print("搜索关键词为:" + key[0] + " 当前搜索地区为:"+quyu[1])
params = (
('dqs', quyu[0]), # 地区参数
('key', key[0]),#搜索关键词
('curPage', '0'),
('pubTime','1'),#一天以内
('jobKind','2')#职位类型
)
url = 'https://www.liepin.com/zhaopin/'
yield scrapy.FormRequest(url=url,method='GET',formdata=params,callback=self.parse,meta={"key_info":key[0],"diqu_info":quyu[1]},dont_filter=True)
# 当前关键词请求完毕,修改数据库状态
print("职位:"+key[0]+"查询完毕")
mysql.update("UPDATE lp_job_names set type = '1' WHERE zwmc = '%s' " % key[0])
#重置职位状态
mysql.update("UPDATE lp_job_names set type = '0' WHERE type = '1'")


def parse(self, response):
urls = response.xpath('//div[@class="job-info"]/h3/a/@href').getall() # 所有招聘链接
key = response.meta["key_info"]
diqu = response.meta["diqu_info"]
if len(urls) == 0:
print(diqu+"没有职位:"+key)
pass # 查询不到数据则跳过
else:
for url in urls: # 循环获取招聘信息
if 'https://www.liepin.com' not in url:
url = 'https://www.liepin.com'+ url
yield scrapy.Request(url=url,callback=self.get_data,meta={"key_info":key,"diqu_info":diqu},dont_filter=True )

next_url = 'https://www.liepin.com' + response.xpath('//div[@class="pagerbar"]/a/@href').getall()[-2] # 下一页地址

if 'javascript:' in next_url: # 判断是否有下一页
pass
else:
print(diqu+"岗位:"+key+" 下一页地址",next_url)
yield scrapy.Request(url=next_url,meta={"key_info":key,"diqu_info":diqu},callback=self.parse,dont_filter=True )

def get_data(self,response):
items = LiepinSpiderItem()
items['key_word'] = response.meta["key_info"]#搜索关键词
items['diqu']= response.meta["diqu_info"]#搜索地区
items['c_type']= self.c_type
items['zhiwei']= response.xpath('//h1/text()').get()#职位
if items['zhiwei'] is None:
pass
else:
items['company'] = response.xpath('//div[@class="title-info"]/h3/a/text()').get()#公司名称
items['salary']= ''.join(response.xpath('//p[@class="job-item-title"]/text()').getall()).strip()#薪资
try:
items['fb_time']= response.xpath('//p[@class="basic-infor"]/time/@title').get() + response.xpath('//p[@class="basic-infor"]/time/text()').get().strip() #发布时间
except:
items['fb_time'] = ''
items['requirement']= '#'.join(response.xpath('//div[@class="job-title-left"]/div[@class="job-qualifications"]/span/text()').getall()) #要求
items['welfare']= '#'.join(response.xpath('//div[@class="comp-tag-box"]/ul/li/span/text()').getall()) #福利
items['job_description']= ''.join(response.xpath('//div[@class="content content-word"]/text()').getall()).strip() #职位描述
items['log_url']= response.xpath('//div[@class="company-logo"]/a/@href').get() #公司logo地址
items['industry']= response.xpath('//ul[@class="new-compintro"]/li[1]/a/text()').get() #行业
company_info = response.xpath('//ul[@class="new-compintro"]/li//text()').getall()
items['company_size'] = items['company_addr'] = ''
for num in range(3,len(company_info)):
if '公司规模' in company_info[num]:
items['company_size']= company_info[num].replace('公司规模:','') #公司规模
else:
items['company_addr']= company_info[num].replace('公司地址:','') #公司地址
# yield items
print(items)
4.2 数据库连接代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
python复制代码# -*- coding: utf-8 -*-
import pymysql


class MyMysql:
def __init__(self):
self.host = 'xxxxx' # ip
self.port = 3306 # 端口
self.user = 'xxxx' # 用户名
self.password = 'xxxx' # 密码
self.dbname = 'xxxx' # 数据库名
self.charset = 'utf8mb4' # 字符类型

# 链接数据库
self.connect()

def connect(self):
# 链接数据库和获取游标
self.db = pymysql.connect(host=self.host, port=self.port, user=self.user, password=self.password,
db=self.dbname, charset=self.charset)
self.cursor = self.db.cursor()

def run(self, sql):
ret = None
try:
ret = self.cursor.execute(sql)
self.db.commit()
except Exception as e:
self.db.rollback()
# finally:
# self.close()
return ret

def rollback(self):
self.db.rollback()
self.close()

def close(self):
self.cursor.close()
self.db.close()

def insert(self, sql):
try:
self.cursor.execute(sql)
self.db.commit()
except pymysql.err.IntegrityError:
pass

def commit(self):
self.db.commit()
self.close()

def update(self, sql):
return self.run(sql)

def delete(self, sql):
return self.run(sql)

def read_one(self, sql):
ret = None
try:
self.cursor.execute(sql)
# 获取得到数据
ret = self.cursor.fetchone()
except Exception as e:
# print('查询失败')
pass
# finally:
# self.close()
return ret

def read_many(self, sql):
ret = None
try:
self.cursor.execute(sql)
# 获取得到数据
ret = self.cursor.fetchall()
except Warning as e:
print('查询失败')
finally:
pass
return ret
3.3 Items代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
python复制代码
# -*- coding: utf-8 -*-
import scrapy


class LiepinSpiderItem(scrapy.Item):
key_word = scrapy.Field()#搜索关键词
zhiwei = scrapy.Field()#招聘职位
company = scrapy.Field()#公司名称
salary = scrapy.Field()#薪资
diqu = scrapy.Field()#地区
fb_time = scrapy.Field()#发布时间
requirement = scrapy.Field()#要求
welfare = scrapy.Field()#福利
job_description = scrapy.Field()#职位描述
log_url = scrapy.Field()#公司logo地址
industry = scrapy.Field()#行业
company_size = scrapy.Field()#公司规模
company_addr = scrapy.Field()#公司地址
c_type = scrapy.Field()#爬取时间类型
4.4 Pipelines代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
python复制代码# -*- coding: utf-8 -*-

import emoji
import pymysql
from pymongo import MongoClient
from twisted.enterprise import adbapi
import copy

adbparams_info = dict(
host='xxxx', # ip
db='xxxx', # 数据库名
user='xxxx', # 用户名
password='xxxx', # 密码
charset='utf8',
cursorclass=pymysql.cursors.DictCursor # 指定cursor类型
)

class LiepinSpiderPipeline(object):
'''异步写入'''
def __init__(self,dbpool):
self.dbpool = dbpool
self.conn = MongoClient('xxxx', 27017) # mongo的链接,可以不用

@classmethod
def from_settings(cls,settings):
# 先将setting中连接数据库所需内容取出,构造一个地点
adbparams = adbparams_info
dbpool = adbapi.ConnectionPool('pymysql', **adbparams)
# 返回实例化参数
return cls(dbpool)

def process_item(self,item,spider):
# 使用Twisted异步的将Item数据插入数据库
item1 = copy.deepcopy(item)
# 存入mysql
self.dbpool.runInteraction(self.do_insert, item1) # 指定操作方法和操作数据
# 存入mongo,获取的职位
self.mongo_insert(item['zhiwei'])

def mongo_insert(self,job):
db = self.conn.crawlab_test # 连接mongo数据库
my_set = db.jobs # 存入集合
data_test1 = {
'job': job
}
my_set.insert_one(data_test1)

def handle_error(self,failure,item):
# 打印异步插入异常
print(failure,"数据库异常")

def do_insert(self, cursor, item):
insert_sql = """insert into lp_job_data(key_word,zhiwei,company,salary,diqu,fb_time,requirement,welfare,
job_description,logo_url,industry,company_size,company_addr,type,c_type)
VALUES('%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','5','%s')
"""% (
item['key_word'],item['zhiwei'],item['company'],item['salary'],item['diqu'],
item['fb_time'],item['requirement'],item['welfare'],item['job_description'].replace("'",'"'),item['log_url'],
item['industry'],item['company_size'],item['company_addr'],item['c_type'])
try:
cursor.execute(insert_sql)
except:
insert_sql = emoji.demojize(insert_sql)#替换emoj
cursor.execute(insert_sql)

def close_spider(self, spider):
self.conn.close()
4.5 Middleware中间件代理配置代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
python复制代码# -*- coding: utf-8 -*-

import json,time
import random
import requests
from liepin_spider import settings


class LiepinSpiderDownloaderMiddleware(object):
def process_request(self, request, spider):
# 设置随机请求头
ua_random = random.choice(settings.USER_AGENT_LIST)
request.headers['User-Agent'] = ua_random

--设置代理请自行添加

def process_exception(self, request, exception, spider):
--异常捕捉,需重新加代理请求
ua_random = random.choice(settings.USER_AGENT_LIST)
request.headers['User-Agent'] = ua_random
return request
4.6 运行代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
python复制代码import os

def start_all():
os.system('scrapy crawl liepin -a s_type="0" -a c_type="2"') #爬取全部地区数据

# def start_type1():
# os.system('scrapy crawl liepin -a s_type="1" ') # 一天爬一次 (一线城市 + 新一线城市)
#
# def start_type2():
# os.system('scrapy crawl liepin -a s_type="2"') # 一周爬一次 (二线城市)
#
# def start_type3():
# os.system('scrapy crawl liepin -a s_type="3"') # 剩下的一月一次

def new_type():
os.system('scrapy crawl liepin -a s_type="4" -a c_type="1"') # 5个城市,部分岗位

if __name__ == '__main__':
# start_type1()
# start_type2()
# start_type3()
new_type()

5.运行代码

5.1 我们来看看最后运行的代码效果

在这里插入图片描述

5.2 数据基本都实现获取了,最后我把岗位关键词、地区数据、爬取结果表结构贴一下吧~

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

听说你想把windows当作linux用?

发表于 2021-10-28

需求

如果你有一台闲置的PC,而且安装了win10或win11,又想把这台PC当做linux服务用,比如做开发机、跑服务。

安装前提

  1. 从应用商店安装应用,需要使用微软账号进行登录,提前注册号微软账号。
  2. 系统必须是windows10或windows11
  3. 公司内部网络会限制系统的更新和从应用商店安装应用,联系IT部门开通对应权限。
  4. 安装过程计算机需要重启,安装前保存好重要文件

安装和启用wsl

wsl2和wsl1区别详见 比较 WSL 1 和 WSL 2

开启硬件虚拟化支持

计算机必须支持虚拟化才能安装wsl,不同的主板进入BIOS设置的方式不同,具体参照该主板操作文档。在菜单中找到虚拟化相关设置,开启虚拟化支持。Intel的一般是Intel Virtual Technology,简称IVT,开启后,在任务管理器-性能-CPU中可以看到虚拟化已启用。

新版本系统安装wsl

如果系统运行 Windows 10 版本 2004 及更高版本(内部版本 19041 及更高版本)或 Windows 11,可使用新版的简易安装方法,自动启用所需的可选组件,下载最新的 Linux 内核,将 WSL 2 设置为默认值,并默认安装 Linux 发行版 Ubuntu。

旧版系统安装wsl

旧版系统安装可参照该文档进行手动安装

使用系统设置启用虚拟机平台和适用于 Linux 的 Windows 子系统功能

  • win10

打开设置 - 应用和功能,右上角位置程序和功能 - 启用或关闭Windows功能 - 勾选适用于Linux的Windows子系统和虚拟机平台,如下图所示

  • win11

打开设置 - 应用,最下面更多windows功能 - 勾选适用于Linux的Windows子系统和虚拟机平台,如下图所示


wsl安装启用ssh服务

以下命令假设wsl使用ubuntulinux发行版

  1. 更新wsl的包库 apt update -y && apt upgrade -y
  2. 安装openssh-server
1
2
3
4
5
6
7
8
sh复制代码# 搜索openssh-server
$ apt search openssh-server
Sorting... Done
Full Text Search... Done
openssh-server/focal-updates,now 1:8.2p1-4ubuntu0.3 amd64 [installed]
secure shell (SSH) server, for secure access from remote machines
# 安装 openssh-server
$ sudo apt install openssh-server
  1. ssh服务配置调整

配置文件路径为/etc/ssh/sshd_config,使用root权限打开文件进行对应修改,sudo vim /etc/ssh/sshd_config,如果使用配置默认值,直接进行第4步操作。

  1. 启动ssh服务
1
2
3
4
5
6
7
8
sh复制代码# 查看sshd服务状态
sudo service ssh status
# 启动sshd服务
sudo service ssh start
# 停止sshd服务
sudo service ssh sttop
# 重新启动sshd服务
sudo service ssh restart
  1. 设置wsl ssh服务开机启动

wsl启动时,ssh服务默认不会自动开启,可以手动开启或配置为开机启动。

  • 添加wsl用户名为sudo用户,并设置不需要密码

    1. sudo visudo打开/etc/sudoers文件
    2. 添加以下修改
      1
      2
      3
      sh复制代码root    ALL=(ALL:ALL) ALL
      在上面一行下面添加
      {wsl-username} ALL=(ALL:ALL) NOPASSWD:ALL
  • 在windows系统新建文件wsl-ssh-start.bat

  • 写入以下命令wsl sudo service ssh start

  • Win+R打开运行,输入shell:startup打开启动文件夹,复制文件wsl-ssh-start.bat到该文件夹。

添加端口映射

wsl的ip在windows系统每次重启后会发生改变,可以在每次重启后使用脚本获取wsl的ip并添加端口映射

  1. 新建文件batch-add-portproxy.ps1添加以下代码
1
2
3
4
sh复制代码# 添加wsl端口转发
$wsl_ip = (wsl hostname -I).trim()
Write-Host "WSL Machine IP: ""$wsl_ip"""
netsh interface portproxy add v4tov4 listenport={对外端口} connectport={wsl-ssh-port} connectaddress=$wsl_ip
  1. 添加开机启动任务
    打开任务计划程序,左边列表里选择任务计划程序库,右键创建基本任务,填写任务名,下一步后选择计算机启动时,下一步选择启动程序,下一步选择脚本路径,下一步完成。

防火墙开放ssh服务的对外端口

windows防火墙默认关闭任何对外端口,局域网访问需要开放端口。
防火墙开放端口:设置-更新和安全-Windows安全中心-防火墙和网络保护-高级设置-入站规则-新建规则-端口-下一页-选择TCP和特定本地端口,后面填写ssh服务的对外端口-下一页-允许连接-继续下一页-最后一页填写名称

局域网机器ssh连接wsl

1
sh复制代码ssh -p {对外端口} wsl-username@windows-ip

配置ssh公钥登录

公钥登录可参照Windows OpenSSH 密钥管理

错误和问题

wsl使用windows代理

windows cmd运行ipconfig,找到本地IP地址,在wsl里设置代理

1
sh复制代码export ALL_PROXY="http://{windows-ip}:{代理端口}"

windows安装和启动ssh服务

windows ssh安装和启用参照微软官方文档 OpenSSH 入门

在 Windows 中,ssh服务的名称也是sshd, 默认从%ProgramData%\ssh\sshd_config 中读取配置数据,该文件相当于linux下sshd服务的配置文件·/etc/ssh/sshd_config`。可以修改文件配置ssh服务的端口,认证方式等。

使用公钥访问windows ssh服务(非wsl)出现Permission denied

1
2
sh复制代码$ ssh pc-win-ssh
ljc43026@10.181.24.27: Permission denied (publickey,keyboard-interactive).

问题原因: windows用户是管理员用户

如果用户是管理员用户,客户端的公钥应该添加到administrators_authorized_keys,该文件路径为%ProgramData%\ssh\administrators_authorized_keys,并且此文件上的 ACL 需要配置为仅允许访问管理员和系统,设置方法参见修复administrators_authorized_keys文件权限。

参考资料:

  • 适用于 Linux 的 Windows 子系统文档
  • 解决访问windows ssh服务出现Permission denied
  • 修复 administrators_authorized_keys 文件的权限
  • How to automatically start ssh server on boot on Windows Subsystem for Linux
    本文由博客一文多发平台 OpenWrite 发布!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Node-RED学习心得(操作篇)

发表于 2021-10-28

架构图

核心知识点

基本功能

本篇会介绍6项常用的子模块,并适时使用案例来介绍它们的功能以及再使用上的一些细节

inject

共通模块最常用到的子模块之一,作为输入用途,可以自行选择payload类型

inject模块

创立inject节点时点击模块可以进入设定,选单中主要分为5个要素:

  1. 模块名称
    • 为inject设定名称
  2. 物件属性
    • 预设为payload
    • 可以自行添加属性
  3. 物件属性资料类型
    • 指定属性类型,包含常见的字串、数字、布林、json等
  4. 主题
    • 指定topic名
  5. 选择输入机制
    • 手动输入模式
    • 循环输入模式
    • 指定时间输入模式

inject模块设定

debug

输出模块,用来将输入结果显示在指定视窗上(通常是除错视窗),点击可以进入设定选单

debug模块

设定选单主要包含:

debug模块设定

  1. 输出属性
    • 预设payload
    • 亦可指定已存在的属性
  2. 选择输出窗口
    • 选择输出视窗
  3. 模块名称
    • debug模块名称

function

使用javascript编写的函式模块,可以提供使用者自定义的功能,我们可以对传入的物件属性进行处理,透过返回msg物件,后续的模块可以得到加工后的结果

function模块

例如我编写一个用来判断result值是否为真的模块,如果成立便新增一个新的属性note,并给予它一个值

1
2
3
4
javascript=复制代码if(msg.payload.result == true){
msg.payload.note = "test"
}
return msg;

或者编写一个判断属性长度的模块

1
2
3
javascript=复制代码var new_msg = {payload:msg.payload.length}
msg.payload = new_msg
return msg;

一样点击function模块进入设定,在函数栏位编写自定义的程式码就可以了

编写function模块

change

改变物件属性类型,可以进行增、删、修改、转移等操作

change模块

点击模块后可以进入设定选单,change模块的重点在操作指令

change模块设定

  1. 模块名称
    • change模块名称
  2. 操作指令
    • 设定
      • 将属性设定为指定值
      • 也可以用来新增一个属性,并给值
    • 修改
      • 搜寻特定值然后取代
    • 删除
      • 删除物件中指定属性
    • 转移
      • 将物件中的属性值转移到另一个属性上
  3. 物件属性
    • 跟上面介绍得差不多,用来指定属性,预设是payload

switch

这是一个很有趣的模块,它类似程式中的switch语句,可以用来当作流程控制

1
2
3
4
5
6
7
8
9
10
11
c=复制代码switch(msg.payload){
case 1:
/*do something*/
break;

case 2:
/*do something*/
break;

...
}

switch模块

选单中4个重点,分别对应switch的功能

switch模块设定

  1. 属性
    • switch(var)
    • 属性栏位相当于设定var
  2. 判断类型
    • 相当于条件判断子
  3. 判断值指定值
    • 依照不同判断子填入的参数
    • case x:
    • 相当于x的功用

其实不只常见的判断子,Node-RED提供非常多的判断模式供使用者使用,这些就留给大家慢慢摸索

switch模块判断子选择

template

template模块可以提供使用者自定HTML模板

switch模块设定

模板内可以输入指定的HTML格式,举个例子来看看这个模块的功用,把以下html程式码写进template模块内

1
2
3
4
5
6
7
8
9
10
htmlembedded=复制代码<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hello~</title>
</head>
<body>
<h2>This is Node-RED<h2>
</body>
</html>

在侧边拦栏位找到httpin模块,拖曳进来并起双击设定

  1. 请求方法
    • 选择http协议的GET method
    • URL可以自订,这裡设定绝对路径/hello
    • 名称表示模块名,可有可无

httpin模块设定

在侧边栏位找到将http response模块,拖曳进来并将template尾部连上它。完成后应该会如下图所示

连接模块

最后然后按下deploy键,在网址栏为输入url 127.0.0.1:1880/hello就可以看到我们设定的html内容

网页显示

实际案例练习

实现目标:

  1. 输入一串json字串,将该字串转换成json物件,例如
1
2
json=复制代码{"id":"device0","result":true}
{"id":"device1","result":false}
  1. 若判断result为true,则新增一个属性note,并且定义它的内容为test字串,并输出json物件
  2. 若判断result为false,则原封不动输出json物件

方法一: 利用function

方法一

  1. 首先加入两个inject模块,payload类型均设定为字串,字串格请参考上面json案例
  2. 在解析栏位选择json模块,双击并设定操作栏位为JSON字串与物件互转
  3. 选择function模块,然后输入以下程式码
1
2
3
4
javascript=复制代码if(msg.payload.result == true){
msg.payload.note = "test"
}
return msg;
  1. 拉取debug模块,并且把上述模块都连接起来

方法二: 利用change与switch

方法二

  1. 首先加入两个inject模块,payload类型均设定为字串,字串格请参考上面json案例
  2. 在解析栏位选择json模块,双击并设定操作栏位为JSON字串与物件互转
  3. 选择switch模块,设定payload.result的判断式

设定判断式

  1. 若result为真,则从输出端口一输出,并且连接上change模块,利用设定操作把payload.note设定成字串”test”

设定change模块

  1. 拉取debug模块,并且把上述模块都连接起来

输出结果

手动注入inject模块,经过测试可以发现右侧除错式窗成功打印我们要的结果,两种方法接测试正确

输出结果

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…459460461…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%