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

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


  • 首页

  • 归档

  • 搜索

搜狐三面:说说你是怎么解决MySQL死锁问题的! 前言

发表于 2021-03-05

前言

咱们使用 MySQL 大概率上都会遇到死锁问题,这实在是个令人非常头痛的问题。本文将会对死锁进行相应介绍,对常见的死锁案例进行相关分析与探讨,以及如何去尽可能避免死锁给出一些建议。

–什么是死锁 –

死锁是并发系统中常见的问题,同样也会出现在数据库MySQL的并发读写请求场景中。当两个及以上的事务,双方都在等待对方释放已经持有的锁或因为加锁顺序不一致造成循环等待锁资源,就会出现“死锁”。常见的报错信息为 ” Deadlock found when trying to get lock… ”。

举例来说 A 事务持有 X1 锁 ,申请 X2 锁,B事务持有 X2 锁,申请 X1 锁。A 和 B 事务持有锁并且申请对方持有的锁进入循环等待,就造成了死锁。

如上图,是右侧的四辆汽车资源请求产生了回路现象,即死循环,导致了死锁。

从死锁的定义来看,MySQL 出现死锁的几个要素为:

a.两个或者两个以上事务

b.每个事务都已经持有锁并且申请新的锁

c.锁资源同时只能被同一个事务持有或者不兼容

d.事务之间因为持有锁和申请锁导致彼此循环等待

说明:后续内容实验环境为 5.7 版本,隔离级别为 RR(可重复读)

– InnoDB 锁类型–

为了分析死锁,我们有必要对 InnoDB 的锁类型有一个了解。

MySQL InnoDB 引擎实现了标准的行级别锁:共享锁( S lock ) 和排他锁 ( X lock )

  • 不同事务可以同时对同一行记录加 S 锁。
  • 如果一个事务对某一行记录加 X 锁,其他事务就不能加 S 锁或者 X 锁,从而导致锁等待。

如果事务 T1 持有行 r 的 S 锁,那么另一个事务 T2 请求 r 的锁时,会做如下处理:

  • T2 请求 S 锁立即被允许,结果 T1 T2 都持有 r 行的 S 锁
  • T2 请求 X 锁不能被立即允许

如果 T1 持有 r 的 X 锁,那么 T2 请求 r 的 X、S 锁都不能被立即允许,T2 必须等待 T1 释放 X 锁才可以,因为 X 锁与任何的锁都不兼容。共享锁和排他锁的兼容性如下所示:

间隙锁( gap lock )

间隙锁锁住一个间隙以防止插入。假设索引列有2, 4, 8 三个值,如果对 4 加锁,那么也会同时对(2,4)和(4,8)这两个间隙加锁。其他事务无法插入索引值在这两个间隙之间的记录。但是,间隙锁有个例外:

  • 如果索引列是唯一索引,那么只会锁住这条记录(只加行锁),而不会锁住间隙。
  • 对于联合索引且是唯一索引,如果 where 条件只包括联合索引的一部分,那么依然会加间隙锁。

next-key lock

next-key lock 实际上就是 行锁+这条记录前面的 gap lock 的组合。假设有索引值10,11,13和 20,那么可能的 next-key lock 包括:

(负无穷,10]

(10,11]

(11,13]

(13,20]

(20,正无穷)

在 RR 隔离级别下,InnoDB 使用 next-key lock 主要是防止幻读问题产生。

意向锁( Intention lock )

InnoDB 为了支持多粒度的加锁,允许行锁和表锁同时存在。为了支持在不同粒度上的加锁操作,InnoDB 支持了额外的一种锁方式,称之为意向锁( Intention Lock )。意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁。意向锁分为两种:

  • 意向共享锁( IS ):事务有意向对表中的某些行加共享锁
  • 意向排他锁( IX ):事务有意向对表中的某些行加排他锁

由于 InnoDB 存储引擎支持的是行级别的锁,因此意向锁其实不会阻塞除全表扫描以外的任何请求。表级意向锁与行级锁的兼容性如下所示:

插入意向锁( Insert Intention lock )

插入意向锁是在插入一行记录操作之前设置的一种间隙锁,这个锁释放了一种插入方式的信号,即多个事务在相同的索引间隙插入时如果不是插入间隙中相同的位置就不需要互相等待。假设某列有索引值2,6,只要两个事务插入位置不同(如事务 A 插入3,事务 B 插入4),那么就可以同时插入。

锁模式兼容矩阵

横向是已持有锁,纵向是正在请求的锁:

–阅读死锁日志–

一个温馨小提示: xmen 平台支持查看死锁主库的死锁日志,访问方式如下:

登录 xmen.intra.ke.com/#/mysql/mys… 点击集群管理 -> 输入集群端口 -> 工具集合 -> 查看死锁日志 点击查询即可查看最近一次死锁日志。

在进行具体案例分析之前,咱们先了解下如何去读懂死锁日志,尽可能地使用死锁日志里面的信息来帮助我们来解决死锁问题。

后面测试用例的数据库场景如下:

MySQL 5.7 事务隔离级别为 RR

表结构和数据如下:

测试用例如下:

通过执行show engine innodb status 可以查看到最近一次死锁的日志。

日志分析如下:

*** (1) TRANSACTION:

TRANSACTION 2322, ACTIVE 6 sec starting index read

事务号为2322,活跃 6秒,starting index read 表示事务状态为根据索引读取数据。常见的其他状态有:

mysql tables in use 1 说明当前的事务使用一个表。

locked 1 表示表上有一个表锁,对于 DML 语句为 LOCK_IX

LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)

LOCK WAIT 表示正在等待锁,2 lock struct(s) 表示 trx->trx_locks 锁链表的长度为2,每个链表节点代表该事务持有的一个锁结构,包括表锁,记录锁以及自增锁等。本用例中 2locks 表示 IX 锁和lock_mode X (Next-key lock)

1 row lock(s) 表示当前事务持有的行记录锁/ gap 锁的个数。

MySQL thread id 37, OS thread handle 140445500716800, query id 1234 127.0.0.1 root updating

MySQL thread id 37 表示执行该事务的线程 ID 为 37 (即 show processlist; 展示的 ID )

delete from student where stuno=5 表示事务1正在执行的 sql,比较难受的事情是 show engine innodb status 是查看不到完整的 sql 的,通常显示当前正在等待锁的 sql。

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:

RECORD LOCKS space id 11 page no 5 n bits 72 index idx_stuno of table cw**.**student trx id 2322 lock_mode X waiting

RECORD LOCKS 表示记录锁, 此条内容表示事务 1 正在等待表 student 上的 idx_stuno 的 X 锁,本案例中其实是 Next-Key Lock 。

事务2的 log 和上面分析类似:

*** (2) HOLDS THE LOCK(S):

RECORD LOCKS space id 11 page no 5 n bits 72 index idx_stuno of table cw**.**student trx id 2321 lock_mode X

显示事务 2 的 insert into student(stuno,score) values(2,10) 持有了 a=5 的 Lock mode X

| LOCK_gap,不过我们从日志里面看不到事务2执行的 delete from student where stuno=5;

这点也是造成 DBA 仅仅根据日志难以分析死锁的问题的根本原因。

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:

RECORD LOCKS space id 11 page no 5 n bits 72 index idx_stuno of table cw**.**student trx id 2321 lock_mode X locks gap before rec insert intention waiting

表示事务 2 的 insert 语句正在等待插入意向锁 lock_mode X locks gap before rec insert intention waiting ( LOCK_X + LOCK_REC_gap )

–经典案例分析–

案例一:并发申请 gap 锁导致死锁

表结构和数据如下所示:

测试用例如下(本测试用例场景是两个事务删除不存在的行,然后再 insert 记录):

死锁日志如下所示:

死锁日志分析如下:

重点说明下 delete 不存在的记录是要加上 gap 锁, 事务日志中显示lock_mode X locks gap before rec .

  1. T2:delete from t4 where kdt_id=15 and admin_id= 1 and biz=’retail’ and role_id=1; 符合条件的

记录不存在,导致T2先持有了( lock_mode X locks gap before rec ) 锁住

(2,20,1,1,’ratail’,1,0)-(3,30,1,’retail’,1,0)的区间,防止符合条件的记录插入。

  1. T1 的 delete 与 T1 的 delete 一样同样申请了( lock_mode X locks gap but rec ) 锁住了

(2,20,1,’retail’,1,0)-(3,30,1,’retail’,1,0)的区间。

  1. T1 的 insert 语句申请插入意向锁,但是插入意向锁和 T2 持有的 X gap ( lock_mode X locks gap before rec ) 冲突,故等待 T2 中的 gap 锁释放。
  2. T2 的 insert 语句申请插入意向锁,但是插入意向锁和 T1 持有 X gap (lock_mode X locks gap before rec )冲突,故等待T1中的 gap 锁释放。

总结来说,就是 T1 (insert) 等待 T2 (delete) , T2 (insert) 等待 T1 (delete) 故而循环等待,出现死锁。

案例二:事务并发 insert 唯一键冲突

表结构和数据如下所示:

测试用例如下:

死锁日志如下:

日志分析如下:

1.事务 T2 insert into t7(id,a) values (26,10) 语句 insert 成功,持有 a=10 的 排他行锁( X

locks rec but no gap )

2.事务 T1 insert into t7(id,a) values (30,10), 因为T2的第一条 insert 已经插入 a=10 的记录,

事务 T1 insert a=10 则发生唯一键冲突,需要申请对冲突的唯一索引加上S Next-key Lock

( 即 lock mode S waiting ) 这是一个间隙锁会申请锁住(,10],(10,20]之间的 gap 区域。

3.事务 T2 insert into t7(id,a) values (40,9)该语句插入的 a=9 的值在事务 T1 申请的 gap 锁

4,10之间, 故需事务 T2 的第二条 insert 语句要等待事务 T1 的 S-Next-key Lock 锁释放,

在日志中显示 lock_mode X locks gap before rec insert intention waiting 。

案例三:普通索引和主键相互竞争导致循环等待

表结构和数据如下所示:

测试用例如下:

死锁日志:

死锁日志分析:

首先要理解的是 对同一个字段申请加锁是需要排队的。

其次表tx中索引 idx_c1 为非唯一普通索引。

(1). T2 执行 select for update 操作持有记录 id=30 的主键行锁:PRIMARY of table test.tx trx id 2077 lock_mode X locks rec but not gap。

(2). T1 语句 update 通过普通索引 idx_c1 更新 c2,先获取 idx_c1 c1=5 的 X 锁 lock_mode X locks rec but not gap, 然后去申请对应主键 id=30 的行锁, 但是 T2 已经持有主键的行数,于是 T1 等待。

(3). T2 执行根据主键 id=30 删除记录,需要申请 id=30 的行锁以及 c1=5 的索引行锁。但是 T1 尚及持有该锁, 故会出现 index idx_c1 of table test.tx trx id 2077 lock_mode X locks rec but not gap waiting .

T2(delete) 等待 T1(update), T1(update) 等待 T2 (select for update)循环等待,造成死锁。

案例四:先 update 再 insert 的并发死锁问题

表结构如下,无数据:

测试用例如下:

死锁日志如下:

死锁分析:

可以看到两个事务 update 不存在的记录,先后获得间隙锁( gap 锁),gap 锁之间是兼容的所以在update环节不会阻塞。两者都持有 gap 锁,然后去竞争插入意向锁。当存在其他会话持有 gap 锁的时候,当前会话申请不了插入意向锁,导致死锁。

今日读者福利:关注公众号:麒麟改bug,即可领取一份阿里内部Java学习笔记+金三银四面试真题分享【附答案解析】

–如何尽可能避免死锁–

1.合理的设计索引,区分度高的列放到组合索引前面,使业务 SQL 尽可能通过索引定位更少的行,减少

锁竞争。

2.调整业务逻辑 SQL 执行顺序, 避免 update/delete 长时间持有锁的 SQL 在事务前面。

3.避免大事务,尽量将大事务拆成多个小事务来处理,小事务发生锁冲突的几率也更小。

4.以固定的顺序访问表和行。比如两个更新数据的事务,事务 A 更新数据的顺序为 1,2;事

务 B 更新数据的顺序为 2,1。这样更可能会造成死锁。

5.在并发比较高的系统中,不要显式加锁,特别是是在事务里显式加锁。如 select … for

update 语句,如果是在事务里(运行了 start transaction 或设置了autocommit 等于0),

那么就会锁定所查找到的记录。

6.尽量按主键/索引去查找记录,范围查找增加了锁冲突的可能性,也不要利用数据库做一些

额外额度计算工作。比如有的程序会用到 “select … where … order by rand();”

这样的语句,由于类似这样的语句用不到索引,因此将导致整个表的数据都被锁住。

7.优化 SQL 和表设计,减少同时占用太多资源的情况。比如说,减少连接的表,将复杂 SQL

分解为多个简单的 SQL。

本文转载自: 掘金

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

RDM不能用了?快来看看这款开源Redis可视化客户端

发表于 2021-03-04

前言

之前,查老师发布过一篇有关于 Redis 可视化客户端:RDM 的介绍文章,有同学看了后留言给查老师说:“RDM 不怎么好用,建议试试 Another Redis Desktop Manager。“

实际上,虽然查老师发的是 RDM,但近段时间一直在用的就是 ARDM。当然了,这不是查老师藏着掖着,而是因为资源不得一点点发吗?而且,还是有同学在用 RDM 的,习惯一个工具之后,随便更换也是需要学习成本不是?

事情不是你想象的那样,你听我狡辩

OK,本篇查老师就要给同学们分享一下这个所谓的 ARDM。

简介

Another Redis Desktop Manager,一个更快,更好,更稳定的 Redis 桌面管理器,兼容 Linux, windows, mac。更重要的是,它不会在加载大量的键时崩溃。[1]

顾名思义,Another Redis Desktop Manager 就是 另一个 RDM 的意思,它在功能方面和 RDM 大体没什么区别,不过在 UI 和体验上的确更胜一筹。

这个项目从 2019 年 2 月份就开始了,开源且免费提供打包版本,更新到今天也不短的时间了,所以关于稳定性的问题就暂时不用担心了。[2]

img

下载

打开 GitHub 直接搜索 AnotherRedisDesktopManager 项目,然后点击项目右下方的最新发行版,就可以跳转到对应的版本下载页面了。

image-20210304213029005

当然,也可以直接复制本文最后参考资料 [2] 的下载地址,然后在 PC 浏览器打开,也同样可以跳转到最新版的下载页面。

然后,根据你的系统情况选择合适的版本下载吧。

image-20210304214136983

下载好了,一个平平无奇的 exe 安装包。

image-20210304215639301

安装

接下来,我们 “傻瓜式” 安装即可。

image-20210304220051383

改动一下安装位置,这个目录专门放开发工具,是查老师以前逐渐养成的个人习惯。

image-20210304220134048

image-20210304220157564

image-20210304220214314

连接服务器

安装完成后,直接打开,界面可真是简洁到家了。

image-20210304220348008

连接服务器的步骤也和 RDM 差不多,点击左上角的 [新建连接]。

进入到新建连接界面之后,依次填写 [Redis 服务器地址,Redis 端口号,Redis 密码,连接名] 后即可点击 [确定] 来新建一个连接。

查老师有话说: 如果你要连接的 Redis 就在本机,并且你没改过什么默认设置(端口、密码等),你甚至只需要在这个界面点一下确定就可以新建好一个连接。

image-20210304220902433

image-20210304221255623

常见使用

虽然,查老师觉得 ARDM 和 RDM 在功能上大体是一样的,但为了照顾小白们的感受,查老师还是按当初介绍 RDM 的步骤再演示一下常见操作。

查看所有键

单击连接名,就可以打开单个连接,默认是处于 0 号数据库,可以根据需求进行数据库切换。另外,打开连接时默认还会在右侧打开当前 Redis 的服务监控。

ARDM查看所有键

存储键

ARDM 中存储键是先新增一个 key,这个 key 默认什么也没存储,你需要再为这个 key 设置下 value,这一步实际就是下面的修改操作。

ARDM存储键

修改值

ARDM修改值

修改过期时间

ARDM修改过期时间

删除键

刚才我们给 name 这个键设置了 5 秒过期之后,唯一存储的数据也没了,我们再新建一个,然后来测试一下删除功能。

ARDM删除键

命令行操作

当你想用命令行操作时,ARDM 同样也可以直接打开控制台连接 Redis 服务器。

ARDM命令行操作

参考资料

[1]Another Redis Desktop Manager GitHub 地址:github.com/qishibo/Ano…

[2]Another Redis Desktop Manager 下载地址:github.com/qishibo/Ano…

后记

C: 好了,ARDM 的介绍就到这儿结束了,至于其他的功能,自行去发现体验吧,那样才更有乐趣,不是吗?

当然,本篇介绍完,查老师暂时就不会再推荐其他 Redis 可视化客户端了,也许后面有后起之秀,到那时候再说吧,也欢迎同学们再留言告诉我。

本文转载自: 掘金

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

ThreadLocal

发表于 2021-03-04

the good goal will make you happy and fightful.

今日电话面试阿里时,突然问到了ThreadLocal.一时没有想起来。。

threadlocal使用方法很简单

1
2
3
dart复制代码static final ThreadLocal<T> sThreadLocal = new ThreadLocal<T>();
sThreadLocal.set()
sThreadLocal.get()

threadlocal而是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据,官方解释如下。

1
2
3
4
5
6
7
8
php复制代码/**
* This class provides thread-local variables. These variables differ from
* their normal counterparts in that each thread that accesses one (via its
* {@code get} or {@code set} method) has its own, independently initialized
* copy of the variable. {@code ThreadLocal} instances are typically private
* static fields in classes that wish to associate state with a thread (e.g.,
* a user ID or Transaction ID).
*/

大致意思就是ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值。

做个不恰当的比喻,从表面上看ThreadLocal相当于维护了一个map,key就是当前的线程,value就是需要存储的对象。

这里的这个比喻是不恰当的,实际上是ThreadLocal的静态内部类ThreadLocalMap为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组下标,而这个下标就是value存储的对应位置。。

作为一个存储数据的类,关键点就在get和set方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
scss复制代码//set 方法
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//实际存储的数据结构类型
ThreadLocalMap map = getMap(t);
//如果存在map就直接set,没有则创建map并set
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

//getMap方法
ThreadLocalMap getMap(Thread t) {
//thred中维护了一个ThreadLocalMap
return t.threadLocals;
}

//createMap
void createMap(Thread t, T firstValue) {
//实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocals
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

从上面代码可以看出每个线程持有一个ThreadLocalMap对象。每一个新的线程Thread都会实例化一个ThreadLocalMap并赋值给成员变量threadLocals,使用时若已经存在threadLocals则直接使用已经存在的对象。

Thread

1
2
3
java复制代码/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

Thread中关于ThreadLocalMap部分的相关声明,接下来看一下createMap方法中的实例化过程。

ThreadLocalMap

set方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
scala复制代码//Entry为ThreadLocalMap静态内部类,对ThreadLocal的若引用
//同时让ThreadLocal和储值形成key-value的关系
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

//ThreadLocalMap构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//内部成员数组,INITIAL_CAPACITY值为16的常量
table = new Entry[INITIAL_CAPACITY];
//位运算,结果与取模相同,计算出需要存放的位置
//threadLocalHashCode比较有趣
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}

通过上面的代码不难看出在实例化ThreadLocalMap时创建了一个长度为16的Entry数组。通过hashCode与length位运算确定出一个索引值i,这个i就是被存储在table数组中的位置。

前面讲过每个线程Thread持有一个ThreadLocalMap类型的实例threadLocals,结合此处的构造方法可以理解成每个线程Thread都持有一个Entry型的数组table,而一切的读取过程都是通过操作这个数组table完成的。

显然table是set和get的焦点,在看具体的set和get方法前,先看下面这段代码。

1
2
3
4
ini复制代码//在某一线程声明了ABC三种类型的ThreadLocal
ThreadLocal<A> sThreadLocalA = new ThreadLocal<A>();
ThreadLocal<B> sThreadLocalB = new ThreadLocal<B>();
ThreadLocal<C> sThreadLocalC = new ThreadLocal<C>();

由前面我们知道对于一个Thread来说只有持有一个ThreadLocalMap,所以ABC对应同一个ThreadLocalMap对象。为了管理ABC,于是将他们存储在一个数组的不同位置,而这个数组就是上面提到的Entry型的数组table。

那么问题来了,ABC在table中的位置是如何确定的?为了能正常够正常的访问对应的值,肯定存在一种方法计算出确定的索引值i,show me code。

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
ini复制代码 //ThreadLocalMap中set方法。
private void set(ThreadLocal<?> key, Object value) {

// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.

Entry[] tab = table;
int len = tab.length;
//获取索引值,这个地方是比较特别的地方
int i = key.threadLocalHashCode & (len-1);

//遍历tab如果已经存在则更新值
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();

if (k == key) {
e.value = value;
return;
}

if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}

//如果上面没有遍历成功则创建新值
tab[i] = new Entry(key, value);
int sz = ++size;
//满足条件数组扩容x2
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

在ThreadLocalMap中的set方法与构造方法能看到以下代码片段。

int i = key.threadLocalHashCode & (len-1)
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)
简而言之就是将threadLocalHashCode进行一个位运算(取模)得到索引i,threadLocalHashCode代码如下。

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
arduino复制代码  //ThreadLocal中threadLocalHashCode相关代码.

private final int threadLocalHashCode = nextHashCode();

/**
* The next hash code to be given out. Updated atomically. Starts at
* zero.
*/
private static AtomicInteger nextHashCode =
new AtomicInteger();

/**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
*/
private static final int HASH_INCREMENT = 0x61c88647;

/**
* Returns the next hash code.
*/
private static int nextHashCode() {
//自增
return nextHashCode.getAndAdd(HASH_INCREMENT);
}

因为static的原因,在每次new ThreadLocal时因为threadLocalHashCode的初始化,会使threadLocalHashCode值自增一次,增量为0x61c88647。

0x61c88647是斐波那契散列乘数,它的优点是通过它散列(hash)出来的结果分布会比较均匀,可以很大程度上避免hash冲突,已初始容量16为例,hash并与15位运算计算数组下标结果如下:

hashCode 数组下标
0x61c88647 7
0xc3910c8e 14
0x255992d5 5
0x8722191c 12
0xe8ea9f63 3
0x4ab325aa 10
0xac7babf1 1
0xe443238 8
0x700cb87f 15
总结如下:

对于某一ThreadLocal来讲,他的索引值i是确定的,在不同线程之间访问时访问的是不同的table数组的同一位置即都为table[i],只不过这个不同线程之间的table是独立的。
对于同一线程的不同ThreadLocal来讲,这些ThreadLocal实例共享一个table数组,然后每个ThreadLocal实例在table中的索引i是不同的。

get()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码//ThreadLocal中get方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

//ThreadLocalMap中getEntry方法
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}

理解了set方法,get方法也就清楚明了,无非是通过计算出索引直接从数组对应位置读取即可。

ThreadLocal实现主要涉及Thread,ThreadLocal,ThreadLocalMap这三个类。关于ThreadLocal的实现流程正如上面写的那样,实际代码还有许多细节处理的部分并没有在这里写出来。

ThreadLocal特性

ThreadLocal和Synchronized都是为了解决多线程中相同变量的访问冲突问题,不同的点是

Synchronized是通过线程等待,牺牲时间来解决访问冲突
ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。
正因为ThreadLocal的线程隔离特性,使他的应用场景相对来说更为特殊一些。在android中Looper、ActivityThread以及AMS中都用到了ThreadLocal。当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。

本文转载自: 掘金

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

《吃透MQ系列》核心基础全在这里了 01 从 MQ 的本质说

发表于 2021-03-04

这是《吃透XXX》技术系列的开篇,这个系列的思路是:先找到每个技术栈最本质的东西,然后以此为出发点,逐渐延伸出其他核心知识。

所以,整个系列侧重于思考力的训练,不仅仅是讲清楚 What,而是更关注 Why 和 How,以帮助大家构建出牢固的知识体系。

回到正文,这是《吃透 MQ》的第一篇文章。本文主要讲解 MQ 的通用知识,让大家先弄明白:如果让你来设计一个 MQ,该如何下手?需要考虑哪些问题?又有哪些技术挑战?

有了这个基础后,我相信后面几篇文章再讲 Kafka 和 RocketMQ 这两种具体的消息中间件时,大家能很快地抓住主脉络,同时分辨出它们各自的特点。

对于 MQ 来说,不管是 RocketMQ、Kafka 还是其他消息队列,它们的本质都是:一发一存一消费。 下面我们以这个本质作为根,一起由浅入深地聊聊 MQ。

01 从 MQ 的本质说起

将 MQ 掰开了揉碎了来看,都是「一发一存一消费」,再直白点就是一个「转发器」。生产者先将消息投递一个叫做「队列」的容器中,然后再从这个容器中取出消息,最后再转发给消费者,仅此而已。

上面这个图便是消息队列最原始的模型,它包含了两个关键词:消息和队列。

1、消息:就是要传输的数据,可以是最简单的文本字符串,也可以是自定义的复杂格式(只要能按预定格式解析出来即可)。

2、队列:大家应该再熟悉不过了,是一种先进先出数据结构。它是存放消息的容器,消息从队尾入队,从队头出队,入队即发消息的过程,出队即收消息的过程。

02 原始模型的进化

再看今天我们最常用的消息队列产品(RocketMQ、Kafka 等等),你会发现:它们都在最原始的消息模型上做了扩展,同时提出了一些新名词,比如:主题(topic)、分区(partition)、队列(queue)等等。

要彻底理解这些五花八门的新概念,我们化繁为简,先从消息模型的演进说起(道理好比:架构从来不是设计出来的,而是演进而来的)

2.1 队列模型

最初的消息队列就是上一节讲的原始模型,它是一个严格意义上的队列(Queue)。消息按照什么顺序写进去,就按照什么顺序读出来。不过,队列没有 “读” 这个操作,读就是出队,从队头中 “删除” 这个消息。

这便是队列模型:它允许多个生产者往同一个队列发送消息。但是,如果有多个消费者,实际上是竞争的关系,也就是一条消息只能被其中一个消费者接收到,读完即被删除。

2.2 发布-订阅模型

如果需要将一份消息数据分发给多个消费者,并且每个消费者都要求收到全量的消息。很显然,队列模型无法满足这个需求。

一个可行的方案是:为每个消费者创建一个单独的队列,让生产者发送多份。这种做法比较笨,而且同一份数据会被复制多份,也很浪费空间。

为了解决这个问题,就演化出了另外一种消息模型:发布-订阅模型。

在发布-订阅模型中,存放消息的容器变成了 “主题”,订阅者在接收消息之前需要先 “订阅主题”。最终,每个订阅者都可以收到同一个主题的全量消息。

仔细对比下它和 “队列模式” 的异同:生产者就是发布者,队列就是主题,消费者就是订阅者,无本质区别。唯一的不同点在于:一份消息数据是否可以被多次消费。

2.3 小结

最后做个小结,上面两种模型说白了就是:单播和广播的区别。而且,当发布-订阅模型中只有 1 个订阅者时,它和队列模型就一样了,因此在功能上是完全兼容队列模型的。

这也解释了为什么现代主流的 RocketMQ、Kafka 都是直接基于发布-订阅模型实现的?此外,RabbitMQ 中之所以有一个 Exchange 模块?其实也是为了解决消息的投递问题,可以变相实现发布-订阅模型。

包括大家接触到的 “消费组”、“集群消费”、“广播消费” 这些概念,都和上面这两种模型相关,以及在应用层面大家最常见的情形:组间广播、组内单播,也属于此范畴。

所以,先掌握一些共性的理论,对于大家再去学习各个消息中间件的具体实现原理时,其实能更好地抓住本质,分清概念。

03 透过模型看 MQ 的应用场景

目前,MQ 的应用场景非常多,大家能倒背如流的是:系统解耦、异步通信和流量削峰。除此之外,还有延迟通知、最终一致性保证、顺序消息、流式处理等等。

那到底是先有消息模型,还是先有应用场景呢?答案肯定是:先有应用场景(也就是先有问题),再有消息模型,因为消息模型只是解决方案的抽象而已。

MQ 经过 30 多年的发展,能从最原始的队列模型发展到今天百花齐放的各种消息中间件(平台级的解决方案),我觉得万变不离其宗,还是得益于:消息模型的适配性很广。

我们试着重新理解下消息队列的模型。它其实解决的是:生产者和消费者的通信问题。那它对比 RPC 有什么联系和区别呢?

通过对比,能很明显地看出两点差异:

1、引入 MQ 后,由之前的一次 RPC 变成了现在的两次 RPC,而且生产者只跟队列耦合,它根本无需知道消费者的存在。

2、多了一个中间节点「队列」进行消息转储,相当于将同步变成了异步。

再返过来思考 MQ 的所有应用场景,就不难理解 MQ 为什么适用了?因为这些应用场景无外乎都利用了上面两个特性。

举一个实际例子,比如说电商业务中最常见的「订单支付」场景:在订单支付成功后,需要更新订单状态、更新用户积分、通知商家有新订单、更新推荐系统中的用户画像等等。

引入 MQ 后,订单支付现在只需要关注它最重要的流程:更新订单状态即可。其他不重要的事情全部交给 MQ 来通知。这便是 MQ 解决的最核心的问题:系统解耦。

改造前订单系统依赖 3 个外部系统,改造后仅仅依赖 MQ,而且后续业务再扩展(比如:营销系统打算针对支付用户奖励优惠券),也不涉及订单系统的修改,从而保证了核心流程的稳定性,降低了维护成本。

这个改造还带来了另外一个好处:因为 MQ 的引入,更新用户积分、通知商家、更新用户画像这些步骤全部变成了异步执行,能减少订单支付的整体耗时,提升订单系统的吞吐量。这便是 MQ 的另一个典型应用场景:异步通信。

除此以外,由于队列能转储消息,对于超出系统承载能力的场景,可以用 MQ 作为 “漏斗” 进行限流保护,即所谓的流量削峰。我们还可以利用队列本身的顺序性,来满足消息必须按顺序投递的场景;利用队列 + 定时任务来实现消息的延时消费 ……

MQ 其他的应用场景基本类似,都能回归到消息模型的特性上,找到它适用的原因,这里就不一一分析了。总之,就是建议大家多从复杂多变的实践场景再回归到理论层面进行思考和抽象,这样能吃得更透。

04 如何设计一个 MQ?

了解了上面这些理论知识以及应用场景后,下面我们再一起看下:到底如何设计一个 MQ?

4.1 MQ 的雏形

我们还是先从简单版的 MQ 入手,如果只是实现一个很粗糙的 MQ,完全不考虑生产环境的要求,该如何设计呢?

文章开头说过,任何 MQ 无外乎:一发一存一消费,这是 MQ 最核心的功能需求。另外,从技术维度来看 MQ 的通信模型,可以理解成:两次 RPC + 消息转储。

有了这些理解,我相信只要有一定的编程基础,不用 1 个小时就能写出一个 MQ 雏形:

1、直接利用成熟的 RPC 框架(Dubbo 或者 Thrift),实现两个接口:发消息和读消息。

2、消息放在本地内存中即可,数据结构可以用 JDK 自带的 ArrayBlockingQueue 。

4.2 写一个适用于生产环境的 MQ

当然,我们的目标绝不止于一个 MQ 雏形,而是希望实现一个可用于生产环境的消息中间件,那难度肯定就不是一个量级了,具体我们该如何下手呢?

1、先把握这个问题的关键点

假如我们还是只考虑最基础的功能:发消息、存消息、消费消息(支持发布-订阅模式)。

那在生产环境中,这些基础功能将面临哪些挑战呢?我们能很快想到下面这些:

1、高并发场景下,如何保证收发消息的性能?

2、如何保证消息服务的高可用和高可靠?

3、如何保证服务是可以水平任意扩展的?

4、如何保证消息存储也是水平可扩展的?

5、各种元数据(比如集群中的各个节点、主题、消费关系等)如何管理,需不需要考虑数据的一致性?

可见,高并发场景下的三高问题在你设计一个 MQ 时都会遇到,「如何满足高性能、高可靠等非功能性需求」才是这个问题的关键所在。

2、整体设计思路

先来看下整体架构,会涉及三类角色:

另外,将「一发一存一消费」这个核心流程进一步细化后,比较完整的数据流如下:

基于上面两个图,我们可以很快明确出 3 类角色的作用,分别如下:

1、Broker(服务端):MQ 中最核心的部分,是 MQ 的服务端,核心逻辑几乎全在这里,它为生产者和消费者提供 RPC 接口,负责消息的存储、备份和删除,以及消费关系的维护等。

2、Producer(生产者):MQ 的客户端之一,调用 Broker 提供的 RPC 接口发送消息。

3、Consumer(消费者):MQ 的另外一个客户端,调用 Broker 提供的 RPC 接口接收消息,同时完成消费确认。

3、详细设计

下面,再展开讨论下一些具体的技术难点和可行的解决方案。

难点1:RPC 通信

解决的是 Broker 与 Producer 以及 Consumer 之间的通信问题。如果不重复造轮子,直接利用成熟的 RPC 框架 Dubbo 或者 Thrift 实现即可,这样不需要考虑服务注册与发现、负载均衡、通信协议、序列化方式等一系列问题了。

当然,你也可以基于 Netty 来做底层通信,用 Zookeeper、Euraka 等来做注册中心,然后自定义一套新的通信协议(类似 Kafka),也可以基于 AMQP 这种标准化的 MQ 协议来做实现(类似 RabbitMQ)。对比直接用 RPC 框架,这种方案的定制化能力和优化空间更大。

难点2:高可用设计

高可用主要涉及两方面:Broker 服务的高可用、存储方案的高可用。可以拆开讨论。

Broker 服务的高可用,只需要保证 Broker 可水平扩展进行集群部署即可,进一步通过服务自动注册与发现、负载均衡、超时重试机制、发送和消费消息时的 ack 机制来保证。

存储方案的高可用有两个思路:1)参考 Kafka 的分区 + 多副本模式,但是需要考虑分布式场景下数据复制和一致性方案(类似 Zab、Raft等协议),并实现自动故障转移;2)还可以用主流的 DB、分布式文件系统、带持久化能力的 KV 系统,它们都有自己的高可用方案。

难点3:存储设计

消息的存储方案是 MQ 的核心部分,可靠性保证已经在高可用设计中谈过了,可靠性要求不高的话直接用内存或者分布式缓存也可以。这里重点说一下存储的高性能如何保证?这个问题的决定因素在于存储结构的设计。

目前主流的方案是:追加写日志文件(数据部分) + 索引文件的方式(很多主流的开源 MQ 都是这种方式),索引设计上可以考虑稠密索引或者稀疏索引,查找消息可以利用跳转表、二份查找等,还可以通过操作系统的页缓存、零拷贝等技术来提升磁盘文件的读写性能。

如果不追求很高的性能,也可以考虑现成的分布式文件系统、KV 存储或者数据库方案。

难点4:消费关系管理

为了支持发布-订阅的广播模式,Broker 需要知道每个主题都有哪些 Consumer 订阅了,基于这个关系进行消息投递。由于 Broker 是集群部署的,所以消费关系通常维护在公共存储上,可以基于 Zookeeper、Apollo 等配置中心来管理以及进行变更通知。

难点5:高性能设计

存储的高性能前面已经谈过了,当然还可以从其他方面进一步优化性能。比如 Reactor 网络 IO 模型、业务线程池的设计、生产端的批量发送、Broker 端的异步刷盘、消费端的批量拉取等等。

4.3 小结

再总结下,要回答好:如何设计一个 MQ?

1、需要从功能性需求(收发消息)和非功能性需求(高性能、高可用、高扩展等)两方面入手。

2、功能性需求不是重点,能覆盖 MQ 最基础的功能即可,至于延时消息、事务消息、重试队列等高级特性只是锦上添花的东西。

3、最核心的是:能结合功能性需求,理清楚整体的数据流,然后顺着这个思路去考虑非功能性的诉求如何满足,这才是技术难点所在。

05 写在最后

这篇文章从 MQ 一发一存一消费这个本质出发,讲解了消息模型的演进过程,这是 MQ 最核心的理论基础。基于此,大家也能更容易理解 MQ 的各种新名词以及应用场景。

最后通过回答:如何设计一个 MQ?目的是让大家对 MQ 的核心组件和技术难点有一个清晰的认识。另外,带着这个问题的答案再去学习 Kafka、RocketMQ 等具体的消息中间件时,也会更有侧重点。

希望大家有所收获,如果有任何意见和建议,欢迎评论区留言反馈!《吃透 MQ 系列》的下一篇是 Kafka,我们下期见!


作者简介:985硕士,前亚马逊工程师,现大厂技术总监

欢迎关注我的个人公众号:武哥漫谈IT,精彩原创不断!

本文转载自: 掘金

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

解决表单重复提交问题的8种解决方案 一 前置知识 二 解决方

发表于 2021-03-04

提出问题: 解决表单重复提交

一 前置知识

1 HTTP是无状态的超文本传输协议,是用于从万维网服务器传输超文本到本地浏览器的传输协议,HTTP是在TCP/IP协议模型上的应用层的一种传输协议

2 查看HTTP请求报文

HTTP请求报文由3部分组成: 请求行+请求头+请求体

1
2
3
4
5
6
7
makefile复制代码POST /user HTTP/1.1                       // 请求行
Host: www.user.com
Content-Type: application/x-www-form-urlencoded
Connection: Keep-Alive
User-agent: Mozilla/5.0. // 以上是请求头

name=world // 请求体(可选,如get请求时可选)

请求行中包含了请求方法,比如上面例子中请求行的POST

3 HTTP协议中的9种方法(其中HTTP1.0定义了三种请求方法:GET, POST 和 HEAD方法,HTTP1.1新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法)

OPTIONS:
OPTIONS请求与HEAD类似,一般也是用于客户端查看服务器的性能。 这个方法会请求服务器返回该资源所支持的所有HTTP请求方法,该方法会用’*‘来代替资源名称,向服务器发送OPTIONS请求,可以测试服务器功能是否正常。

HEAD:
HEAD方法与GET方法一样,都是向服务器发出指定资源的请求。但是,服务器在响应HEAD请求时不会回传资源的内容部分,即:响应主体。这样,我们可以不传输全部内容的情况下,就可以获取服务器的响应头信息。HEAD方法常被用于客户端查看服务器的性能。

GET:
GET请求会显示请求指定的资源。一般来说GET方法应该只用于数据的读取,而不应当用于会产生副作用的非幂等的操作中。它期望的应该是而且应该是安全的和幂等的。这里的安全指的是,请求不会影响到资源的状态。

POST:
POST请求会 向指定资源提交数据,请求服务器进行处理,如:表单数据提交、文件上传等,请求数据会被包含在请求体中。POST方法是非幂等的方法,因为这个请求可能会创建新的资源或/和修改现有资源。

PUT/PATCH:
PUT请求会身向指定资源位置上传其最新内容,PUT方法是幂等的方法。通过该方法客户端可以将指定资源的最新数据传送给服务器取代指定的资源的内容。

PATCH是对PUT方法的补充,用来对已知资源进行局部更新

二者的不同点:

1.PATCH一般用于资源的部分更新,而PUT一般用于资源的整体更新。

2.当资源不存在时,PATCH会创建一个新的资源,而PUT只会对已在资源进行更新。

3.PUT 是幂等的,PATCH是非幂等的

4.PATCH方法出现的较晚,它在2010年的RFC 5789标准中被定义。\

DELETE:
请求服务器删除请求的URI所标识的资源,用于删除

TRACE:
TRACE请求服务器回显其收到的请求信息,该方法主要用于HTTP请求的测试或诊断

CONNECT:
CONNECT方法是HTTP/1.1协议预留的,能够将连接改为管道方式的代理服务器。通常用于SSL加密服务器的链接与非加密的HTTP代理服务器的通信。\

我们看看维基百科对幂等的解释:

幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。 在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。 幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。 这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。
所以,对于编辑表单的请求,我们使用PUT,可以不用做任何保护操作,即多次重复提交也不会对系统造成任何改变,这个时候可能会有杠精说:我是用POST请求后台接口,然后使用update的SQL更新数据库不也是一样的吗?
根据REST规范接口:每个资源都有对应的URI,不同的HTTP Method对应的对资源不同的操作,GET(读取资源信息)、POST(添加资源)、PUT(更新资源信息)、DELETE(删除资源)。几乎所有的计算机语言都可以通过HTTP协议同REST服务器通信。
所以POST请求最好只是用来添加资源,PUT请求用来更新资源信息。

二 解决方法

1 确保按钮只能点击一次

如用户点击查询或提交订单号,按钮变灰或页面显示loding状态(例如展示例如遮罩层等组件)专用于防止用户重复点击。

2 在Session存放唯一标识

用户进入页面时,服务端生成一个唯一的标识值,存到session中,同时将它写入表单的隐藏域中,用户在输入信息后点击提交,在服务端获取表单的隐藏域字段的值来与session中的唯一标识值进行比较,相等则说明是首次提交,就处理本次请求,然后删除session唯一标识,不相等则标识重复提交,忽略本次处理。

3 缓存队列

将请求快速的接收下来,放入缓冲队列中,后续使用异步任务处理队列的数据,过滤掉重复请求,我们可以用LinkedList来实现队列,一个HashSet来实现去重。此方法优点是异步处理、高吞吐,但是不能及时返回请求结果,需要后续轮询处理结果。

4 token+redis

这种方式分成两个阶段:获取token和业务操作阶段。

以支付为例:

第一阶段,在进入到提交订单页面之前,需要在订单系统根据当前用户信息向支付系统发起一次申请token请求,支付系统将token保存到redis中,作为第二阶段支付使用
第二阶段,前端订单系统拿着申请到的token发起支付请求,第一时间删除redis中的token,支付系统会检查redis中是否存在该token,如果有,表示第一次请求支付,开始处理支付逻辑,处理完成后删除redis中的token
当重复请求时候,检查redis中token是否存在,若不存在,则为重复请求

5 基于乐观锁来实现

如果更新已有数据,可以进行加锁更新,也可以设计表结构时使用version来做乐观锁,这样既能保证执行效率,又能保证幂等。乐观锁version字段在更新业务数据时值要自增。

sql为:update table set version = version + 1 where id =1 and version =#{version }

6 Axios拦截器

Axios的介绍:
axios 是一个轻量的 HTTP客户端

基于 XMLHttpRequest 服务来执行 HTTP 请求,支持丰富的配置,支持 Promise,支持浏览器端和 Node.js 端。自Vue2.0起,尤大宣布取消对 vue-resource 的官方推荐,转而推荐 axios。现在 axios 已经成为大部分 Vue 开发者的首选

特性:

1
2
3
4
5
6
7
8
javascript复制代码1 从浏览器中创建 XMLHttpRequests
2 从 node.js 创建 http请求
3 支持 Promise API
4 拦截请求和响应
5 转换请求数据和响应数据
6 取消请求
7 自动转换JSON 数据
8 客户端支持防御XSRF

注意这个特性6取消请求:

6.1 基本使用

1
2
3
4
5
6
7
8
9
10
11
arduino复制代码//安装
npm install axios --S
//导入
import axios from 'axios'
//封装Axios
//利用node环境变量来作判断,用来区分开发、测试、生产环境
if (process.env.NODE_ENV === 'development') {
axios.defaults.baseURL = 'http://dev.xxx.com'
} else if (process.env.NODE_ENV === 'production') {
axios.defaults.baseURL = 'http://prod.xxx.com'
}

6.2 创建如下文件夹

6.3 在lib目录下创建axios.js文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
ini复制代码/* eslint-disable */
import axios from "axios";
import { baseURL } from "@/config";
import md5 from "js-md5";
// 网络请求记录map结构
let pending = {};
//取消请求
let CancelToken = axios.CancelToken;
class HttpRequest {
constructor(baseUrl = baseURL) {
this.baseUrl = baseUrl;
this.queue = {};
}
getInsideConfig(auth) {
var config = {
baseURL: this.baseUrl,
headers: {
Authorization: auth
}
};
return config;
}
distory(url) {
delete this.queue[url];
if (!Object.keys(this.queue).length) {
//Spin.hide()
}
}
interceptors(instance, url) {
instance.interceptors.request.use(
config => {
//检查json数据中是否包含repetitiveRequestLimit属性,若包含,则为此请求添加幂等校验
if(config.data.hasOwnProperty("repetitiveRequestLimit")){
let key = md5(`${config.url}&${config.method}&${JSON.stringify(config.data)}`);
config.cancelToken = new CancelToken(c => {
if (pending[key]) {
if (Date.now() - pending[key] > 5000) {
// 超过5s,删除对应的请求记录,重新发起请求
delete pending[key];
} else {
// 5s以内的已发起请求,取消重复请求
c("repeated");
}
}
});
// 记录当前的请求,已存在则更新时间戳
pending[key] = Date.now();
}else{
console.log('我是没有repetitiveRequestLimit的请求')
}
return config;
},
error => {
return Promise.reject(error);
}
);
instance.interceptors.response.use(
res => {
this.distory(url);
var { data } = res;
return data;
},
error => {
// 错误的请求结果处理,这里的代码根据后台的状态码来决定错误的输出信息
if (error && error.response) {
switch (error.response.status) {
case 400:
error.message = "错误请求";
break;
case 401:
error.message = "未授权,请重新登录";
break;
case 403:
error.message = "拒绝访问";
break;
case 404:
error.message = "请求错误,未找到该资源";
break;
case 405:
error.message = "请求方法未允许";
break;
case 408:
error.message = "请求超时";
break;
case 500:
error.message = "服务器端出错";
break;
case 501:
error.message = "网络未实现";
break;
case 502:
error.message = "网络错误";
break;
case 503:
error.message = "服务不可用";
break;
case 504:
error.message = "网络超时";
break;
case 505:
error.message = "http版本不支持该请求";
break;
default:
error.message = `连接错误${error.response.status}`;
}
} else {
error.message = "连接到服务器失败";
}
return Promise.reject(error.message);
}
);
}
request(options) {
var instance = axios.create();
options = Object.assign(this.getInsideConfig(localStorage.getItem("Authorization")), options);
this.interceptors(instance, options.url);
return instance(options);
}
}
export default HttpRequest;

6.4 config/index.js

1
2
3
4
arduino复制代码//这里可以根据node环境来设置后台Url
//利用node环境变量来作判断,用来区分开发、测试、生产环境
/* eslint-disable */
export var baseURL = process.env.NODE_ENV === 'development'?' http://localhost:8080':' http://localhost:8081'

6.5 api/baseIndex.js

1
2
3
4
javascript复制代码/* eslint-disable */
import HttpRequest from "@/lib/axios";
var axios = new HttpRequest();
export default axios;

6.6 api/requestdemo1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
php复制代码/* eslint-disable */
import axios from './baseIndex'
//原生redis实现分布式锁测试
export var getRedisLock = (object) => {
return axios.request({
url: "/demo1/testRedisLock",
method: "post",
data:object
});
};
//redisson分布式锁测试
export var getRedissonLock = (object) => {
return axios.request({
url: "/demo1/testRedisson",
method: "post",
data:object
});
};

6.7 vue页面引入

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
xml复制代码<template>
<div class="home-container">
<div class="home-content">
<Button @click="getUserData">redis分布式锁测试</Button>
<Button @click="getUserData1">redisson分布式锁测试</Button>
</div>
</div>
</template>
<script>
/* eslint-disable */
import {getRedisLock,getRedissonLock} from '@/api/requestdemo1.js'
export default {
name: 'home',
data() {
return {
}
},
methods: {
getUserData() {
let person={
requestName:'请求名称',
name:'徐超',
salary:"10000",
age:23,
//加上此参数以后,axios会对请求进行幂等操作,即短时间内的重复请求无法发送出去
repetitiveRequestLimit:true
}
getRedisLock(person).then(res=>{
console.log(res);
}).catch((e)=>{
console.log(e);
}).finally(() => {
console.log('finish');
})
},
getUserData1(){
let person={
requestName:'请求名称',
name:'徐超',
salary:"10000",
age:23,
orderNumber:"ADW12314123",
//加上此参数以后,axios会对请求进行幂等操作,即短时间内的重复请求无法发送出去
//repetitiveRequestLimit:true
}
getRedissonLock(person).then(res=>{
console.log(res);
}).catch((e)=>{
console.log(e);
}).finally(() => {
console.log('finish');
})
}
},
}
</script>

<style scoped>
.home-container {
padding: 10px;
padding-top: 5px;
}
.home-content {
padding: 10px;
border-radius: 5px;
background: #fff;
}
</style>

6.8 测试

7 Redis分布式锁

锁我们都知道,在程序中的作用就是同步工具,保证共享资源在同一时刻只能被一个线程访问,Java中的锁我们都很熟悉了,像synchronized 、Lock都是我们经常使用的,但是Java的锁只能保证单机的时候有效,分布式集群环境就无能为力了,而对于解决表单重复提交这个问题的后台解决方案,我们就可以使用到分布式锁。

分布式锁需要满足的特性有这么几点:

1、互斥性:在任何时刻,对于同一条数据,只有一台应用可以获取到分布式锁

2、高可用性:在分布式场景下,一小部分服务器宕机不影响正常使用,这种情况就需要将提供分布式锁的服务以集群的方式部署

3、防止锁超时:如果客户端没有主动释放锁,服务器会在一段时间之后自动释放锁,防止客户端宕机或者网络不可达时产生死锁

4、独占性:加锁解锁必须由同一台服务器进行,也就是锁的持有者才可以释放锁,不能出现你加的锁,别人给你解锁了

7.1 那么Redis分布式锁的本质是什么呢?

介绍两个Redis获得锁的指令(这两个指令包含的获取锁和设置过期时间这两个操作是原子操作):

1 SETNX:意思是 SET if Not exists , 用法是:SETEX key seconds value

2 PSETEX:用法是:PSETEX key milliseconds value

(这个命令和SETEX命令相似,但它以毫秒为单位设置 key 的生存时间,而不是像SETEX命令那样,以秒为单位)

Redis获取锁的最常见写法:

从Redis 2.6.12 版本开始,SET命令可以通过参数来实现和SETNX、SETEX、PSETEX 三个命令相同的效果

SET key value NX EX seconds:加上NX、EX参数后,效果就相当于SETEX

例子:

可以根据当前登陆人的id和请求的uri作为锁的名字,当把key为lock的值设置为”Java”后,再设置成别的值就会失败,即获得锁返回1,未获得锁返回0

所以这条命令体现了锁的互斥性,即在任何时刻,对于同一条数据,只有一台应用可以获取到分布式锁,设置锁的超时时间还做到了防止锁超时

那么问题来了,锁的value值真的可以像上面那边设置的很随意嘛?

7.2 value的值如何设置?

答案: 应该独特唯一,这样就实现了分布式锁的独占性

如果value值不唯一可能会出现如下请求?

1.服务器1获取锁成功

2.服务器1在某个操作上阻塞了太长时间

3.设置的key过期了,锁自动释放了

4.服务器2获取到了对应同一个资源的锁

5.服务器1从阻塞中恢复过来,因为value值一样,所以执行释放锁操作时就会释放掉服务器2持有的锁,这样就会造成问题

设置value的方法如下:

方法1:UUID

1
ini复制代码String uuid = UUID.randomUUID().toString();

方法2:当前线程id

1
ini复制代码String id = Thread.currentThread().getId() + "";

方法3:分布式雪花算法id生成器

1
2
bash复制代码参考:基于Snowflake算法的分布式ID生成器
码云: https://gitee.com/yu120/neural

7.3 如何保证Redis锁高可用呢?

高可用的大概定义是: “高可用性”(High Availability)通常来描述一个系统经过专门的设计,从而减少停工时间,而保持其服务的高度可用性,即在分布式场景下,一小部分服务器宕机不影响正常使用。

不推荐: Redis 单副本

不推荐原因如下:如果redis是单master模式的,当这台机宕机的时候,那么所有的客户端都获取不到锁了

推荐:Redis 多副本(主从), Redis Sentinel(哨兵), Redis Cluster

推荐原因: 为了提高可用性,假设部署主从架构的redis,1个master加1个slave,因为redis的主从同步是异步进行的,可能会出现客户端1设置完锁后,master挂掉,原来master中的数据都会转移到原来的slave中,然后slave提升为master,这样就不会丢失锁。

7.4 Demo实践

7.4.1 项目中引入Jedis客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
xml复制代码<!-- jedis-redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>

Redis分布式锁工具类:

/**

  • @description:
  • @author: geekAntony
  • @create: 2021-01-17 16:52
    **/
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
java复制代码public class RedisLockUtil {

// key的持有时间,5ms
private long EXPIRE_TIME = 5;

// 等待超时时间,1s
private long TIME_OUT = 1000;

// redis命令参数,相当于nx和px的命令合集
private SetParams params = SetParams.setParams().nx().px(EXPIRE_TIME);

// redis连接池,连的是本地的redis客户端
JedisPool jedisPool = new JedisPool("127.0.0.1", 6379);

/**
* 加锁
*
* @param value
* 线程的id,或者其他可识别当前线程且不重复的字段
* @return
*/
public boolean lock(String key,String value) {
Long start = System.currentTimeMillis();
Jedis jedis = jedisPool.getResource();
try {
for (;;) {
// SET命令返回OK ,则证明获取锁成功
String lock = jedis.set(key, value, params);
if ("OK".equals(lock)) {
return true;
}
// 否则循环等待,在TIME_OUT时间内仍未获取到锁,则获取失败
long l = System.currentTimeMillis() - start;
if (l >= TIME_OUT) {
return false;
}
try {
// 休眠一会,不然反复执行循环会一直失败
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
jedis.close();
}
}

/**
* 解锁
*
* @param value
* 线程的id,或者其他可识别当前线程且不重复的字段
* @return
*/
public boolean unlock(String key,String value) {
Jedis jedis = jedisPool.getResource();
// 删除key的lua脚本
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then" + " return redis.call('del',KEYS[1]) " + "else"
+ " return 0 " + "end";
try {
String result =
jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value)).toString();
return "1".equals(result);
} finally {
jedis.close();
}
}
}

前端控制器:

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
typescript复制代码/**
* @program: structure
* @description:
* @author: geekAntony
* @create: 2021-01-19 22:59
**/
@RestController
@RequestMapping("/demo1")
public class TestRedisLock {

private static RedisLockUtil demo = new RedisLockUtil();

@PostMapping(value = "/testRedisLock")
public String add(@RequestBody Person person) {
String id = Thread.currentThread().getId() + "";
boolean isLock = demo.lock("redislockName",id);
try {
//拿到锁的话执行业务操作...
if (isLock) {
//模拟3s业务操作
TimeUnit.SECONDS.sleep(3);
}else{
return "请不要重复发送表单请求";
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 在finally中释放锁
demo.unlock("redislockName",id);
}
return "完成业务逻辑";
}
}

测试:

8 使用Redisson分布式锁

引入Redisson依赖:

1
2
3
4
5
xml复制代码 <dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>

application.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
yaml复制代码#Redis 配置
spring:
redis:
host: 127.0.0.1
port: 6379
database: 1
password:
timeout: 10000
jedis:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: -1ms
#自定义分布式 Redis 客户端 Redisson 配置
redisson:
type: stand-alone #redis服务器部署类型,stand-alone:单机部署、cluster:机器部署.默认为单机部署
address: redis://127.0.0.1:6379 #单机时必须是redis://开头.
database: 1

基础信息配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
arduino复制代码@Data //lombok
@ConfigurationProperties(prefix = "redisson")
public class RedssionProperties {

/**
* redis服务器部署类型。
* stand-alone:单机部署
* cluster:集群部署.
*/
private String type = "stand-alone";
/**
* Redis 服务器地址
*/
private String address;
/**
* 用于Redis连接的数据库索引
*/
private int database = 0;
/**
* Redis身份验证的密码,如果不需要,则应为null
*/
private String password;
/**
* Redis最小空闲连接量
*/
private int connectionMinimumIdleSize = 24;
/**
* Redis连接最大池大小
*/
private int connectionPoolSize = 64;
/**
* Redis 服务器响应超时时间,Redis 命令成功发送后开始倒计时(毫秒)
*/
private int timeout = 3000;
/**
* 连接到 Redis 服务器时超时时间(毫秒)
*/
private int connectTimeout = 10000;
}

Redisson配置类

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
scss复制代码@Configuration
@EnableConfigurationProperties(RedssionProperties.class)
public class RedissonConfig {

private final RedssionProperties redssionProperties;

/**
* 从 Spring 容器中获取 {@link RedssionProperties}实例
*/
public RedissonConfig(RedssionProperties redssionProperties) {
this.redssionProperties = redssionProperties;
}


/**
* redis 服务器单机部署时,创建 RedissonClient 实例,交由 Spring 容器管理
* 只有当配置了 redisson.type=stand-alone 时,才继续生成 RedissonClient 实例并交由 Spring 容器管理
*
* @return
*/
@Bean
@ConditionalOnProperty(prefix = "redisson", name = "type", havingValue = "stand-alone")
public RedissonClient redissonClient() {
/**
* Config:Redisson 配置基类:
* SingleServerConfig:单机部署配置类,MasterSlaveServersConfig:主从复制部署配置
* SentinelServersConfig:哨兵模式配置,ClusterServersConfig:集群部署配置类。
* useSingleServer():初始化 redis 单服务器配置。即 redis 服务器单机部署
* setAddress(String address):设置 redis 服务器地址。格式 -- redis://主机:端口,不写时,默认为 redis://127.0.0.1:6379
* setDatabase(int database): 设置连接的 redis 数据库,默认为 0
* setPassword(String password):设置 redis 服务器认证密码,没有时设置为 null,默认为 null
* RedissonClient create(Config config): 使用提供的配置创建同步/异步 Redisson 实例
* Redisson 类实现了 RedissonClient 接口,真正需要使用的就是这两个 API
*/
Config config = new Config();
config.useSingleServer()
.setAddress(redssionProperties.getAddress())
.setDatabase(redssionProperties.getDatabase())
.setPassword(redssionProperties.getPassword())
.setConnectionPoolSize(redssionProperties.getConnectionPoolSize())
.setConnectionMinimumIdleSize(redssionProperties.getConnectionMinimumIdleSize())
.setTimeout(redssionProperties.getTimeout())
.setConnectTimeout(redssionProperties.getConnectTimeout());
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}

测试:

具体详细前端代码上文可见:

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
xml复制代码<template>
<div class="home-container">
<div class="home-content">
<Button @click="getUserData1">redisson分布式锁测试</Button>
</div>
</div>
</template>

<script>
/* eslint-disable */
import {getRedisLock,getRedissonLock} from '@/api/requestdemo1.js'
export default {
name: 'home',
data() {
return {
userInfo: '',
}
},
methods: {
getUserData1(){
let person={
requestName:'请求名称',
name:'徐超',
salary:"10000",
age:23,
orderNumber:"ADW12314123",
//注释掉此字段以免axios拦截器拦截
//repetitiveRequestLimit:true
}
getRedissonLock(person).then(res=>{
console.log(res);
}).catch((e)=>{
console.log(e);
}).finally(() => {
console.log('finish');
})
}
},
}
</script>

<style scoped>
.home-container {
padding: 10px;
padding-top: 5px;
}
.home-content {
padding: 10px;
border-radius: 5px;
background: #fff;
}
</style>

后台控制器:

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
vbnet复制代码@RestController
@RequestMapping("/demo1")
public class TestRedisLock {

@Autowired
private RedissonClient redissonClient;

private static Logger logger = LoggerFactory.getLogger(TestRedisLock.class);
/**
* RedissonClient.getLock(String name):可重入锁
* boolean tryLock(long waitTime, long leaseTime, TimeUnit unit):尝试获取锁
* 1、waitTime:获取锁时的等待时间,超时自动放弃,线程不再继续阻塞,方法返回 false
* 2、leaseTime:获取到锁后,指定加锁的时间,超时后自动解锁
* 3、如果成功获取锁,则返回 true,否则返回 false。
*/
@PostMapping(value = "/testRedisson")
public String addDemo1(@RequestBody Person person) {
String result = "订单[" + person.getOrderNumber() + "]支付成功.";
//这里可以加入登录用户的id等数据
String key = person.getOrderNumber();
/**
* getLock(String name):按名称返回锁实例,实现了一个非公平的可重入锁,因此不能保证线程获得顺序
* lock():获取锁,如果锁不可用,则当前线程将处于休眠状态,直到获得锁为止
*/
RLock lock = redissonClient.getLock(key);
boolean tryLock = false;
try {
//waitTime是尝试加锁时间,最多等待1s,上锁60s以后自动解锁
tryLock = lock.tryLock(1, 60, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
//上锁失败,则会进入此if
if (!tryLock) {
return "订单[" + person.getOrderNumber() + "]正在支付中,请耐心等待!";
}
try {
logger.info("查询支付状态");
TimeUnit.SECONDS.sleep(1);
logger.info("正在支付订单[" + person.getOrderNumber() + "]");
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
result = "订单号xxx [" + person.getOrderNumber() + "]支付失败:" + e.getMessage();
} finally {
/**
* boolean isLocked():检查锁是否被任何线程锁定,被锁定时返回 true,否则返回 false.
* unlock():释放锁, Lock 接口的实现类通常会对线程释放锁(通常只有锁的持有者才能释放锁)施加限制,
* 如果违反了限制,则可能会抛出(未检查的)异常。如果锁已经被释放,重复释放时,会抛出异常。
*/
if (lock.isLocked()) {
lock.unlock();
}
}
return result;
}
}

测试结果:

三 总结

以上方案

解决方案1,实现起来较为简单,项目开发前期或者不是特别重要的接口中可以使用此方法

解决方案2,3,4不推荐

解决方案5 基于乐观锁来实现,个人感觉占硬盘存储空间,但是实现简单,较为稳定,建议使用

解决方案6 比较新颖,可以在项目中尝试

解决方案7 是Redis实现分布式锁的Demo,依赖高可用Redis

解决方案8 是生产环境中比较流行的解决方式,依赖高可用Redis

参考文章:

基于Redis的分布式锁实现

juejin.cn/post/684490…

这才叫细:带你深入理解Redis分布式锁

mp.weixin.qq.com/s?__biz=MzI…

本文转载自: 掘金

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

Python3+pygame中国象棋 代码完整 非常好 有效

发表于 2021-03-04

这几天看到抖音上有个妹子下象棋超级猛,我的中国象棋也差不到哪去啊,走 做一个。。。。
本中国象棋是用Python3+pygame实现的

一、运行效果

二、代码

下面的代码用到图片素材(images文件夹),下载地址如下:www.itprojects.cn/detail.html…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
python复制代码"""
作者:it项目实例网
网址:www.itprojects.cn
"""

import sys

import pygame

# 要显示的窗口的宽、高
WIDTH, HEIGHT = 750, 667


class ClickBox(pygame.sprite.Sprite):
"""
选中棋子对象
"""
singleton = None

def __new__(cls, *args, **kwargs):
if cls.singleton is None:
cls.singleton = super().__new__(cls)
return cls.singleton

def __init__(self, screen, row, col, team):
super().__init__()
self.image = pygame.image.load("images/r_box.png")
self.rect = self.image.get_rect()
self.row, self.col = row, col
self.rect.topleft = (50 + self.col * 57, 50 + self.row * 57)
self.screen = screen
self.team = team

@classmethod
def show(cls):
if cls.singleton:
cls.singleton.screen.blit(cls.singleton.image, cls.singleton.rect)

@classmethod
def clean(cls):
"""
清理上次的对象
"""
cls.singleton = None


class Dot(pygame.sprite.Sprite):
"""
可落棋子类
"""
group = list()

def __init__(self, screen, position):
super().__init__()
self.image = pygame.image.load("images/dot2.png")
self.rect = self.image.get_rect()
self.row, self.col = position # 将元组拆包
self.rect.topleft = (60 + self.col * 57, 60 + self.row * 57)
self.group.append(self)
self.screen = screen

@classmethod
def show(cls):
for dot in cls.group:
dot.screen.blit(dot.image, dot.rect)

@classmethod
def clean_last_postion(cls):
"""
清除上次落子位置
"""
cls.group.clear()

@classmethod
def click(cls):
"""
点击棋子
"""
for dot in cls.group:
if pygame.mouse.get_pressed()[0] and dot.rect.collidepoint(pygame.mouse.get_pos()):
print("被点击了「可落子」对象")
return dot


class Chess(pygame.sprite.Sprite):
"""
棋子类
"""

def __init__(self, screen, chess_name, row, col):
self.screen = screen
self.image = pygame.image.load("images/" + chess_name + ".png")
self.rect = self.image.get_rect()
self.rect.topleft = (50 + col * 57, 50 + row * 57)
self.team = chess_name[0] # 队伍(红方 r、黑方b)
self.name = chess_name[2] # 名字(炮p、马m等)
self.row = row
self.col = col

def show(self):
self.screen.blit(self.image, self.rect)

@staticmethod
def click(player, chesses):
"""
点击棋子
"""
for chess in chesses:
if pygame.mouse.get_pressed()[0] and chess.rect.collidepoint(pygame.mouse.get_pos()):
if player == chess.team:
print("被点击了")
return chess

def update_postion(self, new_row, new_col):
"""
更新要显示的图片的坐标
"""
self.row = new_row
self.col = new_col
self.rect.topleft = (50 + new_col * 57, 50 + new_row * 57)


class ChessBoard(object):
"""
棋盘类
"""

def __init__(self, screen):
self.screen = screen
self.image = pygame.image.load("images/bg.png")
self.topleft = (50, 50)
self.__create_default_chess()

def __create_default_chess(self):
"""
创建默认棋子
"""
self.map = [
["b_c", "b_m", "b_x", "b_s", "b_j", "b_s", "b_x", "b_m", "b_c"],
["", "", "", "", "", "", "", "", ""],
["", "b_p", "", "", "", "", "", "b_p", ""],
["b_z", "", "b_z", "", "b_z", "", "b_z", "", "b_z"],
["", "", "", "", "", "", "", "", ""],
["", "", "", "", "", "", "", "", ""],
["r_z", "", "r_z", "", "r_z", "", "r_z", "", "r_z"],
["", "r_p", "", "", "", "", "", "r_p", ""],
["", "", "", "", "", "", "", "", ""],
["r_c", "r_m", "r_x", "r_s", "r_j", "r_s", "r_x", "r_m", "r_c"],
]
for row, line in enumerate(self.map):
for col, chess_name in enumerate(line):
if chess_name:
# 将创建的棋子添加到属性map中
self.map[row][col] = Chess(self.screen, chess_name, row, col)
else:
self.map[row][col] = None

def show(self):
# 显示棋盘
self.screen.blit(self.image, self.topleft)
# 显示棋盘上的所有棋子
for line_chess in self.map:
for chess in line_chess:
if chess:
chess.show()

def get_put_down_postion(self, clicked_chess):
"""
计算当前棋子可以移动的位置
"""
# 存储当前棋子可以落子的位置
all_position = list()
# 拿到当前棋子的行、列
row, col = clicked_chess.row, clicked_chess.col
# 拿到当前棋子的team,即时红方r还是黑方b
team = clicked_chess.team

# 计算当前选中棋子的所有可以落子位置
if clicked_chess.name == "p": # 炮
# 一行
direction_left_chess_num = 0
direction_right_chess_num = 0
for i in range(1, 9):
# 计算当前行中,棋子左边与右边可以落子的位置
# 左边位置没有越界
if direction_left_chess_num >= 0 and col - i >= 0:
if not self.map[row][col - i] and direction_left_chess_num == 0:
# 如果没有棋子,则将当前位置组成一个元组,添加到列表
all_position.append((row, col - i))
elif self.map[row][col - i]:
# 如果当前位置有棋子,那么就判断是否能够吃掉它
direction_left_chess_num += 1
if direction_left_chess_num == 2 and self.map[row][col - i].team != team:
all_position.append((row, col - i))
direction_left_chess_num = -1 # 让其不能够在下次for循环时再次判断
# 右边位置没有越界
if direction_right_chess_num >= 0 and col + i <= 8:
if not self.map[row][col + i] and direction_right_chess_num == 0:
# 如果没有棋子,则将当前位置组成一个元组,添加到列表
all_position.append((row, col + i))
elif self.map[row][col + i]:
# 如果当前位置有棋子,那么就判断是否能够吃掉它
direction_right_chess_num += 1
if direction_right_chess_num == 2 and self.map[row][col + i].team != team:
all_position.append((row, col + i))
direction_right_chess_num = -1
# 一列
direction_up_chess_num = 0
direction_down_chess_num = 0
for i in range(1, 10): # 这样就让i从1开始,而不是从0
# 计算当前列中,棋子上边与下边可以落子的位置
# 上边位置没有越界
if direction_up_chess_num >= 0 and row - i >= 0:
if not self.map[row - i][col] and direction_up_chess_num == 0:
# 如果没有棋子,则将当前位置组成一个元组,添加到列表
all_position.append((row - i, col))
elif self.map[row - i][col]:
# 如果当前位置有棋子,那么就判断是否能够吃掉它
direction_up_chess_num += 1
if direction_up_chess_num == 2 and self.map[row - i][col].team != team:
all_position.append((row - i, col))
direction_up_chess_num = -1

# 下边位置没有越界
if direction_down_chess_num >= 0 and row + i <= 9:
if not self.map[row + i][col] and direction_down_chess_num == 0:
# 如果没有棋子,则将当前位置组成一个元组,添加到列表
all_position.append((row + i, col))
elif self.map[row + i][col]:
# 如果当前位置有棋子,那么就判断是否能够吃掉它
direction_down_chess_num += 1
if direction_down_chess_num == 2 and self.map[row + i][col].team != team:
all_position.append((row + i, col))
direction_down_chess_num = -1
elif clicked_chess.name == "z": # 卒
if team == "r": # 红方
if row - 1 >= 0: # 只能向上移动
if not self.map[row - 1][col] or self.map[row - 1][col].team != team:
all_position.append((row - 1, col))
else: # 黑方
if row + 1 <= 9: # 只能向下移动
if not self.map[row + 1][col] or self.map[row + 1][col].team != team:
all_position.append((row + 1, col))
# 左右判断
if (team == "r" and 0 <= row <= 4) or (team == "b" and 5 <= row <= 9): # 左、右一步
# 左
if col - 1 >= 0 and (not self.map[row][col - 1] or self.map[row][col - 1].team != team):
all_position.append((row, col - 1))
# 右
if col + 1 <= 8 and (not self.map[row][col + 1] or self.map[row][col + 1].team != team):
all_position.append((row, col + 1))
elif clicked_chess.name == "c": # 车
# 一行
left_stop = False
right_stop = False
for i in range(1, 9):
# 左边位置没有越界且没有遇到任何一个棋子
if not left_stop and col - i >= 0:
if not self.map[row][col - i]:
# 如果没有棋子,则将当前位置组成一个元组,添加到列表
all_position.append((row, col - i))
else:
left_stop = True
if self.map[row][col - i].team != team:
# 如果当前位置有棋子,那么就判断是否能够吃掉它
all_position.append((row, col - i))
# 右边位置没有越界且没有遇到任何一个棋子
if not right_stop and col + i <= 8:
if not self.map[row][col + i]:
# 如果没有棋子,则将当前位置组成一个元组,添加到列表
all_position.append((row, col + i))
else:
right_stop = True
if self.map[row][col + i].team != team:
# 如果当前位置有棋子,那么就判断是否能够吃掉它
all_position.append((row, col + i))

# 一列
up_stop = False
down_stoop = False
for i in range(1, 10):
# 上边位置没有越界且没有遇到任何一个棋子
if not up_stop and row - i >= 0:
if not self.map[row - i][col]:
# 如果没有棋子,则将当前位置组成一个元组,添加到列表
all_position.append((row - i, col))
else:
up_stop = True
if self.map[row - i][col].team != team:
# 如果当前位置有棋子,那么就判断是否能够吃掉它
all_position.append((row - i, col))
# 下边位置没有越界且没有遇到任何一个棋子
if not down_stoop and row + i <= 9:
if not self.map[row + i][col]:
# 如果没有棋子,则将当前位置组成一个元组,添加到列表
all_position.append((row + i, col))
else:
down_stoop = True
if self.map[row + i][col].team != team:
# 如果当前位置有棋子,那么就判断是否能够吃掉它
all_position.append((row + i, col))
elif clicked_chess.name == "m": # 马
# 需要判断的是4个方向,每个方向对应2个位置
# 上方
if row - 1 >= 0 and not self.map[row - 1][col]: # 如果当前棋子没有被蹩马腿,那么再对这个方向的2个位置进行判断
# 左上
if row - 2 >= 0 and col - 1 >= 0 and (not self.map[row - 2][col - 1] or self.map[row - 2][col - 1].team != team):
all_position.append((row - 2, col - 1))
# 右上
if row - 2 >= 0 and col + 1 <= 8 and (not self.map[row - 2][col + 1] or self.map[row - 2][col + 1].team != team):
all_position.append((row - 2, col + 1))
# 下方
if row + 1 <= 9 and not self.map[row + 1][col]: # 如果当前棋子没有被蹩马腿,那么再对这个方向的2个位置进行判断
# 左下
if row + 2 >= 0 and col - 1 >= 0 and (not self.map[row + 2][col - 1] or self.map[row + 2][col - 1].team != team):
all_position.append((row + 2, col - 1))
# 右下
if row + 2 >= 0 and col + 1 <= 8 and (not self.map[row + 2][col + 1] or self.map[row + 2][col + 1].team != team):
all_position.append((row + 2, col + 1))
# 左方
if col - 1 >= 0 and not self.map[row][col - 1]: # 如果当前棋子没有被蹩马腿,那么再对这个方向的2个位置进行判断
# 左上2(因为有左上了,暂且称为左上2吧)
if row - 1 >= 0 and col - 2 >= 0 and (not self.map[row - 1][col - 2] or self.map[row - 1][col - 2].team != team):
all_position.append((row - 1, col - 2))
# 左下2
if row + 1 <= 9 and col - 2 >= 0 and (not self.map[row + 1][col - 2] or self.map[row + 1][col - 2].team != team):
all_position.append((row + 1, col - 2))
# 右方
if col + 1 <= 8 and not self.map[row][col + 1]: # 如果当前棋子没有被蹩马腿,那么再对这个方向的2个位置进行判断
# 右上2(因为有右上了,暂且称为右上2吧)
if row - 1 >= 0 and col + 2 <= 8 and (not self.map[row - 1][col + 2] or self.map[row - 1][col + 2].team != team):
all_position.append((row - 1, col + 2))
# 右下2
if row + 1 <= 9 and col + 2 <= 8 and (not self.map[row + 1][col + 2] or self.map[row + 1][col + 2].team != team):
all_position.append((row + 1, col + 2))
elif clicked_chess.name == "x": # 象
# 因为象是不能过河的,所以要计算出它们可以移动的行的范围
row_start, row_stop = (0, 4) if team == "b" else (5, 9)
# 有4个方向的判断(没有越界,且没有蹩象腿)
if row - 2 >= row_start and col - 2 >= 0 and not self.map[row - 1][col - 1]: # 左上
if not self.map[row - 2][col - 2] or self.map[row - 2][col - 2].team != team:
all_position.append((row - 2, col - 2))
if row - 2 >= row_start and col + 2 <= 8 and not self.map[row - 1][col + 1]: # 右上
if not self.map[row - 2][col + 2] or self.map[row - 2][col + 2].team != team:
all_position.append((row - 2, col + 2))
if row + 2 <= row_stop and col - 2 >= 0 and not self.map[row + 1][col - 1]: # 左下
if not self.map[row + 2][col - 2] or self.map[row + 2][col - 2].team != team:
all_position.append((row + 2, col - 2))
if row + 2 <= row_stop and col + 2 <= 8 and not self.map[row + 1][col + 1]: # 右下
if not self.map[row + 2][col + 2] or self.map[row + 2][col + 2].team != team:
all_position.append((row + 2, col + 2))
elif clicked_chess.name == "s": # 士
# 因为士是不能过河的,所以要计算出它们可以移动的行的范围
row_start, row_stop = (0, 2) if team == "b" else (7, 9)
if row - 1 >= row_start and col - 1 >= 3 and (not self.map[row - 1][col - 1] or self.map[row - 1][col - 1].team != team):
all_position.append((row - 1, col - 1))
if row - 1 >= row_start and col + 1 <= 5 and (not self.map[row - 1][col + 1] or self.map[row - 1][col + 1].team != team):
all_position.append((row - 1, col + 1))
if row + 1 <= row_stop and col - 1 >= 3 and (not self.map[row + 1][col - 1] or self.map[row + 1][col - 1].team != team):
all_position.append((row + 1, col - 1))
if row + 1 <= row_stop and col + 1 <= 5 and (not self.map[row + 1][col + 1] or self.map[row + 1][col + 1].team != team):
all_position.append((row + 1, col + 1))
elif clicked_chess.name == "j": # 将
# 因为"将"是不能过河的,所以要计算出它们可以移动的行的范围
row_start, row_stop = (0, 2) if team == "b" else (7, 9)
# 有4个方向的判断
if row - 1 >= row_start and (not self.map[row - 1][col] or self.map[row - 1][col].team != team):
all_position.append((row - 1, col))
if row + 1 <= row_stop and (not self.map[row + 1][col] or self.map[row + 1][col].team != team):
all_position.append((row + 1, col))
if col - 1 >= 3 and (not self.map[row][col - 1] or self.map[row][col - 1].team != team):
all_position.append((row, col - 1))
if col + 1 <= 5 and (not self.map[row][col + 1] or self.map[row][col + 1].team != team):
all_position.append((row, col + 1))

all_position = self.judge_delete_position(all_position, clicked_chess)

# 返回可以落子的所有位置
return all_position

def judge_delete_position(self, all_position, clicked_chess):
"""
删除被"将军"的位置
"""
# 定义要删除的列表
deleting_position = list()

# 判断这些位置,是否会导致被"将军",如果是则从列表中删除这个位置
for row, col in all_position:
# 1. 备份
# 备份当前棋子位置
old_row, old_col = clicked_chess.row, clicked_chess.col
# 备份要落子的位置的棋子(如果没有,则为None)
position_chess_backup = self.map[row][col]
# 2. 挪动位置
# 移动位置
self.map[row][col] = self.map[old_row][old_col]
# 修改棋子的属性
self.map[row][col].update_postion(row, col)
# 清楚之前位置为None
self.map[old_row][old_col] = None
# 3. 判断对方是否可以发起"将军"
if self.judge_attack_general("b" if clicked_chess.team == "r" else "r"):
deleting_position.append((row, col))
# 4. 恢复到之前位置
self.map[old_row][old_col] = self.map[row][col]
self.map[old_row][old_col].update_postion(old_row, old_col)
self.map[row][col] = position_chess_backup

# 5. 删除不能落子的位置
all_position = list(set(all_position) - set(deleting_position))

return all_position

def move_chess(self, new_row, new_col):
"""
落子
"""
# 得到要移动的棋子的位置
old_row, old_col = ClickBox.singleton.row, ClickBox.singleton.col
print("旧位置:", old_row, old_col, "新位置:", new_row, new_col)
# 移动位置
self.map[new_row][new_col] = self.map[old_row][old_col]
# 修改棋子的属性
self.map[new_row][new_col].update_postion(new_row, new_col)
# 清楚之前位置为None
self.map[old_row][old_col] = None

def judge_attack_general(self, attact_player):
"""
判断 attact_player方是否 将对方的军
"""
# 1. 找到对方"将"的位置
general_player = "r" if attact_player == "b" else "b"
general_position = self.get_general_position(general_player)

# 2. 遍历我方所有的棋子
for row, line in enumerate(self.map):
for col, chess in enumerate(line):
if chess and chess.team == attact_player:
if chess.name == "z": # 兵
# 传递5个参数(攻击方的标识,攻击方row,攻击方col,对方将row,对方将col)
if self.judge_z_attack(chess.team, chess.row, chess.col, *general_position):
return True
elif chess.name == "p": # 炮
if self.judge_c_and_p_attack(chess.name, chess.row, chess.col, *general_position):
return True
elif chess.name == "c": # 车
if self.judge_c_and_p_attack(chess.name, chess.row, chess.col, *general_position):
return True
elif chess.name == "m": # 马
if self.judge_m_attack(chess.row, chess.col, *general_position):
return True
elif chess.name == "x": # 象
pass
elif chess.name == "s": # 士
pass
elif chess.name == "j": # 将
if self.judge_j_attack(chess.row, chess.col, *general_position):
return True

def judge_j_attack(self, attack_row, attack_col, general_row, general_col):
"""
判断 两个将是否相对
"""
if attack_col == general_col:
# 在同一列
min_row, max_row = (attack_row, general_row) if attack_row < general_row else (general_row, attack_row)

chess_num = 0
for i in range(min_row + 1, max_row):
if self.map[i][general_col]:
chess_num += 1
if chess_num == 0:
return True

def judge_m_attack(self, attack_row, attack_col, general_row, general_col):
"""
判断马是否攻击到"将"
"""
if attack_row == general_row or attack_col == general_col:
return False
else:
# "马走日",利用这个特点会得出,如果此马能够攻击到"将",那么两条边的平方和一定是5
col_length = (attack_col - general_col) ** 2
row_length = (attack_row - general_row) ** 2
if col_length + row_length == 5:
# 判断是否蹩马腿
if col_length == 1:
if general_row < attack_row and not self.map[attack_row - 1][attack_col]:
return True
elif general_row > attack_row and not self.map[attack_row + 1][attack_col]:
return True
elif col_length == 4:
if general_col < attack_col and not self.map[attack_row][attack_col - 1]:
return True
elif general_col > attack_col and not self.map[attack_row][attack_col + 1]:
return True

def judge_c_and_p_attack(self, attack_chess_name, attack_row, attack_col, general_row, general_col):
"""
判断"车"、"炮"能否攻击到对方"将"
"""
check_chess_num = 1 if attack_chess_name == "p" else 0
chess_num = 0
if attack_row == general_row:
# 在同一行
min_col, max_col = (attack_col, general_col) if attack_col < general_col else (general_col, attack_col)
for i in range(min_col + 1, max_col):
if self.map[attack_row][i]:
chess_num += 1
if chess_num == check_chess_num:
return True
elif attack_col == general_col:
# 在同一列
min_row, max_row = (attack_row, general_row) if attack_row < general_row else (general_row, attack_row)
for i in range(min_row + 1, max_row):
if self.map[i][general_col]:
chess_num += 1
if chess_num == check_chess_num:
return True

def judge_z_attack(self, attack_team, attack_row, attack_col, general_row, general_col):
"""
判断卒是否攻击到"将"
"""
if attack_team == "r" and attack_row < general_row:
return False
elif attack_team == "b" and attack_row > general_row:
return False
elif (attack_row - general_row) ** 2 + (attack_col - general_col) ** 2 == 1:
return True

def get_general_position(self, general_player):
"""
找到general_player标记的一方的将的位置
"""
for row, line in enumerate(self.map):
for col, chess in enumerate(line):
if chess and chess.team == general_player and chess.name == "j":
return chess.row, chess.col

def judge_win(self, attack_player):
"""
判断是否获胜
"""
# 依次判断是否被攻击方的所有棋子,是否有阻挡攻击的可能
for line_chesses in self.map:
for chess in line_chesses:
if chess and chess.team != attack_player:
move_position_list = self.get_put_down_postion(chess)
if move_position_list: # 只要找到一个可以移动的位置,就表示没有失败,还是有机会的
return False

return True


class Game(object):
"""
游戏类
"""

def __init__(self, screen):
self.screen = screen
self.player = "r" # 默认走棋的为红方r
self.player_tips_r_image = pygame.image.load("images/red.png")
self.player_tips_r_image_topleft = (550, 500)
self.player_tips_b_image = pygame.image.load("images/black.png")
self.player_tips_b_image_topleft = (550, 100)
self.show_attack = False
self.show_attack_count = 0
self.show_attack_time = 100
self.attack_img = pygame.image.load("images/pk.png")
self.show_win = False
self.win_img = pygame.image.load("images/win.png")
self.win_player = None

def get_player(self):
"""
获取当前走棋方
"""
return self.player

def exchange(self):
"""
交换走棋方
"""
self.player = "r" if self.player == "b" else "b"
return self.get_player()

def show(self):
if self.show_win:
if self.win_player == "b":
self.screen.blit(self.win_img, (550, 100))
else:
self.screen.blit(self.win_img, (550, 450))
return

# 通过计时,实现显示一会"将军"之后,就消失
if self.show_attack:
self.show_attack_count += 1
if self.show_attack_count == self.show_attack_time:
self.show_attack_count = 0
self.show_attack = False

if self.player == "r":
self.screen.blit(self.player_tips_r_image, self.player_tips_r_image_topleft)
# 显示"将军"效果
if self.show_attack:
self.screen.blit(self.attack_img, (230, 400))
else:
self.screen.blit(self.player_tips_b_image, self.player_tips_b_image_topleft)
# 显示"将军"效果
if self.show_attack:
self.screen.blit(self.attack_img, (230, 100))

def set_attack(self):
"""
标记"将军"效果
"""
self.show_attack = True

def set_win(self, win_player):
"""
设置获胜方
"""
self.show_win = True
self.win_player = win_player


def main():
# 初始化pygame
pygame.init()
# 创建用来显示画面的对象(理解为相框)
screen = pygame.display.set_mode((WIDTH, HEIGHT))
# 游戏背景图片
background_img = pygame.image.load("images/bg.jpg")
# 创建游戏对象
game = Game(screen)
# 创建一个游戏棋盘对象
chess_board = ChessBoard(screen)
# 创建计时器
clock = pygame.time.Clock()

# 主循环
while True:
# 事件检测(例如点击了键盘、鼠标等)
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit() # 退出程序

# 如果游戏没有获胜方,则游戏继续,否则一直显示"获胜"
if not game.show_win:
# 检测是否点击了"可落子"对象
clicked_dot = Dot.click()
if clicked_dot:
chess_board.move_chess(clicked_dot.row, clicked_dot.col)
# 清理「点击对象」、「可落子位置对象」
Dot.clean_last_postion()
ClickBox.clean()
# 判断此棋子走完之后,是否"将军"
if chess_board.judge_attack_general(game.get_player()):
# 检测对方是否可以挽救棋局,如果能挽救,就显示"将军",否则显示"胜利"
if chess_board.judge_win(game.get_player()):
game.set_win(game.get_player())
else:
# 如果攻击到对方,则标记显示"将军"效果
game.set_attack()
# 落子之后,交换走棋方
game.exchange()
# 检查是否点击了棋子
clicked_chess = Chess.click(game.get_player(), [chess for line in chess_board.map for chess in line if chess])
if clicked_chess:
# 创建选中棋子对象
ClickBox(screen, clicked_chess.row, clicked_chess.col, clicked_chess.team)
# 清除之前的所有的可以落子对象
Dot.clean_last_postion()
# 真的点击了棋子,那么计算当前被点击的棋子可以走的位置
all_position = chess_board.get_put_down_postion(clicked_chess)
if all_position:
# 清空上次可落子对象
Dot.clean_last_postion()
# 创建可落子对象
for position in all_position:
Dot(screen, position)

# 显示游戏背景
screen.blit(background_img, (0, 0))
screen.blit(background_img, (0, 270))
screen.blit(background_img, (0, 540))

# 显示棋盘以及棋盘上的棋子
chess_board.show()

# 显示被点击的棋子
ClickBox.show()

# 显示可落子对象
Dot.show()

# 显示游戏相关信息
game.show()

# 显示screen这个相框的内容(此时在这个相框中的内容像照片、文字等会显示出来)
pygame.display.update()

# FPS(每秒钟显示画面的次数)
clock.tick(60) # 通过一定的延时,实现1秒钟能够循环60次


if __name__ == '__main__':
main()

本文转载自: 掘金

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

MyBatis Generator逆向工程-你还在手写map

发表于 2021-03-04

MyBatis Generator逆向工程

  • 什么是逆向工程?
  • 导入jar包
  • 配置文件
  • 生成代码

什么是逆向工程?


简单来说,Mybatis逆向工程可以根据数据库的表来逆向生成相关java代码和sql语句,如mapper.java,mapper.xml。虽然mapper接口及其配置文件我们完全可以自己写,不过当表数量很多,关联复杂时,用MyBatis Generator逆向工程就十分方便了。当然了,我们也可以根据业务逻辑修改其生成的mapper。

导入jar包


将mybatis-generator-core相关jar包导入项目,在maven仓库中搜索mybatis generator,复制代码到pom.xml。

不了解Maven的小伙伴,可以点这里。

或者直接下载并导入jar包,github下载地址。在这里插入图片描述

1
2
3
4
5
6
java复制代码<!-- https://mvnrepository.com/artifact/org.mybatis.generator/mybatis-generator-core -->
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.5</version>
</dependency>

配置文件


新建配置文件mbg.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
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
java复制代码<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
<!--这里用的是MyBatis3Simple即简单的增删改查-->
<!--一般使用MyBatis3-->
<context id="DB2Tables" targetRuntime="MyBatis3Simple">

<!-- 逆向生成清除注释 -->
<commentGenerator>
<property name="suppressAllComments" value="true" />
</commentGenerator>

<!-- 数据库连接配置 -->
<jdbcConnection
driverClass="com.mysql.cj.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/experiment9?serverTimezone=UTC"
userId="root"
password="数据库密码">
<property name="nullCatalogMeansCurrent" value="true"/>
</jdbcConnection>

<javaTypeResolver >
<property name="forceBigDecimals" value="false" />
</javaTypeResolver>

<!-- 指定javaBean逆生成位置 -->
<javaModelGenerator
targetPackage="cn.csdn.bean"
targetProject=".\src\main\java">
<property name="enableSubPackages" value="true" />
<property name="trimStrings" value="true" />
</javaModelGenerator>

<!-- 指定sql映射文件生成的位置 -->
<sqlMapGenerator
targetPackage="mapper"
targetProject=".\src\main\resources">
<property name="enableSubPackages" value="true" />
</sqlMapGenerator>

<!-- 指定dao接口生成的位置,mapper接口 -->
<javaClientGenerator
type="XMLMAPPER"
targetPackage="cn.csdn.dao"
targetProject=".\src\main\java">
<property name="enableSubPackages" value="true" />
</javaClientGenerator>

<!-- 指定每个表的生成策略 -->
<table tableName="message" domainObjectName="Message"></table>
<table tableName="product" domainObjectName="Product"></table>
<table tableName="revert" domainObjectName="Revert"></table>
<table tableName="user" domainObjectName="User"></table>

</context>
</generatorConfiguration>

生成代码


配置完后,官网上逆向生成的方法有很多,如命令行,eclipse插件等,这里介绍用java代码调用。

新建MBGTest.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
java复制代码package cn.csdn.test;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

import org.mybatis.generator.api.MyBatisGenerator;
import org.mybatis.generator.config.Configuration;
import org.mybatis.generator.config.xml.ConfigurationParser;
import org.mybatis.generator.internal.DefaultShellCallback;

public class MBGTest {
public static void main(String[] args) throws Exception {
List<String> warnings = new ArrayList<String>();
boolean overwrite = true;
File configFile = new File("mbg.xml");//注意名字和上文配置文件名字相同
ConfigurationParser cp = new ConfigurationParser(warnings);
Configuration config = cp.parseConfiguration(configFile);
DefaultShellCallback callback = new DefaultShellCallback(overwrite);
MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
myBatisGenerator.generate(null);
System.out.println("finish");
}
}

Run As->Java Application

在这里插入图片描述

最后控制台打印finish,刷新目录,就有生成的文件了。

在这里插入图片描述

原创不易,请勿转载(本不富裕的访问量雪上加霜 )

博主首页:blog.csdn.net/qq_45034708

如果文章对你有帮助,记得关注点赞收藏❤

本文转载自: 掘金

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

SmtpJS发送邮件教程 什么是SmtpJS? 开通POP3

发表于 2021-03-04

SmtpJS

  • 什么是SmtpJS?
  • 开通POP3/SMTP服务
  • 导入插件
  • 发送邮件
  • 加密SMTP
  • 含附件

什么是SmtpJS?


SmtpJS是一款通过前端js代码发送邮件的插件,导入插件后,只需简单几行代码就能实现邮件发送。当然了,由于是前端,其安全性自然比后端javamail封装等弱一点,不过SmtpJS十分简易方便,强力安利。

开通POP3/SMTP服务


在介绍SmtpJS使用前,我们需要先准备一个开通POP3/SMTP服务的邮箱,如果已有,可以跳过此步骤。

我们以QQ邮箱为例,登录QQ邮箱后,设置->账户

在这里插入图片描述

找到POP3/SMTP服务点击开启

在这里插入图片描述

完成相应密保验证

在这里插入图片描述

之后获取对应授权码。

在这里插入图片描述

至此你的QQ邮箱就开通了POP3/SMTP服务。

导入插件


去到SmtpJS官网下载smtp.js

我也上传到了github

在这里插入图片描述

如果不想下载的话也可以用官网地址,不过还是推荐前者。

1
2
javascript复制代码<script src="https://smtpjs.com/v3/smtp.js">
</script>

发送邮件


参数说明

参数 说明
Host 你所用邮箱的smtp地址
Username 你的邮箱用户名(如xxx@qq.com)
Password 你的邮箱密码(之前开通服务的授权码)
To 收件人邮箱地址
From 发件人邮箱地址
Subject 邮件主题
Body 邮件内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
java复制代码<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>发送邮件测试</title>
<%
pageContext.setAttribute("APP_PATH",request.getContextPath());
%>
<script type="text/javascript" src="${APP_PATH}/static/js/smtp.js"></script>
</head>
<body>
<script type="text/javascript">
Email.send({
Host : "smtp.yourisp.com",
Username : "username",
Password : "password",
To : 'them@website.com',
From : "you@isp.com",
Subject : "This is the subject",
Body : "And this is the body"
}).then(
message => alert(message)
);
</script>
</body>
</html>

在这里插入图片描述

至此我们就成功的发送了邮件!(。・∀・)ノ

附上各邮箱的端口及smtp地址:

邮箱 pop地址 smtp地址 端口
188 邮箱 pop3.188.com smtp.188.com 25
163 邮箱 pop3.163.com smtp.163.com 25
126 邮箱 pop3.126.com smtp.126.com 25
netease 邮箱 pop.netease.com smtp.netease.com 25
yeah 邮箱 pop.yeah.net smtp.yeah.net 25
QQ 邮箱 pop.qq.com smtp.qq.com 465或587
网易企业邮箱 pop3.163.com smtp.qiye.163.com 994
腾讯企业邮箱 pop.qq.com smtp.exmail.qq.com 25

加密SMTP


在官网点击Encrypt your SMTP Credentials

在这里插入图片描述

仍以QQ邮箱为例,点击Generate security token

在这里插入图片描述

然后生成了一个密钥

在这里插入图片描述

替换成你的密钥即可

1
2
3
4
5
6
7
8
9
javascript复制代码Email.send({
SecureToken : "密钥",
To : 'them@website.com',
From : "you@isp.com",
Subject : "This is the subject",
Body : "And this is the body"
}).then(
message => alert(message)
);

含附件


即多一个Attachments参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
javascript复制代码Email.send({
SecureToken : "密钥",
To : 'them@website.com',
From : "you@isp.com",
Subject : "This is the subject",
Body : "And this is the body",
Attachments : [
{
name : "smtpjs.png",
path : "https://networkprogramming.files.wordpress.com/2017/11/smtpjs.png"
}]
}).then(
message => alert(message)
);

原创不易,请勿转载(本不富裕的访问量雪上加霜 )

博主首页:blog.csdn.net/qq_45034708

如果文章对你有帮助,记得关注点赞收藏❤

本文转载自: 掘金

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

发送验证码-阿里云短信服务 写在前面 前期准备 开发示例

发表于 2021-03-04

文章目录

  • 写在前面
  • 前期准备
    • 账号准备
      • 开通短信服务
      • 申请签名
      • 申请模板
      • 申请AccessKey
      • 充值
  • 开发示例
    • 配置导入
      • 示例代码

写在前面


最近完成了课设,准备总结一下,将陆续写几篇博客,颗粒入仓。

其中注册功能有用到短信验证码,回顾起来还是一段坎坷史,网上短信平台大大小小有很多,但多数有最低充值,而我还遇到了极品客服(社会险恶🙃),必须曝光一下某榛子云,最后终于还是阿里云成功了👍,差点放弃。

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

前期准备


账号准备

注册一个阿里云账号,有则直接登录。

地址:www.aliyun.com/

开通短信服务

传送门

在这里插入图片描述

申请签名

传送门

短信控制台->添加签名

在这里插入图片描述

填写签名,选择验证码(我这里申请过了,没能截到图),还有申请理由等如实填写即可。

在这里插入图片描述

申请模板

传送门

短信控制台->添加模板

在这里插入图片描述

模板名称可随便取,模板内容可参考常用模板库。

在这里插入图片描述

最后耐心等待审核就好了,可以在国内消息(申请国际的点下面)中的签名管理和模板管理中查看审核结果。

在这里插入图片描述

申请AccessKey

点击头像->AccessKey管理

在这里插入图片描述

点击创建

在这里插入图片描述

通过验证后,即可得到AccessKey ID和Secret。至此准备工作就好了。

在这里插入图片描述

充值

每条短信4分5,阿里云的好处是没有最低充值,可以先充一两毛试试水。

在这里插入图片描述

开发示例


传送门

可以查看文档或下载demo进行参考,这里以java为例。

在这里插入图片描述

配置导入

在pom.xml配置。不懂maven的小伙伴可以点这里。

1
2
3
4
5
6
7
8
9
10
java复制代码<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.1</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>1.1.0</version>
</dependency>

或者下载并导入aliyu-java-sdk-core JAR文件。

示例代码

几处需要替换的地方都已注释,就是替换成前期准备的那些。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
java复制代码import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
/**
* 阿里云短信服务:
* 注意:需要 签名名称、模版CODE 以及 RAM访问控制中的 AccessKeyID 和 AccessKeySecret
*/
public class AliyunSmsUtils {

public AliyunSmsUtils() {
super();
// TODO Auto-generated constructor stub
}

//产品名称:云通信短信API产品,开发者无需替换
static final String product = "Dysmsapi";
//产品域名,开发者无需替换
static final String domain = "dysmsapi.aliyuncs.com";

// TODO 此处需要替换成开发者自己的AK(在阿里云访问控制台寻找)
static final String accessKeyId = "CSDN"; // TODO 修改成自己的accessKeyId
static final String accessKeySecret = "CSDN"; // TODO 修改成自己的accessKeySecret

public static SendSmsResponse sendSms(String telephone, String code) throws ClientException {
//可自助调整超时时间
System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
//初始化acsClient,暂不支持region化
IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain);
IAcsClient acsClient = new DefaultAcsClient(profile);
//组装请求对象-具体描述见控制台-文档部分内容
SendSmsRequest request = new SendSmsRequest();
//必填:待发送手机号
request.setPhoneNumbers(telephone);
//必填:短信签名-可在短信控制台中找到
request.setSignName("CSDN"); // TODO 修改成自己的签名
//必填:短信模板-可在短信控制台中找到
request.setTemplateCode("SMS_193235494 CSDN"); // TODO 修改成自己的模板CODE
//可选:模板中的变量替换JSON串,如模板内容为"亲爱的${name},您的验证码为${code}"时,此处的值为
// request.setTemplateParam("{\"name\":\"Tom\", \"code\":\"123\"}");
request.setTemplateParam("{\"code\":\"" + code + "\"}");
//选填-上行短信扩展码(无特殊需求用户请忽略此字段)
//request.setSmsUpExtendCode("90997");
//可选:outId为提供给业务方扩展字段,最终在短信回执消息中将此值带回给调用者
request.setOutId("yourOutId");
//hint 此处可能会抛出异常,注意catch
SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);
if(sendSmsResponse.getCode()!= null && sendSmsResponse.getCode().equals("OK")){
System.out.println("短信发送成功!");
}else {
System.out.println("短信发送失败!");
}
return sendSmsResponse;
}
/* 不删 留着 以后可能有用
public static QuerySendDetailsResponse querySendDetails(String bizId) throws ClientException {
//可自助调整超时时间
System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
//初始化acsClient,暂不支持region化
IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain);
IAcsClient acsClient = new DefaultAcsClient(profile);
//组装请求对象
QuerySendDetailsRequest request = new QuerySendDetailsRequest();
//必填-号码
request.setPhoneNumber("15000000000");
//可选-流水号
request.setBizId(bizId);
//必填-发送日期 支持30天内记录查询,格式yyyyMMdd
SimpleDateFormat ft = new SimpleDateFormat("yyyyMMdd");
request.setSendDate(ft.format(new Date()));
//必填-页大小
request.setPageSize(10L);
//必填-当前页码从1开始计数
request.setCurrentPage(1L);
//hint 此处可能会抛出异常,注意catch
QuerySendDetailsResponse querySendDetailsResponse = acsClient.getAcsResponse(request);
return querySendDetailsResponse;
}
*/

//以下为测试代码,随机生成验证码
private static int newcode;
public static int getNewcode() {
return newcode;
}
public static void setNewcode(){
newcode = (int)(Math.random()*1000000); //每次调用生成一位六位数的随机数
}
public static void main(String[] args) throws ClientException, InterruptedException {
setNewcode();
String code = Integer.toString(getNewcode());
System.out.println("发送的验证码为:"+code);
//发短信
SendSmsResponse response =sendSms("CSDN",code); // TODO 填写你需要测试的手机号码
System.out.println("短信接口返回的数据----------------");
System.out.println("Code=" + response.getCode());
System.out.println("Message=" + response.getMessage());
System.out.println("RequestId=" + response.getRequestId());
System.out.println("BizId=" + response.getBizId());

/* 不删 留着 以后可能有用
System.out.println(" ============================================== ");
Thread.sleep(3000L);
//查明细
if(response.getCode() != null && response.getCode().equals("OK")) {
QuerySendDetailsResponse querySendDetailsResponse = querySendDetails(response.getBizId());
System.out.println("短信明细查询接口返回数据----------------");
System.out.println("Code=" + querySendDetailsResponse.getCode());
System.out.println("Message=" + querySendDetailsResponse.getMessage());
int i = 0;
for(QuerySendDetailsResponse.SmsSendDetailDTO smsSendDetailDTO : querySendDetailsResponse.getSmsSendDetailDTOs())
{
System.out.println("SmsSendDetailDTO["+i+"]:");
System.out.println("Content=" + smsSendDetailDTO.getContent());
System.out.println("ErrCode=" + smsSendDetailDTO.getErrCode());
System.out.println("OutId=" + smsSendDetailDTO.getOutId());
System.out.println("PhoneNum=" + smsSendDetailDTO.getPhoneNum());
System.out.println("ReceiveDate=" + smsSendDetailDTO.getReceiveDate());
System.out.println("SendDate=" + smsSendDetailDTO.getSendDate());
System.out.println("SendStatus=" + smsSendDetailDTO.getSendStatus());
System.out.println("Template=" + smsSendDetailDTO.getTemplateCode());
}
System.out.println("TotalCount=" + querySendDetailsResponse.getTotalCount());
System.out.println("RequestId=" + querySendDetailsResponse.getRequestId());
}*/

}
}

至此就成功的发送了短信验证码(~ ̄▽ ̄)~

在这里插入图片描述

原创不易,请勿转载(本不富裕的访问量雪上加霜 )

博主首页:blog.csdn.net/qq_45034708

如果文章对你有帮助,记得关注点赞收藏❤

本文转载自: 掘金

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

谈线程池

发表于 2021-03-04

该篇文章以转移至github –

github.com/RansongZ/Ti…

本文转载自: 掘金

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

1…711712713…956

开发者博客

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