3 分钟
开发原则
工作期间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时要该多个文件