文章分类 » IT | 编程

构造轮子 vs 改造轮子

最近要做一个side project,跟 Pro Go 一书中的示例很像,有不少可以借鉴的地方。但它搭建的是平台型(platform)的基础框架,对于我的小工具来说太“重”了。
于是陷入了纠结中:是自己从零开始写呢,还是先利用它的平台工具先把应用搭建起来?

分别列了一下其利弊。

构造的利弊

系数 系数
巩固基础知识 6 繁琐 5
没有目标(够用就好吗?) 6
简陋/简单 3
可能要很久才能完成,且不一定能达到预期效果 4

改造的利弊

系数 系数
学习高手的架构思路与技巧 8 太“重”,初步完成后要“瘦身” 7
减负的过程可能繁琐,但也是理解和学习的过程 6

综合来看,还是改造更好!

不得不感叹:看和做真的是两回事啊!
看别人的项目和代码代码的时候:“嗯,对”“有道理”“这设计挺巧妙”“这地方不太合适,这么改更好些”“还可以这样啊”……
需要自己动手去写的时候:“从哪入手呢?”

MIT 6.824 (2022) Lab 1: MapReduce解题思路与问题总结

写在前面:

  • 尊重课程的要求,未公开源码。
  • 本文主要是给有需要的小伙伴一些提示,避开一些弯路和坑。
  • 只是分享我的思路,我的解题方案。一家之言,难免有错漏,请多指教。

如果你问我做题花了多久,我只能回答很久很久:思考怎么开始动手花了很久;调试花了很久。
但我想,小伙伴们应该跟我一样,主要目的是学习,而不是为了交作业。所以,多长时间不重要,有没有收获才重要。

第一只拦路虎

按照课程说明(见文末链接:说明与提示,下文简称“说明与提示”),执行第一个go命令go build -race -buildmode=plugin ../mrapps/wc.go就失败了:-buildmode=plugin not supported on windows/amd64
有点挫败,也有点失望,对Windows失望。

刚好近期学习了Docker,试验下来发现Docker+MIT6.824课程简直绝配:

  • 编译环境随便选,即使你在Windows上开发,也可以让你的代码在Ubuntu环境下编译和运行
  • 源码下载下来,相同包下的相同函数会编译报错(GoLand),相同的目录下(mrapps)有多个main函数也会报错。这样就无法在GoLand中调试代码

Continue Reading »

原来这就是表驱动开发

现在需要你提供这样一个函数:获得指定月份的天数(为了方便讨论,忽略闰年)。你会怎么做?
大概率是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func DaysOfMonthGeneral(month int) int {
	switch month {
	case 1:
		return 31
	case 2:
		return 28
	case 3:
		return 31
	case 4:
		return 30
	case 5:
		return 31
	case 6:
		return 30
	case 7:
		return 31
	case 8:
		return 31
	case 9:
		return 30
	case 10:
		return 31
	case 11:
		return 30
	case 12:
		return 31
	default:
		panic("invalid month")
	}
}

表驱动开发则是这样的:

1
2
3
4
5
var daysPerMonth = []int{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}

func DaysOfMonth(month int) int {
	return daysPerMonth[month-1]
}

这个例子太简单?那再看个例子。

根据分数得到等级。

分数 等级
>=90 A
<90 B
<75 C
<65 D
<50 F

你很可能会这么做:

1
2
3
4
5
6
7
8
9
10
11
12
13
func GetGradeGeneral(score float32) string {
	if score >= 90.0 {
		return "A"
	} else if score >= 75.0 {
		return "B"
	} else if score >= 65.0 {
		return "C"
	} else if score >= 50.0 {
		return "D"
	} else {
		return "F"
	}
}

有没有更灵活的方式呢?

有的,还是表驱动法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var (
	rangeUpper = []float32{50.0, 65.0, 75.0, 90.0, 100.0}
	grades     = []string{"F", "D", "C", "B", "A"}
)

// GetGrade 表驱动法
func GetGrade(score float32) string {
	maxGradeLevel := len(grades) - 1

	gradeLevel := 0
	studentGrade := "A"
	// 数据量大时可考虑二分查找
	for studentGrade == "A" && gradeLevel < maxGradeLevel {
		if score < rangeUpper[gradeLevel] {
			studentGrade = grades[gradeLevel]
		}
		gradeLevel++
	}
	return studentGrade
}

总结

表驱动,就是把复杂的条件分支或运算逻辑转移到对“表”的访问上。

这里说的“表”和数据库中的表没有关系,更像是一张表格或者容器。通常可以用数组或者Map实现。

Java | 对enum的扩展

遇到有一堆枚举值时,很容易就能想到 enum
比如性别(Gender),有男(Male)有女(Female),很容易就会写出下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public enum Gender {
  MALE("男"),
  FEMALE("女");
  
  private final String display;
  
  Gender(String display) {
    this.display = display;
  }
  
  public String getDisplay() {
    return display;
  }
}

可以这样使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Student {
  String name;
  String gender;
  
  // 男女有别
  public void doSomething() {
    Gender g = Gender.valueOf(gender);
    if (Gender.FEMALE == g) {
      // 女生啦啦队
    } else if (Gender.MALE == g) {
      // 男生打篮球
    }
  }
}

以上代码要想正常执行,有个前提条件,属性 gender 的值必须是 FEMALE 或者 MALE。如果数据库中存储的是 FM 呢?甚至是 12
这时,valueOf 方法已经不适用了。因为其参数值必须跟定义的枚举值完全一致。
针对这种情况,我想到了一个扩展。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 接口名想了好久,不知道用什么合适
public interface CodeEnum {
  boolean isIt(String code); // 方法名也拿不定主意
}

public enum Gender implements CodeEnum {
  // 用F-M还是1-2,这里可以随时调整
  MALE("M", "男"),
  FEMALE("F", "女");
  
  private final String display;
  
  Gender(String display) {
    this.display = display;
  }
  
  public boolean isIt(String code) {
    return this.code.equals(code);
  }
  
  public String getDisplay() {
    return display;
  }
}

这时,Student 类可以调整为:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Student {
  String name;
  String gender;
  
  // 男女有别
  public void doSomething() {
    if (Gender.FEMALE.isIt(gender)) {
      // 女生啦啦队
    } else if (Gender.MALE.isIt(gender)) {
      // 男生打篮球
    }
  }
}

抛砖引玉,有更好的想法欢迎留言讨论。

 

如何在Spring管理的事务中使用多线程

keywords:Java, Spring, Transactional, Multi-threading, ExecutorService

为什么要用多线程?

1
2
3
4
5
6
7
8
9
10
11
12
public class BigService {

    public void execute() {
        doSomething();

	List entities = buildManyManyEntities();
	save(entities);

	doOthers();
    }

}

如上的业务代码,经测试大部分时间花在了 save 方法上(耗时:15/22,数据量:5000)。

于是想到了用多线程(entities 数据之间互不影响)。

于是改成了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class BigService {

    public void execute() {
	doSomething();

	List entities = buildManyManyEntities();
	// 使用多线程存储
	saveInParallel(entities);

	doOthers();
    }

    void save(Object entities) {
	// 直接存入数据库
	// 单个对象和集合都可以
    }

    void saveInParallel(List entities) {
	int cpus = RunTime.getRunTime().availableProcessors();
	ExecuteService exec = Executors.newFixedThreadPool(cpus);

	try {
	    for (final Object each : entities) {   
		exec.execute(new Runnable() {
			public void run() {
			    save(each);
			}
		});
	    }
	} finally {
	    exec.shutdown(); // 关闭资源
	}
    }
}

自我感觉良好。结果一测试发现表里没数据。

网上查了一下,找到了原因:新的线程没受Spring控制(项目中事务是由Spring控制的)。

知道了问题,就有了方向。意料之外,情理之中地在Stack Overflow(链接见文末)找到了答案。关键点在于Spring中的Lookup,详述如下。

关键步骤在于由Spring来控制线程类的实例化。

新建多线程工具类:MultiThreadHelper.java 。

1
2
3
4
5
public abstract class MultiThreadHelper {
    // 必须是抽象方法且无参
    // 详见文末Spring官方文档Lookup部分
    public abstract DAOTask createDAOTask();
}

新建线程类:DAOTask.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class DAOTask implements Runnable {
    private DAO dao;
    private Object entity;

    public DAOTask(DAO dao) {
	this.dao = dao;
    }

    public void addEntity(Object entity) {
	this.entity = entity;
    }

    public void run() {
	dao.save(entity);
    }
}

新建Spring的配置文件: multi.xml 。

1
2
3
4
5
6
7
8
<bean id="multiThreadHelper" class="MultiThreadHelper">
    <lookup-method name="createDAOTask" bean="daoTask"/>
</bean>

<!-- 注意要用prototype,每次都生成一个新的 -->
<bean id="daoTask" class="DAOTask" scope="prototype">
    <constructor-arg ref="dao"/>
</bean>

新的BigService类(省略了Spring配置文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class BigService {

    private MultiThreadHelper multiThreadHelper; // 通过Spring注入

    public void execute() {
	doSomething();

	List entities = buildManyManyEntities();
	// 使用多线程存储
	saveInParallel(entities);

	doOthers();
    }

    void save(Object entities) {
	// 直接存入数据库
	// 单个对象和集合都可以
    }

    void saveInParallel(List entities) {
	int cpus = RunTime.getRunTime().availableProcessors();
	ExecuteService exec = Executors.newFixedThreadPool(cpus);

	try {
	    for (final Object each : entities) {   
		// 一定要调用这个Lookup方法生成线程
		DAOTask task = getMultiThreadHelper().createDAOTask();
		task.addEntity(each);
		exec.execute(task);
	    }
	} finally {
	    exec.shutdown(); // 关闭资源
	}
    }
}

最后需要注意的是,要把 run 方法纳入事务范围。传播类型REQUIREDREQUIRES_NEW 没看出明显差别。

大功告成!

时间从本来的22s左右,减少为4s左右。当然,这仅仅是响应时间(从调用开始到调用结束),不代表数据都已经存入库中了(通过日志可以看出来)。

线程数量的选择

之前看过的一本书(《Java并发编程实战》)里说:线程数量等于服务器的CPU数量+1时能实现最优的利用率。

于是分别测试了N+1,N,N-1三种情况,差别不大,N-1的略胜一筹。不知道是不是仅仅因为需要创建的线程数少了,导致响应时间变短。

说明:这里的N指CPU的数量,测试中是4 。

问题

这里存在一个问题:事务不能跟主线程一起统一管理,即如果主线程中发生了错误多线程写入的数据不会回滚。

不过有个小技巧——先删后插,每次调这个方法前都先删一下。

如有更好的方法,欢迎留言讨论。

参考链接

 

一个Hibernate的事务问题

工作中遇到一个问题。为了方便说明,做了简化。

业务要求:有两个接口,要么都成功,要么都失败。即任一个接口调用失败,两个接口相关的数据(如果库里有的话)都要删除。

假设:tab_two 中已有一条数据,再调接口时会报主键冲突。

期望的结果:第一次调用时失败,并把tab_two 中已有的一条数据清除。第二次调用成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class SomeInterface {

    public void execute() {
        try {
            if (m1) {
                method1();
            } else if (m2) {
                method2();
            }
        } catch (Exception e) {
            hasException = true;
        } finally {
            if (hasException) {
                clearData();
            }

        }
    }

    private void method1() {
        // 用Hibernate写表 tab_one
    }

    private void method2() {
        // 用Hibernate写表 tab_two
    }

    private void clearData() {
        // 使用JDBC
        // 删除表 tab_one
        // 删除表 tab_two
    }
}

问题:某一接口失败时,其数据未被清除。

伪码如上。写表Hibernate,删表JDBC。

接口2失败后,tab_one 的数据被清除了,但tab_two的数据还在。

猜测:Hibernate和JDBC有不同的事务处理机制。

以前也有类似的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
    ...
    saveByHibernate();
    // 调存储过程时会报错:用Hibernate存的表没有数据
    invokeProduce();
    ...
}

// 修改后
{
    ...
    saveByHibernate();
    // 把Hibernate存的数据再查一下,执行存储过程就没问题了
    load();
    invokeProduce();
    ...
}

基于以上经验,修改代码:写表删表都用Hibernate。

经过测试,还是不行,失败接口的数据仍然存在。Debug的过程中发现,确实有delete,但删的是表里已有的数据,新数据(当前调用接口的数据)并未被删除,事务结束时被写入了表中。

想到Hibernate对象的三种状态:瞬时态(Transient)、持久态(Persistent)、脱管/游离态(Detached),于是使用了 evict(obj) 方法,结果还是不行。

1
2
3
4
5
6
7
8
9
10
11
public void clearData(Object errorData) {
    getSession().evict(errorData);

    // 使用Hibernate
    // 删除表1对象,删之前先查
    obj1 = load();
    delete(obj1);
    // 删除表2对象
    obj2 = load();
    delete(obj2);
}

通过日志发现,load之前会有insert,好像有个隐式的flush。

最后决定使用 getSession().clear() 试一下,居然意外地解决了。

没太搞清楚其中的原理,按照下图所示的各种状态之间的流转,evict和clear应该是一样的啊。

虽然不知其所以然,毕竟问题是解决了。

hibernate-status

Hibernate版本:4.1.8.Final

Java | @Override 不要再把它当成可有可无的了

@Override,一定不陌生吧!就是没敲过,也一定见过。
是不是觉得它可有可无,多它不多,少它不少?更有甚者,把它当个累赘。
不爱搭理它就算了。IDE帮你生成了你还要删掉它,这就有点过分了哈。
它真的是毫无用处吗?开发JDK和IDE的大神们造了个无用的东西?

看看下面这个例子,请你来找茬儿。

IShape类

1
2
3
public interface IShape {
    String introduceYourself();
}

Rectangle类

1
2
3
4
5
public class Rectangle implements IShape {
    public String introduceYourself() {
        return "我是一个长方形。";
    }
}

Square类

1
2
3
4
5
public class Square extends Rectangle {
    public String introduceYouself() {
        return "你可以叫我长方形,但它并不是我的真名。我的真名叫正方形。";
    }
}

测试类

1
2
3
4
5
6
7
8
public class AnnotationTest {
    @Test
    public void introduceYourself() {
        IShape aShape = new Square();
        assertEquals("你可以叫我长方形,但它并不是我的真名。我的真名叫正方形。", 
        				aShape.introduceYourself());
    }
}

测试结果

java-annotation-override

IDEA JUnit 测试结果

这是为什么呢? 明明是一个正方形,它怎么偏说自己是个长方形呢?​​能看出是哪的问题吗? 如果还是找不到问题,在方法上添加​​​@Override注解看看。 是不是很神奇?

所以,请善待它吧! 其实之前我也没认识到这一点。是最近听了一个关于 Annotation的课才知道它还有这个用处。​

相关链接

HTTP常用返回码及其含义

HTTP返回报文里的数字(状态码)代表什么?

404 是最常见的了。其他的你知道吗?

  • 301

    永久转向。

  • 302

    临时转向。

  • 304

    文件未改变,客户端缓冲版本还可以继续使用。

  • 400

    非法请求。

  • 401

    访问被拒绝,需要用户名、密码。

  • 403

    禁止访问。

  • 404

    没找到页面。

  • 500

    服务器内部错误,通常是程序有问题。

  • 503

    服务器没有应答,如负载过大等。

  • 其他

    更详细的请参见:HTTP Status Codes

Homebrew 的使用 —— 安装|卸载|更新 等

1. 安装 Homebrew

在终端输入以下命令(本文所说的命令,无特殊说明均是在终端执行,不再赘述):

1
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

注:最新命令请参考下方的 GitHub 链接

2. 安装软件包

1
brew install xxx

注: xxx 即对应软件包的名称。

例如,安装 MySQL 的命令如下:brew install mysql

3. 更新 Homebrew

1
brew update

如果更新时报错,根据错误上网搜搜解决办法。实在找不到的,还有个最直接有效的办法:卸载/重装。

4. 更新通过 Homebrew 安装的各种软件包

1
brew upgrade [xxx]

同上,xxx 指某个软件包的名称,为可选参数。如果没有,则更新所有的软件包;否则,只更新指定的软件包。

5. 卸载 Homebrew

1
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/uninstall)"

相关链接

 

HTML静态页面如何传递/获取参数?(已解决)

需求

想实现这样一个功能:打开一个静态HTML页面A,让它自动跳转到页面B

实现

怎么实现呢?

习惯了JSP之类的传参(Request、Session等),忽然想用静态的HTML页面,不知道怎么用了。

比较容易想到的就是通过URL传参,A.html?ref-url=B.html
怎样才能取到这个ref-url的值呢?
字符串……split?
会不会太low?
应该有更简洁的办法吧。

网上搜了搜(见文末参考部分),有主意了。
还是通过URL,但不是用split,而是用正则表达式。

示例

A.html的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<html>
    <head>
        <title>Young Zhu</title>
        <script type="text/javascript">
        function getURL() {
        // 注意这里的 search ,会返回URL里?后面的字符串,包括?本身
        var queryStr = window.document.location.search;
        //alert(queryStr);
        var url = (queryStr.match(/(=)[^&]*/g))[0].substr(1);
        //alert(url);
        return url;
        }
        </script>
    </head>
    <body>
    <script type="text/javascript">
    window.location.href=getURL();
    </script>
    </body>
</html>

用浏览器打开 A.html,在地址栏现有的内容后面加上以下内容:?ref-url=B.html

注意:

  1. 本例中参数名不重要,因为正则表达式是要取=后面的内容。
  2. 为了方便,只写了B.html,测试时请换成真实路径,或者换成其他的网址,例如某Du。

知识点

  1. 获取URL后面的传参字符串
    window.document.location.search

    原来还专门有这样的一个属性 search,而不用自己去解析。

  2. 正则表达式
  3. 页面重定向
    window.location.href=new-url;

参考链接