小七蒙恩来源:https://blog.csdn.net/qq_38105536

科技   2024-11-02 07:24   浙江  
👇推荐大家关注一个公众号👇
点击上方 "编程技术圈"关注, 星标或置顶一起成长
后台回复“大礼包”有惊喜礼包!

日英文

What is adhere to? Is day, and one day, you tell yourself, insist again one day.

什么是坚持?就是一天,又一天,你告诉自己,再坚持一天。

每日掏心话

不要沉迷过去,不要害怕未来,过去。得失也好,成败也罢,无论快乐,还是痛苦,都过去了,你只能回忆,而无法回去。

责编:乐乐 | 来源:简简单单

来源:juejin.cn/post/7416267311848816675

编程技术圈(ID:study_tech)第 3012 期推文


往日回顾:上周,又劝退十几个了。。。

     

      正文     

大家好,我是小乐

传统查询逻辑的问题

在代码评审的过程中,发现有些查询逻辑可读性太差,但是一时又没找到好的方法。复杂的查询往往需要查询大量的信息。例如下面的例子,获取测验的正确率和参与率。还有测验每道题目分别的正确率和参与率。

老师可以在课堂 (Classroom) 上发起测验 (Exam),一个测验对应多道题目 (Question),学生 (Student) 可以对题目进行回答 (StudentQuestionAnswer)

public class ExamInfoQuestionQueryService {

    private ExamQuestionMapper questionMapper;
    private StudentQuestionAnswerMapper studentQuestionAnswerMapper;
    private ExamMapper examMapper;
    private StudentMapper studentMapper;

    /**
     * 获取测验和题目的正确率和参与率
     *
     */

    public ExamDto getQuestionByExamId(String examId) {

        Exam exam = examMapper.selectById(examId);

        // 获取所有题目
        List<Question> questions = questionMapper.selectByExamId(examId);

        List<String> examQuestionIds = questions.stream().map(Question::getId).collect(Collectors.toList());

        // 获取所有学生的答题情况
        List<StudentQuestionAnswer> studentQuestionAnswers = studentQuestionAnswerMapper.selectByExamQuestionIds(examQuestionIds);

        Map<String, List<StudentQuestionAnswer>> questionMap = studentQuestionAnswers.stream().collect(Collectors.groupingBy(StudentQuestionAnswer::getExamQuestionId));

        // 获取所有学生, 为了获取学生数
        List<Student> students = studentMapper.selectByClassroom(exam.getClassroomId());

        ExamDto examDto = new ExamDto();
        // 计算测验正确率
        examDto.setCorrectRate(calculateCorrectCount(studentQuestionAnswers));
        // 计算测验参与度
        examDto.setSubmittedRate(calculateSubmittedRate(studentQuestionAnswers,students));

        List<QuestionDto> questionDtos = questions.stream().map(question -> {

            QuestionDto questionDto = new QuestionDto();
            // 计算题目正确率
            List<StudentQuestionAnswer> questionAnswers = questionMap.getOrDefault(question.getId(), Collections.emptyList());
            
            questionDto.setCorrectRate(calculateCorrectCount(questionAnswers));
            // 计算题目参与率
            questionDto.setSubmittedRate(calculateSubmittedRate(questionAnswers, students));
            
            return questionDto;
            
        }).collect(Collectors.toList());

        examDto.setQuestions(questionDtos);
        return examDto;
    }

    private double calculateSubmittedRate(List<StudentQuestionAnswer> questionAnswers, List<Student> students) {
        return 1.0 * questionAnswers.size() / students.size();
    }

    private long calculateCorrectCount(List<StudentQuestionAnswer> questionAnswers) {
        long correctCount = questionAnswers.stream()
                .filter(StudentQuestionAnswer::isCorrect)
                .count();

        return correctCount / questionAnswers.size();
    }
}

上面方法的特点是需要聚合不同的数据。为了性能考虑使用了大量的 Map, 导致可维护性下降。可维护性差体现在2点:

1.变量和引用离得太远了。因为所有数据都要在组装前里面准备好,所以无法用到的时候再去获取。

2.依靠中间变量传递,难以复用。查询对象的依赖关系如下,因为相互依赖,为了性能考虑会提前算出值传进去。

例如测验的正确率和参与度,这两个指标表面没什么联系,因为底层查询公用了 studentQuestionAnswer,所以 studentQuestionAnswer 需要提前计算了,作为参数分别传到对应的计算方法里面。

如果涉及到更多的复用变量,方法将有更多的参数。理想情况下计算测验正确率,传一个 examId 就可以了,如果是为了复用,定义成下面这种会更好一些,因为只要知道 examId 就足以可以计算出测验的正确率,这个定义复用性是最好的,虽然性能可能也是最差的,因为很多中间变量又要重新计算。

private double calculateExamCorrectRate(String examId);

更优雅的编写方式

我认为更优雅的实现方式就是去掉临时变量。在<重构> 里面也有提到通过方法替代变量的形式,目标就是让逻辑更加直观,暂时不考虑性能问题。理想的实现方式如下:

public class ExamInfoQuestionQueryService2 {

    private ExamQuestionMapper questionMapper;
    private StudentQuestionAnswerMapper studentQuestionAnswerMapper;
    private ExamMapper examMapper;
    private StudentMapper studentMapper;

    /**
     * 获取测验和题目的正确率和参与率
     */

    public ExamDto getQuestionByExamId(String examId) {

        ExamDto examDto = new ExamDto();

        examDto.setCorrectRate(calculateExamCorrectRate(examId));

        examDto.setSubmittedRate(calculateExamSubmittedRate(examId));

        examDto.setQuestions(getExamQuestions(examId).stream().map(question -> {

            QuestionDto questionDto = new QuestionDto();
            // 计算题目正确率
            questionDto.setCorrectRate(calculateQuestionCorrectRate(examId, question));
            // 计算题目参与率
            questionDto.setSubmittedRate(calculateQuestionSubmittedRate(examId, question));
            return questionDto;
        }).collect(Collectors.toList()));
        return examDto;
    }

    private double calculateExamSubmittedRate(String examId) {
        return 1.0 * getStudentQuestionAnswers(examId).size() / getStudents(examId).size();
    }

    private long calculateExamCorrectRate(String examId) {
        return getCorrectStudentAnswerCount(examId) / getStudentQuestionAnswers(examId).size();
    }

    private long getCorrectStudentAnswerCount(String examId) {
        return getStudentQuestionAnswers(examId).stream()
                .filter(StudentQuestionAnswer::isCorrect)
                .count();
    }

    private List<Student> getStudents(String examId) {
        return studentMapper.selectByClassroom(getExam(examId).getClassroomId());
    }

    private Exam getExam(String examId) {
        return examMapper.selectById(examId);
    }

    private List<StudentQuestionAnswer> getStudentQuestionAnswers(String examId) {
        return studentQuestionAnswerMapper.selectByExamQuestionIds(getExamQuestions(examId)
                .stream()
                .map(Question::getId)
                .collect(Collectors.toList()));
    }

    private double calculateQuestionSubmittedRate(String examId, Question question) {
        return 1.0 * getStudentQuestionAnswerPerQuestion(examId).apply(question).size() / getStudents(examId).size();
    }


    private long calculateQuestionCorrectRate(String examId, Question question) {
        return getCorrectCountPerQuestion(examId, question) / getAnswerCount(examId, question);
    }

    private int getAnswerCount(String examId, Question question) {
        return getStudentQuestionAnswerPerQuestion(examId).apply(question).size();
    }

    private long getCorrectCountPerQuestion(String examId, Question question) {
        return getStudentQuestionAnswerPerQuestion(examId).apply(question).stream()
                .filter(StudentQuestionAnswer::isCorrect)
                .count();
    }

    private Function<Question, List<StudentQuestionAnswer>> getStudentQuestionAnswerPerQuestion(String examId) {

        Map<String, List<StudentQuestionAnswer>> studentQuestionAnswerMap = getStudentQuestionAnswers(examId).stream()
                .collect(Collectors.groupingBy(StudentQuestionAnswer::getExamQuestionId));

        return question -> studentQuestionAnswerMap.getOrDefault(question.getId(), Collections.emptyList());
    }


    private List<Question> getExamQuestions(String examId) {
        return questionMapper.selectByExamId(examId);
    }

}

去掉临时变量之后前面提到的2个问题都得以解决,不存在临时变量,需要的时候用get方法替代即可。通过函数组合的方式,没有了中间变量,方法的定义复用性更好。例如增加一个单独查询测验正确率的方法,可以直接调用 calculateCorrectRate(examId) 即可,不用再传一堆变量。

虽然方法多了很多,但每个方法都单一职责,实际上用这种方式注释都可以不用加了,因为每个方法定义都表明了方法的作用。因为方法会比较细,因此针对复杂的查询最好还是单独一个类。

如何解决性能问题

方法重复调用会带来性能问题,如何解决性能问题? 我们观察calculateExamSubmittedRatecalculateExamCorrectRate 共同调用了 getStudentQuestionAnswers 方法,函数的入参其实都是一样的。又因为查询是没有副作用的,相同的参数必定返回相同的结果,因此可以缓存下来。具体实现可以加本地缓存,通过线程变量来把函数的值存下来。解决了性能问题。最后再讨论技术实现方案。

那如果参数会变怎么办,例如 getStudentQuestionAnswerPerQuestion 里面的 question。不先通过批量查询的方式就算加缓存也要 n 次查询。这时可以从函数式编程寻找灵感。

查询函数式

用函数替代变量,看到了函数式的影子。在函数式编程里面,函数有几个特点

  • 函数组合: 多个函数可以合并成一个函数。
  • 引用透明: 函数的返回值只取决于输入值,也就是没有副作用。
  • 函数缓存: 函数是闭包,内部可以存数据。
  • 高阶函数: 函数可以作为参数或者返回值传递。
  • 柯里化: 将接收多个参数的函数转换成一系列嵌套的单个参数的函数。

函数组合和透明引用是加缓存的基础,如果没有透明引用,是加不了缓存的。

getStudentQuestionAnswerPerQuestion 其实是传了2 个参数,一个是 examId,一个是 question。能否把函数柯里化,改成 getStudentQuestionAnswerPerQuestion(examId)(question) 呢? 好处就是通过 getStudentQuestionAnswerPerQuestion(examId) 提前把所有 question 都查出来,再利用这个函数去筛选出单个 question 的值。

/**
* 函数柯里化
*/

private Function<Question, List<StudentQuestionAnswer>> getStudentQuestionAnswerPerQuestion(String examId) {

    Map<String, List<StudentQuestionAnswer>> studentQuestionAnswerMap = getStudentQuestionAnswers(examId).stream()
            .collect(Collectors.groupingBy(StudentQuestionAnswer::getExamQuestionId));

    return question -> studentQuestionAnswerMap.getOrDefault(question.getId(), Collections.emptyList());
}


private long getCorrectCountPerQuestion(String examId, Question question) {
    return getStudentQuestionAnswerPerQuestion(examId).apply(question).stream()
            .filter(StudentQuestionAnswer::isCorrect)
            .count();
}

getStudentQuestionAnswerPerQuestion 计算出所有question 的值,并封装成一个函数返回。因为是闭包,因此 studentQuestionAnswerMap 会被缓存在这个函数里面,当要用到的时候,只要调用 getStudentQuestionAnswerPerQuestion (可以对这个函数的返回值进行缓存 ) 获取函数,再通过 apply 把question 传递进去即可,实际调用就是通过 Map 获取值,并不会每次都去计算 Map 的值,从而解决多次查询的问题。

上层方法如法炮制,最终代码如下

public class ExamInfoQuestionQueryService3 {

    private ExamQuestionMapper questionMapper;
    private StudentQuestionAnswerMapper studentQuestionAnswerMapper;
    private ExamMapper examMapper;
    private StudentMapper studentMapper;

    /**
     * 获取测验和题目的正确率和参与率
     */

    public ExamDto getQuestionByExamId(String examId) {

        ExamDto examDto = new ExamDto();

        examDto.setCorrectRate(calculateExamCorrectRate(examId));

        examDto.setSubmittedRate(calculateExamSubmittedRate(examId));

        examDto.setQuestions(getExamQuestions(examId).stream().map(question -> {

            QuestionDto questionDto = new QuestionDto();
            // 计算题目正确率
            questionDto.setCorrectRate(calculateQuestionCorrectRate(examId).apply(question));
            // 计算题目参与率
            questionDto.setSubmittedRate(calculateQuestionSubmittedRate(examId).apply(question));

            return questionDto;
        }).collect(Collectors.toList()));
        return examDto;
    }

    private double calculateExamSubmittedRate(String examId) {
        return 1.0 * getStudentQuestionAnswers(examId).size() / getStudents(examId).size();
    }

    private long calculateExamCorrectRate(String examId) {
        return getCorrectStudentAnswerCount(examId) / getStudentQuestionAnswers(examId).size();
    }

    private long getCorrectStudentAnswerCount(String examId) {
        return getStudentQuestionAnswers(examId).stream().filter(StudentQuestionAnswer::isCorrect).count();
    }

    private List<Student> getStudents(String examId) {
        return studentMapper.selectByClassroom(getExam(examId).getClassroomId());
    }

    private Exam getExam(String examId) {
        return examMapper.selectById(examId);
    }

    private List<StudentQuestionAnswer> getStudentQuestionAnswers(String examId) {
        return studentQuestionAnswerMapper.selectByExamQuestionIds(getExamQuestionIds(getExamQuestions(examId)));
    }

    private static List<String> getExamQuestionIds(List<Question> questions) {
        return questions.stream().map(Question::getId).collect(Collectors.toList());
    }

    private Function<Question, Double> calculateQuestionSubmittedRate(String examId) {
        return question -> 1.0 * getStudentQuestionAnswerPerQuestion(examId).apply(question).size() / getStudents(examId).size();
    }

    private Function<Question, Double> calculateQuestionCorrectRate(String examId) {
        return question -> 1.0 * getCorrectCountPerQuestion(examId).apply(question) / getAnswerCount(examId).apply(question);
    }

    private Function<Question, Integer> getAnswerCount(String examId) {
        return question -> getStudentQuestionAnswerPerQuestion(examId).apply(question).size();

    }

    private Function<Question, Long> getCorrectCountPerQuestion(String examId) {
        return question -> getStudentQuestionAnswerPerQuestion(examId).apply(question).stream().filter(StudentQuestionAnswer::isCorrect).count();
    }

    private Function<Question, List<StudentQuestionAnswer>> getStudentQuestionAnswerPerQuestion(String examId) {

        Map<String, List<StudentQuestionAnswer>> studentQuestionAnswerMap = getStudentQuestionAnswers(examId).stream().collect(Collectors.groupingBy(StudentQuestionAnswer::getExamQuestionId));

        return question -> studentQuestionAnswerMap.getOrDefault(question.getId(), Collections.emptyList());
    }

    private List<Question> getExamQuestions(String examId) {
        return questionMapper.selectByExamId(examId);
    }

}

使用 kotlin 的话效果更好,因为 kotlin 原生就支持函数式,不像 java 还需要有个 Function。

class ExamInfoQuestionQueryService4 {

    private lateinit var questionMapper: ExamQuestionMapper
    private lateinit var studentQuestionAnswerMapper: StudentQuestionAnswerMapper
    private lateinit var examMapper: ExamMapper
    private lateinit var studentMapper: StudentMapper

    /**
     * 获取测验和题目的正确率和参与率
     */

    fun getQuestionByExamId(examId: String): ExamDto {

        return ExamDto(
            correctRate = calculateExamCorrectRate(examId),
            submittedRate = calculateExamSubmittedRate(examId),
            questions = getExamQuestions(examId).map { question: Question ->
                QuestionDto(
                    // 计算题目正确率
                    correctRate = calculateQuestionCorrectRate(examId)(question),
                    // 计算题目参与率
                    submittedRate = calculateQuestionSubmittedRate(examId)(question)
                )
            }
        )
    }

    private fun calculateExamSubmittedRate(examId: String): Double {
        return 1.0 * getStudentQuestionAnswers(examId).size / getStudents(examId).size
    }

    private fun calculateExamCorrectRate(examId: String): Double {
        return 1.0 * getCorrectStudentAnswerCount(examId) / getStudentQuestionAnswers(examId).size
    }

    private fun getCorrectStudentAnswerCount(examId: String): Int {
        return getStudentQuestionAnswers(examId).count { obj: StudentQuestionAnswer -> obj.isCorrect }
    }

    private fun getStudents(examId: String): List<Student> {
        return studentMapper.selectByClassroom(getExam(examId).classroomId)
    }

    private fun getExam(examId: String): Exam {
        return examMapper.selectById(examId)
    }

    private fun getStudentQuestionAnswers(examId: String): List<StudentQuestionAnswer> {
        return studentQuestionAnswerMapper.selectByExamQuestionIds(getExamQuestions(examId).map(Question::getId))
    }

    private fun calculateQuestionSubmittedRate(examId: String)(Question) -> Double {
        return { question: Question ->
            1.0 * getStudentQuestionAnswerPerQuestion(examId)(question).size / getStudents(examId).size
        }
    }

    private fun calculateQuestionCorrectRate(examId: String)(Question) -> Double {
        return { question: Question ->
            1.0 * getCorrectCountPerQuestion(examId)(question) / getAnswerCount(examId)(question)
        }
    }

    private fun getAnswerCount(examId: String)(Question) -> Int {
        return { question: Question ->
            getStudentQuestionAnswerPerQuestion(examId)(question).size
        }
    }

    private fun getCorrectCountPerQuestion(examId: String)(Question) -> Int {
        return { question: Question ->
            getStudentQuestionAnswerPerQuestion(examId)(question)
                .count { obj: StudentQuestionAnswer -> obj.isCorrect }
        }
    }

    private fun getStudentQuestionAnswerPerQuestion(examId: String)(Question) -> List<StudentQuestionAnswer> {
        val studentQuestionAnswerMap = getStudentQuestionAnswers(examId)
            .groupBy(StudentQuestionAnswer::getExamQuestionId)

        return { question: Question ->
            studentQuestionAnswerMap[question.id] ?: emptyList()
        }
    }

    private fun getExamQuestions(examId: String): List<Question> {
        return questionMapper.selectByExamId(examId)
    }

}

我把上面的实现方式称为函数式查询,利用函数的方式组合和组装查询值。

缓存的实现方式

对方法实现做拦截,且类非接口,最简单的方式就是 cglib 了,cglib 可以通过继承对源方法进行拦截。

通过 BeanPostProcessor 对 bean 进行代理

@Order(Ordered.HIGHEST_PRECEDENCE)
class QueryFunctionalBeanFactory : BeanPostProcessor {

    /**
     * 此方法可以在其他 bean 注入这个 bean 之前对这个 bean 进行替换
     */

    override fun postProcessBeforeInitialization(bean: Any?, beanName: String?): Any? {
        if (bean == null) {
            return null
        }
        val targetClass = AopUtils.getTargetClass(bean)

        // QueryFunctional 是标识要缓存的类
        if (targetClass.isAnnotationPresent(QueryFunctional::class.java)) {
            val enhancer = Enhancer()
            enhancer.setSuperclass(targetClass)
            enhancer.setCallback(object : MethodInterceptor {
                override fun intercept(obj: Any, method: Method, args: Array<out Any>, methodProxy: MethodProxy): Any? {
                    return QueryFunctionalCache.cache(method) {
                        return@cache methodProxy.invokeSuper(obj, args)
                    }
                }
            })
            // 创建 cglib 代理
            val newBean = enhancer.create()

            // 把代理对象设进代理类
            for (fileId in bean.javaClass.fields) {
                fileId.isAccessible = true
                fileId.set(newBean, fileId.get(bean))
            }
            return newBean
        }
        return bean

    }

    override fun postProcessAfterInitialization(bean: Any?, beanName: String?): Any? {
        return bean
    }
}

缓存的逻辑如下

class QueryFunctionalCache {

    companion object {

        /**
         * 线程缓存缓存方法的返回值
         */

        private var cacheDatas = object : ThreadLocal<MutableMap<Method, Any?>>() {
            override fun initialValue(): MutableMap<Method,Any?> {
                return mutableMapOf()
            }
        }
        
        fun <R : Any?> cache(method: Method, any: () -> R): R {

            var cacheMethodReturnValueMap = cacheDatas.get()

            // 因为入参只有一个, 且不会变, 因此只要缓存一份就好
            if (cacheMethodReturnValueMap.containsKey(method)) {
                return cacheMethodReturnValueMap.get(method) as R
            }

            val methodReturnValue = any()

            cacheMethodReturnValueMap = cacheMethodReturnValueMap ?: mutableMapOf()

            cacheMethodReturnValueMap.putAll(mutableMapOf(Pair(method, methodReturnValue)))

            return methodReturnValue
        }
    }
}

上面的代码有个缺陷,同时也函数式查询的一个不足,就是入参必须不变。上面的例子大部分方法都是 examId,每次查询都是不变的,因此每次查询缓存只有一份,加缓存并不需要把入参作为key。只要每次进入查询入口方法的时候缓存,退出的时候删除就可以了。但这也限制了提取的方法的入参都要一样的。

当入口方法参数多的时候,可以提取成一个 dto,后面所有方法都以这一个 dto 为参数进行设计,这样方法更容易组装,因为入参都是同一个 dto,因此想调用什么方法都不用关心参数的问题。但这样函数的复用性就变得很差了,因为 dto 一般不同的查询是不同。

解决方法可以针对通用的查询细化参数,不使用 dto,只要参数是固定的,只缓存一份的规则仍然生效,但里面组合的函数也要改成一样的参数。实际使用中单个查询里面的复用是非常多的,不同查询的复用较少。

总计

函数式查询本质是通过函数替代变量,通过缓存和柯里化解决性能问题。虽然上面的代码经过精简不能用,但组件已经完成,有个项目已用上,效果还不错,把大量的 map 都去掉了,提升了可读性,定位问题的速度也更快了。


你还有什么想要补充的吗?

上周,又劝退十几个了。。。

ChatGPT 4o 国内直接用 !!!

最后给大家推荐一个ChatGPT 4.0国内网站,是我们团队一直在使用的,我们对接是OpenAI官网的账号,给大家打造了一个一模一样ChatGPT,很多粉丝朋友现在也都通过我拿这种号,价格不贵,关键还有售后。

一句话说明:用官方一半价格的钱,一句话说明:用跟官方 ChatGPT4.0 一模一样功能,无需魔法,无视封号,不必担心次数不够。

最大优势:可实现会话隔离!突破限制:官方限制每个账号三小时可使用40次4.0本网站可实现次数上限之后,手动切换下一个未使用的账号【相当于一个4.0帐号,同享受一百个账号轮换使用权限】


为了跟上AI时代我干了一件事儿,我创建了一个知识星球社群:ChartGPT与副业。想带着大家一起探索ChatGPT和新的AI时代

有很多小伙伴搞不定ChatGPT账号,于是我们决定,凡是这三天之内加入ChatPGT的小伙伴,我们直接送一个正常可用的永久ChatGPT独立账户。

不光是增长速度最快,我们的星球品质也绝对经得起考验,短短一个月时间,我们的课程团队发布了8个专栏、18个副业项目

简单说下这个星球能给大家提供什么:


1、不断分享如何使用ChatGPT来完成各种任务,让你更高效地使用ChatGPT,以及副业思考、变现思路、创业案例、落地案例分享。

2、分享ChatGPT的使用方法、最新资讯、商业价值。

3、探讨未来关于ChatGPT的机遇,共同成长。

4、帮助大家解决ChatGPT遇到的问题。

5、提供一整年的售后服务,一起搞副业

星球福利:

1、加入星球4天后,就送ChatGPT独立账号。

2、邀请你加入ChatGPT会员交流群。

3、赠送一份完整的ChatGPT手册和66个ChatGPT副业赚钱手册。

其它福利还在筹划中... 不过,我给你大家保证,加入星球后,收获的价值会远远大于今天加入的门票费用 !

本星球第一期原价399,目前属于试运营,早鸟价149,每超过50人涨价10元,星球马上要来一波大的涨价,如果你还在犹豫,可能最后就要以更高价格加入了。。

早就是优势。建议大家尽早以便宜的价格加入!


PS:欢迎在留言区留下你的观点,一起讨论提高。如果今天的文章让你有新的启发,欢迎转发分享给更多人。

版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!

欢迎加入后端架构师交流群,在后台回复“学习”即可。


最近面试BAT,整理一份面试资料《Java面试BAT通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。在这里,我为大家准备了一份2021年最新最全BAT等大厂Java面试经验总结。

别找了,想获取史上最简单的Java大厂面试题学习资料

扫下方二维码回复面试就好了

猜你还想看

阿里、腾讯、百度、华为、京东最新面试题汇集

看看人家那权限管理系统,那叫一个优雅(附源码)!

牛逼啊!接私活必备的 400 多个开源项目!赶快收藏吧(附源码合集)!

用雪花 id 和 uuid 做 MySQL 主键,被领导怼了

项目从 MySQL 切换 PostgreSQL,踩了太多的坑!!!

,你在看吗?

编程技术圈
(本号原名:程序员小乐) 这里有Java、架构、Python、技术、算法、职场、感悟、面经、资源等,一线大厂干货,10万 + 程序员都在看,做一个有趣的帮助程序员成长的架构师公众号,每天早上07点24,第一时间与你相约。
 最新文章