GoldyMark

人的一生/可以活出不止一生


  • Home

  • Program

  • Music&Drink

  • 一言

  • Categories

  • Archives

  • Search
close

Vol31.选择

Posted on 2018-01-20   |   In 一言   |  
2.pic.jpg

实现Spring动态注册多数据源

Posted on 2018-01-17   |   In program , java   |  

最近在做SaaS应用,数据库采用了单实例多schema的架构(详见参考资料1),每个租户有一个独立的schema,同时整个数据源有一个共享的schema,因此需要解决动态增删、切换数据源的问题。

在网上搜了很多文章后,很多都是讲主从数据源配置,或都是在应用启动前已经确定好数据源配置的,甚少讲在不停机的情况如何动态加载数据源,所以写下这篇文章,以供参考。

使用到的技术

  • Java8
  • Spring + SpringMVC + MyBatis
  • Druid连接池
  • Lombok
  • (以上技术并不影响思路实现,只是为了方便浏览以下代码片段)

思路

当一个请求进来的时候,判断当前用户所属租户,并根据租户信息切换至相应数据源,然后进行后续的业务操作。

代码实现

TenantConfigEntity(租户信息)

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
@EqualsAndHashCode(callSuper = false)
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
public class TenantConfigEntity {
/**
* 租户id
**/
Integer tenantId;
/**
* 租户名称
**/
String tenantName;
/**
* 租户名称key
**/
String tenantKey;
/**
* 数据库url
**/
String dbUrl;
/**
* 数据库用户名
**/
String dbUser;
/**
* 数据库密码
**/
String dbPassword;
/**
* 数据库public_key
**/
String dbPublicKey;
}

DataSourceUtil(辅助工具类,非必要)

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 DataSourceUtil {
private static final String DATA_SOURCE_BEAN_KEY_SUFFIX = "_data_source";
private static final String JDBC_URL_ARGS = "?useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&zeroDateTimeBehavior=convertToNull";
private static final String CONNECTION_PROPERTIES = "config.decrypt=true;config.decrypt.key=";
/**
* 拼接数据源的spring bean key
*/
public static String getDataSourceBeanKey(String tenantKey) {
if (!StringUtils.hasText(tenantKey)) {
return null;
}
return tenantKey + DATA_SOURCE_BEAN_KEY_SUFFIX;
}
/**
* 拼接完整的JDBC URL
*/
public static String getJDBCUrl(String baseUrl) {
if (!StringUtils.hasText(baseUrl)) {
return null;
}
return baseUrl + JDBC_URL_ARGS;
}
/**
* 拼接完整的Druid连接属性
*/
public static String getConnectionProperties(String publicKey) {
if (!StringUtils.hasText(publicKey)) {
return null;
}
return CONNECTION_PROPERTIES + publicKey;
}
}

DataSourceContextHolder

使用ThreadLocal保存当前线程的数据源key name,并实现set、get、clear方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class DataSourceContextHolder {
private static final ThreadLocal<String> dataSourceKey = new InheritableThreadLocal<>();
public static void setDataSourceKey(String tenantKey) {
dataSourceKey.set(tenantKey);
}
public static String getDataSourceKey() {
return dataSourceKey.get();
}
public static void clearDataSourceKey() {
dataSourceKey.remove();
}
}

DynamicDataSource(重点)

继承AbstractRoutingDataSource(建议阅读其源码,了解动态切换数据源的过程),实现动态选择数据源;

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
public class DynamicDataSource extends AbstractRoutingDataSource {
@Autowired
private ApplicationContext applicationContext;
@Lazy
@Autowired
private DynamicDataSourceSummoner summoner;
@Lazy
@Autowired
private TenantConfigDAO tenantConfigDAO;
@Override
protected String determineCurrentLookupKey() {
String tenantKey = DataSourceContextHolder.getDataSourceKey();
return DataSourceUtil.getDataSourceBeanKey(tenantKey);
}
@Override
protected DataSource determineTargetDataSource() {
String tenantKey = DataSourceContextHolder.getDataSourceKey();
String beanKey = DataSourceUtil.getDataSourceBeanKey(tenantKey);
if (!StringUtils.hasText(tenantKey) || applicationContext.containsBean(beanKey)) {
return super.determineTargetDataSource();
}
if (tenantConfigDAO.exist(tenantKey)) {
summoner.registerDynamicDataSources();
}
return super.determineTargetDataSource();
}
}

DynamicDataSourceSummoner(重点中的重点)

从数据库加载数据源信息,并动态组装和注册spring bean,

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
@Slf4j
@Component
public class DynamicDataSourceSummoner implements ApplicationListener<ContextRefreshedEvent> {
// 跟spring-data-source.xml的默认数据源id保持一致
private static final String DEFAULT_DATA_SOURCE_BEAN_KEY = "defaultDataSource";
@Autowired
private ConfigurableApplicationContext applicationContext;
@Autowired
private DynamicDataSource dynamicDataSource;
@Autowired
private TenantConfigDAO tenantConfigDAO;
private static boolean loaded = false;
/**
* Spring加载完成后执行
*/
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
// 防止重复执行
if (!loaded) {
loaded = true;
try {
registerDynamicDataSources();
} catch (Exception e) {
log.error("数据源初始化失败, Exception:", e);
}
}
}
/**
* 从数据库读取租户的DB配置,并动态注入Spring容器
*/
public void registerDynamicDataSources() {
// 获取所有租户的DB配置
List<TenantConfigEntity> tenantConfigEntities = tenantConfigDAO.listAll();
if (CollectionUtils.isEmpty(tenantConfigEntities)) {
throw new IllegalStateException("应用程序初始化失败,请先配置数据源");
}
// 把数据源bean注册到容器中
addDataSourceBeans(tenantConfigEntities);
}
/**
* 根据DataSource创建bean并注册到容器中
*/
private void addDataSourceBeans(List<TenantConfigEntity> tenantConfigEntities) {
Map<Object, Object> targetDataSources = Maps.newLinkedHashMap();
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
for (TenantConfigEntity entity : tenantConfigEntities) {
String beanKey = DataSourceUtil.getDataSourceBeanKey(entity.getTenantKey());
// 如果该数据源已经在spring里面注册过,则不重新注册
if (applicationContext.containsBean(beanKey)) {
DruidDataSource existsDataSource = applicationContext.getBean(beanKey, DruidDataSource.class);
if (isSameDataSource(existsDataSource, entity)) {
continue;
}
}
// 组装bean
AbstractBeanDefinition beanDefinition = getBeanDefinition(entity, beanKey);
// 注册bean
beanFactory.registerBeanDefinition(beanKey, beanDefinition);
// 放入map中,注意一定是刚才创建bean对象
targetDataSources.put(beanKey, applicationContext.getBean(beanKey));
}
// 将创建的map对象set到 targetDataSources;
dynamicDataSource.setTargetDataSources(targetDataSources);
// 必须执行此操作,才会重新初始化AbstractRoutingDataSource 中的 resolvedDataSources,也只有这样,动态切换才会起效
dynamicDataSource.afterPropertiesSet();
}
/**
* 组装数据源spring bean
*/
private AbstractBeanDefinition getBeanDefinition(TenantConfigEntity entity, String beanKey) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(DruidDataSource.class);
builder.getBeanDefinition().setAttribute("id", beanKey);
// 其他配置继承defaultDataSource
builder.setParentName(DEFAULT_DATA_SOURCE_BEAN_KEY);
builder.setInitMethodName("init");
builder.setDestroyMethodName("close");
builder.addPropertyValue("name", beanKey);
builder.addPropertyValue("url", DataSourceUtil.getJDBCUrl(entity.getDbUrl()));
builder.addPropertyValue("username", entity.getDbUser());
builder.addPropertyValue("password", entity.getDbPassword());
builder.addPropertyValue("connectionProperties", DataSourceUtil.getConnectionProperties(entity.getDbPublicKey()));
return builder.getBeanDefinition();
}
/**
* 判断Spring容器里面的DataSource与数据库的DataSource信息是否一致
* 备注:这里没有判断public_key,因为另外三个信息基本可以确定唯一了
*/
private boolean isSameDataSource(DruidDataSource existsDataSource, TenantConfigEntity entity) {
boolean sameUrl = Objects.equals(existsDataSource.getUrl(), DataSourceUtil.getJDBCUrl(entity.getDbUrl()));
if (!sameUrl) {
return false;
}
boolean sameUser = Objects.equals(existsDataSource.getUsername(), entity.getDbUser());
if (!sameUser) {
return false;
}
try {
String decryptPassword = ConfigTools.decrypt(entity.getDbPublicKey(), entity.getDbPassword());
return Objects.equals(existsDataSource.getPassword(), decryptPassword);
} catch (Exception e) {
log.error("数据源密码校验失败,Exception:{}", e);
return false;
}
}
}

spring-data-source.xml

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<!-- 引入jdbc配置文件 -->
<context:property-placeholder location="classpath:data.properties" ignore-unresolvable="true"/>
<!-- 公共(默认)数据源 -->
<bean id="defaultDataSource" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<!-- 基本属性 url、user、password -->
<property name="url" value="${ds.jdbcUrl}" />
<property name="username" value="${ds.user}" />
<property name="password" value="${ds.password}" />
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="5" />
<property name="minIdle" value="2" />
<property name="maxActive" value="10" />
<!-- 配置获取连接等待超时的时间,单位是毫秒 -->
<property name="maxWait" value="1000" />
<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="5000" />
<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="240000" />
<property name="validationQuery" value="SELECT 1" />
<!--单位:秒,检测连接是否有效的超时时间-->
<property name="validationQueryTimeout" value="60" />
<!--建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效-->
<property name="testWhileIdle" value="true" />
<!--申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。-->
<property name="testOnBorrow" value="true" />
<!--归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。-->
<property name="testOnReturn" value="false" />
<!--Config Filter-->
<property name="filters" value="config" />
<property name="connectionProperties" value="config.decrypt=true;config.decrypt.key=${ds.publickey}" />
</bean>
<!-- 事务管理器 -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="multipleDataSource"/>
</bean>
<!--多数据源-->
<bean id="multipleDataSource" class="a.b.c.DynamicDataSource">
<property name="defaultTargetDataSource" ref="defaultDataSource"/>
<property name="targetDataSources">
<map>
<entry key="defaultDataSource" value-ref="defaultDataSource"/>
</map>
</property>
</bean>
<!-- 注解事务管理器 -->
<!--这里的order值必须大于DynamicDataSourceAspectAdvice的order值-->
<tx:annotation-driven transaction-manager="txManager" order="2"/>
<!-- 创建SqlSessionFactory,同时指定数据源 -->
<bean id="mainSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="multipleDataSource"/>
</bean>
<!-- DAO接口所在包名,Spring会自动查找其下的DAO -->
<bean id="mainSqlMapper" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="mainSqlSessionFactory"/>
<property name="basePackage" value="a.b.c.*.dao"/>
</bean>
<bean id="defaultSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="defaultDataSource"/>
</bean>
<bean id="defaultSqlMapper" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="defaultSqlSessionFactory"/>
<property name="basePackage" value="a.b.c.base.dal.dao"/>
</bean>
<!-- 其他配置省略 -->

DynamicDataSourceAspectAdvice

利用AOP自动切换数据源,仅供参考;

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
@Slf4j
@Aspect
@Component
@Order(1) // 请注意:这里order一定要小于tx:annotation-driven的order,即先执行DynamicDataSourceAspectAdvice切面,再执行事务切面,才能获取到最终的数据源
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class DynamicDataSourceAspectAdvice {
@Around("execution(* a.b.c.*.controller.*.*(..))")
public Object doAround(ProceedingJoinPoint jp) throws Throwable {
ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = sra.getRequest();
HttpServletResponse response = sra.getResponse();
String tenantKey = request.getHeader("tenant");
// 前端必须传入tenant header, 否则返回400
if (!StringUtils.hasText(tenantKey)) {
WebUtils.toHttp(response).sendError(HttpServletResponse.SC_BAD_REQUEST);
return null;
}
log.info("当前租户key:{}", tenantKey);
DataSourceContextHolder.setDataSourceKey(tenantKey);
Object result = jp.proceed();
DataSourceContextHolder.clearDataSourceKey();
return result;
}
}

其他

另外,部分信息(例如:租户配置、省市县等共用信息)是存放在公共schema里面的,也就是说一个请求里面有可能包括查询当前租户数据源和公共数据源,建议为公共数据源单独创建一个sqlSessionFactory和sqlMapper(分别对应xml配置里的defaultSqlSessionFactory和defaultSqlMapper),或干脆把公共数据源的数据单独做成一个微服务,以后水平扩展也方便。

参考资料

  1. SaaS多租户数据隔离的三种方案
  2. Spring MVC+Mybatis 多数据源配置
  3. spring 动态创建bean
  4. Saas Spring动态加载、编辑数据源

本周推荐阅读[第四期]

Posted on 2017-07-22   |   In 推荐阅读   |  

本周推荐阅读(第四期)

推荐阅读

可能是迄今为止最好的 GitHub 代码浏览插件

MySQL事务隔离级别和Spring事务关系介绍

公司数据库默认使用的是RR级别,结合Spring的事务管理器,我们可以很方便地在代码里进行事务控制,但前提是我们要清楚知道数据库不同级别的事务隔离行为和Spring的事务传播行为。(目前ERP项目里面有个TransactionService接口,里面有个default方法rollback(),供手动回滚使用,不过在调用dubbo服务的时候没用)

Mybatis TypeHandler的简单应用及源码分析

看完这篇文章之后,我结合之前写的MyBatisEnumHandler,尝试写了一个通用的Handler处理枚举类型字段,使数据库的tinyint字段无缝转换至Java Enum,更方便编码工作。该插件待测试。

本周在地铁上主要在看《目标》,没怎么花时间刷手机……

本周推荐阅读[第三期]

Posted on 2017-07-15   |   In 推荐阅读   |  

本周推荐阅读(第三期)

推荐阅读

2017前端现状–答题救不了前端新人

前后分离架构的探索之路

以上两篇文章虽然是以前端工程师的口吻来叙述,但同时也是一个技术人的心路历程。

以下为摘抄的一段话:

我们不应该有前端后端之分,我们可以有专精之处,但是对于 web 开发这回事该懂的都应该要懂,否则你怎么可能打得赢?同理,如果说后端工程师需要靠写页面来了解前端的话,那么前端也应该有类似的方式来了解后端做的一些事情。

其他

Poi读取Excel引发的内存溢出

这篇文章对比了Poi在用户模式和事件驱动模式下读取Excel占用内存的情况。

POI事件驱动读取Excel分析

对上一篇文章的Poi事件驱动模式进行分析。

webpack2 终极优化

投入低,产出高的优化。(本文说的是编译后结果的优化,然而顺便吐槽一下webpack编译速度令人忧伤)

移动端图片上传旋转、压缩的解决方案

先收藏,以后可以拿来就用。

IntelliJ IDEA 2017.1 JDK 8 性能调优

给MacBook Pro的后端同学看的,MacBook Air实测表示无力,毕竟系统启动后就吃掉3G+内存了(Air只有4G内存)。

思路清奇:通过 JavaScript 获取移动设备的型号

前端监控可以用到这个技巧,获取用户设备类型对解决问题有很大的帮助。

本周推荐阅读[第二期]

Posted on 2017-07-09   |   In 推荐阅读   |  

写在前面的话:

由于每天要坐2小时地铁,所以有时会在地铁上阅读一些文章,在这里做个分享。

以下这几篇文章表达的意思相近,所以我将其都归在这一期里面。

PS:时间比较仓促,加上高中毕业后就没怎么写东西了,不论是思绪还是排版,写得都比较乱,望见谅。以后会锻炼自己这方面的能力。

本周推荐阅读(第二期)

推荐阅读

  • 一本开源的程序员快速成长秘笈
  • 技术工程师的能力与目标
  • 聊聊成为大神路上的过程
  • [译]什么样的工程师才能算老司机

个人感想

沟通能力

  • 学会聆听:聆听和尊重他人意见是沟通能顺利进行的前提。如果总是打断别人的发言,你的信任槽也会相应地减少。当别人不再信任你的时候,你的工作也会很难开展下去;

  • 达成一致:沟通不是辩论赛,一味的坚持己见不见得是上策。沟通的最终目的是解决问题,需要彼此达成一致意见并通力合作;

  • 换位思考:关于这点,我就直接引用一个大佬的观点:

Paul Graham 曾在他的书《黑客与画家》中写到:判断一个程序员是否具备“换位思考”的能力有一个好方法,那就是看他怎样向没有技术背景的人解释技术问题。

以下为摘抄信息:

程序员著名网站之一 StackOverflow 的两位创始人 Jeff Atwood 和 Joel Spolsky 都对此有正面的认识和见解。

Jeff 说:

成为一名杰出的程序员其实跟写代码没有太大关系。

做程序员确实需要一些技术能力,当然还要有坚韧不拔的精神。

但除此之外,更重要的还是要有良好的沟通技巧。

Jole 的观点:

勉强过得去的程序员跟杰出程序员的不同之处,不在于他们掌握了多少种编程语言,也不在于他们谁更擅长 Python 或 Java。

真正关键的是,他们能不能把他们的想法表达清楚,杰出的程序员通过说服别人来达成协作。

通过清晰的注释和技术文档,他们让其他程序员能够读懂他们的代码,这也意味着其他程序员能够重用他们的代码,而不必重新写过。

要不然,他们代码的价值就大打折扣了。

学习能力

犹记得我大一新生入学的时候参加过一个讲座,讲座的主题和内容都忘了,但台上一个师姐说的一句话至今仍然鞭策着我:“不要问我能教会你什么,要问自己能从我这里学到什么。”

刚进公司的时候,我接的任务也是经常延期,而且总是身陷“业务代码”之中(延伸阅读:天天写业务代码的程序员,怎么成为技术大牛,开始写技术代码?)。

后来,我厌倦每一次实现分页需求都要写两条几乎一模一样的SQL,所以研究了一下Mybatis分页拦截器;

后来,我厌倦每次前端传个日期参数进来后端都要手动转换一下,所以学了一下Spring的Converter;

后来,我厌倦每次都要拼接千篇一律的SQL语句,所以写了个自动拼接SQL的工具类(参考了某同事的写法);

后来,我厌倦每次有新需求都要写机械代码(Entity、DTO、DAO等),所以写了个代码生成器脚本;

以上问题都是我在写业务代码过程中遇到的问题,以上产出的“副产品”就是我个人的收获,有些东西甚至可以帮助团队成员提高工作效率。

PS:当然,写业务代码本身的价值在于业务,而不是那些“副产品”。

就技术人而言,我更倾向于以技术痛点为驱动,解决对团队最有价值的技术问题。以团队利益为导向,顺势而为,可以持续为团队带来更大收益。

例如:(抛几块砖头)

  • 测试环境隔三差五地崩,崩一次要花半天修复,有没有办法优化其结构?
  • 每次新建项目都要植入上传图片的代码,有没有办法抽象出来?
  • 每个项目都有定时任务,是否可以独立出来服务化或做成SDK?
  • etc…

如果你没有对重复感到厌倦,那可能是已经麻木了,如温水里的青蛙。

经验

你不是有十年工作经验,你只是一个经验用了十年。

有的人,用一年的时间就获得了三年的工作经验(这里不是指那个加班梗……)。

就技术水平而言,我发现第二年开始我的技术已经出现停滞信号了。做的好像都是独立设计和实现某个模块/平台/系统的工作,但其本质都是需求进来产品出去,并没有涉及到很多新技术方面的知识或挑战。(PS:或者说,手撕需求的能力强了?)

所以我会不时地问自己:自己是不是那个一个经验用i++年的人?

然后就会强迫自己滚去学习了。

解决问题

发现/提出问题并不难,但对于发现/提出的问题,你做了什么?

对自己问这个问题,然后就可以知道自己是个行动派,还是抱怨派;

建立个人品牌

  • 不要经常出现错别字(或<=1):见字如见人;

  • 尝试教导他人:教学相长,教导别人的同时也是让自己不断学习;

  • 当别人对你产生信任,你要做什么事情都会变得简单;

总结

  • 聆听他人意见:劳谦虚己,则附之者众;骄慢倨傲,则去之者多;

  • 学会换位思考:用上帝视觉看待事情,用第一、第三人称视觉思考问题;

  • 多与身边人对比:以铜为镜可以正衣冠,以人为镜可以明得失,以史为镜可以知兴衰;

  • 敢于承担责任:世上哪有等你完全准备好以后才开始做的事情,你觉得完全准备好以后,可能已经散场了;

  • 尝试教导他人:实践这一点将会包含以上所有内容,你是想成为可以1带5的王者,还是那条只会喊666的咸鱼?

关于效率的思考

Posted on 2017-07-01   |   In 效率   |  

记某次发给团队周报的附属内容。

开发团队的效率(浏览地址)

想一下:

  • 自己是否已经成为文中提到的合格程序员?
  • 自己的核心竞争力是什么?一个毕业生、新人多长时间就可以替换掉自己?
  • 自己是那个需要各种保姆、保安照顾的“孩子”吗?
  • 自己的大部分时间主要花在哪里?设计?实现?维护?还债?

加班与效率(浏览地址)

想一下:

  • 什么是有价值的事情?你的输出实现了什么价值?(个人价值?团队价值?部门价值?公司价值?)
  • 你努力的方向是否与团队形成合力?
  • 在遇到问题和困难面前,你是否充分使用了资源和人去解决问题?你为此做了什么?(百度?Google?问身边有经验的人?社区提问?)
  • 在解决问题的过程中,你是否提前思考了“是否可以避免问题”?

PS:如果百度在前2页找不到答案,那就关掉吧,进入Google搜索;

其他

上周末我在与朋友的讨论中,朋友分享了一个挺有意思的问题:

在一条船上,你是一个钢琴师,负责给客人奏乐。有一天你经过走廊发现船体破了个洞,开始进水了,这时候你是应该找补船的人来修补,还是不管继续弹钢琴?

在上面这个问题中,把我们日常的技术岗位替换进去(产品、设计、开发、测试、运维),应该是钢琴师?还是补船的人?

个人认为:分工只是团队价值最大化的手段,而不应该成为个人发展的天花顶。

Vol30.不忘初心

Posted on 2016-12-11   |   In 一言   |  
IMG_1450.JPG

旧瓶新思

Posted on 2016-12-11   |   In program   |  

故事一

魏文王问名医扁鹊说∶“你们家兄弟三人,都精于医术,到底哪一位最厉害呢?”
扁鹊答说∶“长兄最好,中兄次之,我最差。”
文王又问∶“那么为什么你最出名呢?”
扁鹊答说∶“我长兄治病,是治病于病情发作之前。由于一般人不知道他是在事先就铲除了病因,所以他的名气无法传出去,只有我们家的人才知道。我中兄治病,是治病于病情初起之时。一般人以为他只能治轻微的小病,所以他的名气只及于本乡里。而我扁鹊治病,是治病于病情严重之时。一般人都看到我在经脉上穿针管来放血、在皮肤上敷药等大手术,所以以为我的医术高明,名气因此响遍全国。”

回顾这些年的工作,似乎总在“解决”各种各样的问题,却很少去思考:解决这些问题真的有必要和价值吗?解决了之后呢?

我认为,一个人首先要有解决问题的能力。在这之上,每次遇到问题之前强迫自己去思考一下是否有避免问题的办法,并且是正方向、低成本、可实施的。久而久之,就会习惯以这种思维去思考问题。

个人见解
避免可以避免的问题,解决不能避免的问题。

献上一句我很欣赏的名言:

Intellectuals solve problems, geniuses prevent them. –Albert Einstein

故事二

缅甸有这么一个传说:有一条恶龙,每年要求村庄献祭一个处女,每年这个村庄都会有一个少年英雄去与恶龙搏斗,但无人生还。又一个英雄出发时,有人悄悄尾随。龙穴铺满金银财宝,英雄用剑刺死恶龙,然后坐在尸身上,看着闪烁的珠宝,慢慢地长出鳞片、尾巴和触角,最终变成恶龙。

之所以想起这个故事,缘于某天我无聊时翻看以前旧项目的代码(想当年还吐槽过这些代码如何如何烂。。PS:不是我写的),突然发现这些我现在写的这些东西竟与当年的那些烂代码有几分相像。然后重新code review了自己现在写的代码,有的的确很烂却不知道如何变得优雅(连Intellij都吐槽说too complex了……),有的可能设计得只有自己知道如何运行。

在review的过程中,多多少少也能想明白以前旧项目那些烂代码所遭遇到的困境,也能体会到当时作者的心情——不是我不想优雅,奈何条件不允许(时间、成本、变数等)。

Anyway,我们还是应该以此为方向。

以铜为镜可以正衣冠,以人为镜可以明得失,以史为镜可以知兴衰。

Vol29.后悔

Posted on 2016-11-04   |   In 一言   |  
IMG_1416.JPG

如何通过ODBC连接transbase数据库

Posted on 2016-10-20   |   In program , database   |  

最近在研究transbase数据库,发现transbase提供JDBC和ODBC两种连接驱动。前者比较简单就不叙述了,这里主要说一下如何通过ODBC驱动连接transbase数据库。

Read more »
12…6
GoldyMark

GoldyMark

55 posts
13 categories
31 tags
RSS
GitHub
© 2016 - 2018 GoldyMark
Powered by Hexo
Theme - NexT.Muse