需求说明
产品要求实现一个订单编号,此编号规则如下
订单编号规则:
订单编号举例
比如业务A,在2020-08-04日有三个订单,那么订单编号如下:
- A202008040001
- A202008040002
- A202008040003
比如业务A,在2020-08-05日有4个订单,那么订单编号如下:
- A202008050001
- A202008050002
- A202008050003
- A202008050003
通过上面的例子可以看到,后面的“自增ID”每天都会从1开始增加,在一个分布式系统中,要做到每天从1开始不重复并且自增的效果;想到的第一个实现方案就是redis的Incr命令(Redis Incr 命令将 key 中储存的数字值增一)。
需求实现
配置redis
依赖redis相关jar包
因为此模块继承了spring-boot-starter-parent,所以不需要指定版本
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
编写配置redis的config
package com.nicai.config;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
/**
* redis集群配置
*
* @author guozhe
* @date 2020/08/04
*/
@Configuration
@EnableConfigurationProperties(RedisProperties.class)
public class RedisClusterConfig {
private final RedisProperties redisProperties;
public RedisClusterConfig(RedisProperties redisProperties) {
this.redisProperties = redisProperties;
}
/**
* Thread-safe factory of Redis connections配置
*
* @return factory of Redis
*/
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration(redisProperties.getCluster().getNodes());
redisClusterConfiguration.setPassword(redisProperties.getPassword());
return new JedisConnectionFactory(redisClusterConfiguration);
}
/**
* 创建String类型的redis模板
*
* @param redisConnectionFactory factory of Redis
* @return String-focused extension of RedisTemplate
*/
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate template = new StringRedisTemplate(redisConnectionFactory);
template.afterPropertiesSet();
return template;
}
}
如果是配置范型的RedisTemplate,需要设置值的序列化规则为:StringRedisSerializer,原因可以参考此文章:Spring Boot中使用RedisTemplate优雅的操作Redis,并且解决RedisTemplate泛型注入失败的问题
测试redis的config代码
package com.nicai.config;
import com.yuanfeng.accounting.BaseAdminSpringTest;
import com.yuanfeng.accounting.Constants;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import java.util.concurrent.TimeUnit;
/**
* @author guozhe
* @date 2020/08/04
*/
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest(classes = AdminApplication.class)
public class RedisClusterConfigTest {
private static final String TEST_KEY = Constants.REDIS_KEY_PREFIX + "test:hello";
private static final String TEST_VALUE = "world";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
public void testStringRedisTemplateGetAndSet() {
stringRedisTemplate.opsForValue().set(TEST_KEY, TEST_VALUE);
String value = stringRedisTemplate.opsForValue().get(TEST_KEY);
Assert.assertEquals(TEST_VALUE, value);
stringRedisTemplate.delete(TEST_KEY);
Assert.assertNull(stringRedisTemplate.opsForValue().get(TEST_KEY));
}
@Test
public void testIncr() {
String key = TEST_KEY;
ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
valueOperations.set(key, "1", 24, TimeUnit.HOURS);
String initValue = valueOperations.get(key);
log.info("key={}, init value={}", key, initValue);
Assert.assertEquals("1", initValue);
Long increment = valueOperations.increment(key);
log.info("key={}, after increment={}", key, increment);
Assert.assertEquals(Long.valueOf(2), increment);
stringRedisTemplate.delete(key);
Assert.assertNull(valueOperations.get(key));
}
}
基于redis编写唯一ID生成服务
添加抽象的唯一id生成服务
package com.nicai.service;
import cn.hutool.core.util.BooleanUtil;
import com.alibaba.fastjson.JSON;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
/**
* 分布式ID生成服务
*
* @author guozhe
* @date 2020/08/04
*/
@Slf4j
public abstract class AbstractRedisDistributedIDGenerateService<T extends AbstractRedisDistributedIDGenerateService.Context> {
/**
* 初始化key时的默认值
*/
private static final long DEFAULT_VALUE = 0;
protected final StringRedisTemplate redisTemplate;
public AbstractRedisDistributedIDGenerateService(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 获取下一个ID,直接从redis中获取自增后的值;
*
* @return 下一个ID, 如果redis出现异常则返回null,请使用者自行处理
*/
public final Optional<Long> nextId() {
// 从redis中获取自增id
Long id = incr(getKey());
return Objects.isNull(id) ? Optional.empty() : Optional.of(id);
}
/**
* 获取下一个ID,根据传入的上下文和redis中自增后的值最终组装成下一个ID;
* 获取之后会交给子类检查此ID是否重复,如果重复会从子类中获取最新的ID,然后更新redis中的值
*
* @param context 拼装id时需要的上下文
* @return 下一个ID
*/
public final String nextId(T context) {
Optional<Long> id = nextId();
// 如果可以从redis中获取值,则说明redis服务正常,需要判重;否则直接从数据库中获取下一个id
String nextId = id.isPresent() ? ifDuplicatedThenUpdate(context, assemblyNextId(context, id.get())) :
getNewIdFromDbAndUpdateRedis(context, null);
if (log.isDebugEnabled()) {
log.debug("context={},redisIncrId={} nextId={}", JSON.toJSONString(context), id, nextId);
}
return nextId;
}
/**
* 检查获取到的ID是否重复
* 如果重复则说明由于redis的一些原因导致的重复,返回最新的redis中应该存在的值
*
* @param nextId 下一个ID
* @return 如果当前ID没有重复,则返回null,否则如果重复了则返回redis中应该有的值
*/
protected abstract boolean checkIfDuplicated(String nextId);
/**
* 从数据库获取下一个id
*
* @param duplicatedId 重复的id,此入参可能为null,子类需要自己处理
* @return 数据库获取下一个id
*/
protected abstract Long maxIdFromDatabase(String duplicatedId);
/**
* 子类根据redis当前的值自行组装最终的ID
*
* @param context 上下文
* @param redisValue redis当前的值
* @return 最终的ID
*/
protected abstract String assemblyNextId(T context, Long redisValue);
/**
* 获取redis自增的key
*
* @return redis自增的key
*/
protected abstract String getKey();
/**
* 调用redis的自增方法
* 如果key不存在则先设置key,再调用自增方法
*
* @param key 需要自增的key
* @return 自增之后的值,如果redis出现异常则返回null
*/
Long incr(String key) {
Long increment = null;
try {
// 先检查redis中是否有key,如果没有,先设置key并且设置过期时间
if (BooleanUtil.isFalse(redisTemplate.hasKey(key))) {
initOrUpdateValue(key, getKeyInitValue());
}
increment = redisTemplate.opsForValue().increment(key);
} catch (Exception e) {
log.error("调用redis的自增方法异常,error_message={}", e.getMessage(), e);
}
log.debug("key = {}, increment={}", key, increment);
return increment;
}
/**
* 获取初始化key时的value值,默认是0,自增之后id从1开始;
* 如果子类想从其他数字开始则自己覆盖此方法即可
*
* @return 初始化key时的value值
*/
protected long getKeyInitValue() {
return DEFAULT_VALUE;
}
/**
* 获取key的超时时间,单位是小时,由子类设置
*
* @return 超时时间,单位小时
*/
protected abstract long getTimeOutHours();
/**
* 判断是否重复,如果重复则从别的渠道(由子类自己决定从哪个渠道)更新
*
* @param context 拼装id时需要的上下文
* @param nextId 下一个id
* @return 如果重复则返回新的nextId,否则返回入参传入的nextId
*/
private String ifDuplicatedThenUpdate(T context, String nextId) {
// 判断是否重复,如果重复则从数据库中获取,否则直接返回当前值
return checkIfDuplicated(nextId) ? getNewIdFromDbAndUpdateRedis(context, nextId) : nextId;
}
/**
* 从数据库获取新id并更新redis中的值
*
* @param context 拼装id时需要的上下文
* @param nextId 下一个id
* @return 根据数据库的id获得的新id
*/
private String getNewIdFromDbAndUpdateRedis(T context, String nextId) {
Long maxIdFromDatabase = maxIdFromDatabase(nextId);
String newId = assemblyNextId(context, maxIdFromDatabase);
log.warn("nextId={} 在数据库中已经存在,maxIdFromDatabase={} 重新获取新的newId={}", nextId, maxIdFromDatabase, newId);
initOrUpdateValue(getKey(), maxIdFromDatabase);
return newId;
}
/**
* 初始化或者更新redis中的自增的值
*
* @param key redis中的key
* @param value 需要设置的值
*/
private void initOrUpdateValue(String key, Long value) {
try {
redisTemplate.opsForValue().set(key, String.valueOf(value), getTimeOutHours(), TimeUnit.HOURS);
} catch (Exception e) {
log.error("设置redis值异常,value={} error_message={}", value, e.getMessage(), e);
}
}
/**
* 上下文;子类自己定义上下文,然后根据上下文的数据来最终组装ID
*/
public interface Context {
}
/**
* 凭证编号上下文
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class AContext implements Context {
/**
* 业务类型
*/
private String businessType;
}
}
添加一个A服务的唯一id生成服务实现
package com.nicai.service.impl;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.yuanfeng.accounting.Constants;
import com.yuanfeng.accounting.dao.ManualVoucherDAO;
import com.yuanfeng.accounting.entity.ManualVoucherEntity;
import com.yuanfeng.accounting.exception.AccountingException;
import com.yuanfeng.accounting.service.AbstractRedisDistributedIDGenerateService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* 分布式唯一ID生成-A实现类
* 编号规则:用途+日期+自增ID,如:A202007310001;A202007310002;A202008070001;
*
* @author guozhe
* @date 2020/08/04
*/
@Slf4j
@Service
public class DistributedIDGenerateServiceAImpl extends AbstractRedisDistributedIDGenerateService<AbstractRedisDistributedIDGenerateService.AContext> {
/**
* 业务类型
*/
private static final String BUSINESS_TYPE = "A:";
/**
* ID长度不足4位时在前面填充的字符
*/
private static final char FILLED_CHAR = '0';
/**
* 最后的自增ID的长度
*/
private static final int INCREMENT_LENGTH = 4;
/**
* 过期小时数,即在24小时候过期
*/
private static final int EXPIRATION_HOURS = 24;
public DistributedIDGenerateServiceAImpl(StringRedisTemplate redisTemplate) {
super(redisTemplate);
}
@Override
protected boolean checkIfDuplicated(String nextId) {
return false;
}
@Override
protected Long maxIdFromDatabase(String duplicatedId) {
return 1L;
}
@Override
protected String assemblyNextId(VoucherNumberContext context, Long redisValue) {
return String.join(Constants.BLANK, context.getBusinessType(), getDatePeriod(),
StrUtil.fillBefore(String.valueOf(redisValue), FILLED_CHAR, INCREMENT_LENGTH));
}
@Override
protected String getKey() {
return String.join(Constants.REDIS_KEY_DELIMITER, Constants.REDIS_KEY_PREFIX, BUSINESS_TYPE, getDatePeriod());
}
@Override
protected long getTimeOutHours() {
return EXPIRATION_HOURS;
}
}