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

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


  • 首页

  • 归档

  • 搜索

Python开发基础总结(四)XML+time+OO 一、X

发表于 2021-11-13

「这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战」

一、XML 的使用

  1. 处理xml消息包比较好用的模块是xml.etree.ElementTree。
  2. Element执行xml的根节点。
  3. elem.find(path):查找根节点下面路径为path的子节点。
  4. elem.findall(path):同样的子节点可能有多个,这里会返回一个列表。
  5. elem.findtext(path):获取指定路径子节点的内容,这个我们会经常使用。
  6. elem.get(key);获取属性的值。
  7. 上面如果没用,则返回none
  8. elem.append:添加自节点。
  9. elem.tag:返回tag值,也就是name。
  10. elem.text:返回内容。
  11. elem.attrib:返回属性的字典。
  12. SubElement:生成一个节点,自动添加为父节点的子节点。
  13. tostring:转化为xml文本字符串。但是不包括xml头。如果编码方式为UTF-8或者GB2312,gb2312都会产生xml头;如果是utf-8,则不会产生xml头
  14. fromstring:从字符串转化为ElementTree对象。和XML同样的功能。
  15. elem.set();设置属性值

二、time 的使用

  1. time.sleep()函数函数具有c下sleep函数功能,单位为秒,但是可以接受浮点数。这样可以表示毫秒。
  2. ti = datetime.datetime.now()可以显示当前的时间,包括当前的微秒也可以显示出来。两个的差值可以表示时间 的间隔:microsecondLong = timeLong.seconds * 1000000 + timeLong.microseconds。差值的成员是seconds和microseconds

三、OO 的使用

1、如果不想让成员变量或者方法被外部使用(也就是private特性),可以以__双下划线开通。

2、属性不但可以定义在init中,也可以定义在任意的方法中通过self定义。不过最好在init中定义。

3、Python也可以实现抽象基类,也就是接口:

​注意:不可以对私有属性和方法执行@abstractmethod。否则会失败。因为,私有的无法被重写,所以,无法生成被实例化。

4、__str__属性可以将对象转换为字符串,也就是调用print(object)是会打印的字符串。

5、call(魔法方法)可以将对象作为函数来调用。给它一个入参就可以。:

1
2
3
4
5
6
7
lua复制代码def __call__(self, protoVer):

        return api.protoModules[protoVer].TimeTicks(

            (time.time()-self.birthday)*100

            )

它的作用:比较常用的是作为回调,因为他可以保存状态信息。它和闭包类似,可能比闭包的可读性要好一点。

6、对象实例是否可以删除?

7、Python参考手册要好好看一下。

8、python的static方法使用的是装饰器语法:@staticmethod.

9、对类的调用还有一个方法:CALSS.method(object)。

10、子类中,如果想调用父类的方法,可以通过:

parent.method(self).

不过还有更好的方法:

super(child, self).foo()//注意:这里是根据子类的类型获取父类的方法。它的好处是不用明显给出基类的类型。

11、cls:类方法的第一个参数。通常表示类的类型,可以通过cls()来生成实例。

1
2
3
4
5
6
7
8
9
python复制代码  @classmethod
def spawn(cls, *args, **kwargs):
"""Return a new :class:`Greenlet` object, scheduled to start.

The arguments are passed to :meth:`Greenlet.__init__`.
"""
g = cls(*args, **kwargs)
g.start()
return g

12、继承,如果子类定义了__init__函数,子类的init函数不会默认调用父类的init函数,需要手动调用:parent.init。这一点是和c++有区别的。如果子类没有定义__init__,则子类会调用父类的__init__。这里可以发现,其实,子类如果定义了init函数,是对父类的init的一个覆盖。

13、super 注意:!!!它只能用在新式的类定义中。什么是新式的?原来只是基类定义时继承object!!!。

14、继承如何继承方法:只要继承一个类,就会继承这个类所有的方法,包括__init__,del。但是如果子类重写某方法,就会覆盖父类的方法,不会再调用父类的方法了。如果想调用父类的方法,可以通过super的方式调用。

15、继承如何继承属性:只要不覆盖父类__init__方法,或者调用了父类的__init__方法,就会继承父类__init__属性的方法。继承后也可以更改这些属性。

16、父类如何防止被继承:方法或者属性以__开头,则可以防止被继承。

17、根据我的经验,其实可以以一种本质的方式理解Python的继承:Python的类就是一些方法的集合,继承一个类就是继承这个类的所有的方法。如果在子类中定义一个方法,其实是更改了这个类的符号。而属性,则可以在所有的方法中定义,只要调用了定义属性的方法,调用父类,则是继承父类的属性,调用子类定义属性的方法,则是定义子类的方法。

18、property:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ruby复制代码class c(object):
def __init__(self):
self._num = 1
@property
def num(self):
return self._num * 10
@num.setter
def num(self, v):
self._num = v
@num.deleter
def num(self):
pass
o = c()
print(o.num)
o.num = 20
print(o.num)

19、这样的好处是,可以在操作属性时,不用显示为方法调用,更加可读。同时又可以统一入口。 :注意,它也必须继承 object 才可以。

19、 OO中的垃圾回收:Python的垃圾回收使用的是符号引用计数。那么,如果在一个函数中申请一个对象,然后返回它的一个属性或者方法,这个时候对象的符号引用已经去掉,对象是否会释放?

1
2
3
4
5
6
7
8
9
10
ruby复制代码class child(parent):
def __init__(self):
self.i = 8888

def foo(self):
print('-----------------------')

def __del__(self):
print('now in del child')
super(child, self).__del__()

第一种情况,返回的是属性

def refun():

1
2
3
ini复制代码    o = child()
return o.i
I = refun()

这个时候,对象o会马上释放。因为o.i其实就是一个对象的引用,和o没有关系

第二种情况,返回的是方法

1
2
3
4
ini复制代码def refun():
o = child()
return o.foo
foo = refun()

这个时候,对象o要等到foo释放的时候再释放,因为foo中包含了o的引用(foo的入参self)

20、对于对象的属性,如果属性是可读写的,则第一步没有必要用@property修饰。可以直接使用。后面如果有需要,在进行修饰。这样既减少了工作,修改时,也不会对原有代码进行改动。

​

本文转载自: 掘金

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

Python开发基础总结(三)排序+迭代+生成器+装饰器 一

发表于 2021-11-13

「这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战」

一、排序

1、 list自己提供了排序的函数:sort。

2、 sort的参数:

a) cmp是一个比较函数,输入两个元素,比较大小,返回值为-1,0,1.

b) key也是一个函数,入参为一个元素,返回这个元素的关键字。

c) reverse是一个标志位,表示升序还是降序。默认False是升序,True表示降序。

3、使用key和reverse的性能,优于cmp函数。时间是cmp函数的一半。

二、迭代的使用

1、 迭代比直接使用列表遍历效率根据高。比如字典的keys函数返回的列表,以及iterkeys返回的迭代器。

2、 reversed() 内建函数将返回一个反序访问的迭代器.参数必须为序列。

3、 enumerate:返回一个迭代器:有索引值。

4、 for eachLine in myFile 替 换 for eachLine in myFile.readlines() :

5、 注意:在迭代的过程中不可以更改序列,否则会引发问题,导致迭代出错。

6、 可以自己定义一个类,可以迭代使用。不过需要定义方法:iter,next。

7、 filter(function, iterable):可以对迭代使用过滤器。

三、生成器的使用

1、 yield关键字可以阻塞住函数的执行,并且保存当前的执行环境,整个包被称为生成器。

2、 生成器可以通过调用生成器函数来创建。生成器函数是指包含关键字yield的函数。

3、 生成器可以通过.next()来执行。每调用一次,就执行代码,直到遇到yield关键字停止,并且返回yield关键字后面的表达式的值。

4、 可以通过调用send()函数来发送消息到生成器中。a = yield l:表示将send的入参赋值给a。

5、 throw:允许客户端传入要抛出的任何异常。

6、 和throw相同,只不过是要抛出一个特定的异常:GeneratorExit。

7、 send只接受一个参数,但是可以通过传递元组的方式传递多个参数。

8、 类的方法也可以返回生成器,因为他本质上就是一个函数。

9、 在生成器使用的时候,如何获取它自身的send和nex函数?通过send二次传入是有些风险的,非常可能造成交叉引用,无法垃圾回收造成内存泄露。

10、 第一次,必须调用next来启动生成器。

四、装饰器的使用

1、 装饰器本质上来说就是函数(或者是可调用对象),他们接受函数对象。装饰器仅仅用来装饰或者修饰函数的包装,返回一个修改后的函数对象,并将其赋值原来的标示符,并永久失去对原有函数的访问。

2、 什么是带参数的装饰器?其实就是一个函数,这个函数可以返回一个装饰器,同时这个函数可以接受参数。

3、 不带参数的装饰器要返回一个函数,这个函数就是用来替换原有的标示符的。

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码def decofun(fun):
    def _mydeco(*args, **kwargs):
        print('before fun!')
        ret = fun(*args, **kwargs)
        print('after fun', ret)
        return ret
    return _mydeco#新的函数,用于替换原有标示符
@decofun
def funtest():#funtest被替换为decofun
    print('now in funtest!')
    return 1
funtest()

4、 装饰器是可以重叠的,那么他们的顺序怎么样:

a) @decofun2

b) @decofun

c) def funtest():

d) print(‘now in funtest!’)

e) return 1

f) 原理是,funtest首先被decofun包装,然后再被decofun2包装。也就是,调用的时候,首先调用的是最上面的装饰器(也就是decofun2)的函数前面部分,然后再调用decofun的函数前面部分,之后再调用funtest。funtest返回后,首先调用的是decofun的函数后面部分,再调用decofun2后面部分。类似于一个栈的结构。

5、 装饰器不要滥用。如果一个装饰器只用了一次,要考虑他存在的必要了。

6、 携带参数的装饰器:

7、 def decoarg(arg):

a) def decofun3(fun):

b) def _mydeco(*args, **kwargs):

c) print(‘decoarg before fun!’, arg)

d) ret = fun(*args, **kwargs)

e) print(‘decoarg after fun’, ret)

f) return ret

g) return _mydeco

h) return decofun3

8、 装饰器用到的一个最重要的技术,就是闭包。装饰器函数返回的其实就是一个闭包。

9、 装饰器也可以修饰类的__方法:

class testc:

1
2
ruby复制代码  def __init__(self):
        self.i = 1

@decoarg(1)

@decofun2

@decofun

def call(self):

1
lua复制代码    print('i is %d' % self.i)

注意:装饰器修饰类方法是无法被子类继承的(或者说子类的方法是没有被修饰的)。因为他本质上就是一个函数。

10、 装饰器也可以使对象,比如:

a) class obj:

b) def init(self, fun):

c) self.fun = fun

d)

e) def call(self, *args, **kwargs):

f) print(‘decofun before fun!’, args, kwargs)

g) ret = self.fun(*args, **kwargs)

h) print(‘decofun after fun’, ret)

i) return ret

j) @objdeco

k) def funtest(a, b=2):

l) print(‘funtest1 a , b =’, a, b)

a) 这种方法看起来复杂了,但是可能会在有时候会比较有用。

11、 装饰器可以修饰类。这个时候装饰器接收的是一个类名,而返回的也是这个类名。它可以为这个类添加一些属性或者进行一些操作。

​

本文转载自: 掘金

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

1s 爬取到 1131 只数码兽,送给《数码宝贝:最后的进化

发表于 2021-11-13

「这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战」

童年回忆《数码宝贝:最后的进化》10 月 30 日在中国内地上映。所有和我们同龄的人都仍然记得数码宝贝,并且印象最深的还是第一部, 那永远的第一部!

写在前面

话说你们有没有对着自家的狗喊过 “亚古兽进化”

1s 爬取到 1131 只数码兽,送给《数码宝贝:最后的进化》> Python 爬虫小课 4-9

如果说我没有遇见迪路兽的话.——嘉儿。

如果说我没有来到数码宝贝世界的话——阿助。

如果说我没有跟大家一起来冒险的话——美美。

我们就不可能变成现在这个样子了——光子郎。

没错,因为有数码宝贝随时陪着我们的缘故——阿和。

因为我们还有那么多好伙伴——阿武。

所以我们更明白团结合作的重要——素娜。

也因此我们更能活出真正的自己——太一。

我记忆中的《数码宝贝》 是 99 年版本的,你呢?

上篇博客结尾说道要爬取一下数码宝贝动画里面的所有数码兽,最近也赶上了《数码宝贝:最后的进化》上映,献给我们的童年。

爬取前的分析

本次爬取的网址为:www.digimons.net/digimon/chn… ,对于最终的目标需要定义一下。

等待我们要爬取的信息有,数码兽的名字,类型,首次登场,图片(图片已数码兽名字命名)。

对于这些信息通过 requests 模块即可实现。

爬取代码编写时间

页面中默认展示的只有一部分数据,但是查看源码之后发现所有数据已经全部返回给了前台,所以直接写好正则表达式匹配即可。

具体的数据正则为:

1
html复制代码<li class="(.*?)"><span class="date">(?P<date>.*?)</span><span class="(.*?)">(?P<level>.*?)</span><span class="name"><a href="(?P<link>.*?)" target="_blank">(?P<name>.*?)</a></span><span class="debut">(?P<show>.*?)</span></li>

注意上述正则表达式中 (?P<date>.*?) 为获取的组数据建造一个别名,后续可以在使用 group 方法的时候,通过设置的名字即可进行获取。

不过使用别名需要配合 re.search 方法使用,在re.findall 方法中并没有实际效果。

接下来将 Python 代码进行补齐操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
python复制代码import requests
import re

headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36"
}

def get_all():
r = requests.get(
"http://www.digimons.net/digimon/chn.html", headers=headers)
r.encoding = "utf-8"
if r.status_code == requests.codes.ok:
pattern = re.compile(
r'<li class="(.*?)"><span class="date">(?P<date>.*?)</span><span class="(.*?)">(?P<level>.*?)</span><span class="name"><a href="(?P<link>.*?)" target="_blank">(?P<name>.*?)</a></span><span class="debut">(?P<show>.*?)</span></li>')
items = pattern.findall(r.text)
print(items)

上述代码运行完毕,将获取到如下图所示效果,1s 中就获取到 1131 只数码兽。

1s 爬取到 1131 只数码兽,送给《数码宝贝:最后的进化》> Python 爬虫小课 4-9

继续分析,还需要抓取数码兽对应的具体图片,分析图片的地址为 www.digimons.net/digimon/mam…

格式命名为:
http://www.digimons.net/digimon/{数码兽英文名称}/数码兽英文名称.jpg 对于编码来说先看一下获取到数据的单条数据具体格式:

1
txt复制代码('c_6', '2018年03月', 'level mark6', '究极体', 'bryweludramon/index.html', '布利维路德龙兽', '液晶玩具 数码兽摇摆机 20周年纪念版')

其中重要的数据为 'bryweludramon/index.html' 里面包含 数码兽的英文名称,需要对其进行字符串截取操作

1
2
3
4
python复制代码item = ('c_5', '2020年11月', 'level mark5', '完全体',
'were_garurumon_sagittarius/index.html', '狼人加鲁鲁兽:射手形态', '动画 数码兽大冒险:')

en_name = item[4][0: item[4].find('/')]

注意使用 find 方法,不要使用 index ,使用 index 如果没有查找到子字符串,会提示错误。

获取到英文数码兽名称之后就可以对图片进行抓取操作了,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
python复制代码import requests
import re

headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36"
}


def get_img(name, en_name):
img_url = f"http://www.digimons.net/digimon/{en_name}/{en_name}.jpg"
r = requests.get(img_url, headers=headers, timeout=5)
content = r.content
with open(f"{en_name}.jpg", "wb") as f:
f.write(content)

if __name__ == "__main__":
item = ('c_5', '2020年11月', 'level mark5', '完全体',
'were_garurumon_sagittarius/index.html', '狼人加鲁鲁兽:射手形态', '动画 数码兽大冒险:')

en_name = item[4][0: item[4].find('/')]
get_img("狼人加鲁鲁兽:射手形态", en_name)

以上代码在运行的时候,注意控制一下图片响应的时间,因为 digimons.net 网站属于国外服务器,存在响应时间问题,如果抓取不到做好出错提示。

写在后面

最终的爬取结果大家自行完善即可,数据抓取到本地,可以翻阅一张张熟悉的数码宝贝照片,满满的都是回忆。

1s 爬取到 1131 只数码兽,送给《数码宝贝:最后的进化》> Python 爬虫小课 4-9

本文转载自: 掘金

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

【设计模式系列】我用装饰器模式造了一台豪华跑车

发表于 2021-11-13

这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战

前言

一般情况下如果我们想要对一个对象进行扩展,会使用继承或组合的方式来进行,这个扩展的动作发生在编译时,比如在继承扩展时,子类的所有对象在编译时就已经确定有什么特性。

而如果需要在运行时按照不同的情况进行不同的功能扩展,并且不会对现有对象的功能进行添加和删除,则需要用装饰器模式来实现。

装饰器模式定义

装饰器模式(Decorator Design Pattern)用于在运行时修改对象的功能。同时,同一类的其他实例将不受此影响,因此单个对象将获得修改后的行为。装饰器设计模式是结构设计模式(如适配器模式、桥接模式、组合模式)的一种,它使用抽象类或具有组合的接口来实现。

假设我们想要实现不同类型的汽车,我们可以创建接口Car来定义汽车有哪些功能(接口中的方法),然后我们可以有一个基本款汽车实现类,进一步我们可以将基本款扩展到跑车和豪华车。实现层次结构如下图所示。

但是,如果我们想在运行时得到一辆同时具有跑车和豪华车特性的汽车,那么实现就会变得复杂,如果我们想进一步指定应该首先添加哪个特性,它就会变得更加复杂。现在想象一下,如果我们有10种不同的汽车,那么使用继承和组合的实现逻辑将会变得特别难以管理。为了解决这种编程问题,我们在java中应用了装饰器模式。

实现装饰器模式一般需要以下类型的组件。

组件接口

定义要实现的方法的接口或抽象类。在我们的例子中,Car将是组件接口。

1
2
3
4
java复制代码public interface Car {

public void assemble();
}

组件实现

组件接口的基本实现。我们可以将BasicCar类作为组件实现。

1
2
3
4
5
6
java复制代码public class BasicCar implements Car {
@Override
public void assemble() {
System.out.print("基本款汽车.");
}
}

装饰器

Decorator类实现了组件接口,并且它与组件接口具有has-a关系。组件变量应该可以被子装饰器类访问,因此我们将这个变量设置为protected。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class CarDecorator implements Car {

protected Car car;

public CarDecorator(Car c){
this.car=c;
}

@Override
public void assemble() {
this.car.assemble();
}

}

扩展基本装饰器功能并相应地修改组件行为。我们可以有具体的装饰器类,如LuxuryCar和SportsCar。

跑车装饰器实现:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class SportsCar extends CarDecorator {

public SportsCar(Car c) {
super(c);
}

@Override
public void assemble(){
super.assemble();
System.out.print("添加跑车特性");
}
}

豪华车装饰器实现:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class LuxuryCar extends CarDecorator {

public LuxuryCar(Car c) {
super(c);
}

@Override
public void assemble(){
super.assemble();
System.out.print("添加豪华车特性");
}
}

测试类:

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class DecoratorPatternTest {

public static void main(String[] args) {
Car sportsCar = new SportsCar(new BasicCar());
sportsCar.assemble();
// 可以灵活地对目标对象BasicCar进行扩展(装饰)
Car sportsLuxuryCar = new SportsCar(new LuxuryCar(new BasicCar()));
sportsLuxuryCar.assemble();
}

}

在测试代码中,我们可以对目标对象BasicCar灵活扩展,并且SportCar和LuxuryCar的顺序可以改变。

装饰器模式类图

小结

装饰器设计模式有助于提供运行时修改功能,因此更加灵活。当选择数量较多时,易于维护和扩展。

装饰器模式在Java IO类中使用较多,如FileReader、BufferedReader等。

如果对你有帮助,点个赞是对我最大的鼓励。

本文转载自: 掘金

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

Spring IOC容器初始化原理分析 (第五节)

发表于 2021-11-13

「这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战」

1.前言

本文主要讲Spring IOC容器初始化过程中的onRefresh()方法 和 registerListeners() 方法,如想看之前的内容可查看 Spring IOC容器初始化原理分析 (第四节)

这两个方法都是在Spring IOC 容器初始化过程中 refresh()中调用的,其中 onRefresh()方法官方的定义是 初始化上下文其它特殊的bean , registerListeners() 方法,则见名知意就是检查并注册listener。

2.refresh()源码

如果需要复制代码,可前往Spring IOC容器初始化原理分析 (第一节)

refresh.jpg

  1. onfresh() 方法讲解

首先点进源码看看,默认是一个空方法。
图片.png
接着看看官方的说法,上面的注释说了它是一个模板方法,我们可以重写该方法,以添加特定于上下文的刷新工作。可以用来初始化上下文其他特殊bean,在单例实例化前调用。

这里官方没有实现,我们可以查看一下扩展包中的实现,这里主要是了解一下这个方法的作用。

图片.png

如图我们以第一个org.springframework.web.context.support.AbstractRefreshableWebApplicationContext#onRefresh为例,点进去看看,源码如下:

图片.png

这个方法主要是初始化主题的。我们接着点进去看看,它调用的是:org.springframework.ui.context.support.UiApplicationContextUtils#initThemeSource

图片.png
图片.png

  1. 在这个方法中,他首先判断容器中是否包含一个 名为 themeSource 的bean.
  2. 如果有的话,则从容器中取到这个bean,判断一下容器的parent 是否属于 ThemeSource 且 themeSource 是否属于 HierarchicalThemeSource ,如果是 则把 themeSource 转成 HierarchicalThemeSource 对象,设置一下它的 ParentThemeSource。返回themeSource
  3. 如果没有的话,则他会帮我们定义一个 HierarchicalThemeSource 类型的 themeSource 变量置为null,判断当前 context 的parent 是否属于 ThemeSource 。 如果是则把 themeSource 实例化成DelegatingThemeSource 对象,并设置下它的 ParentThemeSource,否则把它实例化成 ResourceBundleThemeSource 对象。最终返回themeSource。

总结:可以看到在这个方法中,它就给我们定义了一个特殊bean (themeSource)。所以这个方法的主要作用就是添加一些特殊bean。

4.registerListeners() 方法

老样子,我们接着进源码,这里为了大家更直观的看,我就直接把源码复制过来,直接在上面讲

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
scss复制代码protected void registerListeners() {
// 这里实际是挨个注册静态的监听器
for (ApplicationListener<?> listener : getApplicationListeners()) {
getApplicationEventMulticaster().addApplicationListener(listener);
}

// 从容器中获取所有实现了 ApplicationListener 接口的bean的name 放入applicationListenerBeans
String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
for (String listenerBeanName : listenerBeanNames) {
getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
}

// Publish early application events now that we finally have a multicaster...
// 发布早期的监听器
Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents;
this.earlyApplicationEvents = null;
if (earlyEventsToProcess != null) {
for (ApplicationEvent earlyEvent : earlyEventsToProcess) {
getApplicationEventMulticaster().multicastEvent(earlyEvent);
}
}
}

这里我把重点列出来细讲一下:

  1. getApplicationListeners() : 返回容器中静态的 ApplicationListener 集合,源码如下:

图片.png
图片.png

  1. getApplicationEventMulticaster() 拿到前面注册的 ApplicationEventMulticaster,如果没有就抛异常。 这里它的注册过程在 Spring IOC容器初始化原理分析 (第四节) 中 写的比较详细,这里就不讲了。

图片.png

  1. addApplicationListener() 这就是添加一个 ApplicationListener
    图片.png

这里面它们面共同组成了 观察者设计模式, 当容器中的某些内部状态发生改变时,ApplicationEventMulticaster 会通知 ApplicationListener。

  • ApplicationEventMulticaster 即(Subject 角色)
  • AbstractApplicationEventMulticaster 或它的子类 SimpleApplicationEventMulticaster
    (Concrete Subject 角色) 实现了 ApplicationEventMulticaster 中的增加,移除,通知观察者的类
  • ApplicationListener (Observer 角色) 提供了一个被调用的方法
  • ApplicationListener的子类则是(Concrete Observer 角色)
    不懂可以看上面的文章。
  1. 第二个for循环和第一个基本一样,不同的是一个是保存 ApplicationListener对象引用,一个是保存对象名这里我就不细讲了。
  2. this.earlyApplicationEvents :这里保存的容器早期发布的一些事件
  3. multicastEvent() :观察者模式中 Subject 角色 通知所有观察者的方法,当 earlyApplicationEvents 不为空时,ApplicationEventMulticaster就调用 multicastEvent(event),通知所有的观察者。源码如下:
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
typescript复制代码@Override
public void multicastEvent(ApplicationEvent event) {
// 调用 resolveDefaultEventType(event) 得到 ResolvableType对象
multicastEvent(event, resolveDefaultEventType(event));
}

@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
Executor executor = getTaskExecutor();
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
if (executor != null) {
executor.execute(() -> invokeListener(listener, event));
}
else {
invokeListener(listener, event);
}
}
}

// 调用 ResolvableType 的静态方法 生成 ResolvableType
private ResolvableType resolveDefaultEventType(ApplicationEvent event) {
return ResolvableType.forInstance(event);
}


public static ResolvableType forInstance(Object instance) {
Assert.notNull(instance, "Instance must not be null");
// 判断这个对象是否属于 ResolvableTypeProvider 如果属于 给它转换成 ResolvableType,当他不等于 null 时,返回
if (instance instanceof ResolvableTypeProvider) {
ResolvableType type = ((ResolvableTypeProvider) instance).getResolvableType();
if (type != null) {
return type;
}
}
// 否则的话直接调用 public static ResolvableType forClass(@Nullable Class<?> clazz) 返回
return ResolvableType.forClass(instance.getClass());
}

5.今天的分析就到这里了,后面没讲完的,我会继续在后续内容中更新。

本文转载自: 掘金

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

SSIS学习使用六:基本的SSIS工作流管理和脚本任务介绍

发表于 2021-11-13

这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战

翻译参考

本文主要参考翻译自 The Stairway to Integration Services 系列文章的 Basic SSIS Workflow Management – Level 6 of the Stairway to Integration Services,目的在于对 SSIS 有一个全面清晰的认识,所有内容在原文的基础上进行实操,由于版本差异、个人疑问等多种原因,未采用完全翻译的原则,同时也会对文中内容进行适当修改,希望最终可以更有利于学习和了解 SSIS,

感谢支持!

基本的 SSIS 工作流管理

介绍

之前的部分,重点介绍了实现增量加载(loading)数据。本篇我们将关注在 SSIS 控制流(SQL Server Integration Services Control Flow)中使用 “优先约束”(Precedence Constraints) 管理工作流。

SSIS控制流包含 任务、容器 和 优先约束(Tasks, Containers, and Precedence Constraints)。任务 和 容器 在SSIS控制流中是可执行的项目;优先级约束 管理何时或是否执行 任务 或 容器。

添加一个包(Package)

开始控制流之前,打开存在的 “FirstSSIS” 项目,右键在解决方案管理器中的 “SSIS包”(SSIS Packages) 虚拟文件夹,点击 “新建SSIS包”(New SSIS Package)

当点击”新建包”后,一个新的名为 “Package1.dtsx” 包被创建并显示。右键 Package1.dtsx 点击 “重命名”,该包会进入 “重命名模式”(rename mode),重命名为 Precedence.dtsx:

添加一个脚本任务Script Task

脚本任务Script Task,通过 C# 或 VB.Net 语言实现编程和更强大功能的扩展!

如在第一个demo中见到的,没有优先约束也是有效的SSIS包。

首先,从 “控制流”工具箱 向 “控制流” 添加脚本任务。单击工具箱中的 脚本任务(Script Task),然后将其拖到 “控制流” 中。

脚本任务可以说是 SQL Server Integration Services 中功能最强大的 SSIS任务。你可以使用它来创建其他SSIS任务中包含的许多功能。还可以使用脚本任务将新功能添加到SSIS控制流中。

右键 “脚本任务” 点击 “编辑”,打开脚本任务编辑器(Script task Editor)

下面将介绍脚本任务功能的基础部分。

首先,你可能想检查 “ScriptLanguage” 属性,此处可以选择在每个脚本任务中使用 Visual Basic 或 C# 作为你的编程语言。

可以通过在 SSDT 的编辑器中,选择 “工具” → “选项” → “商业智能设计器” → “Integration Services设计器”,在 脚本-语言 下拉菜单中更改 “脚本”任务的默认语言。

点击常规页(General page),改变Name属性为”Script Task 1”。

返回脚本页(Script page)中。点击 “ReadOnlyVariables” 属性,点击右侧省略号打开可用变量(available Variables)列表。选择如下显示的 “System::TaskName” 变量。点击“确定”,关闭该选项窗口。

此处我们需要在当前屏幕配置脚本。点击 “编辑脚本”(Edit Script) 按钮。

关于打开 “编辑脚本” 报错的问题

如下,打开时报错”未能加载文件或程序集”

原本以为这个报错是和没有安装完整的”Visual Studio”有关,但是后来测试在其他没有 “完整”VS 的电脑上却没有这个问题。

系统和SQL Server版本、配置都是相同的。进行系统更新和sfc检查等,都没解决这个报错。

在打开的脚本代码编辑中,滚动找到 Main 函数并添加如下代码:

1
2
cs复制代码var sTaskName = Dts.Variables["TaskName"].Value.ToString();
MessageBox.Show(sTaskName + " completed!");

显示如下:

该代码实现的是:第一句创建一个名为 sTaskName 的string类型的变量(C#),并将脚本任务的 ReadOnlyVariables 属性中添加的 System::TaskName 的值赋值给它。通过访问 Dts 命名空间的变量集 获取 SSIS变量值(即Dts.Variables部分)。Variables集合的键为SSIS变量的名称(此处获取 TaskName)。

然后获取属性值,它是一个对象,需要获取其String类型值。

这样就实现了从一个 SSIS变量读取值到”脚本”(Script Task)变量(在C#中)

在SSIS中,变量具有作用域(scope)并属于命名空间。作用域是SSIS变量所属的容器,每一个SSIS变量都与一个命名空间关联。默认有两个命名空间 —— System 和 User。TaskName 属于系统命名空间,用 <NameSpace>::<VariableName> 表示。

变量名在一个给定的作用域和命名空间内是唯一的。也就是,System 命名空间下 TaskName 变量,然后,也可以在 User 命名空间下,添加同样叫 TaskName 的变量

(此处原文有问题,写的是不同命名空间下变量名不可以重复,因为不同命名空间下,都是可以变量名存在相同的[命名空间进行了限定],同一个命名空间下变量名不可以重复,不同命名空间下变量名互不影响)

下一句显示一个包含任务名(来自 sTaskName 变量)的消息提示框。

点击右上角的”X”,关闭脚本任务代码编辑器(Script Task Code Editor)。脚本会自动保存。在 脚本任务编辑器(Script Task Editor) 中点击确定(OK),完成脚本任务配置。

下面进行测试。点击 “启动调试” 或 F5 运行SSIS包。将会看到如下消息框:

停止调试运行。

添加第二个脚本任务

添加 TaskName 变量似乎需要很多额外的工作。不过此处有更好的方法。右键点击 “Script Task 1”,点击复制(“Copy”),然后 右键 点击 除”Script Task 1”之外 的控制流的空白部分,点击粘贴(“Paste”)。一个新脚本任务添加到控制流中。名字为Script Task 1 1。将其重命名为”Script Task 2”

重新执行SSIS包,将会显示两次消息框。

使用优先约束(Cases 0 和 1)

前面的示例是一个”用例”(use case)例子。该use case演示了没有优先约束的情况。可以将此称为 “用例0”(Use Case 0)。

对于当前正在操作的”用例1”(Use Case 1),我们在 Script Task 1 和 Script Task 2 之间添加优先约束。

点击Script Task 1,Script Task 1底部扩展出来的绿色箭头就是一个优先约束(Precedence Constraint)。点击并拖拽优先约束到Script Task 2。

运行SSIS包,Script Task 1 会首先执行,然后再执行 Script Task 2。

另外需要注意的一点是 优先约束 的类型是 OnSuccess,即优先级”评估”(evaluate)之前,必须先完成并成功执行上一个任务 —— Script Task 1。

停止当前的调试运行。

这里的语义(semantics)很重要。优先级约束始于 Script Task 1,终止于 Script Task 2。始发任务连接到优先级约束起点,该起点具有“半气泡”(half-bubble)特性,在该起点连接到始发任务。端点在连接到 “终止任务”(terminating task) 的地方有一个箭头。

优先约束评估(Precedence constraints evaluate)。它们测试一个或多个条件,如果条件为真,则约束条件允许 “终止任务” 开始执行。

“单线程”(Single-Threading)

“单线程”描述为一次做一件事情的一种方式。在 SSIS 控制流中使用一个优先约束是一种实现单线程的方式。

使用优先约束是在控制流中完成确定性工作流(deterministic workflow)的唯一方法。

SSIS控制流程中是否可以使用不确定的工作流(non-deterministic workflow)?答案是”是的”。右击连接 Script Task 1 和 Script Task 2 的优先约束,然后点击”删除”。点击工作流空白处,按下 F4 键(或者直接在右侧找到”属性”),查看包属性。

MaxConcurrentExecutables属性控制SSIS控制流中可以同时执行多少个可执行文件,默认值为-1。SSIS允许的并发执行的默认数量 等于 检测到的CUP核心数量加2。例如:如果你有一个4核的服务器,SSIS将会允许6个并发执行。

设置 MaxConcurrentExecutables 属性为1。

重新执行SSIS包。现在一次仅仅执行一个脚本任务。这个顺序是非确定的(non-deterministic)并因此不可预测的(unpredictable)。

停止调试,重设 MaxConcurrentExecutables 属性为-1,并保存。

总结

本篇中,我们添加一个新的SSIS包到解决方案中,构建我们第一个脚本任务,并通过 “OnSuccess” 优先约束连接它们。测试了从 SSIS脚本任务 内部读取SSIS变量值,查看和设置了 SSIS包 的 MaxConcurrentExecutables 属性。

本文转载自: 掘金

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

Spring Boot(九):自定义 Starter 🍺

发表于 2021-11-13

「这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战」。

starter 是 Spring Boot 中一种非常重要的机制,它可以将繁杂的配置统一集成到 starter 中,我们只需要通过 maven 将 starter 依赖导入到项目中,Spring Boot 就能自动扫描并加载相应的默认配置。

starter 的出现让开发人员从繁琐的框架配置中解放出来,将更多的精力专注于业务逻辑的开发,极大地提高了开发效率。

在一些特殊情况下,我们也可以将一些通用功能封装成自定义的 starter 进行使用,以下详细介绍如何自定义 starter。

  1. 命名规范

Spring Boot 提供的 starter 以 spring-boot-starter-xxx 形式命名。为了与 Spring Boot 生态提供的 starter 区分,官方建议第三方开发者或技术(例如 Druid、Mybatis 等等)厂商自定义的 starter 使用 xxx-spring-boot-starter 的形式命名,例如 mybatis-spring-boot-starter、druid-spring-boot-starter 等等。

  1. 模块规范

Spring Boot 官方建议我们在自定义 starter 时,创建两个 Module: autoConfigure Module 和 starter Module,其中 starter Module 依赖于 autoConfigure Module。当然,这只是 Spring Boot 官方的建议,并不是硬性规定,若不需要自动配置代码和依赖项目分离,我们也可以将它们组合到同一个 Module 里。

3 分析第三方 starter

若要自定义 starter,我们可以从 mybatis-spring-boot-starter 与 mybatis-spring-boot-autoconfigure 中获取一些启发:

mybatis-spring-boot-starter 仅仅将其下引入的依赖整合到一起并对外提供一个依赖坐标(同时依赖于 mybatis-spring-boot-autoconfigure),方便外部引用(解决版本冲突):

MybatisAutoConfiguration 是 mybatis-spring-boot-starter 的自动配置类,其中又加载了 MybatisProperties 配置类信息:

该自动配置类必须被 Spring 容器所识别并加载,才能加载其中 @Bean 定义的 Bean 组件,这时就需要依靠 Spring Boot 启动时加载的 spring.factories 配置文件来标识 MybatisAutoConfiguration 自动配置类,从而加载其中的 Bean。

大致了解 mybatis-spring-boot-starter 的起步依赖后,我们就可以来自定义所需的 starter 了。

⭐如果对自动配置原理仍有疑惑,可参考下该篇博客:juejin.cn/post/700560…

  1. 实现自定义 Starter

4.1 需求

自定义 scorpion-redis-spring-boot-starter,要求当导入 redis 坐标时,Spring Boot 自动创建 Jedis 的 Bean。

4.2 实现步骤

  1. 创建 scorpion-redis-spring-boot-autoconfigure 模块
  2. 创建 scorpion-redis-spring-boot-starter 模块,并依赖于 scorpion-redis-spring-boot-autoconfigure 模块
  3. 在 scorpion-redis-spring-boot-autoconfigure 模块中初始化 Jedis 的 Bean,并定义 META-INF/spring.factories 文件
  4. 在测试模块中引入自定义的 scorpion-redis-spring-boot-starter 依赖,测试获取 Jedis 的 Bean,操作 redis。

4.3 创建&开发 autoconfigure 模块

4.3.1 创建 scorpion-redis-spring-boot-autoconfigure 项目

创建一个 maven 模块:

4.3.2 修改 pom.xml

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

<modelVersion>4.0.0</modelVersion>

<groupId>com.wyk</groupId>
<artifactId>scorpion-redis-spring-boot-autoconfigure</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>

<!--如果是Maven项目需要手动指定父项目-->
<!--如果是Spring Boot项目则会自动引入该父项目坐标-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
<relativePath/>
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 引入Jedis依赖 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
</dependencies>

</project>

4.3.3 编写 RedisAutoConfiguration

  • @ConditionalOnClass(Jedis.class) : 当项目中存在 Jedis 类(pom.xml 引入该依赖),则可以加载 RedisAutoConfiguration 自动配置类,因为 return new Jedis() 需要使用到 Jedis.class!
  • @ConditionalOnMissingBean(name = "jedis") : 当 Spring 容器中已存在 Jedis Bean,且 name = "jedis",那么则无需再重复定义一个 Jedis Bean 通过 @Bean 返回到 Spring 容器中了!

注:不要把 pom.xml 文件引入的类与 spring 容器中的类混淆了!

pom.xml 引入的是外部类,无法通过 @Component、``@Repository、@Controller、@Service等注解将其注入到 Spring 容器中,只能通过@Bean` 将其注入到 Spring 容器。

Spring 容器:容器是Spring框架实现功能的核心,容器不只是帮我们创建了对象那么简单,它负责了对象整个的生命周期的管理——创建、装配、销毁。关于 Spring 的这个容器你最常听闻的一个术语就是 IOC 容器。总之一句话,我的应用程序里不用再过问对象的创建和管理对象之间的依赖关系了,都由 IOC 容器代劳,也就是说,我把对象创建、管理的控制权都交给 Spring 容器,这是一种控制权的反转,所以 Spring 容器才能称为 IOC 容器。不过这里要厘清一点:并不是说只有 Spring 的容器才叫 IOC 容器,基于 IOC 容器的框架还有很多,并不是 Spring 特有的。

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
java复制代码package com.wyk.autoconfigure;

import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.Jedis;

/**
* @Author Author Unknown
* @Date 2021/11/11 7:41
* @Description RedisAutoConfiguration: 自动配置类
*/
@Configuration
@EnableConfigurationProperties(RedisProperties.class)
@ConditionalOnClass(Jedis.class)
public class RedisAutoConfiguration {

@Bean
@ConditionalOnMissingBean(name = "jedis")
public Jedis jedis(RedisProperties redisProperties) {
return new Jedis(redisProperties.getHost(), redisProperties.getPort());
}
}

4.3.4 编写 RedisProperties

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
java复制代码package com.wyk.autoconfigure;

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
* @Author Author Unknown
* @Date 2021/11/11 7:42
* @Description Redis: 配置信息类
*/
@ConfigurationProperties(prefix = RedisProperties.REDIS_PREFIX)
public class RedisProperties {

public static final String REDIS_PREFIX = "redis";

private String host = "localhost";
private int port = 6379;

public String getHost() {
return host;
}

public void setHost(String host) {
this.host = host;
}

public int getPort() {
return port;
}

public void setPort(int port) {
this.port = port;
}
}

4.3.5 定义 META-INF/spring.factories

1
2
properties复制代码org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.wyk.autoconfigure.RedisAutoConfiguration

4.3.6 完整的模块目录

4.4 创建&开发 starter 模块

4.4.1 创建 scorpion-redis-spring-boot-starter 模块

与 autoconfigure 同理,不再赘述:

4.4.2 修改 pom.xml

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.wyk</groupId>
<artifactId>scorpion-redis-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>scorpion-redis-spring-boot-starter</name>
<description>Demo project for Spring Boot</description>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
<relativePath/>
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--引入scorpion-redis-spring-boot-autoconfigure-->
<dependency>
<groupId>com.wyk</groupId>
<artifactId>scorpion-redis-spring-boot-autoconfigure</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>

4.4.3 完整的模块目录

4.5 测试模块引入并测试自定义 Starter

4.5.1 pom.xml

1
2
3
4
5
6
xml复制代码<!-- 测试自定义 Starter: scorpion-redis-spring-boot-starter: 所以引入该依赖 -->
<dependency>
<groupId>com.wyk</groupId>
<artifactId>scorpion-redis-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

4.5.2 Spring Boot 启动类中测试

EnableStarterApplication.java:

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
java复制代码package com.enable.starter;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
//import org.springframework.context.annotation.Bean;
import redis.clients.jedis.Jedis;

@SpringBootApplication
public class EnableStarterApplication {

public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(EnableStarterApplication.class, args);

Jedis jedis = context.getBean(Jedis.class);
System.out.println(jedis);

jedis.set("name", "kun");
String name = jedis.get("name");
System.out.println(name);
}

// 如果在这里将 Jedis 加载到 Spring 容器中,则不会加载 RedisAutoConfiguration 中的 Jedis Bean! ==> 即不会输出 "RedisAutoConfiguration.."(定义在RedisAutoConfiguration中)
// @Bean
// public Jedis jedis() {
// return new Jedis("localhost", 6379);
// }

}

配置类生效:

自定义 Starter 测试成功!

希望本文对你有所帮助🧠

欢迎在评论区留下你的看法🌊,我们一起讨论与分享🔥

本文转载自: 掘金

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

2021 阿里云 CentOS77 1核1G LNMP 装

发表于 2021-11-13

阿里云 Centos 7.7 1核1G LNMP 装机

原文地址 : 3pub.cn/index.php/a…
之前写的 腾讯云 Centos 安装 PHP 开发环境 文章时间久了最近新买服务器重新装机踩坑

装机很艰辛呀 ! 最初操作系统选择Centos6.5 内存 512MB 在装机过程中遇到各种坑, 填起来太过费劲 yum源过期 . php7.4 sqllite3 还要有编译时各种内存不足问题. 上面说的问题最好的解决方案就是 加钱 升级配置

系统查看

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
shell复制代码$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 1
On-line CPU(s) list: 0
Thread(s) per core: 1
Core(s) per socket: 1
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 85
Model name: Intel(R) Xeon(R) Platinum 8269CY CPU @ 2.50GHz
Stepping: 7
CPU MHz: 2500.000
BogoMIPS: 5000.00
Hypervisor vendor: KVM
Virtualization type: full
L1d cache: 32K
L1i cache: 32K
L2 cache: 1024K
L3 cache: 36608K
NUMA node0 CPU(s): 0
Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl nonstop_tsc eagerfpu pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch invpcid_single fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm mpx avx512f avx512dq rdseed adx smap avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 arat avx512_vnni

$ cat /etc/redhat-release
CentOS Linux release 7.7.1908 (Core)
$ getconf LONG_BIT
64 #

Yum 安装依赖

1
2
3
shell复制代码$ yum install -y gcc gcc-c++ autoconf libjpeg libjpeg-devel enchant-devel pam-devel  libc-client libc-client-devel libpng libpng-devel freetype freetype-devel libpng libpng-devel libxml2 libxml2-devel zlib zlib-devel glibc glibc-devel  bzip2 bzip2-devel ncurses curl openssl-devel gdbm-devel db4-devel libXpm-devel  libX11-devel gd-devel gmp-devel readline-devel libxslt-devel expat-devel  xmlrpc-c xmlrpc-c-devel sqlite-devel libaio

$ yum install -y glibc.i686 or yum install -y glibc.i386 # 可以不装

创建swap 分区

通过free -m来查看下内存使用状况

1
2
3
4
shell复制代码$ free -m
total used free shared buff/cache available
Mem: 990 271 616 0 102 597
Swap: 0 0 0

创建一个2GB大小的文件

1
2
3
4
5
shell复制代码$ mkdir -p /opt/images
$ dd if=/dev/zero of=/opt/images/swap bs=1024 count=2048000
2048000+0 records in
2048000+0 records out
2097152000 bytes (2.1 GB) copied, 18.6413 s, 113 MB/s

把创建的文件变成SWAP分区

1
2
3
shell复制代码$ mkswap /opt/images/swap
Setting up swapspace version 1, size = 2047996 KiB
no label, UUID=56051496-4f81-47f0-85bf-e4d850714726

启用这个SWAP文件

1
2
shell复制代码$ swapon /opt/images/swap
swapon: /opt/images/swap: insecure permissions 0644, 0600 suggested.
1
2
3
4
shell复制代码$ free -m
total used free shared buff/cache available
Mem: 990 274 62 0 653 572
Swap: 1999 0 1999

阿里云的源有坑需要按照下面方法修改

/etc/yum.repos.d/目录下的CentOS-Base.repo、epel.repo文件进行修改

1
2
3
4
5
6
7
8
9
shell复制代码把CentOS-Base.repo文件中的以下网址
http://mirrors.aliyun.com/centos/
http://mirrors.aliyuncs.com/centos/
http://mirrors.cloud.aliyuncs.com/centos/
修改成
http://mirrors.aliyun.com/centos-vault/centos/

把epel.repo文件中的
enabled=1修改enabled=0

创建用户及用户组

1
2
shell复制代码$ groupadd www
$ useradd -g www www #创建www用户到www用户组

Nginx

1
2
3
shell复制代码$ cd /usr/local/src/
$ wget http://nginx.org/download/nginx-1.9.8.tar.gz
$ tar -xf nginx-1.9.8.tar.gz

下载 pcre 并编译: www.linuxfromscratch.org/blfs/view/s…

1
2
3
4
5
shell复制代码$ wget https://downloads.sourceforge.net/pcre/pcre-8.41.tar.bz2
$ tar jxf pcre-8.41.tar.bz2
$ cd pcre-8.41
$ ./configure --prefix=/usr/local/pcre --enable-utf8 --enable-unicode-properties
$ make && make install

开始加载的nginx 模块及编译参数

1
2
3
shell复制代码$ cd /usr/local/src/nginx-1.9.8
$ ./configure --prefix=/usr/local/nginx --user=www --group=www --with-http_stub_status_module --with-http_ssl_module --with-pcre=/usr/local/src/pcre-8.41
$ make && make install

设置软链 nginx 支持全局调用

1
2
3
4
shell复制代码$ ln -s /usr/local/nginx/sbin/nginx  /usr/bin/nginx
$ nginx -t
nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful

Redis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
shell复制代码$ cd /usr/local/src
$ wget http://download.redis.io/releases/redis-6.2.2.tar.gz
$ tar xf redis-6.2.2.tar.gz
$ cd redis-6.2.2
$ mkdir -p /usr/local/redis/bin
$ mkdir -p /usr/local/redis/etc
$ make && make install

make[1]: Leaving directory `/usr/local/src/redis-6.2.2/src'

$ cp redis.conf /usr/local/redis/etc/
$ cd /usr/local/src/redis-6.2.2/src
$ mv mkreleasehdr.sh redis-benchmark redis-check-aof redis-cli redis-server redis-check-rdb redis-sentinel redis-trib.rb /usr/local/redis/bin

$ ln /usr/local/redis/bin/redis-server /usr/bin/redis-server
$ ln /usr/local/redis/bin/redis-cli /usr/bin/redis-cli
# 配置 守护进程及密码 vim /usr/local/redis/etc/redis.conf
# requirepass 密码 // 注释打开
# daemonize no 改 daemonize yes
$ redis-server /usr/local/redis/etc/redis.conf
$ netstat -tunple # 检测6379 进程是否被redis-server 占用

Mysql

卸载·mysql·

1
2
3
4
5
6
shell复制代码$  rpm -aq|grep -i 'mysql'; # 检索输出的所有mysql 相关 下面进行卸载
$ rpm -e mysql-community-libs-compat-5.7.21-1.el6.x86_64
$ rpm -e mysql-community-client-5.7.21-1.el6.x86_64
...
# 删除mysql的服务:chkconfig --list|grep -i mysql
$ chkconfig --del mysql
1
2
3
4
5
6
7
8
9
shell复制代码$ cd /usr/local/src
# 服务端及相关工具
$ wget https://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-community-server-5.7.33-1.el7.x86_64.rpm;
# 客户端及相关工具
$ wget https://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-community-client-5.7.33-1.el7.x86_64.rpm;
# 服务端和客户端的公共文件
$ wget https://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-community-common-5.7.33-1.el7.x86_64.rpm;
# 客户端共享库
$ wget https://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-community-libs-5.7.33-1.el7.x86_64.rpm;

按照以下顺序进行安装,因为它们之间存在依赖关系

1
2
3
4
5
shell复制代码# common --> libs --> clients --> server
$ rpm -ivh mysql-community-common-5.7.33-1.el7.x86_64.rpm --force --nodeps
$ rpm -ivh mysql-community-libs-5.7.33-1.el7.x86_64.rpm --force --nodeps
$ rpm -ivh mysql-community-client-5.7.33-1.el7.x86_64.rpm --force --nodeps
$ rpm -ivh mysql-community-server-5.7.33-1.el7.x86_64.rpm --force --nodeps

安装时抛错 –force –nodeps 完美解决 原因:这是由于yum安装了旧版本的GPG keys造成的

参考地址: www.cnblogs.com/royfans/p/7…

检测安装

1
2
3
4
5
6
7
8
shell复制代码$ which mysql
/usr/bin/mysql
$ mysql -V

mysql Ver 14.14 Distrib 5.7.33, for Linux (x86_64) using EditLine wrapper
Installing MySQL system tables..../bin/mysqld: error while loading shared libraries: libaio.so.1: cannot open shared object file: No such file or directory

解决办法:yum install -y libaio

启动mysql服务

1
shell复制代码$ systemctl start mysqld

启动服务 异常抛错 检测错误日志

1
2
3
shell复制代码$ tail -n 100 /var/log/mysqld.log
2021-04-26T20:03:15.474974Z 0 [Warning] Failed to open optimizer cost constant tables
2021-04-26T20:03:15.475065Z 0 [ERROR] Fatal error: Can't open and lock privilege tables: Table 'mysql.user' doesn't exist

修改 my.cnf datadir=/var/lib/mysql 改为 datadir=/opt/mysql/data

修改Mysql 密码

查看临时密码

1
2
3
4
shell复制代码$ grep password /var/log/mysqld.log
2021-04-23T21:34:34.260324Z 1 [Note] A temporary password is generated for root@localhost: Dn.Z-4srP1)A
$ mysql -h root -p
set password = password("asasd");

PHP

安装php7.4 和 php8

1
2
3
4
5
6
7
shell复制代码$ cd /usr/local/src
$ wget https://www.php.net/distributions/php-7.4.16.tar.gz --no-check-certificate
$ tar zxf php-7.4.16.tar.gz
$ wget https://www.php.net/distributions/php-8.0.3.tar.gz --no-check-certificate
$ tar xzf php-8.0.3.tar.gz
$ mkdir -p /usr/local/php7
$ mkdir -p /usr/local/php8

安装 libmcrypt 库

1
2
3
4
5
6
shell复制代码$ cd /usr/local/src
$ wget ftp://mcrypt.hellug.gr/pub/crypto/mcrypt/libmcrypt/libmcrypt-2.5.7.tar.gz
$ tar xf libmcrypt-2.5.7.tar.gz
$ cd libmcrypt-2.5.7
$ ./configure
$ make && make install

异常:configure: WARNING: unrecognized options: –with-mcrypt php7.2 不支持以上三个选项,删掉即可

解决安装问题: libxml2 configure: error: xml2-config not found. Please check your libxml2 installation.

1
2
3
4
5
6
shell复制代码$ cd /usr/local/src
$ wget ftp://xmlsoft.org/libxml2/libxml2-2.9.1.tar.gz
$ tar zxf libxml2-2.9.1.tar.gz
$ cd libxml2-2.9.1
$ ./configure
$ make && make install

安装tclsh方法

下载源码 到 www.tcl.tk/software/tc… 下载 tcl8.5.19-src.tar.gz
解压 tar -xzvf tcl8.5.19-src.tar.gz,比如下载放在了/usr/local/src目录
解压安装 如下,安装到usr/tcl目录下

1
2
3
4
5
6
7
shell复制代码$ wget https://prdownloads.sourceforge.net/tcl/tcl8.5.19-src.tar.gz
$ tar -xzvf tcl8.5.19-src.tar.gz
$ cd /usr/local/src/tcl8.5.19/unix
$ ./configure --prefix=/usr/tcl
$ make && make install

创建软链接:创建快捷名字tclsh,放到usr/bin下面 ln /usr/tcl/bin/tclsh8.5 /usr/bin/tclsh

开始编译PHP

PHP 7.4

1
2
3
shell复制代码$ cd /usr/local/src/php
$ cd /usr/local/src/php-7.4.16
$ ./configure --prefix=/usr/local/php7/ --with-config-file-path=/usr/local/php7/etc --with-config-file-scan-dir=/usr/local/php7/etc/conf.d --enable-fpm --with-fpm-user=www --with-fpm-group=www --enable-soap --with-openssl --with-openssl-dir --with-zlib --with-iconv --with-bz2 --enable-calendar --with-curl --with-cdb --enable-dom --enable-exif --enable-ftp --enable-gd --with-gettext --with-gmp --with-mhash --enable-mbstring --enable-pdo --with-pdo-mysql --with-zlib-dir --with-readline --enable-session --enable-shmop --enable-simplexml --enable-sockets --enable-sysvmsg --enable-sysvsem --enable-sysvshm --with-xsl --enable-mysqlnd --with-mysqli --without-pear --enable-pcntl --enable-bcmath

编译抛错 No package 'oniguruma' found 参考: www.limstash.com/articles/20…

解决方案 :

1
2
3
4
5
6
shell复制代码$ yum install https://rpms.remirepo.net/enterprise/7/remi/x86_64/oniguruma5php-6.9.6-1.el7.remi.x86_64.rpm
$ yum install https://rpms.remirepo.net/enterprise/7/remi/x86_64/oniguruma5php-devel-6.9.6-1.el7.remi.x86_64.rpm

$ make && make install

编译前一定保证内存足够, 内存不足

PHP 8.0

1
2
3
4
5
6
7
8
shell复制代码$ cd /usr/local/src/php-8.0.3
$ ./configure --prefix=/usr/local/php8/ --with-config-file-path=/usr/local/php8/etc --with-config-file-scan-dir=/usr/local/php8/etc/conf.d --enable-fpm --with-fpm-user=www --with-fpm-group=www --enable-soap --with-openssl --with-openssl-dir --with-zlib --with-iconv --with-bz2 --enable-calendar --with-curl --with-cdb --enable-dom --enable-exif --enable-ftp --enable-gd --with-gettext --with-gmp --with-mhash --enable-mbstring --enable-pdo --with-pdo-mysql --with-zlib-dir --with-readline --enable-session --enable-shmop --enable-simplexml --enable-sockets --enable-sysvmsg --enable-sysvsem --enable-sysvshm --with-xsl --enable-mysqlnd --with-mysqli --without-pear --enable-pcntl --enable-bcmath

$ make && make install
$ php8 --version
PHP 8.0.3 (cli) (built: Apr 27 2021 05:45:18) ( NTS )
Copyright (c) The PHP Group
Zend Engine v4.0.3, Copyright (c) Zend Technologies

安装PHP扩展

redis 扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
shell复制代码$ wget https://pecl.php.net/get/redis-5.3.4.tgz
$ tar xf redis-5.3.4.tgz
$ cd redis-5.3.4
$ /usr/local/php7/bin/phpize
Configuring for:
PHP Api Version: 20190902
Zend Module Api No: 20190902
Zend Extension Api No: 320190902

$ ./configure --with-php-config=/usr/local/php7/bin/php-config
$ make && make install
...
Installing shared extensions: /usr/local/php7/lib/php/extensions/no-debug-non-zts-20190902/

$ make clean
$ /usr/local/php8/bin/phpize
$ ./configure --with-php-config=/usr/local/php8/bin/php-config
$ make && make install
...
Installing shared extensions: /usr/local/php8/lib/php/extensions/no-debug-non-zts-20200930/

mongodb扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
shell复制代码$ wget https://pecl.php.net/get/mongodb-1.9.1.tgz
$ tar xf mongodb-1.9.1.tgz
$ cd mongodb-1.9.1

$ ./configure --with-php-config=/usr/local/php7/bin/php-config
$ make && make install
...
Installing shared extensions: /usr/local/php7/lib/php/extensions/no-debug-non-zts-20190902/

$ make clean
$ /usr/local/php8/bin/phpize
$ ./configure --with-php-config=/usr/local/php8/bin/php-config
$ make && make install
...
Installing shared extensions: /usr/local/php8/lib/php/extensions/no-debug-non-zts-20200930/

Swoole 扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
shell复制代码$ wget https://pecl.php.net/get/swoole-4.6.6.tgz
$ /usr/local/php7/bin/phpize
$ ./configure --with-php-config=/usr/local/php7/bin/php-config
$ make && make install
Installing shared extensions: /usr/local/php7/lib/php/extensions/no-debug-non-zts-20190902/
Installing header files: /usr/local/php7/include/php/

$ make clean
$ /usr/local/php8/bin/phpize
$ ./configure --with-php-config=/usr/local/php8/bin/php-config
$ make && make install
Installing shared extensions: /usr/local/php8/lib/php/extensions/no-debug-non-zts-20200930/
Installing header files: /usr/local/php8/include/php/

php.ini 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
shell复制代码$ vim /usr/local/php7/etc/php.ini

extension=/usr/local/php7/lib/php/extensions/no-debug-non-zts-20190902/swoole.so
extension=/usr/local/php7/lib/php/extensions/no-debug-non-zts-20190902/redis.so
extension=/usr/local/php7/lib/php/extensions/no-debug-non-zts-20190902/mongodb.so

$ vim /usr/local/php8/etc/php.ini

extension=/usr/local/php8/lib/php/extensions/no-debug-non-zts-20200930/swoole.so
extension=/usr/local/php8/lib/php/extensions/no-debug-non-zts-20200930/redis.so
extension=/usr/local/php8/lib/php/extensions/no-debug-non-zts-20200930/mongodb.so

disable_functions = pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,phpinfo,eval,passthru,exec,system,chroot,chgrp,chown,shell_exec,proc_open,proc_get_status,ini_alter,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru,stream_socket_server,fsocket,copy,extract,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,phpinfo,eval,passthru,exec,system,chroot,chgrp,chown,shell_exec,proc_open,proc_get_status,ini_alter,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru,stream_socket_server,fsocket,copy,extract

启动服务

1
2
3
4
5
6
7
8
9
shell复制代码# 启动redis
$ redis-server /usr/local/redis/etc/redis.conf
# 启动php-fpm 默认9000端口
$ /usr/local/php7/sbin/php-fpm -c /usr/local/php7/etc/php.ini -y /usr/local/php7/etc/php-fpm.conf # -t 参数检测
# 启动nginx
$ nginx # nginx -t 检测

# 启动mysql 上面已经启动
$ systemctl start mysqld

本文转载自: 掘金

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

MySQL / MariaDB 触发器的创建、使用、查看、删

发表于 2021-11-13

本文首发:kalacloud.com/blog/how-to…

触发器(Trigger)是 MySQL 中非常实用的一个功能,它可以在操作者对表进行「增删改」 之前(或之后)被触发,自动执行一段事先写好的 SQL 代码。

本教程带领大家在实践中学习,你将学到触发器在实际应用场景中的重要应用。

在这个教程中,你是「卡拉云银行」的程序员,你正在搭建一套银行客户管理系统。在这套系统中,你需要设置在INSERT 表之前检测操作者是否输入错误数据、在 UPDATE 时,记录操作者的行为 log ,以及在DELETE 时,判断删除的信息是否符合删除规则。 这三类操作都可以使用 MySQL 触发器来实现。

如果你正在数据库的基础上搭建一套数据库管理工具或企业内部工具,推荐你试试我开发的卡拉云,详情见后文。

本教程将带你一起实践的案例

  • BEFORE INSERT : 在插入数据前,检测插入数据是否符合业务逻辑,如不符合返回错误信息。
  • AFTER INSERT : 在表 A 创建新账户后,将创建成功信息自动写入表 B 中。
  • BEFORE UPDATE :在更新数据前,检测更新数据是否符合业务逻辑,如不符合返回错误信息。
  • AFTER INSERT :在更新数据后,将操作行为记录在 log 中
  • BEFORE DELETE :在删除数据前,检查是否有关联数据,如有,停止删除操作。
  • AFTER DELETE :删除表 A 信息后,自动删除表 B 中与表 A 相关联的信息。

先决条件

在开始之前,请确保您具备以下条件:

  • 一台配置好的 Ubuntu 服务器,root 账号。
  • 服务器上配置好 MySQL Server(配置 MySQL 请看MySQL安装及连接MySQL教程)
  • MySQL root 账号

创建示例数据库

我们先创建一个干净的示例数据库,方便大家可以跟随本教程一起实践。我们会在这个数据库中演示 MySQL 触发器的多种工作方式。

首先,以 root 身份登录到你的 MySQL 服务器:

1
sql复制代码mysql -u root -p

出现提示时,请输入你 MySQL root 账号的密码,然后点击 ENTER 继续。看到 mysql> 提示后,运行以下命令,创建 demo_kalacloud 数据库:

1
sql复制代码CREATE database demo_kalacloud;
1
2
java复制代码Output
Query OK, 1 row affected (0.00 sec)

接下来,切换到新建的 demo_kalacloud 数据库:

1
sql复制代码USE demo_kalacloud;
1
2
复制代码Output
Database changed

接着创建一个 customers 表。我们使用这个表记录银行客户的信息。这个表包括 customer_id,customer_name,和level。咱们先把客户分为两个级别:BASIC和VIP。

1
2
3
4
5
sql复制代码create table customers(
customer_id BIGINT PRIMARY KEY,
customer_name VARCHAR(50),
level VARCHAR(50)
) ENGINE=INNODB;
1
2
java复制代码Output
Query OK, 0 rows affected (0.01 sec)

接着,我们向 customers 表中添加一些客户记录。

1
2
3
4
sql复制代码
Insert into customers (customer_id, customer_name, level )values('1','Jack Ma','BASIC');
Insert into customers (customer_id, customer_name, level )values('2','Robin Li','BASIC');
Insert into customers (customer_id, customer_name, level )values('3','Pony Ma','VIP');

分别运行三个 INSERT 命令后,命令行输出成功信息。

1
2
java复制代码Output
Query OK, 1 row affected (0.01 sec)

我们使用 SELECT 检查一下三条信息是否已经写入表中:

1
sql复制代码Select * from customers;

显示 customers 表

下面我们创建另一个表customer_status,用于保存 customers 表中客户的备注信息。

这个表包含 customer_id 和 status_notes 字段:

1
sql复制代码Create table customer_status(customer_id BIGINT PRIMARY KEY, status_notes VARCHAR(50)) ENGINE=INNODB;

然后,我们再创建一个 sales 表,这个表与 customer_id 关联。保存与客户有关的销售数据。

1
sql复制代码Create table sales(sales_id BIGINT PRIMARY KEY, customer_id BIGINT, sales_amount DOUBLE ) ENGINE=INNODB;
1
2
java复制代码Output
Query OK, 0 rows affected (0.01 sec)

最后一步,我们再建一个 audit_log 表,用来记录操作员操作「卡拉云银行」客户管理系统时的操作行为。方便管理员在发生问题时,有 log 可查。

1
sql复制代码Create table audit_log(log_id BIGINT PRIMARY KEY AUTO_INCREMENT, sales_id BIGINT, previous_amount DOUBLE, new_amount DOUBLE, updated_by VARCHAR(50), updated_on DATETIME ) ENGINE=INNODB;
1
2
java复制代码Output
Query OK, 0 rows affected (0.02 sec)

至此,你作为「卡拉云银行」的程序员,已经把客户管理系统的demo_kalacloud 数据库和四张表建立完成。接下来,我们将对这个管理系统的关键节点增加对应的触发器。

扩展阅读:《如何使用 MySQL 慢查询日志进行性能优化 - Profiling、mysqldumpslow 实例详解》

1.BEFORE INSERT触发器使用方法

作为严谨的银行客户管理系统,对任何写入系统的数据都应该提前检测,以防止错误的信息被写进去。

在写入前检测数据这个功能,我们可以使用BEFORE INSERT 触发器来实现。

在操作者对 sales 表中的sales_amount 字段进行写操作时,系统将在写入(INSERT)前检查数据是否符合规范。

我们先来看一下,创建触发器的基本语法。

1
2
3
4
5
6
sql复制代码DELIMITER //
CREATE TRIGGER [触发器的名字]
[触发器执行时机] [触发器监测的对象]
ON [表名]
FOR EACH ROW [触发器主体代码]//
DELIMITER ;

触发器的结构包括:

  • DELIMITER //:MySQL 默认分隔符是; 但在触发器中,我们使用 // 表示触发器的开始与结束。
  • [触发器的名字]:这里填写触发器的名字
  • [触发器执行时机]:这里设置触发器是在关键动作执行之前触发,还是执行之后触发。
  • [触发器监测的对象]:触发器可以监测 INSERT、UPDATE、DELETE 的操作,当监测的命令对触发器关联的表进行操作时,触发器就被激活了。
  • [表名]:将这个触发器与数据库中的表进行关联,触发器定义在表上,也附着在表上,如果这个表被删除了,那么这个触发器也随之被删除。
  • FOR EACH ROW:这句表示只要满足触发器触发条件,触发器都会被执行,也就是说带上这个参数后,触发器将监测每一行对关联表操作的代码,一旦符合条件,触发器就会被触发。
  • [触发器主体代码]:这里是当满足触发条件后,被触发执行的代码主体。这里可以是一句 SQL 语句,也可以是多行命令。如果是多行命令,那么这些命令要写在 BEGIN...END 之间。

**注:**在创建触发器主体时,还可以使用OLD和NEW 来获取 SQL 执行INSERT,UPDATE和DELETE 操作前后的写入数据。这里没看明白没关系,我们将会在接下来的实践中,展开讲解。

讲到这里,大家看了一大堆云里雾里的概念,如果没看懂,也别担心。接下来进入实践环节,只要跟着贴代码看返回结果,很快你就能够通透理解触发器了。

现在,我们来创建第一个触发器,BEFORE INSERT (在执行 insert 之前,执行触发器)。这个触发器用于监测操作者在写入 sales 表中的 sales_amount 值时,这个值是否大于 10000 ,如果大于,那么返回错误信息进行报错。

登录 MySQL Server 后,我们创建一个触发器:

1
2
3
4
5
6
7
8
9
10
sql复制代码DELIMITER //
CREATE TRIGGER validate_sales_amount
BEFORE INSERT
ON sales
FOR EACH ROW
IF NEW.sales_amount>10000 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = "你输入的销售总额超过 10000 元。";
END IF//
DELIMITER ;

上面这段代码中,我们使用IF...THEN...END IF 来创建一个监测 INSERT 语句写入的值是否在限定的范围内的触发器。

这个触发器的功能时监测 INSERT 在写入sales_amount 值时,这个新增的(NEW)值是否符合条件( > 10000)。

当操作员录入一个超过 10000 的数字,会返回如下错误信息:

1
2
ini复制代码SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '你输入的销售总额超过 10000 元。';

我们来试试看,看看触发器是否已启用。

我们向 sales_amount 中插入一条 11000 的值。

1
sql复制代码Insert into sales(sales_id, customer_id, sales_amount) values('1','1','11000');

ERROR 1644 触发器不允许写入

命令行返回错误信息,这就是我们刚刚创建触发器时,填入的错误信息。与我们的设置一致。

下面我们 insert 一个值小于 10000 的数字:

1
sql复制代码Insert into  sales(sales_id, customer_id, sales_amount) values('1','1','7700');

输入值为 7700 小于设定的 10000 ,insert 命令执行成功。

1
2
java复制代码Output
Query OK, 1 row affected (0.01 sec)

我们调出 sales 表,看看是否插入成功:

1
sql复制代码Select * from sales;

输出确认数据在表中:

![确认数据在表中]](kalacloud.com/static/6a4d…)

通过这张表,我们可以看到,7700 已经插入到表中。

刚刚我们演示了在执行 insert 命令前,检测某个值是否符合设定,接着我们来看在执行 insert 之后,使用触发器将不同的值保存到不同的表中。

扩展阅读:《如何在两台服务器之间迁移 MySQL / MariaDB 数据库 阿里云腾讯云迁移案例》

2.AFTER INSERT触发器使用方法

接着我们讲解 AFTER INSERT ,触发器在监测到我们成功执行了 INSERT 命令后,再执行触发器中设置好的代码。

例如:在银行账户系统中,当我们新建一个账户后,我们将创建成功信息写入对应的 customer_status 表中。

在这个案例中,你作为「卡拉云银行」的程序员,现在要创建一个AFTER INSERT触发器,在创建新客户账户后,将成功信息写入customer_status 表中

要创建AFTER INSERT触发器,请输入以下命令:

1
2
3
4
5
6
7
sql复制代码DELIMITER //
CREATE TRIGGER customer_status_records
AFTER INSERT
ON customers
FOR EACH ROW
Insert into customer_status(customer_id, status_notes) VALUES(NEW.customer_id, '账户创建成功')//
DELIMITER ;
1
2
java复制代码Output
Query OK, 0 rows affected (0.00 sec)

这个触发器在操作者向 customers 表中 INSERT 新客户信息后,再向 customer_status 表对应的行中写入成功信息。

现在我们 INSERT 一条信息,看看触发器是否已启用:

1
sql复制代码Insert into customers (customer_id, customer_name, level )values('4','Xing Wang','VIP');
1
2
java复制代码Output
Query OK, 1 row affected (0.01 sec)

记录 INSERT 成功,接着我们来检查customer_status表中是否写入了对应的成功数据。

1
sql复制代码Select * from customer_status;

检查是否成功

这里可以看到,我们向 customers 表插入了一个customer_id 为 4 的新用户 ,随后,触发器根据代码自动向customer_status 表中也插入了一个 customer_id 为 4 的开户成功信息。

AFTER INSERT 特别适合这种状态变更的关联写入操作。比如开户、暂停、注销等各类状态变更。

到这里,触发器在INSERT执行前、后的应用,我们已经讲完了,接着我们来讲 UPDATE 触发器。

扩展阅读:《MySQL 配置文件 my.cnf / my.ini 逐行详解》

3.BEFORE UPDATE触发器使用方法

BEFORE UPDATE触发器与BEFORE INSERT 触发器非常类似,我们可以使用BEFORE UPDATE 触发器在更新数据之前,先做一次业务逻辑检测,避免发生误操作。

刚刚我们创建示例数据库时,创建了两个级别的客户,VIP 和 BASIC 级别。卡拉云银行的客户一旦升级至 VIP,就不能再降级至 BASIC 级别了。

我们使用 BEFORE UPDATE 来贯彻这一规则,这个触发器将在 UPDATE 语句执行之前,先判断是否为降级行为,如果是,则输出报错信息。

我们来创建这个触发器:

1
2
3
4
5
6
7
8
9
10
sql复制代码DELIMITER //
CREATE TRIGGER validate_customer_level
BEFORE UPDATE
ON customers
FOR EACH ROW
IF OLD.level='VIP' THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'VIP 级别客户不能降级为普通级别客户';
END IF //
DELIMITER ;

我们可以使用 OLD 来获取执行 UPDATE 命令前,客户的 level 值。同样,我们使用该IF...THEN...END IF语句来对 level 值是否符合规则进行判断。

我们先来查看一下 customers 表中的数据。

1
bash复制代码select * from customers;

不允许降级

好,我们选一个已经是 VIP 级别的客户,对他进行降级操作,看看我们的触发器是否能够正确执行。

接下来,运行以下 SQL 命令,试试能不能将 customer_id 为 3 的 VIP 客户降级成 BASIC 客户:

1
sql复制代码Update customers set level='BASIC' where customer_id='3';

执行代码后,命令行返回错误信息:

在允许范围内可通过

这说明我们刚刚设置的触发器已经起作用了。

接着我们来试试,对一个BASIC级别的客户运行相同的命令,看看能不能把他升级到VIP级别:

1
sql复制代码Update customers set level='VIP' where customer_id='2';

执行成功:

1
2
sql复制代码Output
Rows matched: 1 Changed: 1 Warnings: 0

我们再来看一下 customers 表中的数据情况:

1
bash复制代码select * from customers;

客户已经升级为 VIP

可以看到刚才 customer_id 为 2 的 BASIC 客户已经升级为 VIP 客户。

BEFORE UPDATE 触发器用于在更新数据前进行确认,很好的守护了系统的业务规则。接着我们来看看 AFTER UPDATE 在客户管理系统中的应用。

扩展阅读:《MySQL Workbench 操作 MySQL / MariaDB 数据库中文指南》

4.AFTER INSERT触发器使用方法

本节我们来演示 AFTER UPDATE 在实际中的应用。AFTER UPDATE 多用于 log 记录,在管理系统多操作者使用的环境中,管理员需要设置操作 log 记录,以便在出问题时,可以查看操作者对表编辑的操作,可追根溯源。

我们先来创建一个对 sales 表操作的 log 记录触发器。

当操作者对 sales 表进行修改后,操作记录会被写入 audit_log 表中。

触发器将监测用户 ID 、更新前的销售总额、更新后的销售总额、操作者 ID、修改时间等信息,作为 log 存入 audit_log 表中。

使用以下命令建立这个 log 记录触发器:

1
2
3
4
5
6
7
sql复制代码DELIMITER //
CREATE TRIGGER log_sales_updates
AFTER UPDATE
ON sales
FOR EACH ROW
Insert into audit_log(sales_id, previous_amount, new_amount, updated_by, updated_on) VALUES (NEW.sales_id,OLD.sales_amount, NEW.sales_amount,(SELECT USER()), NOW() )//
DELIMITER ;

当操作者对 sales 表中的一条客户信息进行 UPDATE 操作时,触发器会在UPDATE操作之后,将操作行为记录在 audit_log 中。包括 sales_id ,修改 sales_amount 值的前后变化。

销售总额的变化是审计的关键数据,所以要把它记录在 audit_log 中。使用OLD 来获取更新前的 sales_amount 值,使用 NEW 来获取更新后的值。

另外我们还要记录修改 sales 表的操作者信息及操作时间。

你可以使用 SELECT USER() 来检测当前操作用户的账号,用 NOW() 语句抓去当前服务器日期和时间。

为了测试这个触发器,我们先在 sales 表中创建一条信息记录:

1
sql复制代码Insert into sales(sales_id, customer_id, sales_amount) values('5', '2','8000');
1
2
java复制代码Output
Query OK, 1 row affected (0.00 sec)

接下来,我们来更新这条记录:

1
sql复制代码Update sales set sales_amount='9000' where sales_id='5';

您将看到以下输出:

1
2
sql复制代码Output
Rows matched: 1 Changed: 1 Warnings: 0

理论上,我们更新了 sales 表后,触发器应该触发了操作,将我们刚刚的修改记录到了audit_log 表中。我们用以下命令,看看audit_log 表中是否已经有记录了。

1
sql复制代码Select * from audit_log;

如下表,触发器更新了audit_log 表,表中包含了sales_amount 更新前的旧值和更新后的新值。

log 自动记录触发器

至此,使用 AFTER UPDATE 制作的 log 自动记录触发器就完成了。

下一节,我们来学习 DELETE 相关的触发器。

扩展阅读:《如何查看 MySQL 数据库、表、索引容量大小?找到占用空间最大的表》

5.BEFORE DELETE触发器使用方法

BEFORE DELETE触发器会在DELETE语句执行之前调用。

这些类型的触发器通常用于在不同的相关表上强制执行参照完整性。

BEFORE DELETE 的应用场景通常是确保有关联的数据不被错误的误删除掉。

例如:sales 表通过customer_id 与customers表相关联。如果操作者删除了customers 表中的一条数据,那么 sales 表中某些数据就失去了关联线索。

为了避免这种情况的发生,我们需要创建一个 BEFORE DELETE触发器,防止记录被误删除。

1
2
3
4
5
6
7
8
9
10
sql复制代码DELIMITER //
CREATE TRIGGER validate_related_records
BEFORE DELETE
ON customers
FOR EACH ROW
IF OLD.customer_id in (select customer_id from sales) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '这位客户有相关联的销售记录,不能删除。';
END IF//
DELIMITER ;

现在,我们试着删除有销售关联信息的客户:

1
sql复制代码Delete from customers where customer_id='2';

所以,你会看到以下输出:

触发器不允许删除

这个触发器做到了先检测 sales 是否与正要被删除的 customers 表中的数据有关联,防止有关联信息的数据被误删除。

不过有时候,我们需要删除主数据后,再让系统自动帮我们删除与之相关联的其他所有数据。这时,我们就要用到 AFTER DELETE 这个触发器了。

扩展阅读:《在 MySQL 中 DATETIME 和 TIMESTAMP 时间类型的区别及使用场景 - 实战案例讲解》

6.AFTER DELETE触发器使用方法

接着说说 AFTER DELETE ,一旦记录被成功删除,这个触发器就会被激活。

这个触发器在实际场景用的应用也比较广泛。比如银行系统中的升级降级操作,当客户花掉自己的账户积分后,激活触发器,触发器可以判断剩余积分是否满足客户当前等级,如果不满足,自动做降级操作。

AFTER DELETE触发器的另一个用途是在删除主表中的数据后,与这个主表关联的数据,一起自动删除。

我们来看一下这个触发器如何创建:

1
2
3
4
5
6
7
sql复制代码DELIMITER //
CREATE TRIGGER delete_related_info
AFTER DELETE
ON sales
FOR EACH ROW
Delete from customers where customer_id=OLD.customer_id;//
DELIMITER ;

接下来,我们来试试这个触发器。删除销售记录中 customer_id 为 2 的销售记录:

1
sql复制代码Delete from sales where customer_id='2';
1
2
java复制代码Output
Query OK, 1 row affected (0.00 sec)

接着我们检查以下 customers 表中的关联信息是否一起自动删除:

1
sql复制代码Select * from customers where customer_id='2';

命令行会返回 Empty Set 的结果,我们刚刚删除了 sales 表中的信息后,customers 表中的关联信息也被一起删除了。

在范围内允许删除

以上就是 MySQL 触发器的六种使用方式和对应的场景。

扩展阅读:《最好用的 10 款 MySQL / MariaDB 管理工具横向测评 - 免费和付费到底怎么选?》

7.查看触发器

(1)直接查看触发器

当我们想查看数据库中的触发器有哪些时,可用以下命令:

1
bash复制代码SHOW TRIGGERS;

后面加上 \G 是触发器列表竖排列:

1
bash复制代码SHOW TRIGGERS \G

显示所有触发器

刚刚我们创建的触发器都罗列在这个列表当中了。

(2)在 triggers 表中查看触发器信息

在 MySQL Server 中,数据库 information_schema 的 triggers 表中存着所有触发器的信息。所有我们可以通过 SELECT 来查看。

1
bash复制代码SELECT * FROM information_schema.triggers WHERE trigger_name= '触发器名称';

当然,也可以不指定触发器名称,来查看所有。

1
bash复制代码SELECT * FROM information_schema.triggers \G

扩展阅读:《如何在 MySQL / MariaDB 中查询数据库中带有某个字段/列名的所有表名》

8.删除触发器

最后,咱们来说说如何删除触发器。删除命令也很简单,Drop trigger 触发器名字 即可。

1
ini复制代码Drop trigger [触发器名称];

例如,咱们把刚刚创建的最后一个触发器删掉:

1
sql复制代码Drop trigger delete_related_info;
1
2
java复制代码Output
Query OK, 0 rows affected (0.00 sec)

特别提示:我们不能对已经创建好的触发器进行修改。如果你想修改,只能先删除,再重新创建。

扩展阅读:《MySQL / MariaDB 中如何存储图片 BLOB 数据类型详解》

9.总结

在本教程中,我们展示了触发器的六种形式,即在INSERT、DELETE、UPDATE 执行前或后执行触发器,以及对应的六个实战案例。

  • BEFORE INSERT : 在插入数据前,检测插入数据是否符合业务逻辑,如不符合返回错误信息。
  • AFTER INSERT : 在表 A 创建新账户后,将创建成功信息自动写入表 B 中。
  • BEFORE UPDATE :在更新数据前,检测更新数据是否符合业务逻辑,如不符合返回错误信息。
  • AFTER INSERT :在更新数据后,将操作行为记录在 log 中
  • BEFORE DELETE :在删除数据前,检查是否有关联数据,如有,停止删除操作。
  • AFTER DELETE :删除表 A 信息后,自动删除表 B 中与表 A 相关联的信息。

接着推荐一下卡拉云,只要你会写 MySQL ,就能使用卡拉云搭建自己的数据工具,比如,数据看板,企业 CRM、ERP,权限管理后台,对账系统等。

卡拉云是新一代低代码开发工具,免安装部署,可一键接入包括 MySQL 在内的常见数据库及 API。可根据自己的工作流,定制开发。无需繁琐的前端开发,只需要简单拖拽,即可快速搭建企业内部工具。数月的开发工作量,使用卡拉云后可缩减至数天,欢迎免费试用卡拉云。

卡拉云可一键接入常见的数据库及 API

卡拉云可一键接入常见的数据库及 API

卡拉云可根据公司工作流需求,轻松搭建数据看板或其他内部工具,并且可一键分享给组内的小伙伴。

![卡拉云5分钟搭建企业内部工具(kalacloud.com/5400a60956e…)

下图为使用卡拉云在 5 分钟内搭建的「优惠券发放核销」后台,仅需要简单拖拽即可快速生成前端组件,只要会写 SQL,便可搭建一套趁手的数据库工具。**欢迎免费试用卡拉云。**

导入导出

希望本教程对你有所帮助。更多有关 MySQL 教程,欢迎访问卡拉云查看更多。

有关 MySQL 教程,可继续拓展学习:

  • 如何远程连接 MySQL 数据库,阿里云腾讯云外网连接教程
  • 如何在 MySQL / MariaDB 中导入导出数据,导入导出数据库文件、Excel、CSV
  • 如何在两台服务器之间迁移 MySQL 数据库 阿里云腾讯云迁移案例
  • MySQL 重置自增 ID (AUTO_INCREMENT)教程 - 完美保留表数据的终极解决方案

本文转载自: 掘金

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

Go语言 RPC 极速入门指南 Hello, World

发表于 2021-11-13

这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战

RPC是远程过程调用(Remote Procedure Call)的缩写形式,是分布式系统中不同节点间流行的通信方式。

一个完整的RPC架构里面包含了四个核心的组件,分别是Client ,Server,Client Stub以及Server Stub,这个Stub大家可以理解为存根。分别说说这几个组件:

  • 客户端(Client),服务的调用方。
  • 服务端(Server),真正的服务提供者。
  • 客户端存根,存放服务端的地址消息,再将客户端的请求参数打包成网络消息,然后通过网络远程发送给服务方。
  • 服务端存根,接收客户端发送过来的消息,将消息解包,并调用本地的方法。

Hello, World

Go 语言的 RPC 包的路径为 net/rpc,可以猜测该RPC包是建立在 net 包基础之上的。下面我们尝试基于 rpc 实现一个打印的例子。

服务端 server

先构造一个 HelloService 类型,其中的 Hello 方法用于实现打印功能:

server

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
go复制代码package main

import (
"log"
"net"
"net/rpc"
)

// HelloService is rpc server obj
type HelloService struct {}

func (p *HelloService) Hello(request string, reply *string) error {
*reply = "hello:" + request
return nil
}

//将HelloService类型的对象注册为一个RPC服务
func main(){
rpc.RegisterName("HelloService", new(HelloService))

listener, err := net.Listen("tcp", ":1234")
if err != nil {
log.Fatal("ListenTCP error:", err)
}

conn, err := listener.Accept()
if err != nil {
log.Fatal("Accept error", err)
}

rpc.ServeConn(conn)

}

其中Hello方法必须满足Go语言的RPC规则:

  • 方法只能有两个可序列化的参数,其中第二个参数是指针
    类型
  • 并且返回一个error类型,同时必须是公开的方法。

rpc.Register 函数调用会将对象类型中所有满足 RPC 规则的对象方法注册为 RPC 函数,所有注册
的方法会放在 “HelloService” 服务空间之下。

建立一个唯一的TCP链接,并且通过 rpc.ServeConn 函数在该 TCP 链接上为对方提供 RPC 服务。

客户端 clinet

客户端请求HelloService服务的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
go复制代码package main

import (
"fmt"
"log"
"net/rpc"
)

func main() {
client, err := rpc.Dial("tcp", "localhost:1234")
if err != nil {
log.Fatal("dialing err:", err)
}

var reply string
err = client.Call("HelloService.Hello", "test_rpc", &reply)
if err != nil {
log.Fatal(err)
}

fmt.Println(reply)
}

通过rpc.Dial拨号RPC服务,然后通过client.Call调用具体的RPC方法。在调用
client.Call时,第一个参数是用点号链接的RPC服务名字和方法名字,第二和第三个参数分别我们定
义RPC方法的两个参数。

开启服务器端,开启客户端。客户端的执行结果如下:

1
2
go复制代码$ go run client.go
hello:test_rpc

本文转载自: 掘金

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

1…358359360…956

开发者博客

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