在接口自动化测试过程中,构造测试数据是必不可少的一个环节,但如何恢复测试数据也同样值得关注。业内常见的做法有:
以上几种方法中,最后一种是最便捷、也是应用最为广泛的。
但现实中,可能部分接口的业务流程并不存在完全闭环的情况,比如我们某个项目有个新增企业的业务,如果按照方式4,那么调用顺序就是新增企业-->修改企业信息-->查询企业信息并断言修改字段是否生效-->删除企业。但实际项目中只有增、查、改接口,并没有删除接口(设计如此)。尤其是新增接口,先会调用一个查询接口,获取第三方数据库视图中的企业列表,拿到添加企业信息的相关字段,再调用新增接口添加到我们系统中来,新增时会校验该企业信息是否已存在,不存在则新增,存在则返回错误码。
而在没有提供删除接口的情况下,自动化测试过程中就要确保:
之前我们组小伙伴所写的自动化测试用例中,使用的是上述第一种方式,即每次新增不一样的企业数据,新增后不删除(原因是开发没有提供删除接口,SQL语句涉及的表较多,且表与表之间存在诸多关联,刚好视图中的数据够多,可以一直添加下去,只要保证每次添加的数据不一致就可以了)。这样的实现方式也不是不可以,只是会带来诸多弊端,而今天文章所表述的内容,就是对该实现方式进行优缺点分析,并基于第二种执行SQL删除数据的实现方式进行改造。
上述背景中已经提到视图中的数据够多,可以一直添加下去,只需保证每次添加的数据不一致即可。添加企业数据前会先调用查询接口,读取第三方企业数据列表,那么就需要设计一种方式使每次读取到的企业数据不一致。原实现方式如下:
配置文件内容如下:
[company_data]
x = 1 # page
y = 1000 # page_size
next_y = 242 # index
[product_data]
x = 1
y = 1000
next_y = 146
读写配置文件方法如下:
import configparser, os
def get_next_data(ini_name):
"""
ini_name:配置文件中,配置的名字,写在[]里面的那串
方法返回列表,按顺序分别是 x,y,next_y
对应的意思是 当前要请求的页码,当前请求的条数,当前从第几条获取
"""
# 取文件路径
path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 合成配置文件的路径
ini_path = path + '/config/page_config.ini'
cf = configparser.ConfigParser()
cf.read(ini_path)
# 取上一次请求的页码
x = int(cf.get(ini_name, 'x'))
y = int(cf.get(ini_name, 'y'))
next_y = int(cf.get(ini_name, 'next_y'))
if next_y + 1 < y:
# 如果+1还小于当前页码数据,那么下次还在当前页数据
cf.set(ini_name, 'next_y', str(next_y + 1))
else:
# 如果+1等于当前页码数据,那么下次就翻页,x也要+1
cf.set(ini_name, 'x', str(x + 1))
cf.set(ini_name, 'next_y', str(0))
cf.write(open(ini_path, 'w'))
return [x, y, next_y]
在上述方法中,会先读取ini文件中的x、y和next_y,也就是page、page_size、index,如x=1,y=1000,next_y=242 作为入参传给查询接口,x表示请求第1页,y表示每页1000条,next_y则作为索引值、提取这1000条数据中的第242条数据
import api_test.config.page_config as page_config
@pytest.mark.rs_smoke
@allure.story("企业管理")
def test_company_manager(self, rs_resource, rs_admin_login, rs_get_admin_user_info, use_db):
"""测试企业增改查接口"""
user_id = rs_admin_login
cpy_id = rs_get_admin_user_info
# 调用方法获取上次读取的位置
config_data = page_config.get_next_data("company_data")
x = config_data[0]
y = config_data[1]
next_y = config_data[2]
get_company_list = rs_resource.get_company_list(cpy_id, user_id, x, y)
company_list = get_company_list["d"]
company_name = company_list[next_y]["a"]
company_simple = company_list[next_y]["a"]
company_num = company_list[next_y]["d"]
company_manager = self.fake.name()
company_phone = self.fake.phone_number()
company_pwd = 123456
company_type = 3
sort_id = str(random.randint(1, 100))
try:
with allure.step("调用添加企业接口"):
add_company = rs_resource.add_company(cpy_id, user_id, company_name, company_manager, company_phone,
company_pwd, company_simple, company_num, company_type)
# 当错误码返回254,说明该企业信息已存在
# 此时再调用get_next_data方法,next_y +1
while add_company["a"] == 254:
config_data = page_config.get_next_data("company_data")
x = config_data[0]
y = config_data[1]
next_y = config_data[2]
get_company_list = rs_resource.get_company_list(cpy_id, user_id, x, y)
company_list = get_company_list["d"]
company_name = company_list[next_y]["a"]
company_simple = company_list[next_y]["a"]
company_num = company_list[next_y]["d"]
add_company = rs_resource.add_company(cpy_id, user_id, company_name, company_manager, company_phone,
company_pwd, company_simple, company_num, company_type)
assert add_company["a"] == 200
self.company_user_id = add_company["d"]
select_db = use_db.execute_sql(
f"SELECT * FROM t_r_company_base WHERE user_id = {self.company_user_id}") # 查询数据库是否存在新增的数据
assert company_name in str(select_db)
从上面的测试用例可以看出:
在真正回归测试过程中,上述方案是可以正常运行的,但也面临诸多问题,下面深入分析该设计的优缺点:
说了这么多,总结下来就是:配置文件如果提交,就意味着每次都要提交,然后每个人在执行前git pull一次、执行完了git push一次;不提交的话就意味着每个人的配置文件不一样,其中一个人运行的次数越多,其他人运行的效率就越低。好吧,确实挺痛苦。说到底还是设计模式的问题,配置文件在git管理下不能轻易产生变动,运行结果的好坏更不能严重依赖配置文件!
既然面临那么多问题,那就只有改造了,俗话说“不破不立”!写代码,不仅要保证代码能运行起来,还要从各个方面确保代码的健壮性。设计产品和设计测试用例也是一样。以下是整体业务分析和测试用例改造过程:
早在开始之初就听说添加企业这个接口涉及的表查询、插入特别多,会产生诸多的业务关联数据,处理起来也会比较麻烦,今日一试,果然如此。
开发没有提供删除接口,但有新增接口,如果我能找到新增企业时插入了哪些数据,再反向将这些数据一一删除,不也就相当于实现了删除接口的功能了吗?说干就干,先分析接口日志(过程确实比较长,核心思想就是:根据后台日志一点一滴梳理数据的流向,插入了哪张表、哪些数据,并找出数据表中的唯一能代表该条数据的字段值,如ID、手机号等,以便于后面设计删除该条数据的SQL语句):
我一遍手动新增一个企业,一遍观察后台日志。在新增企业接口请求及返回的打印日志如下:
在插入账户表前,会先调用一个c.f.r.m.AccountMapper.queryUserPhone的方法,查询数据库中是否存在相同的手机号,不存在则返回0,只有返回数据为0 ,才会执行后续的插入数据操作
如果手机号不重复,则会调用注册接口,成功后返回数据:respContent: 473036,即用户的user_id
后面就是调用注册接口后一系列的关联表查询和写入数据了
insert into t_r_user_info ( user_id, user_name, phone, `status`, product_id, create_time ) values ( 473036, 浙江华甸, 13912300000, 1, 2022, 1667443614864 )
查看数据表,数据用户信息数据已经写入,由于手机号是唯一的,所以可以根据手机号删除本表对应数据
select user_id, account_name, pass_word, `status`, product_id, create_time, update_time, ext1, ext2, ext3 from t_r_account where account_name = 13912300000 and product_id = 2022 and status = 1 limit 1
查看数据表,账户数据已经写入,统一也可以根据手机号删除本表对应数据
select id, company_id, post_code, post_name, remark, `status`, creator, create_time, ext1, ext2, ext3,classify from t_r_post where post_code = 16 and company_id = 0 order by status DESC limit
查询post_code为16,company_id为0的这条数据
因为是查询操作,所以无需还原数据
insert into t_r_user_post ( user_id, post_id, post_code, post_name, creator, create_time ) values ( 473036, 4, 16, 客商调度, 429381, 1667443614864 )
插入后的数据如下,可以根据id和user_id来删除本条数据
select id, company_id, post_code, post_name, remark, `status`, creator, create_time, ext1, ext2, ext3,classify from t_r_post where post_code = 17 and company_id = 0 order by status DESC limit
查询到的数据如下:
因为是查询操作,所以无需还原数据
insert into t_r_user_post ( user_id, post_id, post_code, post_name, creator, create_time ) values ( 473036, 7, 17, 客商管理员, 429381, 1667443614864 )
插入数据如下:
其实稍微观察一下就可以发现,t_r_user_post表写了两次数据,也就是插入了两条user_id为473036的数据(因为新增的用户会默认同时存在两个角色,因此本表会插入两条数据,一个岗位ID为16-客商调度,一个岗位ID为17-客商管理员)。所以可以根据user_id删除对应数据
insert into t_r_user_company(user_id, cpy_id, type, create_time, creator_id, updater_id, update_time, ext1, ext2, ext3,company_id) values (473036, 10306, 2, 1667443614864, 429381, null, null, null, null, null,10306
插入数据如下,可以根据user_id删除对应数据
insert into t_r_company_extend (company_id, ext_name, ext_code, ext_value, create_time) values (10306, 简称,cpySimpleName, 浙江华甸防雷科技, 1667443614864) , (10306, 企业编码,cpyCode, xchdfl, 1667443614864
根据日志和SQL语句可以得知,插入了两条数据,插入的数据如下图所示。后续可以根据cpy_id删除本表对应数据
insert into t_r_location ( company_id, `name`, addr, addr_detail, addr_point, `status`, contact_company, contact_name, contact_phone, user_id, create_time ) values ( 10306, 浙江华甸防雷科技, 山西省-长治市-襄垣县, 山西省长治市襄垣县古韩镇山西机电职业技术学院(东湖校区), 113.065637,36.542198, 1, 浙江华甸防雷科技股份有限公司(String), 浙江华甸, 13912300000, 473036, 1667443614864 )
插入数据如下,后续可以根据company_id删除记录表中的数据
select l.id, l.company_id, l.`name`, l.addr, l.addr_detail, l.addr_point, l.`status`, l.contact_company, l.contact_name, l.contact_phone, l.user_id, l.create_time from t_r_location l left join t_r_company_base b on l.company_id = b.id where b.ext1 = 1 and b.status = 1
查询到的数据如下:
因为是查询操作,所以无需还原数据
insert into t_r_transport_line ( company_id, `name`, load_location_id, unload_location_id, create_time, creator_user_id ) values ( 10306, 简称2-浙江华甸防雷科技, 10000, 10306, 1667443614864, 429381
插入数据如下,因为线路是双向的,所以有两条数据。后续可以根据company_id删除数据
insert into t_r_transport_line_point(line_id, location, sort, type, status, remark, create_time, creator_id, updater_id, update_time, ext1, ext2, ext3) values (10610, 113.065637,36.542198, 1, 1, 1, 山西省长治市襄垣县古韩镇山西机电职业技术学院(东湖校区), 1667443614864, 429381, null, null, null, null, null) , (10610, 118.92252,40.410167, 0, 1, 1, 河北省秦皇岛市青龙满族自治县青龙镇小楸子沟门, 1667443614864, 429381, null, null, null, null, null) , (10611, 113.065637,36.542198, 0, 1, 1, 山西省长治市襄垣县古韩镇山西机电职业技术学院(东湖校区), 1667443614864, 429381, null, null, null, null, null) , (10611, 118.92252,40.410167, 1, 1, 1, 河北省秦皇岛市青龙满族自治县青龙镇小楸子沟门, 1667443614864, 429381, null, null, null, null, null
根据日志和SQL语句可以得知,插入了4条数据,插入的数据如下图所示
这里数据删除比较麻烦,这里要先从t_r_transport_line表中根据company_id查到id,该id也就是t_r_transport_line_point表中的line_id,再根据line_id进行删除。
一共执行5条insert操作
insert into t_r_orgination ( code, `name`, parent_id, `type`, company_id, product_id, create_time ) values ( 10306, 浙江华甸防雷科技股份有限公司, 0, 1, 10306, 2022, 1667443614864 )
insert into t_r_orgination ( `name`, parent_id, `type`, company_id, product_id, create_time ) values ( 运输中心, 13442, 2, 10306, 2022, 1667443614898 )
insert into t_r_orgination ( `name`, parent_id, `type`, company_id, product_id, create_time ) values ( 司机, 13443, 2, 10306, 2022, 1667443614898 )
insert into t_r_orgination ( `name`, parent_id, `type`, company_id, product_id, create_time ) values ( 押运员, 13443, 2, 10306, 2022, 1667443614898 )
insert into t_r_orgination ( code, `name`, parent_id, `type`, company_id, product_id, create_time ) values ( 473036, 浙江华甸, 13442, 3, 10306, 2022, 1667443614864 )
一共插入了5条数据,后续可以根据company_id删除对应数据
select id, code, `name`, parent_id, `type`, company_id, product_id, create_time, update_time, ext1, ext2, ext3,person_count,im_group_id from t_r_orgination where id = 13442
因为是查询操作,所以无需还原数据
update t_r_orgination SET code = 10306, `name` = 浙江华甸防雷科技股份有限公司, parent_id = 0, `type` = 1, company_id = 10306, product_id = 2022, create_time = 1667443614864, person_count = 1, im_group_id = 0 where id = 13442
select id, code, `name`, parent_id, `type`, company_id, product_id, create_time, update_time, ext1, ext2, ext3,person_count,im_group_id from t_r_orgination where id = 0
从日志可以看出,没查到任何数据
一共插入了10条数据,后续可以根据company_id删除数据
通过日志可以看出,没有任何参数,所以未查出数据
insert into t_r_operator_log ( order_id, code, `type`, remark, company_id, user_id, create_time ) values ( 473036, PR001, 1, 【企业注册】手机号:13912300000 用户Id:473036, 10000, 429381, 1667443614908 )
插入数据如下,后续可以根据company_id删除数据
Preparing: select id, code, `name`, parent_id, `type`, company_id, product_id, create_time, update_time, ext1, ext2, ext3,person_count,im_group_id from t_r_orgination WHERE company_id = 10306 and code = 10306 and `type` = 1 order by create_time desc,id desc limit 1
update t_r_orgination SET code = 10306, `name` = 浙江华甸防雷科技股份有限公司, parent_id = 0, `type` = 1, company_id = 10306, product_id = 2022, create_time = 1667443614864, update_time = 1667443614915, person_count = 1, im_group_id = 111150 where id = 13442
更新数据如下,后续可以根据company_id删除数据
一共插入了21条数据,插入数据如下,后续可以根据company_id删除数据
从上述的日志分析过程可以看出,添加企业这个动作,一共涉及了15张数据表的查询、插入、更新和删除操作,其中14张表涉及到数据插入。查询就不管了,只分析哪几张表写入了哪些数据以及如何删除即可。经过梳理总结,得出:
SQL语句如下,将其直接封装到一个静态方法delete_company_data中,直接在测试用例中调用即可,这样做的好处是:
@staticmethod
def delete_company_data(use_db, phone_number):
"""
删除新增企业产生的相关数据
:param use_db: 数据库实例
:param phone_number: 企业联系人手机号
:return:
"""
# 查询企业ID
select_add_company_id = f"SELECT id FROM t_r_company_base WHERE `contact_phone`={phone_number};"
# 查询用户ID
select_add_user_id = f"SELECT user_id FROM t_r_company_base WHERE `contact_phone`={phone_number};"
add_company_id = use_db.execute_sql(select_add_company_id)[0]
add_user_id = use_db.execute_sql(select_add_user_id)[0]
# 查询线路ID
select_line_id = f"SELECT id FROM t_r_transport_line WHERE `company_id`={add_company_id};"
line_ids = use_db.execute_sql(is_fetchall=True, sql=select_line_id)
line_id_1 = line_ids[0][0]
line_id_2 = line_ids[1][0]
# 根据company_id删除资质告警资料配置
delete_sql_1 = f"DELETE FROM t_r_qualification_alert_config WHERE company_id={add_company_id};"
# 根据company_id删除组织表相关数据
delete_sql_2 = f"DELETE FROM t_r_orgination WHERE company_id={add_company_id};"
# 根据company_id删除操作日志
delete_sql_3 = f"DELETE FROM t_r_operator_log WHERE company_id={add_company_id};"
# 根据company_id删除操作资料统计
delete_sql_4 = f"DELETE FROM t_r_res_sum_total WHERE company_id={add_company_id};"
# 删除途经点信息
delete_sql_5 = f"DELETE FROM t_r_transport_line_point WHERE line_id={line_id_1} or line_id={line_id_2};"
# 删除运输线路表数据
delete_sql_6 = f"DELETE FROM t_r_transport_line WHERE company_id={add_company_id};"
# 删除坐标表数据
delete_sql_7 = f"DELETE FROM t_r_location WHERE company_id={add_company_id};"
# 删除企业扩展表数据
delete_sql_8 = f"DELETE FROM t_r_company_extend WHERE company_id={add_company_id};"
# 删除人员客商关系表数据
delete_sql_9 = f"DELETE FROM t_r_user_company WHERE company_id={add_company_id};"
# 删除人员岗位表数据
delete_sql_10 = f"DELETE FROM t_r_user_post WHERE user_id={add_user_id};"
# 删除账户表数据
delete_sql_11 = f"DELETE FROM t_r_account WHERE user_id={add_user_id};"
# 删除用户信息表数据
delete_sql_12 = f"DELETE FROM t_r_user_info WHERE user_id={add_user_id};"
# 删除企业物料绑定关系数据
delete_sql_13 = f"DELETE FROM t_r_msds_company WHERE code={add_company_id};"
# 删除企业基础表数据
delete_sql_14 = f"DELETE FROM t_r_company_base WHERE id={add_company_id};"
# 执行各个SQL
use_db.execute_sql(delete_sql_1)
use_db.execute_sql(delete_sql_2)
use_db.execute_sql(delete_sql_3)
use_db.execute_sql(delete_sql_4)
use_db.execute_sql(delete_sql_5)
use_db.execute_sql(delete_sql_6)
use_db.execute_sql(delete_sql_7)
use_db.execute_sql(delete_sql_8)
use_db.execute_sql(delete_sql_9)
use_db.execute_sql(delete_sql_10)
use_db.execute_sql(delete_sql_11)
use_db.execute_sql(delete_sql_12)
use_db.execute_sql(delete_sql_13)
use_db.execute_sql(delete_sql_14)
前面查询日志过程中已经手动添加了一个企业,手机号为13213213132,这里来验证上面定义的删除策略能否成功删除数据。直接在测试用例类中新增一条用例,引用删除数据方法,执行SQL。
def test_1111(self, use_db, rs_resource):
rs_resource.delete_company_data(use_db=use_db, phone_number=13213213132)
执行结果如下,不过这个方法并没有定义执行SQL后打印任何内容,所以在执行完成后只是正常运行没报错,看不出来是否成功删除了数据,后面还存在优化空间。
我通过手动查询各个数据表,确认各个关联数据均已删除。再次选择同一条企业数据进行新增时,依然能新增成功。
本地调试通过后,即可改造测试用例中的逻辑。改造内容如下:
@pytest.mark.rs_smoke
@allure.story("企业管理")
def test_06_company_manager(self, rs_resource, rs_admin_login, rs_get_admin_user_info, use_db):
"""测试企业增改查接口"""
user_id = rs_admin_login
cpy_id = rs_get_admin_user_info
# 此处去除读取ini配置文件逻辑
# 随便传入一个页码,就算每次都从这一页开始,后面也会删除数据,不会导致数据重复
page = 120
get_company_list = rs_resource.get_company_list(cpy_id, user_id, page, 10)
company_list = get_company_list["d"]
company_name = company_list[0]["a"]
company_simple = company_name[0:5]
company_num = company_list[0]["d"]
company_manager = self.fake.name()
company_phone = self.fake.phone_number()
company_pwd = 123456
company_type = 3
sort_id = str(random.randint(1, 100))
try:
logger.info(f"新增企业'{company_name}'信息...")
with allure.step("调用添加企业接口"):
add_company = rs_resource.add_company(cpy_id, user_id, company_name, company_manager, company_phone,
company_pwd, company_simple, company_num, company_type)
while add_company["a"] == 254:
# 以防万一,还是加入了页码自增+1逻辑,防止这一页的数据被手动用过
page = page + 1
get_company_list = rs_resource.get_company_list(cpy_id, user_id, page, 10)
company_list = get_company_list["d"]
company_name = company_list[0]["a"]
company_simple = company_name[0:5]
company_num = company_list[0]["d"]
add_company = rs_resource.add_company(cpy_id, user_id, company_name, company_manager, company_phone,
company_pwd, company_simple, company_num, company_type)
assert add_company["a"] == 200
self.company_user_id = add_company["d"]
select_db = use_db.execute_sql(
f"SELECT * FROM t_r_company_base WHERE user_id = {self.company_user_id}") # 查询数据库是否存在新增的数据
assert company_name in str(select_db)
logger.info(f"企业'{company_name}'新增信息成功")
logger.info(f"修改企业'{company_name}'信息...")
with allure.step("调用修改企业信息接口"):
select_db = use_db.execute_sql(
f"SELECT id FROM t_r_company_base WHERE user_id = {self.company_user_id}") # 查询新增的数据的id
self.company_id = int(select_db[0])
modify_company = rs_resource.modify_company(cpy_id, user_id, self.company_id, company_name,
company_simple, company_manager)
assert modify_company["a"] == 200
logger.info(f"修改企业'{company_name}'信息成功")
logger.info(f"修改企业'{company_name}'账号...")
with allure.step("调用修改企业账号接口"):
company_new_phone = self.fake.phone_number()
modify_company_phone = rs_resource.modify_company_phone(cpy_id, user_id, self.company_user_id,
self.company_id, company_new_phone)
assert modify_company_phone["a"] == 200
logger.info(f"修改企业'{company_name}'账号成功")
logger.info(f"修改企业'{company_name}'密码...")
with allure.step("调用修改企业密码接口"):
modify_company_pwd = rs_resource.modify_company_pwd(cpy_id, user_id, self.company_user_id,
self.company_id, company_new_phone,
company_pwd='654321')
assert modify_company_pwd["a"] == 200
logger.info(f"修改企业'{company_name}'密码成功")
logger.info(f"修改企业'{company_name}'排序...")
with allure.step("调用修改企业排序接口"):
modify_company_sort = rs_resource.modify_company_sort(cpy_id, user_id, self.company_id, sort_id)
assert modify_company_sort["a"] == 200
logger.info(f"修改企业'{company_name}'排序成功")
logger.info(f"查询企业'{company_name}'信息...")
with allure.step("调用查询企业信息接口"):
query_company = rs_resource.query_company(cpy_id, user_id, company_num, company_type)
assert query_company["a"] == 200
logger.info(f"查询企业'{company_name}'信息成功")
logger.info(f"给企业'{company_name}'绑定物料...")
with allure.step("调用企业绑定物料接口"):
get_exist_product_list = rs_resource.get_exist_product_list(cpy_id, user_id, x=1, y=1000)
exist_product_list = get_exist_product_list["d"]
exist_productID_list = []
for i in exist_product_list:
aa = i["aa"]
exist_productID_list.append(aa)
exist_product_id = random.choice(exist_productID_list)
add_company_product = rs_resource.add_company_product(cpy_id, user_id, self.company_id, company_type,
exist_product_id)
assert add_company_product["a"] == 200
select_db = use_db.execute_sql(
f"SELECT * FROM t_r_msds_company WHERE code = {self.company_id}") # 查询数据库是否存在新增的数据
assert str(exist_product_id) in str(select_db)
logger.info(f"企业'{self.company_id}'和物料的绑定关系成功")
logger.info("删除新增企业产生的相关数据")
rs_resource.delete_company_data(use_db=use_db, phone_number=company_new_phone)
except AssertionError as e:
logger.info(f"企业'{company_name}'增改查失败")
raise e
6.运行测试
测试用例运行通过,改造完成。
以上就是结合实际自动化测试案例,对数据恢复的思考和改造实践。下面简单总结一下此次改造过程中的一些心得:
当然,以上并不一定就是最优的设计,还存在诸多优化空间,如果你有更好的方案,欢迎留言交流!
导读/Introduction2021年9月14日,Oracle正式对外发布新的长期支持版本JDK17。据Oracle官方公告,腾讯KonaJDK再次蝉联JDK17中国企业贡献度排名第一,全球企业贡献度排名第四。在积极参与社区贡献的同时,腾讯继TencentKonaJDK8、TencentKonaJDK11开源之后,再次开源TencentKonaJDK17以及TencentKonaJDK11向量计算版。积极社区贡献KonaJDK9月14日,JDK17正式对外发布。据Oracle官方公告,腾讯KonaJDK团队蝉联JDK17中国企业贡献度排名第一,并再次作为全球Notable贡献者被Oracle点名致谢。在短短两年时间里,腾讯为社区贡献了190+commits,涉及Hotspot(Compiler、Runtime、GC)、SVC、CoreLibraries和Infrastructure等领域,其中比较突出的是HotspotC2性能与可靠性、VectorAPI、ZGC、jmap针对大堆HeapDump加速等。在安全领域也持续发力,国内公司首个OpenJDK安全漏洞报告并确认,编号为CVE-
问题描述n个大小不同的圆盘按照从小到大的顺序放在A柱子上,要求每次搬动1个圆盘,且在搬动过程中,大圆盘在下,小圆盘在上,将所有圆盘从A柱子移动到C柱子,中间可以借助B柱子,请实现搬动过程。解决方案1如果只有一个圆盘直接从A柱子搬动到C柱子:A->C。2如果有2个圆盘上面小圆盘直接从A搬动到B柱子暂放:A->B;下面大圆盘直接从A搬到C柱子:A->C;B暂放的小圆盘直接搬到C柱子:B->C。3如果有3个圆盘上面2个小圆盘从A搬动到B柱子暂放:H(2,A)->B(A->C;A->B;C->B);下面大圆盘直接从A搬到C柱子:A->C;B暂放的2个小圆盘搬动到C柱子:H(2,B)->C(B->A;B->C;A->C)。4规律1个圆盘直接搬动:原柱子->目的主子;多个圆盘采用汉诺塔搬动方法:圆盘数量,原柱子,目的柱子。代码示例:defhano(n,a,b,c): #用汉诺塔方法将n个圆盘从a柱子移动到c柱子 if(n<1): print('圆盘数量错误') return if
线程线程指的就是代码的执行过程进程其实是一个资源单位,而进程内的线程才是CPU上的执行单位在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程 线程顾名思义,就是一条流水线工作的过程,一条流水线必须属于一个车间,一个车间的工作过程是一个进程 车间负责把资源整合到一起,是一个资源单位,而一个车间内至少有一个流水线 流水线的工作需要电源,电源就相当于CPU,所以进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是cpu上的执行单位。 多线程(即多个控制线程)的概念是:在一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间,相当于一个车间内有多条流水线,都共用一个车间的资源。 例如: 北京地铁与上海地铁是不同的进程,而北京地铁里的13号线是一个线程,北京地铁所有的线路共享北京地铁所有的资源,比如所有的乘客可以被所有线路拉。复制 线程详解线程和进程的区别1.同一进程下的多个线程共享该进程内的资源2.创建线程的开销远远小于进程创建线程的开销远远小于进程 假设我们的软件是一个工厂,该工厂有多条流水线,流水线工作需要电源,电源只有一个即CPU(
Introduction导言任何看到显著增长的应用程序或网站,最终都需要进行扩展,以适应流量的增加。以确保数据安全性和完整性的方式进行扩展,对于数据驱动的应用程序和网站来说十分重要。人们可能很难预测某个网站或应用程序的流行程度,也很难预测这种流行程度会持续多久,这就是为什么有些机构选择“可动态扩展的”数据库架构的原因。在这篇概念性文章中,我们将讨论一种“可动态扩展的”数据库架构:分片数据库。近年来,分片(Sharding)一直受到很多关注,但许多人并没有清楚地了解它是什么,或者对数据库进行分片可能有意义的场景。我们将讨论分片是什么,它的一些主要优点和缺点,以及一些常见的分片方法。下方是本文目录,帮助您接下来的阅读WhatisSharding?什么是分片?分片(Sharding)是一种与水平切分(horizontalpartitioning)相关的数据库架构模式——将一个表里面的行,分成多个不同的表的做法(称为分区)。每个区都具有相同的模式和列,但每个表有完全不同的行。同样,每个分区中保存的数据都是唯一的,并且与其他分区中保存的数据无关。从水平切分(horizontalpartition
1:Vmware虚拟软件里面安装好Ubuntu操作系统之后使用ifconfig命令查看一下ip;2:使用Xsheel软件远程链接自己的虚拟机,方便操作。输入自己ubuntu操作系统的账号密码之后就链接成功了;3:修改主机的名称vi/etc/hostname和域名和主机映射对应的关系vi/etc/hosts,改过之后即生效,自己可以ping一下,我这里ip对应master,比如pingmaster之后发现可以ping通即可;(centos操作系统修改主机名命令:vi/etc/sysconfig/network,修改内容为HOSTNAME=master)4:修改过主机名称和主机名与ip对应的关系之后;开始上传jdk,使用filezilla这个工具将jdk文件以及其他文件上传到ubuntu操作系统中;鼠标左击选中想要上传的文件拖到右边即可,如下所示:上传成功之后可以检查一下,这里默认上传到root目录下面;显示已经上传成功即可;5:上传之后创建一个文件夹用于存放上传的文件或者压缩包;记住-C是大写,小写的-c会报错,见下面的测试结果;解压缩之后可以进到自己创建的hadoop目录下面看看效果,
初级图论。 CHANGELOG 2022.5.26:修改文章。 2022.6.8:添加SAT的定义。 2022.6.10:添加DAG的支配树。 2022.9.30:添加DAG链剖分。 1.同余最短路 说难也不算难,但是挺有意思的一个知识点。应用不广泛。 前置知识:SPFA/Dijkstra求最短路。 1.1算法简介 同余最短路用于求解在某个范围内有多少重量可以由若干物品做完全背包凑出,或者说,有多少数值可由给定的一些数进行系数非负的线性组合得到。 我们尝试具体描述这样的问题。给出\(n\(n\leq50)\)个数\(a_i\(1\leqa_i\leq10^5)\),求\([L,R](1\leqL\leqR\leq10^{18})\)以内满足能表示成\(\sum\limits_{c_i\geq0}a_ic_i\)的正整数个数。 同余最短路的核心思想在于观察到:如果一个数\(r\)可以被表出,那么任何\(r+xa_i(x>0)\)也可以被表出。因此只需选出任意一个\(a_i\),求出每个模\(a_i\)同余的同余类\(K_j\)当中最小的能被表出的数\(f_j\),即可快速判断一
论文链接:https://arxiv.org/abs/2004.02015 或者 https://www.aclweb.org/anthology/2020.acl-main.494.pdf 先来吐个槽:啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊,为什么我的导师又嫌我PPT做的很烂,(Ĭ^Ĭ) 收,开始正文。 这篇文章是在知乎找的推荐,然后看的-->ACL2020最新论文概览 贴一下推荐的概述:解释生成式神经网络在现实中的应用。在自然语言处理中,现有生成式方法从输入文本中选择单词或短语作为解释,但是忽略了它们之间的相互作用。在本文工作中,通过检测特征交互来构建层次性的解释。这种解释可视化了单词和短语如何在层次结构中在不同级别上进行组合,这可以帮助用户理解黑盒模型的决策。通过自动和人工评估,在两个基准数据集上使用三个文本分类器(LSTM,CNN和BERT)对所提出的方法进行了评估。 这篇论文,我还真的看了挺久了,看了很多资料,结果老板又嫌弃我的PPT,说我的PPT没有重点,字太多,我已经改了,这次没有那么多字了,我都放在备
CSRF概念:CSRF跨站点请求伪造(Cross—SiteRequestForgery),跟XSS攻击一样,存在巨大的危害性, 你可以这样来理解:攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来说这个请求是完全合法的,但是却完成了攻击者所期望的一个操作,比如以你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至于购买商品、虚拟货币转账等。如下:其中WebA为存在CSRF漏洞的网站,WebB为攻击者构建的恶意网站,UserC为WebA网站的合法用户。 CSRF攻击攻击原理及过程如下: 1.用户C打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A; 2.在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A; 3.用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B; 4.网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A; 5.浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求。网站A并不知道该请求其实是由B
一、概述 Sortable.js Sortable.js是一款优秀的js拖拽库,支持ie9及以上版本ie浏览器和现代浏览器,也可以运行在移动触摸设备中。不依赖jQuery。支持Meteor、AngularJS、React、Vue、Knockout框架和任何CSS库,如Bootstrap、ElementUI。你可以用来拖拽div、table等元素。 先来看一下效果图: 二、安装插件 npminstallsortablejs--save复制 vuedraggable依赖Sortable.js,所以下载了vuedraggable,我们便可以直接引入Sortable使用Sortable的特性。 vuedraggable是Sortable一种加强,实现组件化的思想,可以结合Vue,使用起来更方便 三、使用 需要注意的是elementtable务必指定row-key,row-key必须是唯一的,如ID,不然会出现排序不对的情况。 test.vue <template> <divstyle="width:800px"> <el-table:dat
BEGIN DBMS_RESOURCE_MANAGER.CREATE_PLAN( plan=>'PLAN1', comment=>'TestPlan...', sub_plan=>FALSE);--默认值即为FALSE END; / BEGIN DBMS_RESOURCE_MANAGER.CREATE_CONSUMER_GROUP( consumer_group=>'GROUP_OLTP', comment=>'GroupofOLTPUSERS...', mgmt_mth=>'ROUND-ROBIN');--该用户组内各会话的CPU资源采取轮询的分配方法 END; / BEGIN DBMS_RESOURCE_MANAGER.CREATE_CONSUMER_GROUP( consumer_group=>'GROUP_OLAP', comment=>'GroupofOLAPUSERS...', mgmt_mth=>'ROUND-ROBIN'); END; / BEGIN DBMS_RESOURCE_MANAGER.
Linux的压缩格式有三种 1.TAR压缩格式 2.ZIP压缩格式 3.RAR压缩格式 TAR压缩包 TAR(TapeArchive,TAR)是Linux下的包管理工具,具有5种功能:打包、查询、释放、更新、追加。 命令格式:tar[主选项+辅助选项][文件或目录] -c创建新的tar包 -t列出tar包文件的列表 -x从tar包中释放文件 -r把备份文件追加到已备份文件的末尾 辅助选项说明 -f备份文件或设备,必选项 -v显示命令执行的详细信息 -z用gzip来压缩/解压缩文件 -j用bzip2来压缩/解压缩文件 -C指定文件解压后的存放路径 【举例】在/mnt/目录下有s1、s2、s3和1.txt四个文件,现完成以下打包和解包要求: (1)将这4个文件打包成bak.tar放在/mnt/目录下 (2)将这4个文件打包成bak2.tar放在/mnt/bak目录下 (3)将这4个文件分别以gzip和bzip2方式压缩为bak.tar.gz和bak.tar.bz2存放在/mnt/bak目录下 (4)查看各个包文件内的文件列表,并将这4个文件删除。 (5)向tar包中追加/mnt/newfi
LoadRunner中的90%响应时间是什么意思?这个值在进行性能分析时有什么作用?本文争取用最简洁的文字来解答这个问题,并引申出“描述性统计”方法在性能测试结果分析中的应用。 为什么要有90%用户响应时间?因为在评估一次测试的结果时,仅仅有平均事务响应时间是不够的。为什么这么说?你可以试着想想,是否平均事务响应时间满足了性能需求就表示系统的性能已经满足了绝大多数用户的要求? 假如有两组测试结果,响应时间分别是 {1,3,5,10,16} 和 {5,6,7,8,9},它们的平均值都是7,你认为哪次测试的结果更理想? 假如有一次测试,总共有100个请求被响应,其中最小响应时间为0.02秒,最大响应时间为110秒,平均事务响应时间为4.7秒,你会不会想到最小和最大响应时间如此大的偏差是否会导致平均值本身并不可信? 为了解答上面的疑问,我们先来看一张表: 在上面这个表中包含了几个不同的列,其含义如下: CmdID 测试时被请求的页面 NUM
总算解决一大心头之患了,比想象中容易,通宵两夜,刷完了十个实验,这个实验就是最后的了。感慨颇多。特地写篇总结。 想做一件事,就立马去做把。你会发现没那么困难,往往最大的困难,是心里的困难。 培养了HDL(HardwareDescriptionLanguage)思维,并行,串行混合。它先是一个电路,再才是一个程序,电路为主,程序为辅,用RTL的思维去思考。 这个实验也不怎么难,就是一个大的模拟题,当年ACM给我良好的代码功底受益颇多。 能硬件级别揣摩CPU的一点点运行方式,但是还有很多疑问,需要看书去解决。 这次的实验只是在心里面扎了一个种子(种子当然很重要啦~),需要看完CSAPP,和硬件/软件接口那两本书才能成长为苍天大树。 实验驱动型+看书解决型,现阶段最好的学习方法了~ 以下是本次实验的数据通路 modulecpu modulecpu(clk,reset,ALU_OP,inst,rs,rt,rd,rs_data,rt_data,rd_data,ZF,OF,Write_Reg,PC,PC_new,rd_rt_s,W_Addr,imm,W_Data,im
百度网盘上传时,如果是超过256KB的文件,将计算整个文件的MD5和文件前256KB内容的MD5,并对两个MD5值加密后请求后端执行秒传。后端通过两个MD5和长度信息判断是否存在该文件,如果存在则完成秒传。 有个读者在微信上问我:百度网盘的秒传功能是如何实现的? 这个问题我其实有想过,我猜测大概是前端计算一个文件的哈希值(比如MD5)发送给后端,网盘服务器判断是否存在这个文件,如果存在就直接在后端完成文件的“转存”,直接告诉前端:上传成功。 不过这是我自己猜测的,到底对不对,一直也没有去验证过。 我把我的猜测告诉了他,结果他问了一句:如果发生哈希冲突了怎么办呢?。 我想了一下又说:那就多加几个哈希! 不过百度网盘到底是怎么做的呢?这位读者既然问到了,我就趁机花了几分钟研究了一下,算是解答了这个疑惑,增加了知识。 MD5冲突 首先,只用一个哈希值,已经有事实证明是会发生冲突的,而不只是理论上。 比如我在知乎上找到了一个例子,下面两段不同的数据,只相差两个字节: 分别计算md5,结果是一样的: 所以,如果只用一个哈希值就判定是同一个文件,那就比较容易会出现张冠李戴的情况。 甚至,有人
importlombok.extern.slf4j.Slf4j; importorg.springframework.util.StringUtils; importjava.util.ArrayList; importjava.util.List; importjava.util.Stack; importjava.util.stream.Collectors; @Slf4j publicclassJournalUtil{ publicstaticvoiddebug(Stringmsg){ Stringprefix=getParentCallerClassMethod(Thread.currentThread().getStackTrace()); if(!StringUtils.isEmpty(JournalHolder.getStickyLog())) msg="["+JournalHolder.getStickyLog()+"]"+msg; log.debug("["+prefix+"]"+msg); } publicstaticvoiddebug(Stringform
yield是C#为了简化遍历操作实现的语法糖,我们知道如果要要某个类型支持遍历就必须要实现系统接口IEnumerable,还需要实现IEnumerator, staticvoidMain(string[]args) { foreach(variteminnewProgram().SayHappyNewYear()) { Console.WriteLine(item); } MyList2myList2=newMyList2(); foreach(variteminmyList2) { Console.WriteLine(item); } }复制 使用yield的写法 publicIEnumerable<String>SayHappyNewYear() { yieldreturn"恭";//yield在这里充当隐式的IEnumerator接口 yieldreturn"喜"; yieldreturn"发"; yieldreturn"财"; yieldbreak;//向迭代器发出结束迭代的信号 }复制
代码放着了,有空研究呵呵 //IDA*(迭代式深度搜索) //把SIZE改成4就可以变成15数码问题 /* IDA*算法在是A*与ID算法的结合物,用了A*算法的预估方法和预估值(f=g+h),用了ID算法的迭代深入(最初从Manhatton距离开始) 较之A*算法,IDA*算法不需要Open表和Closed表,大大节省了内存空间,而且IDA*算法可以不采用递归式程序设计方法,这样也可以节约堆栈空间。 A*,例如目标是D,起始为A,首先初始化将每个节点到D的直线距离赋给节点做代价函数,然后在访问了A之后,马上预测A的子节点B/C,求得B的实际代价为A到B的花费加上B的原始代价.同理取得C的实际代价,之后在A的所有子节点中选择代价最小的节点进行扩展。上面的过程重复进行直到找到目标。 迭代加深(ID),ID算法将深度设置为dep,对于一个树做深度优先的遍历(节制条件:所有节点的深度不大于dep),如果没有找到目标,那么将dep++,重复上面的过程直到找到目标。 IDA*算法(迭代深度优先算法),将上面的A*和ID算法结合起来,也就是,在进行搜索时,使用耗散值替代ID中的深度值(f=
/** <p>给你一个<strong>只包含正整数</strong>的<strong>非空</strong>数组<code>nums</code>。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。</p> <p></p> <p><strong>示例1:</strong></p> <pre> <strong>输入:</strong>nums=[1,5,11,5] <strong>输出:</strong>true <strong>解释:</strong>数组可以分割成[1,5,5]和[11]。</pre> <p><strong>示例2:</strong></p> <pre> <strong>输入:</strong>n