工作期间bug、暴雷、事故的反思、记录

1、测试后的代码修改后一定要进行回归测试

当测试后的代码,开发又发现问题或者进行重构后,一定要进行回归测试。因为:

  • 修复bug,往往会引入新的bug
  • 自已的眼光具有局限性,当局者迷,很难发现新的问题

(1)情景

开发时,并考虑到项目会部署到多台实例上,在经过QA测试(单实例测试)之后,才知道会部署到多实例上。于是我在检查代码时发现我使用的synchronized并不能保证在多实例的互斥性。于是,我便修改为使用自定义互斥机制(通过redis一条记录是否存在、实现的简单互斥机制)。然后并没有交由QA进行回归测试,于是又引入了更严重的错误。导致上线后才被发现,造成不好的影响

修改前的代码

synchronized (this){
		if(xxxDao.get(config).equals(xxx)){
				xxxDao.update(newXxx);
		}
}

修改后的代码

//getLock、deleteLock 为加锁、放锁函数
if(getLock(NUMBER_LOCK) && xxxDao.get(config).equals(xxx)){
		xxxDao.update(newXxx);
		deleteLock(NUMBER_LOCK);
}

修改后的代码错误分析

  • 获得锁语句和其他条件在同一条if语句内,导致某些路径的锁得不到释放

(2)解决方案

  • 完善的单元测试
  • 请QA进行回归测试

2、加锁的每条路径都要释放

一般情况下使用synchronized就可以很方便的实现互斥锁。但是在多实例或分布式环境下是不可以使用,往往需要自定义实现分布式锁。如使用Redis实现。 redis实现的锁一般是非阻塞的。声明如下:

//获取锁,如果获取的到,返回true,否则返回false
boolean getLock(String key);
//删除锁,释放锁
void deleteLock(String key);

这样做不像synchronized代码块,存在以下问题

  • 不可重入
  • 不会自动释放锁,需要手动调用释放
  • 非阻塞
  • 锁的全局性,一旦发生锁没有及时释放,将影响巨大。比如某线程获取到锁,并未释放锁时重启,导致错误

于是在某次业务中,我在使用过程中,出现某些执行路径没有释放锁的问题

(1)错误示例

//getLock、deleteLock 为加锁、放锁函数
if(getLock(NUMBER_LOCK) && xxxDao.get(config).equals(xxx)){
		xxxDao.update(newXxx);
		deleteLock(NUMBER_LOCK);
}

(2)正确示例

if(getLock(NUMBER_LOCK)){
	if(xxxDao.get(config).equals(xxx)){
		xxxDao.update(newXxx);
	}
	deleteLock(NUMBER_LOCK);
}

(3)解决方案

  • 启动时,首先释放所有锁
  • 进行严密的单元测试
  • 仔细检查所有执行路径,防止出现未释放锁的情况
  • 资源的使用一定要释放

3、未持久化的数据不要返回给用户

返回的数据是需要持久化的数据,一定到将数据持久化之后成功之后,再返回给用户否则可能出现如下情况:

  • 数据持久化失败,但是用户拿到了数据,
    • 用户依赖该数据进行操作将会发生错误,使错误难以定位,可能推迟错误的发现

(1)错误示例

		//旧数据
		Data data = xxxDao.get(id);
		//新数据
		Data newDate = new Data();
		//newData.setXxx(xxx);
		//getLock为自定义互斥机制:当可以获取锁时返回true,否则返回false
		if(getLock(XXX_LOCK)  /*&& 其他条件*/){
				xxxDao.update(newNumber); //更新
				deleteLock(XXX_LOCK);
		}
		return newData; //直接返回新数据

以上代码有两个问题:

  • (严重)加锁语句与其他条件在同一条if语句内,导致某些路径永远无法释放锁
  • (本条目)可能未持久化的数据(newData)返回给了用户

(2)正确做法

		//旧数据
		Data data = xxxDao.get(id);
		//新数据
		Data newDate = new Data();
		//newData.setXxx(xxx);
		//getLock为自定义互斥机制:当可以获取锁时返回true,否则返回false
		if(getLock(XXX_LOCK)  /*&& 其他条件*/){
				xxxDao.update(newNumber); //更新
				deleteLock(XXX_LOCK);
				return newData; //返回新数据
		} else {
				return data; //返回旧数据
		}

(3)正误对比

  • 正确做法虽然仍存在问题1:严重,但是在调试过程中可以发现返回的数据一直没有更新,错误一般可以在开发期间被发现
  • 错误做法,在调试过程中,发现返回数据是新的数据,很有可能忽略问题1:严重,导致上线后发现异常,又很难排查

4、不要拷贝代码

(1)表现形式

  • 将一段代码从一个文件拷贝到另一个文件
  • 将一个代码文件从一个项目拷贝到另一个项目

(2)原因

  • 当代码存在bug或变更时拷贝了多少份,就要改多少份

(3)实例

做一个SVNHook可配置化的内容。有多个SVN仓库对应的多个项目。代码从一个项目拷贝到另一个项目。当发现BUG时要该多个文件