JAVA分散式開發中遇到的哪些坑(一)
一、Spring使用過程中的踩坑記錄

image
- Spring通過註解使用多資料來源
坑:@Autowired 按 byType 自動注入,而 @Resource 則預設按 byName 自動注入,@Primary是優先選擇。
例如,在專案中是有兩個Redis源,這兩個Redis Bean分別為dataRedisTemplate和redisTemplate。
Redis Bean1:dataRedisTemplate,clusterNodes為${data-redis.cluster.nodes}
@Bean(name = "dataRedisTemplate") public RedisTemplate dataRedisTemplate() { RedisTemplate template = new RedisTemplate(); template.setConnectionFactory(sessionLettuceConnectionFactory); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(jackson2JsonRedisSerializer); template.setHashKeySerializer(jackson2JsonRedisSerializer); template.setHashValueSerializer(new StringRedisSerializer()); template.afterPropertiesSet(); return template; } // factory @Resource @Qualifier(value = "dataLettuceConnectionFactory") private RedisConnectionFactory dataLettuceConnectionFactory; // clusterNodes @Value("${spring.data-redis.cluster.nodes}") private String clusterNodes;
Redis Bean2:redisTemplate,clusterNodes為${redis.cluster.nodes}
@Primary @Bean(name = "redisTemplate") public RedisTemplate<String, Object> redisTemplate() { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(lettuceConnectionFactory); redisTemplate.setKeySerializer(new StringRedisSerializer()); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(new StringRedisSerializer()); redisTemplate.afterPropertiesSet(); return redisTemplate; } // factory @Resource @Qualifier(value = "lettuceConnectionFactory") private RedisConnectionFactory lettuceConnectionFactory; // clusterNodes @Value("${spring.redis.cluster.nodes}") private String clusterNodes;
在一個應用中要把資料放入到“Redis Bean1:dataRedisTemplate”對應的Redis中,於是我在這個應用中使用方式如下:
@Autowired private RedisTemplate dataRedisTemplate; // 根據key獲取資料 Object obj = dataRedisTemplate.opsForValue().get(key);
實際上使用的是“Redis Bean2:redisTemplate”對應的Redis。
破解方式一:把@Autowired換成@Resource 註解。如下:
@Autowired private RedisTemplate dataRedisTemplate; // 把@Autowired換成@Resource @Resource private RedisTemplate dataRedisTemplate;
@Autowired和@Resource最大的區別就是:@Autowired 按 byType 自動注入,而 @Resource 則預設按 byName 自動注入。
這裡還需要注意一個註解@Primary,官方的說明如下:
Indicates that a bean should be given preference when multiple candidates are qualified to autowire a single-valued dependency. If exactly one 'primary' bean exists among the candidates, it will be the autowired value. @Primary 優先方案,被註解的實現,優先被注入
通常情況下@Autowired是通過byType的方法注入的,可是在多個實現類的時候,byType的方式不再是唯一,而需要通過byName的方式來注入,而這個name預設就是根據變數名來的。
也就是說,如果沒有在redisTemplate()上面增加@Primary的話是沒有問題的,因為有多個實現時,@Autowired是會通過byName的方式來注入的,但是按照上面說的,因為有了@Primary,@Autowired註解會優先使用Bean redisTemplate。
還有一種解決方案是增加@Qualifier(value = "dataRedisTemplate"),如下:
@Autowired private RedisTemplate dataRedisTemplate; // 換成:增加@Qualifier(value = "dataRedisTemplate") @Autowired @Qualifier(value = "dataRedisTemplate") private RedisTemplate dataRedisTemplate;
- Spring事務@Transactional失效問題
坑:若同一類中的其他沒有@Transactional 註解的方法內部呼叫有@Transactional 註解的方法,有@Transactional 註解的方法的事務被忽略,不會發生回滾。
FooService.class
public interface FooService { void insertRecord(); void insertThenRollback() throws Exception; void invokeInsertThenRollback() throws Exception; void invokeInsertThenRollbackTwo() throws Exception; }
FooServiceImpl.class
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @Component public class FooServiceImpl implements FooService { @Autowired private JdbcTemplate jdbcTemplate; @Autowired private FooService fooService; @Override @Transactional public void insertRecord() { jdbcTemplate.execute("INSERT INTO FOO (BAR) VALUES ('AAA')"); } @Override @Transactional(rollbackFor = Exception.class) public void insertThenRollback() throws Exception { jdbcTemplate.execute("INSERT INTO FOO (BAR) VALUES ('BBB')"); throw new Exception(); } @Override public void invokeInsertThenRollback() throws Exception { insertThenRollback(); } @Override public void invokeInsertThenRollbackTwo() throws Exception { fooService.insertThenRollback(); } }
執行
import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.AdviceMode; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.transaction.annotation.EnableTransactionManagement; @SpringBootApplication @EnableTransactionManagement(mode = AdviceMode.PROXY) @Slf4j public class DeclarativeTransactionDemoApplication implements CommandLineRunner { @Autowired private FooService fooService; @Autowired private JdbcTemplate jdbcTemplate; public static void main(String[] args) { SpringApplication.run(DeclarativeTransactionDemoApplication.class, args); } @Override public void run(String... args) throws Exception { // 標記1:輸出 AAA 1 fooService.insertRecord(); log.info("AAA {}",jdbcTemplate.queryForObject("SELECT COUNT(*) FROM FOO WHERE BAR='AAA'", Long.class)); // 標記2:輸出 BBB 0,事務生效 try { fooService.insertThenRollback(); } catch (Exception e) { log.info("BBB {}",jdbcTemplate.queryForObject("SELECT COUNT(*) FROM FOO WHERE BAR='BBB'", Long.class)); } // 標記3:輸出 BBB 1,事務未生效 // ***這個地方就是最容易踩坑的地方!!!*** try { fooService.invokeInsertThenRollback(); } catch (Exception e) { log.info("BBB {}",jdbcTemplate.queryForObject("SELECT COUNT(*) FROM FOO WHERE BAR='BBB'", Long.class)); } // 標記4:輸出 BBB 1,事務生效(如果把標記3的程式碼註釋掉,輸出 BBB 0) // ***這是避免踩坑的一種方式*** try { fooService.invokeInsertThenRollbackTwo(); } catch (Exception e) { log.info("BBB {}",jdbcTemplate.queryForObject("SELECT COUNT(*) FROM FOO WHERE BAR='BBB'", Long.class)); } } }
二、RocketMQ使用過程中的踩坑記錄

image
-
Rocket預設開啟了VIP通道導致10909failed問題
坑:Rocket預設開啟了VIP通道,VIP通道埠為10911-2=10909。若Rocket伺服器未啟動埠10909,則報connect to <:10909> failed。
解決方案:不走VIP通道。
producer.setVipChannelEnabled(false); consumer.setVipChannelEnabled(false);
- Rocket instanceName引數未做配置導致重複消費問題
坑:一個是Rocket如果沒有配置instanceName,那麼會使用pid做instanceName,如果instanceName一樣會重複消費,因為叢集消費模式是按instanceName做為唯一消費例項。
檢視原始碼,如果沒有指定instanceName預設會把pid做為instanceName,如下:
if (this.instanceName.equals("DEFAULT")) { this.instanceName = String.valueOf(UtilAll.getPid()); } public static int getPid() { RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean(); String name = runtime.getName(); // format: "pid@hostname" try { return Integer.parseInt(name.substring(0, name.indexOf('@'))); } catch (Exception e) { return -1; } }
解決方案:運維配置有 $MQ_INSTANCE_NAME
環境變數,不同機器不一樣,所以可以使用: mq.consumer.instanceName:${MQ_INSTANCE_NAME:預設值}
進行配置。
@Value("${rocketmq.consumer.instanceName:${MQ_INSTANCE_NAME:fota}}") private String clientInstanceName; consumer.setInstanceName(clientInstanceName);