当前位置:数据分析 > 那些年的那些魔法密码

那些年的那些魔法密码

  • 发布:2023-10-05 13:41

首先解释一下什么是魔法密码?神码的意思是神奇的代码(也有不好的意思),这里是用来表示警告的!

往事不堪回首!我想当年(2017年)公司的技术团队是新组建的,系统也是新搭建的。为了赶工期,一切都以速度为目标,快速试错、快速交付上线。项目管理规范被忽视,技术规范控制没有及时跟上。工程师交付的代码质量非常差。发生多起严重生产故障,后果严重,教训惨痛!

虽然我当时是一名建筑师,但我就像一名消防员。 毫不夸张地说,凡是出现生产故障问题的地方,我们就得去救火!

原因有3个:

  • 团队新组建,成员水平参差不齐。有的人不错,但有的人基础确实不够扎实。即使是不懂面向对象的人也来写Java代码。
  • 项目管理没有做好,没有按时制定和实施统一的代码规范。
  • 关键时刻不容疏忽。在巨大的压力下,快速灭火的唯一方法就是尽量减少使用问题的影响。

所以,当年救火的过程中,填补了很多坑;审查结束后,做了一些总结记录,并对问题进行深入分析,找出根本原因,希望避免再次发生,从而获得一些宝贵的经验总结。

今天我将谈论这些年来给我带来深刻教训的代码片段。这里我不进行批评,只是对学习进行反思和总结,也希望读者能够得到一些启发。 让我们仔细看看都有哪些神奇的密码。到底有多少个神奇的密码呢?

大神代码片段1:redis命令使用错误

0

上面的代码片段非常简单,就是一个简单封装的jedis工具类。乍一看没有问题,但是因为这段代码,在生产中出现了奇怪的问题。生产环境部署多个业务系统,使用同一个redis集群。部分业务系统的redis值频繁被清除,数据莫名丢失。需要很长时间才能发现。

最后通过redis背后的服务监控,使用FlushALL命令检查代码,通过全局搜索代码,发现某个业务系统注销时调用了这个工具类。 jedis工具类的init方法内部使用了flushAll命令。这个命令将删除整个数据库,这是一个非常棘手的用法。当时修改为flushdb(其实是有隐患的,如果有多个应用或者同一个Redis db库,就会被flush)。

其实在用户退出的业务中,只需要清除相关缓存,即删除(del)对应的键值即可,根本不需要刷新。

神奇片段2:Redis Key的错误使用

1 {IMG_1: Ahr0CHM6LY9PBWCYMDIZLMNUYMXVZ3MUY2JSB2JSB2CVMTYWMDG4LZIWMWMY8XNJAWODGTMJAYMDMDMJKTNJQ3MDIMDM0LNBUZBUZ w ==/}

各种条件组合起来创建Redis Key,导致Key很长。

这样的大KEY数量过多,会占用较多的内存资源,查询效率会较低。过去我们的系统中出现了超过300万条这样又长又臭的字符串,导致查询效率下降。 ,并一度杀死了该服务。

建议使用Redis KEY尽可能短。如果太长怎么办?需要进行处理,比如使用hashCode或者md5。

另外,通常不建议使用“_”来连接,而是使用“:”。冒号的优点之一就是可以自动划分目录结构,使得查询定位更加方便。重构代码。

4 {IMG_4: Ahr0Chm6ly9pbwcymdizlmnuymxvz3Muy2JSB2CVMTYWMDG4LZIWMWMY8XNJAWODGTMJAYMDCWODCWMZE5NZC5LNBUZ w ==/}

以下是过去出现过的一些不正确的redis使用方式

(1) 无有效期

看下面的代码,是通用的工具方法!这样,调用者写入redis后,key和value就会一直存在,redis将无法随着时间的推移而运行。毕竟空间是有内存支撑的,而且空间是非常有限的。而且,99%以上的实际业务都需要有效期,即需要设置过期时间。

将其放入没有过期时间的普通缓存是一个不好的做法!

(2) 使用“*”星号进行模糊匹配

此类key太多会严重拖累redis服务。 redis查询此类key时,需要进行全表扫描,性能会迅速下降。所以这个KEY方法要谨慎使用,最好不要使用。

(3) Redis 存储过多内容

Redis 的存储容量有限。实际使用中,建议不要存储太大的内容块,而是要控制好。就像这个KEY:DELIVERYEXCEPTIONMSG_LIST。内容1.5GB,节点分片2GB内存,消耗80%。

7 {IMG_7: Ahr0Chm6ly9pbwcymdizlmnuymxvz3Muy2JSB2CVMTYWMDG4LZIWMJMWMY8XNJAWODGTMJAYMJUXMDU3NDG5MZTCZNTCZNTY0LNBUZBUZ w ==/}

以上不正确的使用方法都让我们深受其害,导致redis多次崩溃!所以请正确使用Redis,否则会给您带来麻烦。

归根结底,主要原因是代码规范和质量控制不到位,开发人员意识不够,写代码时比较随意。

针对以上问题,我们可以封装一个Redis操作工具类,供开发者直接调用。避免因误操作而造成不必要的问题。

如下调用示例所示,RedisOpsProvider工具类已经屏蔽了Redis的所有基本操作。如果调用者不包含过期时间,则默认相对体验过期时间,例如1天。

神奇代码片段3:@Transactional事务注解的错误使用

Spring @Transactional 事务注解在此代码片段中使用。

这段代码中共有三个操作:

首先是写入主业务表save(客户);

第二个是将相关数据写入附件表记录登录保存(附件);

第三个远程调用是外部接口sendCustomerToWeChat(customer);

第一个和第二个使用事务来实现事务一致性,但第三个使用异步线程,并且还跨服务进行远程调用

B www.sychzs.cnasync (() -> {
Sendcustomrtowechat (客户);
Return "ok";
});

这里的交易不能保证数据的一致性。

大神代码片段4:添加分布式锁后仍然出现重复编码

1 {IMG_10: Ahr0Chm6ly9pbwcymdizlmnuymxvz3Muy2jsb2jsb2cvmTyWMDG4LZIWMWMY8XNJAWODGTMJAYMJQXNZITCYMDUTCYMDENNNJIWLNBU Zw ==/}

看到函数中添加了@Transactional事务注解,函数内部加锁了redis分布式锁RedisLocker.lock(lockName);应该可以正常生成业务代码,但结果却并非如此。 增加了redis全局锁,但还是有重复编码

在高并发环境下可以使用锁失效。正常的做法是要么在事务外加锁,要么分解重写需要控制事务的代码块。

加锁失败的原因是:由于Spring的AOP,会在update/save方法之前打开事务,然后在这之后加锁。当锁定代码执行时,就会提交事务,因此锁定代码块执行是在事务内执行的,则可以推断执行代码块时事务尚未提交;

其他线程进入锁代码块后,读取的库存数据不是最新的。

正确的做法是去掉最外层@Transactional。具体问题分析参见《高并发环境下生成序列编码重复问题分析》。

神奇片段5:跨业务调用数据列表导致内存溢出

公告列表查询逻辑非常简单。通过查找公告数据列表,按照当前人的地域、组织、品牌类别、职位等条件筛选数据集合。早期,如果两个数据在同一个数据库,使用同一个服务,通过SQL条件查询和过滤,是没有问题的。

但是后来微服务拆分之后,公告业务数据和人员结构被分成了两个独立的应用服务和两个数据库。 人员组织和权限是独立的数据库和独立的应用服务; 公告业务数据是独立的服务和数据库。

现在,查询还跨多个服务进行聚合以显示最终结果。即需要将两个服务列表集合数据进行聚合、匹配、过滤后显示结果。

在大循环中,查询部门、职位、人员权限判断,然后通过远程RPC接口调用人员接口数据。

每个登录的人都会产生近千种接口调用和本地数据业务查询的组合。假设有10,000人使用它,这意味着有1000万远程调用。打电话,如果有10万人访问,就会有1亿个电话。面对巨大的网络IO,谁能顶得住?多么大的一个坑啊!

在测试环境进行测试时,访客量较少,没有进行测试。事实上,并没有进行大规模的压力测试。

这段代码上线后,直接导致公告业务的服务申请内存溢出,服务多次死掉。如果你欺骗了一个人,你就不会付出生命的代价!

分阶段优化修改:

在调用循环之前,先准备一些数据,而不是进入循环调用远程查询,以减少跨机网络通信的时间和次数。经过优化修改后,系统可以正常运行,运行稳定。

事实上,这种做法虽然通过阶段性优化解决了问题,勉强通过了测试,但仍有很大的改进和优化空间。

跨多个服务调用:聚合——>条件过滤——>显示

多个List之间的聚合、遍历、复制,实际上是消耗资源的。当并发量达到一定程度时,机器就无法承受。

优化方向转向使用ES。发布公告和写作的时候,做一些平铺工作,做一些模板和权限逻辑的映射处理,查询的时候直接查询ES,然后做一些简单的标签符号替换。改造后实现了10万级QPS和毫秒级响应。

ES修改版本代码:

魔法代码片段6:作弊型判定

这种代码本质上是代码规范的问题,也是开发者基本素质的问题。虽然这不是一个致命的问题并且产生了正确的结果,但根据代码规范不应该这样写。

问题:

  • 比较字符串时不要使用“==”而使用equals;
  • 既然是是或否的判断,就直接使用boolean类型即可,增加代码的可读性和健壮性;

稍微修改一下,不然真的看不了。

延伸知识点:

基本数据类型用“==”进行比较时,会比较它们的值。
当将引用数据类型与“==”进行比较时,它们会比较它们的堆内存地址。
Object equals() 的初始默认行为是比较 对象 的内存地址值,但在 String、Integer、Date 等类中,equals 已被重写用于比较对象的成员变量值是否相同,而不是比较类的堆内存地址。

查看String等于JDK8源码

对于整数 var = ?在-128到127范围内赋值,会在IntegerCache.cache中生成Integer对象,并且会重用已有的对象。对象引用地址是相同的,所有超出这个范围的数据,都会直接在堆上生成新的对象。这是一个大坑! ! !

对于基本数据类型(如byte、short、char、int、long、float、double、boolean等)的值比较,使用“==”进行比较。
对于参考数据类型(如String、Short、Char、Integer、Long、Float、Double、Boolean、Date等)的值比较,使用equals进行比较。
推荐使用java.util.Objects#equals(JDK7中引入的工具类)

神代码片段7:邪凡空境

1 {IMG_18:Ahr0Chm6ly9pbwcymdizlmnuymxvz3Muy2JSB2CVMTYWMDG4LZIWMWMY8XNJAWODGTMJAYMJQYMJAXNDQ55552NZLNBU Zw ==/}

这段代码非常简单易懂,但是当它发布到生产环境时,却造成了严重的灾难。堪称史上最严重的BUG。下面描述该过程的细节。

1。问题生成过程描述:

  • 同一手机号码(吴小兵)的用户名下有多个账户,且该用户操作的部分账户无效;
  • 然后使用无效账号登录,即可正常登录系统,并继续修改手机号码;
  • 修改手机号码时,由于程序查询逻辑不够严谨,导致主用户为空,查询全表数据;
  • 所有用户数据更新到同一个手机号码,问题爆发!
  • 10点35分左右,发现UC系统卡住,UC数据库有表锁时间过长的报警。开发开始解决问题。 11点20分,在问答环节收到了最终用户(吴小兵)的反馈,收到了很多反馈(会计、审计、定价)的电话号码。
  • 通过查询数据库、日志和链接定位问题,12:30左右发布修复补丁,从备份数据(前一天凌晨3点的数据)中恢复数据,刷新数据弥补早上发生的数据差异。
  • 1:30开始排查和修复各业务产生的数据(服务订单、设计软件任务清单、工厂订单、裂变活动、交付安装、MSCS订单);
  • 影响最严重的是工厂订单,产生了5万多条生产传单数据,其中2.5万多条传到制造,准备在工厂车间进行生产调度。

2。详细的故障排除记录

阿里云服务日志详细分析

2021-12-08 09:54:43.999

吴Xbing是登录我们平台的普通B端用户。他在自己的账户管理模块(账号:CZJR022@xx09243)中进行了解绑账户操作,这是一项非常常见且正常的业务操作。他也如期完成了正常的操作。

解绑操作成功后,系统内部会调用清除缓存接口,系统日志显示如下:

解绑功能后,主账号MainUserId被清除。

2021-12-08 10:38:08.528

吴小兵,又进行了修改手机号码的操作

悲剧正常发生,就是一开始的代码中,where条件为空,相当于查询了整张表!这个SQL也可以从链接日志中捕获

批量更新手机号码的主要用户手机号码开始出现。库里其他账号都更新成了这个吴小兵的手机号了,呜呼! ! ! !

手机号码字段中的所有数据都更新为相同的。问题爆发后,对该服务进行了紧急消防行动,并向最终用户发出了紧急服务关闭通知。因数据修复,服务暂停一小时。

由于该服务不做每小时增量数据备份,因此只能用前一天凌晨3点的数据来恢复数据库。如今的增量数据(900多条)只能通过解析系统日志来一一恢复。从日志中找出匹配的修复程序。

3。剩余问题

  • 一些用PDF和XML编写的设计文件已经固化。设计文件无法更新,只能重新生成。实在是太惨了!
  • 如果部分账号状态不一致,只能通过恢复前后数据对比来更新号码。

4。对问题的反思

  • 无效账号依然可以登录,这是程序的一大bug。
  • 当条件为空时检查整个表格。我们要汲取经验,举一反三。要求我们写程序时要严谨,加强自检,做出判断。

5。强化解决方案

  • 框架层面解决无效当前用户的全局拦截和验证,屏蔽特定业务操作;
  • 加强代码,检查空、非空、必填等核心逻辑代码,对参数进行必要的验证;
  • Aspect AOP全局拦截查询、更新、删除等全表操作的SQL,并且拦截和阻塞无参数;
  • 重要数据质量和安全监控,状态一致,数据一致性非常重要;
  • 优化完善数据备份策略,重要数据按时间段多次备份。

经过这次惨痛的教训,我决定在框架层做一些功能来拦截不符合规则的SQL,即拦截不带where条件参数的SQL。具体代码如下:

@拦截(
        {
                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        }
)
@成分公共类 AllQueryInterceptor 实现拦截器 {

    /**
     * 白名单:允许全表查询的表名
     */
    @Value("${www.sychzs.cn:}")
    私有字符串whiteTableName;

    /**
     * 允许无where条件,只允许限制,且限制最大数量
     */
    @Value("${limit.size:10000}")
    私有长限制大小;

    /**
     * 全局控制是否开启验证开关
     */
    @Value("${all.query.check:true}")
    私有布尔 allQueryCheck;

    私有静态最终 Logger LOGGER = LoggerFactory.getLogger(AllQueryInterceptor.class);

    私有静态最终模式 p = Pattern.compile("\\s+");

    @覆盖
    公共对象拦截(调用调用)抛出Throwable {
        MappedStatementmappedStatement = (MappedStatement) invoking.getArgs()[0];
        对象参数 = invoking.getArgs()[1];
        BoundSqlboundSql=mappedStatement.getBoundSql(参数);

        if(!sqlHavingWhere(boundSql) && !sqlHavingLimit(boundSql) && allQueryCheck){LOGGER.debug(boundSql.getSql());
            throw new BusinessException("检测到您存在操作全表记录的风险,请联系系统管理员!");
        }别的{
            返回调用.proceed();
        }
    }

    私有语句 getStatement(String sql){
        语句语句= null;
        尝试 {
            语句 = CCJSqlParserUtil.parse(sql);
        } catch (JSQLParserException e) {
            LOGGER.error("转换sql失败,原sql={}",sql);
        }
        返回语句;
    }

    /**
     * 判断是否有限制
     * @参数boundSql
     * @返回
     */
    私有布尔 sqlHavingLimit(BoundSqlboundSql){
        尝试 {
            IPage 页面 = getPage(boundSql);
            if (null != page && page.getSize() >= 0L && page.getSize()<=limitSize){
                返回真;
            }别的 {
                StringoriginalSql=boundSql.getSql();
                返回originalSql.contains(CommonConstants.SqlKeywords.LIMIT);
            }} catch (异常 e) {
            LOGGER.error("判断该sql是否涉及全表操作异常,原因{}",e);
        }
        返回真;
    }

    /**
     * 判断sql是否涉及全表操作
     * @参数boundSql
     * @返回
     */
    私有布尔 sqlHavingWhere(BoundSqlboundSql){
        尝试 {
            StringoriginalSql=boundSql.getSql();
            语句 stmt = getStatement(originalSql);
            如果(空!= stmt){
                // 允许全量操作的表在白名单中释放
                if(whiteTableName(getTableNames(stmt))){
                    返回真;
                }
                // where没有条件或者只有一个删除标识条件,则认为是全表操作
                设置 where = getWhere(stmt);
                如果(其中==空){
                    LOGGER.debug("sql={}",originalSql);
                    返回假;
                }else if(where!=null && where.size() == 1 && CommonConstants.SqlKeywords.DEL_FLAG.equals(where.iterator().next().toUpperCase())) {LOGGER.debug("sql={}",originalSql);
                    返回假;
                }
            }
        } catch (异常 e) {
            LOGGER.error("判断该sql是否涉及全表操作异常,原因{}",e);
        }

        返回真;
    }

    /**
     * 获取分页数据
     * @参数boundSql
     * @返回
     */
    私有 IPage getPage(BoundSqlboundSql){
        对象 paramObj =boundSql.getParameterObject();
        IPage页=空;
        if (paramObj IPage 实例) {
            页面 = (IPage)paramObj;
        } else if (paramObj instanceof Map) {
            迭代器 var8 = ((Map)paramObj).values().iterator();

            while(var8.hasNext()) {
                对象 arg = www.sychzs.cn();
                if (arg IPage 实例) {
                    页面 = (IPage)arg;
                    休息;
                }
            }
        }
        返回页面;
    }

    /**
     * 获取表名
     * @param 语句
     * @返回*/
    private List getTableNames(Statement 语句){
        List tableNames = new ArrayList<>();
        if(语句!= null){
            TablesNamesFinder tableNamesFinder = new TablesNamesFinder();
            tableNames =tablesNamesFinder.getTableList(语句);
        }
        返回表名;
    }

    /**
     * 判断表名是否在允许全表查找的白名单中
     * @参数表名
     * @返回
     */
    private boolean whiteTableName(List tableNames){
        for(字符串表名:表名){
            // 有些表名含有``,去掉
            if(tableName.startsWith("`") && tableName.endsWith("`")){
                表名 = 表名.substring(1,表名.length()-1);
            }
            if(whiteTableName.contains(tableName)){
                返回真;
            }
        }
        返回假;
    }

    私有列表 getPlainSelect(Statement stmt){
        List plainSelectList = new ArrayList<>();选择 select = (选择) stmt;
        SelectBody selectBody = select.getSelectBody();
        if(PlainSelect 的 selectBody 实例){
            PlainSelect plainSelect = (PlainSelect) selectBody;
            plainSelectList.add(plainSelect);
        }别的{
            SetOperationList setOperationList = (SetOperationList)selectBody;
            for(SelectBody setOperation : setOperationList.getSelects()){
                PlainSelect plainSelect = (PlainSelect) setOperation;
                plainSelectList.add(plainSelect);
            }
        }
        返回普通选择列表;
    }

    /**
     * 获取其中的参数
     * @参数
     * @返回
     */
    private Set getWhere(Statement stmt){
        Set 其中ItemSet =new HashSet<>();
        列表 plainSelectList = getPlainSelect(stmt);
        for(PlainSelect plainSelect : plainSelectList){
            getWhereItem(plainSelect.getWhere(),whereItemSet);
        }返回whereItemSet;
    }

    /**
     * 获取哪里节点参数
     * @param 右表达式
     * @param 左表达式
     * @param tblNameSet
     */
    private void getWhereItem(表达式 rightExpression,表达式 leftExpression,Set tblNameSet){
        if(rightExpression != null){
            if (rightExpression 列实例) {
                列右列 = (列) 右表达式;
                tblNameSet.add(rightColumn.getColumnName());
            }if (rightExpression 函数实例) {
                getFunction((Function) rightExpression,tblNameSet);
            }别的 {
                getWhereItem(rightExpression,tblNameSet);
            }
        }
        if(左表达式!= null){
            if (leftExpression 列实例) {
                列左列 = (列) 左表达式;
                tblNameSet.add(leftColumn.getColumnName());} if (leftExpression 函数实例) {
                getFunction((Function) leftExpression,tblNameSet);
            }别的 {
                getWhereItem(leftExpression,tblNameSet);
            }
        }
    }

    /**
     * 获取其中的字段
     * @参数
     * @返回
     */
    private void getWhereItem(表达式 where, Set tblNameSet){
        if(其中实例二元表达式) {
            二进制表达式 二进制表达式 = (二进制表达式) 其中;
            表达式 rightExpression = binaryExpression.getRightExpression()instanceof 括号?((括号)binaryExpression.getRightExpression()).getExpression():
            表达式 leftExpression = binaryExpression.getLeftExpression()instanceof 括号?getWhereItem(rightExpression,leftExpression,tblNameSet);
        }else if(instanceof 括号){
            getWhereItem(((括号) where).getExpression(),tblNameSet);
        }else if(其中instanceof InExpression){
            InExpression inExpression = (InExpression) 其中;
            表达式 leftExpression = inExpression.getLeftExpression() 括号实例?((Parenthesis) inExpression.getLeftExpression()).getExpression(): inExpression.getLeftExpression();
            getWhereItem(null,leftExpression,tblNameSet);
        }
    }

    /**
     * 获取select里面function里面的字段
     * @参数函数
     * @param selectItemSet
     * @返回
     */
    private void getFunction(Function function, Set selectItemSet){
        if(function.getParameters()==null || function.getParameters().getExpressions()==null){
            返回;
        }
        List list=function.getParameters().getExpressions();列表.forEach(数据->{
            if (函数的数据实例) {
                getFunction((Function)data,selectItemSet);
            }else if (列的数据实例) {
                Column列=(列)数据;
                selectItemSet.add(column.getColumnName());
            }别的{
                getWhereItem(数据,selectItemSet);
            }
        });

    }

神代码片段7:地狱式18层if-else-for嵌套

就像上面的十八层地狱代码,看完之后你真的想吐血! 由于篇幅限制,这里只展示了一小部分。这种神奇的代码在我们早年的老项目中大量存在。

这也是前人留下的珍贵手工作品。这种代码根本就没有什么设计。写这个代码的人没有道德操守。当时编写代码的作者由于某些原因辞职了。我们的系统上线后,用了近一个月的时间。我已经一年多没敢修改这个惊人的代码了!从我接任的那天起,我就遭受了各种磨难和折磨。我只知道心里的痛,难受!

企业需要增加需求。我们说这个需求不能添加,暂时解决不了。系统重构版本发布后我们将提出新的要求。商界人士不明白为什么他们每天都受到批评和责骂。以前可以,但是现在不行了。哈!哈!哈!

对于业务报出的Bug,我们必须要强硬,所以只能火上浇油,花大量的时间研究作者的写作意图,然后仔细地进行一些局部的修改。每次修复bug后,大家都会有测试的冲动,我对发布上线感到很不安!

我终于下定决心重建这个项目。经过两次大版本重构和多次修改,我终于封存了原项目代码仓库留作纪念!

准确的说,我们通过领域驱动的设计方法,彻底解放了这段神奇的代码,变废为宝!详细操作方法请参考另一篇文章《我领域驱动设计的DDD》

总结

1。上面只列出了一小部分典型的神码,还有很多没有贴出来;主要是经过多次重构和设计,神典慢慢消失在历史的长河中。 。也希望各位读者多总结分享,从中得到一些启发。

2。实际工作中神码无处不在。在神典的世界里,你总是有可能得到意想不到的惊喜;为了减少工作中的后顾之忧,为了更好的生活,写代码的时候多思考,多设计。

3。一个复杂的项目通常是由多人合作完成的团队。团队需要建立一套严格的代码规范。老兵应该多做代码审查,并贯彻执行,否则团队协作的成果会大打折扣。

4。作为一名码农,需要不断加强道德修养,交付好产品,拒绝交付废品;最直接的目的是为了防止后人的蔑视和批评。

相关文章