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

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


  • 首页

  • 归档

  • 搜索

spring声明式事务Transactional的那些坑

发表于 2021-07-27

事务有四大特性,分别是原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability),把这四大特性的英文首字母拼起来,就是ACID,这个单词对应的中文意思是“酸”,什么东西比较酸呢,柠檬(🍋),把它们联系起来,就容易记着这几个特性了。
在MySQL数据库中,默认的存储引擎Innodb是支持事务的,另一个比较常用的存储引擎MyISAM是不支持事务的。
插曲说一下,一直对MyISAM怎么读比较纠结,我查了一下,在mysql的邮件列表,有人问过这个问题,提到比较多的读法是 [My-I-sam],有相同困惑的朋友可以借鉴一下。
这四个特性的具体含义,这里就不再细说了,我们焦点放在在实际的编程中怎么避免踩坑。

声明式事务的属性

spring对事务的支持有两种方式:编程式事务和声明式事务。编程式事务对代码有侵入性,用起来也比较繁琐,一般也很少使用,声明式事务通过添加 @Transactional 注解来支持,事务的属性会添加在注解上。
声明式事务的几个属性需要了解。

事务传播级别

  1. TransactionDefinition.PROPAGATION_REQUIRED

@Transactional 注解默认的传播方式就是REQUIRED,在该传播模式下,如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。

  1. TransactionDefinition.PROPAGATION_SUPPORTS

表示如果有事务,就加入到当前事务,如果没有,那也不开启事务执行。这种传播级别可用于查询方法,因为SELECT语句既可以在事务内执行,也可以不需要事务;

  1. TransactionDefinition.PROPAGATION_MANDATORY

表示必须要存在当前事务并加入执行,否则将抛出异常。这种传播级别可用于核心更新逻辑,比如用户余额变更,它总是被其他事务方法调用,不能直接由非事务方法调用;

  1. TransactionDefinition.PROPAGATION_REQUIRES_NEW

表示不管当前有没有事务,都必须开启一个新的事务执行。如果当前已经有事务,那么当前事务会挂起,等新事务完成后,再恢复执行;

  1. TransactionDefinition.PROPAGATION_NOT_SUPPORTED

表示不支持事务,如果当前有事务,那么当前事务会挂起,等这个方法执行完成后,再恢复执行;

  1. TransactionDefinition.PROPAGATION_NEVER

不支持事务,而且在检测到当前有事务时,会抛出异常拒绝执行;

  1. TransactionDefinition.PROPAGATION_NESTED

表示如果当前有事务,则开启一个嵌套级别事务,如果当前没有事务,则开启一个新事务。

事务隔离级别

在 org.springframework.transaction.annotation.Isolation 枚举中定义了@Transactional支持的隔离级别:

  • **DEFAULT **使用数据库指定的隔离级别
  • **READ_UNCOMMITTED **读未提交
  • **READ_COMMITTED **读已提交
  • **REPEATABLE_READ **可重复读
  • **SERIALIZABLE **可串行化

超时属性

一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒,默认值为-1。-1代表使用底层事务系统默认的超时时间。

只读属性

对于只有读取数据查询的事务,可以指定事务类型为 readonly,即只读事务。只读事务不涉及数据的修改,数据库会提供一些优化手段,适合用在有多条数据库查询操作的方法中。
可以通过下面的语句查询数据库是否开启了自动提交,如果开启了,默认一条语句就是一个事务。

1
sql复制代码SHOW VARIABLES LIKE 'autocommit';

实际上如果一个事务中只有一条查询语句,是没有必要加readonly属性的,因为不存在不一致的场景。
如果一个事务中有多条查询语句,在可重复读的隔离级别下,在这个事务中能查到的结果是一致的。
举个例子:
对一批量比较大的数据的查询。一般我们不会一次性把所有数据全捞出来,而是采用分页的形式多次查询,先count数量,再使用offset和limit分批查询,如果不使用事务的话,分批查询出来的数据的个数和count查询的数量可能不一致。

回滚规则

默认只有在RunTimeException和Error的时候会进行回滚,checkedException是不会回滚的,这个可以进行指定。

使用举例

@Transactional 注解可作用于接口、类、方法上,一般常用的是把注解加在方法上,达到一个细粒度的控制。

1
2
3
4
java复制代码// 当发生异常时进行回滚,使用默认的传播级别REQUIRED
@Transactional(rollbackFor = Exception.class)
// 当发生MyServiceException异常时进行回滚,使用的传播级别为 REQUIRES_NEW
@Transactional(rollbackFor = MyServiceException.class, propagation = Propagation.REQUIRES_NEW)

容易踩坑的点

  1. 把@Transactional配置在了私有方法上

声明式事务使用的是动态代理,注解只能作用在public方法上,不过一般ide就可以给检查出来这种错误。

1
2
3
4
5
6
7
8
java复制代码@Nullable
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
...
}
  1. 同类下方法调用了带@Transactional注解的方法,事务不起作用
1
2
3
4
5
6
7
8
9
java复制代码public class A {
public void func1() {
func2();
}

@Transactional(rollbackFor = Exception.class)
public void func2() {
}
}

如上面的代码,在类A中有两个公有方法:func1 和 func2,func2上有事务注解,如果func1为入口,对func1的调用,func2上的事务注解是不起作用的。
这个地方很容易踩坑,为什么会这样呢,这与spring aop的动态代理有关系,对于使用 this 的方式调用,这种只是自调用,并不会使用代理对象进行调用,也就无法执行切面类。

  1. 传播方式配置错误,和预期不一样
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public class A {
@Autowired private B b;

@Transactional(rollbackFor = Exception.class)
public void func1() {
for (int i = 0; i < 10; i++) {
try {
b.func2();
} catch (Exception ignore) {
}
}
// ...
}
}

public class B {
@Transactional(rollbackFor = Exception.class)
public void func2() {
// 如果有异常就回滚
}
}

开发中,经常会有需求是希望批处理任务中某一个有异常能回滚掉,而不影响同一批中的其他正常任务。
如上面的代码所示, func2 中调用了 func1 ,并catch了异常,期望的是: func2 中出现异常, func2 回滚, func1 及for循环中的其他调用不会滚。
而事实上却和我们的期望不一样。
如果 func2 回滚的话,虽然在 func1 中catch了异常,func1还是会回滚,因为func2使用了默认的 REQUIRED 的传播方式,它会加入到这个事务中去,如果 func2 抛出了异常,这个事务就会标记为 rollbackOnly ,导致整个事务回滚。
解决方法:

  • 第一种,去掉func1的事务注解;
  • 第二种,设置func2的事务传播方式为 REQUIRES_NEW ;
  1. 没有指定数据源

曾经踩过坑,是这样的,一开始我们的项目只引入了一个库,只有一个数据源,那么**@Transactional 是不需要指定数据源的。
后面又引入了一个库,现在就有两个数据源了。
那么
@Transactional **是不知道去回滚哪个数据源的数据的。
所以,对于有多个数据源的系统一定要指定要回滚的数据源,统一通过 value 或 transactionManager 指定数据源。

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
@AliasFor("transactionManager")
String value() default "";

@AliasFor("value")
String transactionManager() default "";
}

本文转载自: 掘金

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

手把手教你使用 Python 制作贪吃蛇游戏|Python

发表于 2021-07-27

本文正在参加「Python主题月」,详情查看 活动链接

贪吃蛇游戏是有史以来最受欢迎的街机游戏之一。在这个游戏中,玩家的主要目标是在不撞墙或不撞墙的情况下抓住最大数量的水果。在学习 Python 或 Pygame 时,可以将创建蛇游戏视为一项挑战。这是每个新手程序员都应该接受的最好的初学者友好项目之一。学习构建视频游戏是一种有趣而有趣的学习。

我们将使用Pygame来创建这个蛇游戏。Pygame是一个开源库,专为制作视频游戏而设计。它具有内置的图形和声音库。它也是初学者友好的和跨平台的。

🛬 安装

要安装 Pygame,您需要打开终端或命令提示符并输入以下命令:

1
Python复制代码pip install pygame

安装 Pygame 后,我们就可以创建我们很酷的蛇游戏了。

🛰 使用 Pygame 创建贪吃蛇游戏的分步方法:

💌 第 1 步:首先,我们正在导入必要的库。

  • 之后,我们将定义游戏将在其中运行的窗口的宽度和高度。
  • 并以 RGB 格式定义我们将在游戏中用于显示文本的颜色。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Python复制代码# 导入库
import pygame
import time
import random

snake_speed = 15

# 窗口大小
window_x = 720
window_y = 480

# 定义颜色
black = pygame.Color(0, 0, 0)
white = pygame.Color(255, 255, 255)
red = pygame.Color(255, 0, 0)
green = pygame.Color(0, 255, 0)
blue = pygame.Color(0, 0, 255)

🏓 第 2 步:导入库后,我们需要使用pygame.init() 方法初始化 Pygame 。

  • 使用上一步中定义的宽度和高度创建一个游戏窗口。
  • 这里pygame.time.Clock() 将在游戏的主要逻辑中进一步用于改变蛇的速度。
1
2
3
4
5
6
7
8
9
Python复制代码# 初始化pygame
pygame.init()

# 初始化游戏窗口
pygame.display.set_caption('GeeksforGeeks Snakes')
game_window = pygame.display.set_mode((window_x, window_y))

# FPS(每秒帧数)控制器
fps = pygame.time.Clock()

🎯 第 3 步:初始化蛇的位置及其大小。

  • 初始化蛇位置后,在定义的高度和宽度的任意位置随机初始化水果位置。
  • 通过将方向设置为 RIGHT,我们确保每当用户运行程序/游戏时,蛇必须向右移动到屏幕上。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Python复制代码# 定义蛇默认位置
snake_position = [100, 50]

# 定义蛇体的前 4 个块
snake_body = [ [100, 50],
[90, 50],
[80, 50],
[70, 50]
]
# 水果位置
fruit_position = [random.randrange(1, (window_x//10)) * 10,
random.randrange(1, (window_y//10)) * 10]
fruit_spawn = True

# 设置默认的蛇方向向右
direction = 'RIGHT'
change_to = direction

🥇 第 4 步:创建一个函数来显示玩家的得分。

  • 在这个函数中,首先我们要创建一个字体对象,即字体颜色会出现在这里。
  • 然后我们使用渲染来创建一个背景表面,每当我们的分数更新时,我们就会改变它。
  • 为文本表面对象创建一个矩形对象(文本将在此处刷新)
  • 然后,我们使用blit显示我们的分数 。 blit需要两个参数screen.blit(background,(x,y))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Python复制代码# 初始分数
score = 0

# 显示评分功能
def show_score(choice, color, font, size):

# 创建字体对象 score_font
score_font = pygame.font.SysFont(font, size)

# 创建显示表面对象 core_surface
score_surface = score_font.render('Score : ' + str(score), True, color)

# 为文本表面对象创建一个矩形对象
score_rect = score_surface.get_rect()

# 显示文字
game_window.blit(score_surface, score_rect)

🎴 第 5 步:现在创建一个游戏结束函数,该函数将代表蛇被墙壁或自身击中后的分数。

  • 在第一行,我们创建了一个字体对象来显示乐谱。
  • 然后我们创建文本表面来渲染乐谱。
  • 之后,我们将设置文本在可播放区域中间的位置。
  • 使用blit显示分数并通过使用 flip() 更新表面来更新分数。
  • 我们使用 sleep(2) 在使用 quit() 关闭窗口之前等待 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
Python复制代码# 游戏结束功能
def game_over():

# 创建字体对象 my_font
my_font = pygame.font.SysFont('times new roman', 50)

# 创建将在其上绘制文本的文本表面
game_over_surface = my_font.render('Your Score is : ' + str(score), True, red)

# 为文本表面对象创建一个矩形对象
game_over_rect = game_over_surface.get_rect()

# 设置文本位置
game_over_rect.midtop = (window_x/2, window_y/4)

# blit 将在屏幕上绘制文本
game_window.blit(game_over_surface, game_over_rect)
pygame.display.flip()

# 2 秒后我们将退出程序
time.sleep(2)

# 停用 pygame 库
pygame.quit()

# 退出程序
quit()

⏰ 第 6 步:现在我们将创建我们的主要功能,它将执行以下操作:

  • 我们将验证负责蛇移动的密钥,然后我们将创建一个特殊条件,即不允许蛇立即向相反方向移动。
  • 在那之后,如果蛇和水果发生碰撞,我们将把分数增加 10,新的水果将被跨越。
  • 在那之后,我们正在检查蛇是否被墙击中。如果一条蛇撞墙,我们将调用游戏结束功能。
  • 如果蛇撞到自己,游戏结束函数将被调用。
  • 最后,我们将使用之前创建的 show_score 函数显示分数。
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
Python复制代码# Main Function
while True:

# 处理关键事件
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_UP:
change_to = 'UP'
if event.key == pygame.K_DOWN:
change_to = 'DOWN'
if event.key == pygame.K_LEFT:
change_to = 'LEFT'
if event.key == pygame.K_RIGHT:
change_to = 'RIGHT'

# 如果同时按下两个键
# 我们不想让蛇同时向两个方向移动
if change_to == 'UP' and direction != 'DOWN':
direction = 'UP'
if change_to == 'DOWN' and direction != 'UP':
direction = 'DOWN'
if change_to == 'LEFT' and direction != 'RIGHT':
direction = 'LEFT'
if change_to == 'RIGHT' and direction != 'LEFT':
direction = 'RIGHT'

# 移动蛇
if direction == 'UP':
snake_position[1] -= 10
if direction == 'DOWN':
snake_position[1] += 10
if direction == 'LEFT':
snake_position[0] -= 10
if direction == 'RIGHT':
snake_position[0] += 10

# 蛇体生长机制
# 如果水果和蛇发生碰撞,那么分数将增加 10
snake_body.insert(0, list(snake_position))
if snake_position[0] == fruit_position[0] and snake_position[1] == fruit_position[1]:
score += 10
fruit_spawn = False
else:
snake_body.pop()

if not fruit_spawn:
fruit_position = [random.randrange(1, (window_x//10)) * 10,
random.randrange(1, (window_y//10)) * 10]

fruit_spawn = True
game_window.fill(black)

for pos in snake_body:
pygame.draw.rect(game_window, green, pygame.Rect(
pos[0], pos[1], 10, 10))

pygame.draw.rect(game_window, white, pygame.Rect(
fruit_position[0], fruit_position[1], 10, 10))

# 游戏结束条件
if snake_position[0] < 0 or snake_position[0] > window_x-10:
game_over()
if snake_position[1] < 0 or snake_position[1] > window_y-10:
game_over()

# 触碰蛇身
for block in snake_body[1:]:
if snake_position[0] == block[0] and snake_position[1] == block[1]:
game_over()

# 连续显示分数
show_score(1, white, 'times new roman', 20)

# 刷新游戏画面
pygame.display.update()

# 每秒帧数/刷新率
fps.tick(snake_speed)

下面是实现

snake.gif

快速总结——Python 贪吃蛇游戏

其实源码已经都列出来了,不过肯定还有小伙伴想直接拿完整的,需要的可以在评论区留言,暂时还没放在GitHub上,直接放文章里又感觉代码拖得太长了

本文章为系列文章,后续会继续更新Python、Java、HTML等做的小游戏。我希望本系列教程能够帮助到您,博主也在学习进行中,如有什么错误的地方还望批评指正。如果您喜欢这篇文章并有兴趣看到更多此类文章,可以看看这里(Github/Gitee) 这里汇总了我的全部原创及作品源码,关注我以查看更多信息。

🧵 更多相关文章

  • Python 异常处理|Python 主题月
  • Python 多线程教程|Python 主题月
  • Python Socket 编程要点|Python 主题月
  • 30 个 Python 教程和技巧|Python 主题月
  • Python 语句、表达式和缩进|Python 主题月
  • Python 关键字、标识符和变量|Python 主题月
  • 如何在 Python 中编写注释和多行注释|Python 主题月
  • 通过示例了解 Python 数字和类型转换|Python 主题月
  • Python 数据类型——从基础到高级学习|Python 主题月
  • Python 中的面向对象编程一之类、对象和成员|Python 主题月

🍰 往日优秀文章推荐:

  • 每个人都必须知道的 20 个 Python 技巧|Python 主题月
  • 100 个基本 Python 面试问题第一部分(1-20)|Python 主题月
  • 100 个基本 Python 面试问题第二部分(21-40)|Python 主题月
  • 100 个基本 Python 面试问题第三部分(41-60)|Python 主题月
  • 100 个基本 Python 面试问题第四部分(61-80)|Python 主题月
  • 100 个基本 Python 面试问题第五部分(81-100)|Python 主题月

如果你真的从这篇文章中学到了一些新东西,喜欢它,收藏它并与你的小伙伴分享。🤗最后,不要忘了❤或📑支持一下哦

本文转载自: 掘金

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

【对线面试官】CountDownLatch和CyclicBa

发表于 2021-07-27

文章以纯面试的角度去讲解,所以有很多的细节是未铺垫的。

鉴于很多同学反馈没看懂【对线面试官】系列,基础相关的知识我确实写过文章讲解过啦,但有的同学就是不爱去翻。

为了让大家有更好的体验,我把基础文章也找出来(重要的知识点我还整理过电子书,比如说像多线程、集合、Spring这种面试必考的早就已经转成PDF格式啦)

我把这些上传到网盘,你们有需要直接下载就好了。做到这份上了,不会还想白嫖吧?点赞和转发又不用钱。

链接:pan.baidu.com/s/1pQTuKBYs… 密码:3wom

欢迎关注我的微信公众号【Java3y】来聊聊Java面试


【对线面试官】系列 一周两篇持续更新中!

原创不易!!求三连!!

本文转载自: 掘金

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

Spring Boot 实现多图片上传并回显,涨姿势了~

发表于 2021-07-27

作者:不学无数的程序员

链接:www.jianshu.com/p/3e28b9444…

这两天公司有需求让做一个商户注册的后台功能,其中需要商户上传多张图片并回显。由于之前没做过这方面的东西,此篇文章用以记录一些知识点,以便后续查看。

上传

Controller的代码非常简单,由于用了SpringMVC框架,所以直接用MultipartFile来接即可。由于是多图片上传所以用数组来接。此处应该注意参数名应该和<input>中的name值相对应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@RequestMapping("/pic")
@ResponseBody
public ResponseEntity<String> pic(MultipartFile [] pictures) throws Exception {
ResponseEntity<String> responseEntity = new ResponseEntity<>();
long count = Arrays.asList(pictures).stream().
map(MultipartFile::getOriginalFilename).
filter(String::isEmpty).count();
if (count == pictures.length){
responseEntity.setCode(ResponseEntity.ERROR);
throw new NullOrEmptyException("图片不能同时为空");
}
responseEntity.setCode(ResponseEntity.OK);
responseEntity.setMessage("上传成功");
return responseEntity;
}

前端页面的代码,此处的name值和Controller的参数名称是对应的:

1
2
3
4
5
6
7
8
9
10
11
12
13
jsx复制代码<div class="container">
<div class="avatar-upload">
<div class="avatar-edit">
<input type='file' name="pictures" id="imageOne" accept=".png, .jpg, .jpeg"/>
<label for="imageOne"></label>
</div>
<div class="avatar-preview">
<div id="imageOnePreview"
style="background-image: url(http://ww3.sinaimg.cn/large/006tNc79ly1g556ca7ovqj30ak09mta2.jpg);">
</div>
</div>
</div>
</div>

js代码回显

1
2
3
4
5
6
7
8
9
10
11
12
13
14
jsx复制代码function readURLOne(input) {
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function(e) {
$('#imageOnePreview').css('background-image', 'url('+e.target.result +')');
$('#imageOnePreview').hide();
$('#imageOnePreview').fadeIn(650);
}
reader.readAsDataURL(input.files[0]);
}
}
$("#imageOne").change(function() {
readURLOne(this);
});

js代码上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
jsx复制代码function getUpload(){
//获取form表单中所有属性 key为name值
var formData = new FormData($("#picForm")[0]);
$.ajax({
url: '/pic',
type: 'POST',
dataType:"json",
data: formData,
processData: false,
contentType: false,
success:(function(data) {
window.confirm(data.message);
window.location.reload();
}),
error:(function(res) {
alert("失败");
})
});
}

效果展示

初始页面如下

上传完图片以后回显为

点击提交以后可将图片上传至后台

配置上传图片的属性

默认情况下只允许上传1MB以下的图片,如果要设置上传图片大小。那么需要在配置文件中如下配置

1
2
3
4
5
6
swift复制代码spring:
servlet:
multipart:
enabled: true
max-file-size: 20MB
max-request-size: 20MB

关于文件的配置有下面几个

1
2
3
4
5
6
bash复制代码spring.servlet.multipart.enabled=true # 是否支持多文件上传
spring.servlet.multipart.file-size-threshold=0B # 文件写入磁盘的阈值
spring.servlet.multipart.location= # 上传文件的保存地址
spring.servlet.multipart.max-file-size=1MB # 上传文件的最大值
spring.servlet.multipart.max-request-size=10MB # 请求的最大值
spring.servlet.multipart.resolve-lazily=false # 是否在文件或参数访问时延迟解析多部分请求

Spring Boot 基础教程和示例源码:github.com/javastacks/…

异常处理

异常处理用了Springboot提供的全局异常处理机制。只需要在类上加入@ControllerAdvice注解即可。在方法上加入@ExceptionHandler(想要拦截的异常类)就能拦截所有Controller的异常了。

如果想要拦截指定为特定的Controller只需要在@ControllerAdvice(basePackageClasses=想要拦截的Controller)

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
tsx复制代码@ControllerAdvice
@Slf4j
public class CommonExceptionHandler extends ResponseEntityExceptionHandler {

@ExceptionHandler(NullOrEmptyException.class)
@ResponseBody
public ResponseEntity<String> nullOrEmptyExceptionHandler(HttpServletRequest request, NullOrEmptyException exception){
log.info("nullOrEmptyExceptionHandler");
return handleErrorInfo(request, exception.getMessage());
}

@ExceptionHandler(value = Exception.class)
@ResponseBody
public ResponseEntity<String> defaultErrorHandler(HttpServletRequest request, Exception exception){
log.info("defaultErrorHandler");
return handleErrorInfo(request, exception.getMessage());
}

private ResponseEntity<String> handleErrorInfo(HttpServletRequest request, String message) {
ResponseEntity<String> responseEntity = new ResponseEntity<>();
responseEntity.setMessage(message);
responseEntity.setCode(ResponseEntity.ERROR);
responseEntity.setData(message);
responseEntity.setUrl(request.getRequestURL().toString());
return responseEntity;
}
}

遇到的坑

1、如果返回值是模板文件的文件名,那么无论是类上还是方法上都不能加@ResponseBody注解,因为如果加了的话会被解析成Json串返回。

2、注意前端所传参数名和后端接收参数名一一对应。不然会报405错误

3、使用IDEA开发如果使用了lombok那么需要在Annotation Processors中将Enable annotation processing打对勾

近期热文推荐:

1.1,000+ 道 Java面试题及答案整理(2021最新版)

2.终于靠开源项目弄到 IntelliJ IDEA 激活码了,真香!

3.阿里 Mock 工具正式开源,干掉市面上所有 Mock 工具!

4.Spring Cloud 2020.0.0 正式发布,全新颠覆性版本!

5.《Java开发手册(嵩山版)》最新发布,速速下载!

觉得不错,别忘了随手点赞+转发哦!

本文转载自: 掘金

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

【py小游戏系列】pygame游戏库|Python 主题月

发表于 2021-07-27

本文正在参加「Python主题月」,详情查看 活动链接

Pygame是一组功能强大而有趣的模块,可用于管理图形、动画乃至声音,可以让我们很轻松的开发复杂的游戏。通过使用Pygame来处理在屏幕上绘制图像等任务,不用考虑众多繁琐而艰难的编码的工作,而是将重点放在程序的高级逻辑上。

安装Python

官网地址:www.python.org/downloads/

本系列博文使用的是Python3.6.8

64位系统可以下载Windows x86-64 executable installer,下载完成后双击Python安装包,然后通过图形界面安装,接着设置Python的安装路径,完后将Python3和Python3的Scripts目录配置到环境变量即可。

安装Pygame

地址:www.lfd.uci.edu/~gohlke/pyt…

image.png

或者直接用pip install pygame安装就行,方便快速。

认识pygame

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
python复制代码from sys import exit  # 导入sys库中的exit函数

import pygame # 导入pygame库
from pygame.locals import * # 导入pygame库中的一些常量

# 定义窗口的分辨率
SCREEN_WIDTH = 480
SCREEN_HEIGHT = 320


def run_game():
# 初始化游戏并创建一个屏幕对象
pygame.init()
# 初始化一个用于显示的窗口
# 对象screen是一个surface,在Pygame中,surface是屏幕的一部分,用于显示游戏元素
# 这里pygame.display.set_mode()返回的surface表示整个游戏窗口
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
# 设置窗口标题
pygame.display.set_caption("This is my first pygame-program")

# 开始游戏的主循环
while True:
# 监视键盘和鼠标事件
# 从消息队列中循环取
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()

run_game()

image.png

从上面的注释就知道,这是一个使用了pygame并且用上了游戏退出游戏。我们设置上了自己的游戏名称:This is my first pygame-program。

当然我们也可以载入背景图,使用游戏界面有背景。

image.png

1
2
3
4
5
6
arduino复制代码background = pygame.image.load('resources/image/background.jpg')
while True:
#绘制背景
screen.blit(background, (0, 0))
# 更新屏幕
pygame.display.update()

这里面有一个需要注意的逻辑。我们无论什么时候做游戏,都需要一个while True。也就是死循环。如果游戏没有结束之前,都需要用这个死循环去更新游戏逻辑。

整体而言,游戏入门并不算难,也很容易就可以学会。上面的思想还是说的比较清楚的。有需要拿完整的游戏源码的话,请移步到公众号:诗一样的代码。既然进来了,原创不易。小伙伴点个赞再走呗。

本文转载自: 掘金

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

还在用Jenkins?试试Gitlab的CI/CD功能吧,贼

发表于 2021-07-27

SpringBoot实战电商项目mall(50k+star)地址:github.com/macrozheng/…

摘要

之前写过一篇文章《再见 Jenkins !几行脚本搞定自动化部署,这款神器有点厉害!》 ,讲的是使用Gogs+Drone来实现自动化部署。最近发现Gitlab的CI/CD功能也能实现自动化部署,用起来也挺简单!如果你使用的是Gitlab作为Git仓库的话,不妨试试它的CI/CD功能。本文还是以SpringBoot的自动化部署为例,实践下Gitlab的CI/DI功能,希望对大家有所帮助!

安装

通过Gitlab的CI/CD功能实现自动化部署,我们需要安装Gitlab、Gitlab Runner、Maven这些服务。

安装Gitlab

首先我们来安装下Gitlab,对Gitlab安装和使用不了解的朋友可以参考下《10分钟搭建自己的Git仓库》 。

  • 使用如下命令运行Gitlab服务,这里需要注意的是添加了hostname属性,这样我们就可以通过域名来访问Gitlab了(为了避免一些不必要的麻烦),GITLAB_ROOT_PASSWORD这个环境变量可以直接设置Gitlab中root账号的密码;
1
2
3
4
5
6
7
8
9
10
bash复制代码docker run --detach \
--hostname git.macrozheng.com \
--publish 10443:443 --publish 1080:80 --publish 1022:22 \
--name gitlab \
--restart always \
--volume /mydata/gitlab/config:/etc/gitlab \
--volume /mydata/gitlab/logs:/var/log/gitlab \
--volume /mydata/gitlab/data:/var/opt/gitlab \
-e GITLAB_ROOT_PASSWORD=12345678 \
gitlab/gitlab-ce:latest
  • 我们需要通过git.macrozheng.com这个域名来访问Gitlab,如果你没有域名的话,可以通过修改本机的host文件来实现;
1
复制代码192.168.7.134 git.macrozheng.com
  • 由于我们的Gitlab运行在1080端口上,我们想要不加端口来访问,可以使用Nginx来反向代理下,对Nginx不熟悉的朋友可以看下《Nginx的这些妙用,你肯定有不知道的!》 ,在Nginx的配置文件夹中添加git.conf配置文件,内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ini复制代码server {
listen 80; # 同时支持HTTP
server_name git.macrozheng.com; #修改域名

location / {
proxy_pass http://192.168.7.134:1080; # 设置代理服务访问地址
index index.html index.htm;
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
  • 之后我们就可以通过git.macrozheng.com这个域名来访问Gitlab了,输入账号密码root:12345678即可登录;

  • 将我们的SpringBoot应用代码上传到Gitlab上去,这样Gitlab就准备完毕了!这里需要注意的是,如果你在启动Gitlab的时候没有指定hostname的话,你的项目HTTP访问地址会是容器的ID,使用该地址会无法访问Git仓库!

安装Gitlab Runner

Gitlab只是个代码仓库,想要实现CI/CD还需安装gitlab-runner,gitlab-runner相当于Gitlab中任务的执行器,Gitlab会在需要执行任务时调用它。

  • 首先下载gitlab-runner的Docker镜像,选用alpine-bleeding,这个版本非常小巧!
1
bash复制代码docker pull gitlab/gitlab-runner:alpine-bleeding
  • 使用如下命令运行gitlab-runner;
1
2
3
4
bash复制代码docker run --name gitlab-runner --restart always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /mydata/gitlab-runner:/etc/gitlab-runner \
-d gitlab/gitlab-runner:alpine-bleeding
  • 此时我们如果查看gitlab-runner的容器日志的话,会发现如下错误,config.toml文件找不到,这个问题不必担心,当我们将gitlab-runner注册到Gitlab时,会自动生成该文件;
1
bash复制代码ERROR: Failed to load config stat /etc/gitlab-runner/config.toml: no such file or directory  builds=0
  • 接下来我们需要把gitlab-runner注册到Gitlab,打开Project->Settings->CI/CD功能,获取到runner注册需要使用的地址和token;

  • 接下来使用如下命令,进入gitlab-runner容器的内部;
1
bash复制代码docker exec -it gitlab-runner /bin/bash
  • 在容器内使用如下命令注册runner;
1
bash复制代码gitlab-runner register
  • 注册时会出现交互界面,提示你输入注册地址、token、执行器类型等信息,ssh执行器能远程执行Linux命令,非常好用,推荐使用这个!

  • 注册完成后,我们可以发现config.toml文件已经生成,内容如下,以后想修改runner配置的时候,直接改这个文件就行了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ini复制代码concurrent = 1
check_interval = 0

[session_server]
session_timeout = 1800

[[runners]]
name = "docker-runner"
url = "http://192.168.7.134:1080/"
token = "c2kpV6tX6woL8TMxzBUN"
executor = "ssh"
[runners.custom_build_dir]
[runners.cache]
[runners.cache.s3]
[runners.cache.gcs]
[runners.cache.azure]
[runners.ssh]
user = "root"
password = "123456"
host = "192.168.7.134"
port = "22"
  • 在Gitlab的CI/CD设置中,我们可以发现,有个runner成功注册了!

安装Maven

SpringBoot项目打包需要依赖Maven,我们需要在服务器上先安装好它。

  • 下载Maven的Linux安装包,下载地址:maven.apache.org/download.cg…

  • 下载完成后使用如下命令解压到指定目录;
1
2
bash复制代码cd /mydata
tar -zxvf apache-maven-3.8.1-bin.tar.gz
  • 修改/etc/profile文件,添加环境变量配置:
1
2
bash复制代码export MAVEN_HOME=/mydata/apache-maven-3.8.1
export PATH=$PATH:$MAVEN_HOME/bin
  • 通过查看Maven版本来测试是否安装成功。
1
bash复制代码mvn -v
1
2
3
4
bash复制代码Maven home: /mydata/apache-maven-3.8.1
Java version: 1.8.0_292, vendor: AdoptOpenJDK, runtime: /mydata/java/jdk1.8/jre
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "3.10.0-957.el7.x86_64", arch: "amd64", family: "unix"

安装JDK

CentOS上默认安装的是JRE,使用Maven需要安装JDK。

  • 下载JDK 8,下载地址:mirrors.tuna.tsinghua.edu.cn/AdoptOpenJD…

  • 下载完成后将JDK解压到指定目录;
1
2
3
bash复制代码cd /mydata/java
tar -zxvf OpenJDK8U-jdk_x64_linux_xxx.tar.gz
mv OpenJDK8U-jdk_x64_linux_xxx.tar.gz jdk1.8
  • 在/etc/profile文件中添加环境变量JAVA_HOME。
1
2
3
4
5
6
bash复制代码vi /etc/profile
# 在profile文件中添加
export JAVA_HOME=/mydata/java/jdk1.8
export PATH=$PATH:$JAVA_HOME/bin
# 使修改后的profile文件生效
. /etc/profile

使用

一切准备就绪,接下来通过Gitlab的CI/CD功能就可以实现SpringBoot应用的自动化部署了!

  • 首先在项目的根目录下添加.gitlab-ci.yml文件,定义了两个任务,一个任务会将应用代码打包成Jar包并复制到指定目录,另一个任务会通过运行脚本run.sh打包应用的Docker镜像并运行;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
yaml复制代码# 打包任务
build-job:
stage: build
# 指定标签,只有具有该标签的runner才会执行
tags:
- docker
script:
# 使用Maven打包
- mvn clean package
# 将jar包、Dockerfile、运行脚本复制到指定目录
- cp target/mall-tiny-gitlab-1.0-SNAPSHOT.jar /mydata/build/mall-tiny-gitlab-1.0-SNAPSHOT.jar
- cp Dockerfile /mydata/build/Dockerfile
- cp run.sh /mydata/build/run.sh

# 部署任务
deploy-job:
stage: deploy
tags:
- docker
script:
# 进入指定目录并执行运行脚本
- cd /mydata/build
- chmod +x run.sh
- ./run.sh
  • 这里值得一提的是,默认情况下runner只会执行具有相同标签的Job,由于我们对Job和runner都设置了标签为docker,所以我们这里是可以执行的。如果你没有设置标签的话,需要在runner的编辑界面设置下让runner可以执行没有标签的Job;

  • 由于我们的gitlab-runner采用的是ssh的执行器,它会登录到我们指定的服务器,执行我们在.gitlab-ci.yml中定义的script命令,在此之前还会先从Git仓库中获取代码,所以我们还需修改下服务器上的host文件;
1
2
bash复制代码vim /etc/hosts
192.168.7.134 git.macrozheng.com
  • 接下来就是要把脚本提交到Git仓库上去,提交后会在Project->CI/CD->Pipelines中发现正在执行的任务;

  • 打开Pipeline的详情页面,可以发现我们定义的两个任务都已经执行成功了;

  • 打开Job的详情界面,我们可以看到任务执行过程中输出的日志信息;

  • 如果你想手动执行Pipeline,而不是提交触发的话,可以在Pipelines页面点击Run Pipeline按钮即可;

  • 运行成功后,可以通过如下地址访问项目:http://192.168.7.134:8088/swagger-ui/

总结

如果你用Gitlab作为Git仓库的话,使用它的CI/CD功能来实现自动化部署确实很不错!安装一个轻量级gitlab-runner,编写简单的.gitlab-ci.yml脚本文件即可实现。其实我们之前以及介绍过很多种自动化部署方案,比如Jenkins、Gogs+Drone、Gitlab CI/CD,我们可以发现一个共同点,这些方案都离不开Linux命令。 所以说要想玩转自动化部署,还是得先玩转Linux命令!

参考资料

官方文档:docs.gitlab.com/ee/ci/

项目源码地址

github.com/macrozheng/…

本文 GitHub github.com/macrozheng/… 已经收录,欢迎大家Star!

本文转载自: 掘金

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

MySQL千万级数据的表如何优化

发表于 2021-07-26

这里先说明一下,网上很多人说阿里规定500w数据就要分库分表。实际上,这个500w并不是定义死的,而是与MySQL的配置以及机器的硬件有关。MySQL为了提升性能,会将表的索引装载到内存中。但是当表的数据到达一定的量的时候,会导致内存无法存储这些索引,无法存储索引,就只能进行磁盘IO,从而导致性能下降。

实战调优

我这里有张表,数据有1000w,目前只有一个主键索引

1
2
3
4
5
6
7
8
9
10
mysql复制代码CREATE TABLE `user` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`uname` varchar(20) DEFAULT NULL COMMENT '账号',
`pwd` varchar(20) DEFAULT NULL COMMENT '密码',
`addr` varchar(80) DEFAULT NULL COMMENT '地址',
`tel` varchar(20) DEFAULT NULL COMMENT '电话',
`regtime` char(30) DEFAULT NULL COMMENT '注册时间',
`age` int(11) DEFAULT NULL COMMENT '年龄',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10000003 DEFAULT CHARSET=utf8;

image.png
查询所有大概16s。可谓是相当慢了。通常我们一个后台系统,比如这个是一个电商平台,这个是用户表。后台管理系统,一般会查询这些用户信息,做一些操作,比如后台直接新增用户啊,或者删除用户啊这些操作。

所以这里就诞生了两个需求,一个是查询count,一个是分页查询

我们分别来测试一下count用的时间和分页查询所用的时间

1
2
3
4
5
mysql复制代码select * from user limit 1, 10   //几乎不用时
select * from user limit 1000000, 10 //0.35s
select * from user limit 5000000, 10 //1.7s
select * from user limit 9000000, 10 //2.8s
select count(1) from user //1.7s

从上面查询所用时间可以看出来,如果是分页查询的话,查询的数据越往后用时是越长的,查询count也需要1.7s。这显然是不符合我们的要求的。所以,这里我们就需要优化。首先我们这里进行索引优化试试

首先看一下这是只有主键索引的执行计划:

image.png

1
mysql复制代码alter table `user` add INDEX `sindex` (`uname`,`pwd`,`addr`,`tel`,`regtime`,`age`)

image.png

看上面的执行计划,虽然type是从all->index,走了sindex索引,但是实际上查询速度并没有发生改变。

其实,创建联合索引,是为了有条件查询的时候速度更快,而不是全表查询

1
2
mysql复制代码select * from user where uname='6.445329111484186' //3.5s(无联合索引)
select * from user where uname='6.445329111484186' //0.003s(有联合索引)

所以这就是有联合索引和无索引的差距

这里基本上可以证明,加了索引和不加索引,进行全表查询的时候,效率就是会很慢

既然索引这个结果已经不好使了,那就只能找其他方案了。根据我之前mysql面试里面讲的,count我们可以单独存储到一个表里面

1
2
3
4
5
6
mysql复制代码CREATE TABLE `attribute` (
`id` int(11) NOT NULL,
`formname` varchar(50) COLLATE utf8_bin NOT NULL COMMENT '表名',
`formcount` int(11) NOT NULL COMMENT '表总数据',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

image.png
这里说一下,这种表一般不会查所有,只会查询一条,所以建表的时候,可以建成hash

1
mysql复制代码select formcount from attribute where formname='user' //几乎不用时

count就进行优化完了。如果上面有选择条件的话,就可以建立索引,通过走索引筛选的形式来查询,这样就可以不用读这个count了。

那么,count是没问题了,分页查询优化要如何优化呢?这里可以使用子查询来优化

1
2
mysql复制代码select * from user where
id>=(select id from user limit 9000000,1) limit 10 //1.7s

其实子查询这种写法,判断id,其实就是通过覆盖索引来查询。效率会大大增加。不过我这里测试是1.7s,以前在公司优化这方面的时候,比这个查询时间要低,大家也可以自己生成数据自己测试

但是如果说数据量太大了,我还是建议走es或者进行一些默认选择,count可以单独列出来

至此,一个千万级的数据分页查询的优化就完成了。

本文转载自: 掘金

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

抽奖活动业务设计与思考

发表于 2021-07-26

​

1.背景

在很多APP中,我们经常可以看到很多九宫格或者砸金蛋的抽奖活动,例如掘金的签到抽奖。

这个需求目的相对,主要是为了形成内部积分闭环,每日免费赠送一次抽奖机会不仅可以促进产品日活,让用户养成习惯(好的用户是靠养出来的),

用户打开app或者网页端,签到免费获得一次抽奖。

2.抽奖活动业务分析

这里以简单的举例进行业务分析,并不是以掘金抽奖具体业务分析:

  1. 动态展示奖品。
  2. 动态展示抽奖需要消耗的积分数。
  3. 展示当前剩余积分。
  4. 中奖记录分类展示,分为大奖和小奖,并已跑马灯形式展示。
  5. 每个奖品可以配置中奖权重,奖品个数。

如下图所示(来自deepin画图软件随便画了一下)

image.png

​

3.方案规划

  • 主要方案设计
    1. 抽奖活动系统与积分业务系统相关,单抽奖系统是相对独立的。
    2. 后台可以配置中奖奖品与权重,以及每个奖品个数。
    3. 保证抽奖奖品个数有限制的不可多送
  • 前端抽奖架构样例

image.png

当客户端访问活动抽奖系统时,查询出配置的奖品列表,用户可用积分,活动时间段及其他相关配置,抽奖页面跑马灯等。

在这里访问奖品列表以及跑马灯(抽奖记录日志数据)可以进行缓存处理,因为不涉及时效性问题,图上只需要在活动系统与数据库查询之间进行数据缓存。

  • 解决方案

如何保证抽奖奖品个数有限制的不可多送?其实这种问题如今已经很常见了,具体可以在网上可以寻找很多思路,我这里使用了中间件redis 进行标记奖品处理,第一是为了方便服务器水平扩展,第二不使用太多中间件(例如不涉及zk,以及消息队列等):我的做法是后台每次上奖品时候把奖品缓存进redis,进行预加载处理,利用redis inc 以及数据库的锁去保证库存问题。

image.png

当后台服务器通过修改奖品配置,例如奖品权重,去覆盖缓存中的抽奖数据,客户端最主要是和redis打交道,当用户中间,比如奖品类型是需要判断库存的,那么我们就通过数据比较,最后修改数据库,当数据库执行成功时候才认为用户中奖。当redis inc后的数值大于或者等于该奖品库存数量(当然,也可以再次抽奖时候该奖品不混入奖池中)。相信优秀的产品经理一定会考虑到程序猿会写bug,所以顺水推舟时候,会设计一个安慰奖作为抽奖意外,比如设计一个10积分,当某用户抽奖抽到了“大奖”,由于发大奖逻辑异常,这个时候就可以返回一个默认奖

  • 发奖逻辑方案

对于发奖,这个就很简单了,根据中奖类型,走不同的逻辑,典型的if…else if..,这里可以使用 策略模式 优化代码。

4.数据库设计

  • 活动配置表
字段 数据类型 描述
id bigint(19) 主键
image varchar(512) 活动大图
name varchar(128) 活动名称
start_time datetime 活动开始时间
end_time datetime 活动结束时间
consume_type varchar(20) 消耗类型(integral积分,jewel宝石)
consume_num bigint(19) 消耗数量(单位个)
status tinyint(2) 活动状态(1开启,2关闭)
create_time datetime 创建时间
create_user varchar(20) 创建用户
update_time datetime 修改时间
update_user varchar(20) 修改用户
is_delete tinyint(2) 是否删除
  • 奖品表
字段 数据类型 描述
id bigint(19) 主键
icon varchar(512) 奖品的icon
name varchar(64) 奖品名称
type tinyint(2) 奖品类型(1积分,2会员,3实物大奖)
stock_count int(11) 库存(单位个)
enable_stock tinyint(2) 开启库存校验(0关闭,1开启)
weight double(2,4) 奖品权重
activity_id bigInt(19) 绑定活动的id
create_time datetime 创建时间
create_user varchar(20) 创建用户
update_time datetime 修改时间
update_user varchar(20) 修改用户
is_delete tinyint(2) 是否删除
  • 抽奖日志表(哪个用户什么时间抽中了什么奖品)
字段 数据类型 描述
id bigint(19) 主键
user_id bigint(19) 用户id
prize_id bigint(19) 奖品id
create_time datetime 创建时间
…

5.后台样例

image.png

6.尾言

大多数情况下,抽奖活动为了平衡产品内部 数据流水问题,比如积分,当用户积分过多,会造成积分的“通胀”,也就是没有花出去的地方,比如王者荣耀的钻石抽奖,就是为了平衡钻石,当大量的用户拥有过多的,而没有花出去的地方,这就导致了钻石特别不值钱,例如天天酷跑。在掘金中,设计一个每日签到抽奖的,每日签到作用是促进日活,签到和抽奖结合,也是促进日活的方案,可以增加APP的打开频次和网页的点击量(流量),并且容易养成一批忠实用户,接下来的日子里,掘金的兑换商店会做出来,钻石是产品内部唯一通用货币,送的钻石如此之多,我猜,屯钻石的用户应该会很快兑换到奖品,从而慢慢弱化抽奖,作为一个钻石消耗口,后面钻石可能不会有这么多出现。

​ 不想了,老实打代码吧,剩下的交个数据分析的人,我们只要正常造就行~(希望掘金不要揍我 ^_^ )

本文转载自: 掘金

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

三分钟入门 InnoDB 存储引擎中的表锁和行锁

发表于 2021-07-26

各位对 ”锁“ 这个概念应该都不是很陌生吧,Java 语言中就提供了两种锁:内置的 synchronized 锁和 Lock 接口,使用锁的目的就是管理对共享资源的并发访问,保证数据的完整性和一致性,数据库中的锁也不例外。

“锁” 是数据库系统区别于文件系统的一个关键特性,其对象是事务,用来锁定的是数据库中的对象,如表、页、行等。需要注意的是,每种数据库对于锁的实现都是不同的,并且对于 MySQL 来说,每种存储引擎都可以实现自己的锁策略和锁粒度,比如 InnoDB 引擎支持行锁和表锁,而 MyISAM 引擎只支持表锁。

本文所讲的锁针对的是我们最常用的 InnoDB 存储引擎。

表锁与行锁

所谓 “表锁 (Table Lock)”,就是会锁定整张表,它是 MySQL 中最基本的锁策略,并不依赖于存储引擎,就是说不管你是 MySQL 的什么存储引擎,对于表锁的策略都是一样的,并且表锁是开销最小的策略(因为粒度比较大)。

由于表级锁一次会将整个表锁定,所以可以很好的避免死锁问题。当然,锁的粒度大所带来最大的负面影响就是出现锁资源争用的概率也会最高,导致并发率大打折扣。

而所谓 “行锁(Row Lock)”,也称为记录锁,顾名思义,就是锁住某一行(某条记录 row)。需要的注意的是,MySQL 服务器层并没有实现行锁机制,行级锁只在存储引擎层实现 !!!

读锁和写锁

首先说明一点,对于 InnoDB 引擎来说,读锁和写锁可以加在表上,也可以加在行上。

对于并发读和并发写的问题,可以通过实现一个由两种类型的锁组成的锁系统来解决。这两种类型的锁通常被称为 共享锁(Shared Lock,S Lock) 和 排他锁(Exclusive Lock,X Lock),也叫 读锁(readlock) 和 写锁(write lock):

  • 共享锁 / 读锁:允许事务读(select)数据
  • 排他锁 / 写锁:允许事务删除(delete)或更新(update)数据

读锁是共享的,或者说是相互不阻塞的。多个事务在同一时刻可以同时读取同一个资源,而互不干扰。写锁是排他的,也就是说一个写锁会阻塞其他的读锁和写锁,这样就能确保在给定的时间里,只有一个事务能执行写入,并防止其他用户读取正在写入的同一资源。

用行级读写锁来举个例子吧:如果一个事务 T1 已经获得了某个行 r 的读锁,那么此时另外的一个事务 T2 是可以去获得这个行 r 的读锁的,因为读取操作并没有改变行 r 的数据;但是,如果某个事务 T3 想获得行 r 的写锁,则它其必须等待事务 T1、T2 释放掉行 r 上的读锁才行。

兼容关系如下表(兼容是指对同一张表或记录的锁的兼容性情况):

X 锁 S 锁
X 锁 不兼容 不兼容
S 锁 不兼容 兼容

从上表可以看出,只有共享锁和共享锁是兼容的,而排他锁和谁都是不兼容的。

意向锁

InnoDB 存储引擎支持 多粒度(granular)锁定,就是说允许事务在行级上的锁和表级上的锁同时存在。

那么为了实现行锁和表锁并存,InnoDB 存储引擎就设计出了 意向锁(Intention Lock) 这个东西:

Intention locks are table-level locks that indicate which type of lock (shared or exclusive) a transaction requires later for a row in a table.

很好理解:意向锁是一个表级锁,其作用就是指明接下来的事务将会用到哪种锁。

有两种意向锁:

  • 意向共享锁(IS Lock):当事务想要获得一张表中某几行的共享锁行级锁)时,InnoDB 存储引擎会自动地先获取该表的意向共享锁(表级锁)
  • 意向排他锁(IX Lock):当事务想要获得一张表中某几行的排他锁(行级锁)时,InnoDB 存储引擎会自动地先获取该表的意向排他锁(表级锁)

各位其实可以直接把 ”意向“ 翻译成 ”想要“,想要共享锁、想要排他锁,你就会发现原来就这东西啊(滑稽)。

意向锁之间是相互兼容的:

IS 锁 IX 锁
IS 锁 兼容 兼容
IX 锁 兼容 兼容

但是与表级读写锁之间大部分都是不兼容的:

X 锁 S 锁
IS 锁 不兼容 兼容
IX 锁 不兼容 不兼容

注意,这里强调一点:上表中的读写锁指的是表级锁,意向锁不会与行级的读写锁互斥!!!

来理解一下为什么说意向锁不会与行级的读写锁互斥。举个例子,事务 T1、事务 T2、事务 T3 分别想对某张表中的记录行 r1、r2、r3 进行修改,很普通的并发场景对吧,这三个事务之间并不会发生干扰,所以是可以正常执行的。

这三个事务都会先对这张表加意向写锁,因为意向锁之间是兼容的嘛,所以这一步没有任何问题。那如果意向锁和行级读写锁互斥的话,岂不是这三个事务都没法再执行下去了,对吧。

OK,看到这里,我们来思考两个问题:

1)为什么没有意向锁的话,表锁和行锁不能共存?

2)意向锁是如何让表锁和行锁共存的?

首先来看第一个问题,假设行锁和表锁能共存,举个例子:事务 T1 锁住表中的某一行(行级写锁),事务 T2 锁住整个表(表级写锁)。

问题很明显,既然事务 T1 锁住了某一行,那么其他事务就不可能修改这一行。这与 ”事务 T2 锁住整个表就能修改表中的任意一行“ 形成了冲突。所以,没有意向锁的时候,行锁与表锁是无法共存的。

再来看第二个问题,有了意向锁之后,事务 T1 在申请行级写锁之前,MySQL 会先自动给事务 T1 申请这张表的意向排他锁,当表上有意向排他锁时其他事务申请表级写锁会被阻塞,也即事务 T2 申请这张表的写锁就会失败。

如何加锁

在说加锁之前,我们有必要了解下解锁机制。对于 InnoDB 来说,随时都可以加锁,但是并非随时都可以解锁。具体来说,InnoDB 采用的是两阶段锁定协议(two-phase locking protocol):即在事务执行过程中,随时都可以执行加锁操作,但是只有在事务执行 COMMIT 或者 ROLLBACK 的时候才会释放锁,并且所有的锁是在同一时刻被释放。

说完了解锁机制,再来讲讲加锁机制。

先来看如何加意向锁,它比较特殊,是由 InnoDB 存储引擎自己维护的,用户无法手动操作意向锁,在为数据行加读写锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。

再来看如何加表级锁:

1)隐式锁定:对于常见的 DDL 语句(如 ALTER、CREATE 等),InnoDB 会自动给相应的表加表级锁

2)显示锁定:在执行 SQL 语句时,也可以明确显示指定对某个表进行加锁(lock table user read(write))

1
2
sql复制代码lock table user read; # 加表级读锁
unlock tables; # 释放表级锁

如何加行级锁:

1)对于常见的 DML 语句(如 UPDATE、DELETE 和 INSERT ),InnoDB 会自动给相应的记录行加写锁

2)默认情况下对于普通 SELECT 语句,InnoDB 不会加任何锁,但是在 Serializable 隔离级别下会加行级读锁

上面两种是隐式锁定,InnoDB 也支持通过特定的语句进行显式锁定,不过这些语句并不属于 SQL 规范:

3)SELECT * FROM table_name WHERE ... FOR UPDATE,加行级写锁

4)SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE,加行级读锁

另外,需要注意的是,InnoDB 存储引擎的行级锁是基于索引的(这个下篇文章会详细解释),也就是说当索引失效或者说根本没有用索引的时候,行锁就会升级成表锁。

举个例子(这里就以比较典型的索引失效情况 “使用 or“ 来举例),有数据库如下,id 是主键索引:

1
2
3
4
5
sql复制代码CREATE TABLE `test` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;

新建两个事务,先执行事务 T1 的前两行,也就是不要执行 rollback 也不要 commit:

这个时候事务 T1 没有释放锁,并且由于索引失效事务 T1 其实是锁住了整张表,此时再来执行事务 2,你会发现事务 T2 会卡住,最后超时关闭事务:

🎉 关注公众号 | 飞天小牛肉,即时获取更新

  • 博主东南大学硕士在读,携程 Java 后台开发暑期实习生,利用课余时间运营一个公众号『 飞天小牛肉 』,2020/12/29 日开通,专注分享计算机基础(数据结构 + 算法 + 计算机网络 + 数据库 + 操作系统 + Linux)、Java 技术栈等相关原创技术好文。本公众号的目的就是让大家可以快速掌握重点知识,有的放矢。关注公众号第一时间获取文章更新,成长的路上我们一起进步
  • 并推荐个人维护的开源教程类项目: CS-Wiki(Gitee 推荐项目,现已累计 1.8k+ star), 致力打造完善的后端知识体系,在技术的路上少走弯路,欢迎各位小伙伴前来交流学习 ~ 😊
  • 如果各位小伙伴春招秋招没有拿得出手的项目的话,可以参考我写的一个项目「开源社区系统 Echo」Gitee 官方推荐项目,目前已累计 900+ star,基于 SpringBoot + MyBatis + MySQL + Redis + Kafka + Elasticsearch + Spring Security + … 并提供详细的开发文档和配套教程。公众号后台回复 Echo 可以获取配套教程,目前尚在更新中。

本文转载自: 掘金

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

基于opencv和Python37完成的人脸识别|Pyth

发表于 2021-07-26

本文正在参加「Python主题月」,详情查看 活动链接

目录完成后的文件目录

一、需要准备的材料

1.笔记本电脑(带有摄像头的电脑)

2.python3.7,pycharm

第三方包的安装准备

二,本文采用pip进行安装

在开始菜单栏搜索dos,然后回车启动命令提示符。

在python3.7的Scripts文件夹中可以找到pip.exe。

在命令提示符中输入Scripts文件夹的绝对路径

例:cd C:\python3.7\Scripts

注:cd为Change directory,即更换目录,cd后有空格。

更换目录成功后,输入pip.exe,启动pip

三,启动pip后,就可以开始安装Python的第三方包了,注意要让电脑联网。

第三方包的安装

(1)opencv 的安装,输入:pip install opencv-python。

注:numpy与OpenCV绑定安装,无需自己输入命令。

(2) pillow的安装,输入: pip install pillow

注:pillow为图像处理包。

(3) contrib的安装,输入:pip install opencv-contrib-python

四、人脸识别的程序实现

1.FaceDetection,人脸检测

废话不多说,先上代码

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
ini复制代码import numpy as np
import cv2

# 人脸识别分类器
faceCascade = cv2.CascadeClassifier(r'C:\python3.7\Lib\site-packages\cv2\data\haarcascade_frontalface_default.xml')

# 识别眼睛的分类器
eyeCascade = cv2.CascadeClassifier(r'C:\python3.7\Lib\site-packages\cv2\data\haarcascade_eye.xml')

# 开启摄像头
cap = cv2.VideoCapture(0)
ok = True

while ok:
# 读取摄像头中的图像,ok为是否读取成功的判断参数
ok, img = cap.read()
# 转换成灰度图像
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 人脸检测
faces = faceCascade.detectMultiScale(
gray,
scaleFactor=1.2,
minNeighbors=5,
minSize=(32, 32)
)

# 在检测人脸的基础上检测眼睛
for (x, y, w, h) in faces:
fac_gray = gray[y: (y+h), x: (x+w)]
result = []
eyes = eyeCascade.detectMultiScale(fac_gray, 1.3, 2)

# 眼睛坐标的换算,将相对位置换成绝对位置
for (ex, ey, ew, eh) in eyes:
result.append((x+ex, y+ey, ew, eh))

# 画矩形
for (x, y, w, h) in faces:
cv2.rectangle(img, (x, y), (x+w, y+h), (255, 0, 0), 2)

for (ex, ey, ew, eh) in result:
cv2.rectangle(img, (ex, ey), (ex+ew, ey+eh), (0, 255, 0), 2)

cv2.imshow('video', img)

k = cv2.waitKey(1)
if k == 27: # press 'ESC' to quit
break

cap.release()
cv2.destroyAllWindows()

注:1.人脸识别分类器的路径在不同的电脑上不同,一般来讲,在python3.7\Lib\site-packages\cv2\data中,注意是绝对路径,如果嫌目录太长,可以将分类器和程序放在一起。

注:2.经过我的慎重考虑,我决定不放出我的人脸,请各位读者自行尝试,大概就是一个蓝色的矩形框住你的脸,两个绿色的矩形框住你的眼睛,按esc可退出。

2.FaceDataCollect,人脸数据收集

还是先上代码

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
ini复制代码import cv2
import os
# 调用笔记本内置摄像头,所以参数为0,如果有其他的摄像头可以调整参数为1,2

cap = cv2.VideoCapture(0)

face_detector = cv2.CascadeClassifier('haarcascade_frontalface_default.xml')

face_id = input('\n enter user id:')

print('\n Initializing face capture. Look at the camera and wait ...')

count = 0

while True:

# 从摄像头读取图片

sucess, img = cap.read()

# 转为灰度图片

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 检测人脸

faces = face_detector.detectMultiScale(gray, 1.3, 5)

for (x, y, w, h) in faces:
cv2.rectangle(img, (x, y), (x+w, y+w), (255, 0, 0))
count += 1

# 保存图像
cv2.imwrite("Facedata/User." + str(face_id) + '.' + str(count) + '.jpg', gray[y: y + h, x: x + w])

cv2.imshow('image', img)

# 保持画面的持续。

k = cv2.waitKey(1)

if k == 27: # 通过esc键退出摄像
break

elif count >= 1000: # 得到1000个样本后退出摄像
break

# 关闭摄像头
cap.release()
cv2.destroyAllWindows()

注:1.在运行该程序前,请先创建一个Facedata文件夹并和你的程序放在一个文件夹下。

友情提示:请将程序和文件打包放在一个叫人脸识别的文件夹下。可以把分类器也放入其中。

注:2.程序运行过程中,会提示你输入id,请从0开始输入,即第一个人的脸的数据id为0,第二个人的脸的数据id为1,运行一次可收集一张人脸的数据。

注:3.程序运行时间可能会比较长,可能会有几分钟,如果嫌长,可以将 #得到1000个样本后退出摄像 这个注释前的1000,改为100。

如果实在等不及,可按esc退出,但可能会导致数据不够模型精度下降。

3.face_training,人脸数据训练

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
python复制代码import numpy as np
from PIL import Image
import os
import cv2
# 人脸数据路径
path = 'Facedata'

recognizer = cv2.face.LBPHFaceRecognizer_create()
detector = cv2.CascadeClassifier("haarcascade_frontalface_default.xml")

def getImagesAndLabels(path):
imagePaths = [os.path.join(path, f) for f in os.listdir(path)] # join函数的作用?
faceSamples = []
ids = []
for imagePath in imagePaths:
PIL_img = Image.open(imagePath).convert('L') # convert it to grayscale
img_numpy = np.array(PIL_img, 'uint8')
id = int(os.path.split(imagePath)[-1].split(".")[1])
faces = detector.detectMultiScale(img_numpy)
for (x, y, w, h) in faces:
faceSamples.append(img_numpy[y:y + h, x: x + w])
ids.append(id)
return faceSamples, ids


print('Training faces. It will take a few seconds. Wait ...')
faces, ids = getImagesAndLabels(path)
recognizer.train(faces, np.array(ids))

recognizer.write(r'face_trainer\trainer.yml')
print("{0} faces trained. Exiting Program".format(len(np.unique(ids))))

注:1.第8行的LBPHFaceRecognizer_create()为contrib中的函数,笔者之前自己摸索时,没有安装此包,因此卡了很久,印象深刻。

注:2.运行该程序前,请在人脸识别文件夹下创建face_trainer文件夹。

4.face_recognition 人脸检测

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
ini复制代码import cv2

recognizer = cv2.face.LBPHFaceRecognizer_create()
recognizer.read('face_trainer/trainer.yml')
cascadePath = "haarcascade_frontalface_default.xml"
faceCascade = cv2.CascadeClassifier(cascadePath)
font = cv2.FONT_HERSHEY_SIMPLEX

idnum = 0

names = ['Allen', 'Bob']

cam = cv2.VideoCapture(0)
minW = 0.1*cam.get(3)
minH = 0.1*cam.get(4)

while True:
ret, img = cam.read()
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

faces = faceCascade.detectMultiScale(
gray,
scaleFactor=1.2,
minNeighbors=5,
minSize=(int(minW), int(minH))
)

for (x, y, w, h) in faces:
cv2.rectangle(img, (x, y), (x+w, y+h), (0, 255, 0), 2)
idnum, confidence = recognizer.predict(gray[y:y+h, x:x+w])

if confidence < 100:
idnum = names[idnum]
confidence = "{0}%".format(round(100 - confidence))
else:
idnum = "unknown"
confidence = "{0}%".format(round(100 - confidence))

cv2.putText(img, str(idnum), (x+5, y-5), font, 1, (0, 0, 255), 1)
cv2.putText(img, str(confidence), (x+5, y+h-5), font, 1, (0, 0, 0), 1)

cv2.imshow('camera', img)
k = cv2.waitKey(10)
if k == 27:
break

cam.release()
cv2.destroyAllWindows()

注:1. 11行的names中存储人的名字,若该人id为0则他的名字在第一位,id位1则排在第二位,以此类推。

注:2. 最终效果为一个绿框,框住人脸,左上角为红色的人名,左下角为黑色的概率。

五,结语

在这里我要感谢个人博客

www.cnblogs.com/xp12345的技术支…
照着他的步骤成功的完成了人脸识别,改动地方不多,希望能对你们有帮助!

本文转载自: 掘金

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

1…591592593…956

开发者博客

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