Shawn's Blog

一个伪程序员的伪技术博客

0%

0X00 前言

都说“生活需要仪式感“,那对我来说的第一次跳槽还是值得记录的,用处肯定是没什么,但是总归是给自己一点点生活上的仪式感吧。而且脑子记住的东西可能过几年就忘记了,真正记录成文字的东西存活时间就会长久很多。(记录在网络上就算我自己把博客删了,都还会有垃圾站原封不动的抄走,简直不可能丢🤣)

本来说离职当天就来写这篇文章的,结果当天晚上同时请客去吃饭又去唱了歌,回家都凌晨两点了,隧作罢。现在离职手续全部都办好了,新公司的文件们也都准备好了,就等假期过后直接到新公司入职。所以也终于有时间坐下来不用考虑工作相关的任何事情来写一写自己的想法了~

这不是日记,按蓝青峰跟朱潜龙的说法正经人谁写日记啊🤪(狗头)

0X01 为什么离开

从我以实习生的身份加入创宇到最后离开一共是 1159 天,算下来是三年多一点点,时间也算是很久的了。毕竟我大学也就只有前三年是在学校的,后面就出来实习了。现在走呢也不是说因为公司有啥问题或者我被开除了,只是说我自己觉得是时候出去看看了,不想常年在一家公司里待下去

这次跳槽主要是这三个问题导致的:

  1. 💰️首先是“工资问题“;大家出来工作,99.99%的人都得是为了赚钱吧。我因为是实习转正的,所以薪资起点会比较低,即使后来的调薪比例都还挺高的也会导致最后的薪资并没有怎么多;所以说自己也想要换个地方拿到更高的薪资,要恰饭的嘛;

  2. 👨‍💻其次是“专业技能“;开发者嘛,想要自我提升的人还是很多的。但是因为我做的事情是针对少数人使用的内部系统,所以对个人的技术力需求和考验并没有什么。在工作中不太能接触到更高的东西和更宽的东西,自己私下时间学习到的更高更宽的知识又比较难应用起来。虽然在舒适圈是真的舒适,但是想要提升的话总有一天要打破舒适圈的,我觉得这一天该到了;

  3. 👀️最后是“眼界和见识“;我个人觉得我已经在同一个岗位做同一个业务时间够久了,也想要见到其他不同的行业和人们。

所以总结下来,就是说目前我:使用“Python“编写“Django“程序,然后运行在内网环境里,给“内部审核系统“提供稳定可靠的“web 服务“,每个月赚到“**“的工资。现在双引号里所有的东西我觉得都是时候可以改变的,对于技术栈来说的“T 型人才“的横和竖都是可以发展的了;对于工资来说就更是了,起薪低的情况下很难有大额上涨的,所以跳槽也是一个大家都理解的操作。

阅读全文 »

0X00 What’s this

我们知道 MySQL 中存在“事务”这么个事物(我是故意拗口的,哈哈哈哈哈哈哈);我们也知道事务“一荣俱荣,一损俱损”(要么事物内所有查询均生效,要么均不生效)。那么现在问题来了,银行数据库中有两个事务在同时进行,我们来看一下这两个事务

一条 SQL 就是一个查询,不一定是 SELECT xxx 才叫查询。

1
2
3
4
5
6
7
8
9
10
11
-- 事务 A    shawn 转账给 bluce 100 块钱
BEGIN;
UPDATE account SET money = money - 100 WHERE username = 'shawn'; -- 这行代码标记为 A-1
UPDATE account SET money = money + 100 WHERE username = 'bluce'; -- 这行代码标记为 A-2
COMMIT;

-- 事务 B
BEGIN;
SELECT money FROM account WHERE username = 'shawn'; -- 这行代码标记为 B-1
SELECT money FROM account WHERE username = 'bluce'; -- 这行代码标记为 B-2
COMMIT;

现在有这么一个情况:shawn 和 bluce 两人账上都有 100 块钱,此时事务 A 和 B 同时开始执行,那么事务 B 所查询到的两人的账户余额究竟是多少呢?这个问题并没有一个确切的结果,因为 B 在查询的时候,转账正在进行中。不过要回答这个问题也不是做不到,就需要引入标题中提到的“隔离等级”这个概念了。

我们在 MySQL 中使用这四种隔离等级:READ UNCOMMITTED/READ COMMITTED/REPEATABLE READ/SERIALIZABLE。隔离等级的不同就意味着在同一个时间节点下查询到的内容可能不同,数据的可靠性也会变得不同。接下来我们来简单了解一下这四种隔离等级,并且通过一个测试数据库来验证一下。

0X01 How to try it

首先要建一个数据库来做测试,表结构和两条基础数据长这样,可以通过随便什么方法把表先建好。

1
2
3
4
5
6
7
8
CREATE TABLE `account` (
`id` int(11) NOT NULL,
`username` varchar(10),
`money` int(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

INSERT INTO `account` VALUES (1, 'shawn', 100),(2, 'bluce', 100);

然后我们打开两个 MySQL 的终端连接(一定是两个),其中一个用来执行事务 A,一个用来修改隔离等级然后观察结果。

修改隔离等级的时候,可以选择“全局”或者“会话”两种级别,我们在测试阶段就只需要修改“会话”级就可以了。用的是SET session transaction isolation level read committed;这样的方法,其中最后的read committed就是其中一个隔离等级。

注意在会话级别修改隔离等级应该是在你 select 去验证的会话里执行,而非执行事务的会话。换个说法就是“隔离等级”是针对事务外的,而非针对事务。

阅读全文 »

0X00 使用 with as 语法

我们写程序经常会操作文件,我们都知道写文件要 open/write/close ,尤其是 close ,没有的话文件就会出问题(有些内容在缓存里,没写入磁盘)。不过我们现在写文件应该没什么人这样写了,都是用with open('filename', 'w') as f的方式来操作文件了。如果说这样做的好处,那多数人都会说“不用手动关闭文件了”,错肯定没错的。

上下比起来,上面的方式不仅多了一点点代码,而且随着中间逻辑代码变多,很可能会导致最后忘记 close,从而引发 bug。

0X01 这就是上下文管理器

上面 with xxx as xxx 的调用方式就是在调用上下文管理器。简单来说上下文管理器就是:在执行你编写的代码(with xxx as xxx后面那坨)之前,操作一波;再在你编写的代码执行完后,操作一波。我们简单理解一下就是在 with open('file_name', 'w') as f 内层缩进的代码执行完成后自动帮你执行了f.close()(当然没这么简单,有兴趣可以去看一下 open 的源码,但是大体逻辑是这样的)。

阅读全文 »

0X00 objects 是个啥

想必所有用过 Django 的人都会用到 Django 自带的 ORM 进行数据库查询。那既然用过 Django 的 ORM 就来看一下这段代码好了, models.Stuent.objects.filter(name='Shawn') 这段代码是什么意思呢?很简单,就是查询到名字为”Shawn”的学生信息。具体来说, models 应该是一个放了多个 model 的文件,Student 是一个具体的模型,filter 是筛选,name='Shawn' 则是筛选条件。那么问题来了,中间那个 objects 是个啥呢?(你知道?知道还在这儿看啥,有这空看看其他文章,打打游戏看看电影不好吗🤣)

通过 type 可知,这个 objects 是一个 django.db.models.manager.Manager 的实例(或者是他子类的实例)。然后我们来看看这个Manager是个什么❓

Django ORM

A Manager is the interface through which database query operations are provided to Django models. At least one Manager exists for every model in a Django application.

从 Django 文档得知“Manager 是 Django 用来进行数据库查询的一个接口,在 Django 应用中每个 model 都需要至少有一个 Manager”。

阅读全文 »

0X00 按惯例得有一个标题

众所周知save是 Django 中最常用的保存数据的方法。但是一般来说大家经常会把“常用“理解成“万能“,然后能用的时候就全用这一种方式。不过编程 中是没有所谓的“一招鲜吃遍天“的,Django 之所以提供了那么多中保存数据的方法也侧面证实了这一点。

首先来看一下我遇到的这个问题:

1
2
3
4
5
6
7
8
from .models import Student
from .utils import calculate_score

queryset = Student.objects.all()
for student in queryset:
student.score = calculate_score(student) # 调用一个工具函数计算该学生的成绩
student.save()

这段代码乍一看没有什么问题,因为计算的值是通过calculate_score这个函数进行的,所以不能使用queryset.update(xxx)的方法。然后咱们看一下 Django 文档是如何描述 queryset 的。

QuerySets are lazy – the act of creating a QuerySet doesn’t involve any database activity. You can stack filters together all day long, and Django won’t actually run the query until the QuerySet is evaluated.

QuerySets are lazy

nternally, a QuerySet can be constructed, filtered, sliced, and generally passed around without actually hitting the database. No database activity actually occurs until you do something to evaluate the queryset.

When QuerySets are evaluated

QuerySets are lazy 内容总结来说就是“Django 中的 QuerySet 只有在用的时候才会真的去数据库里查,而不是生成 QuerySet 的时候“。后面的 When QuerySet are evaluated 则标明了什么叫做“真正在使用“,给出了下面几个条件,当你做这些事情的时候就是“真正在使用“了。这些条件包括:迭代、 切片、列表等(我英文水平小学三年级,解读地不对的地方还希望大家指出)。 所以显然我们对这个 queryset 来了个 for 循环就满足了上述的“迭代“,所以这时候数 Django 就会真正的从数据库中将数据真正的 取出来

现在问题来了。我们思考一个问题,如果我一秒钟能计算10 个学生的成绩,然后整个Student表有 3W 学生,得出“处理所有学生信息需要消耗 50 分钟的时间“这样的结论(每秒 10 条和一共 3W 是乱写的,真实数据通常比这个大得多)。

如果在执行这个循环的时候,某位同学修改了自己的的信息,比如手机号,会发生什么? 有两种可能:第一种可能是这位同学修改自己手机号的时候计算分数的循环已经把他的分数计算完了,那么他的手机号修改也生效了(这种最好)。但是如果他改手机号的时候循环还没到他呢?假设他把手机号从原来的 123 改成了 456,那么他改完手机号的一瞬间数据库里存进去的确实是 456,没有问题。但是 queryset 里是他改手机号以前取出来的 123,这时候循环到他了,计算完之后来了一个student.save(),如此一来他刚刚改好的手机号码就又回到了 123。

所以说这种写法并不会 100% 出现问题。整个循环耗时越久,出现问题的可能性越大;系统中数据变更越频繁,出现问题的可能性越大。当然了,bug 就是 bug,不能因为 bug 没触发就无所畏惧了,还是得解决的。通常来说有两种解决方法,下面是第一种

1
2
3
for student in queryset:
new_score = calculate_score(student)
Student.objects.filter(id=student.id).update(score=new_score)

这种方式仍然比较 young 比较 simple 比较 naive,不过又不是不能用

但是这种用法显然是不好的,而且 update 本来也不是让我们这么用的。所以我们还是得回到save上,Django 其实已经提供了一个参数给 save 了,可以用下面这种方法

1
2
3
for student in queryset:
student.score = calculate_score(student)
student.save(update_fields=['score'])

也就是在 save 的时候带上具体需要更新哪个字段,其他的就不更新了。而且通过传递的参数也可以看出,指定的是多个字段,如果有需要修改多个字段的话,就只修改这一个就好了。

不过其实这里还是有一个潜在问题的,那就是说:恰好我们在更新 score 的时候,其他地方也在更新这个。不过这个更多的时候就是我们程序逻辑的问题了,因为在几乎同一时间对一个字段进行修改,然后修改的双方又互相不知道的话,总是会出问题的。