一.前言:在分布式高并发环境下,有些时候我们需要生成一些包含业务逻辑性的唯一标识,例如订单编号:可能由业务字符 +当前时间+唯一字符串 : ZFB:(支付宝)+20200505121212+000001,这个时候就我们需要保证在高并发情况下后六位数字不重复。
二. 下面我列举几种高并发模式下生成唯一标识的方式:
1.利用全球唯一的UUID生成唯一标识。优势是本地生成,不占用宽带,但是id字符串是随机的。不能满足业务需求。
2.基于数据库自增生成唯一标识,利用数据库的自增属性,多台服务器同时访问一个数据库,可以实现。每次生成唯一标识 都需要访问数据库,占用宽带。(不建议使用)
3.基于redis生成全局唯一标识。(没有用过,不做评价)
4.Twitter的Snowflake雪花算法生成唯一标识。(没有用过,不做评价,据说还是可以的,阿里使用的)
5.号段法生成全局唯一标识。美团使用)号段可以理解成批量获取。比如从数据库获取 ID 时,就可以批量获取多个 ID 并缓存在本地,提升效率。批量获取多个id可以有效减少数据库的访问次数,同时多台服务获取的号段不同可以保证唯一,将号段缓存起来,可以保证访问速度。
三.下面介绍号段法生成id。
CREATE TABLE `t_ecommerce_id` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`type` varchar(255) NOT NULL COMMENT '用于不同的业务id生成',
`max_id` bigint(20) NOT NULL COMMENT '当前最大Id',
`step` int(10) NOT NULL COMMENT '号段的长度',
`version` int(10) NOT NULL COMMENT '版本',
`create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
1.我们通过type可以控制不同的的业务需求生成对应的唯一标识。
2.max_id 保存最新的号段,每次服务过来获取号段时,都需要更新max_id。
3.step 为每次获取的号段长度,我这里是10000。
4.version 为版本号,使用乐观锁保证每次获取号段时,为max_id没有被其它服务修改。
总体思想是假如 我们有三台服务器,第一台获取0-10000个号段放入本地缓存中,数据库max_id 更新为10000,第二台查询号段时,发现max_id为10000,只能获取10000-20000之间的号段,并更新max_id为20000,第三台查询到max_id为20000,获取20000-30000之间的号段并更新max_id为30000,当号段用完时,重新获取号段,使用版本version控制产生脏读对数据库的影响。
为了使业务逻辑与id的获取分开,现在我们需要做到如下几点:
1. 业务系统获取 id 时只能从缓存中获取,并且缓存已经存在,保证效率。
2.更新缓存中的号段使用异步的方式。
3.当号段达到最大值时,从0开始获取号段。
首先实现第一步:业务系统获取 id 时只能从缓存中获取,并且缓存已经存在,保证效率。
由于我使用的项目是springboot的项目 所有我将id缓存类交给spring管理,作为缓存,因为spring的对象是单例的,如果不想交给spring管理可以使用单例模式,缓存号段。
1.号段实体类:GenerateId.java 这里使用lombok
import java.util.Date;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "t_ecommerce_id")
public class GenerateId{
@GeneratedValue(strategy=GenerationType.IDENTITY)
@Id
private Integer id;
private String type;
private int maxId;
private int step;
private int version;
private Date createTime;
private Date updateTime;
}
号段生成类,也可以认为是缓存号段的类。GenerateIdManager.java
这里我使用了三个缓存map,为了逻辑更清晰一点,其实可以用一个号段实体类的map实现,这样需要在号段实体类中加个当前id字段,需要加个注解不参与数据库映射,然后写个当前id自增的方法,每次服务获取id完成后都调用一下这个方法就完成自增了,总的说写代码的方法千千万,想怎么实现自己去写就行了
需要注意这一行代码:UpdateGenerateIdThreadPool.insertMsg(type); 当当前id等于最大值的百分之七十的时候,就实现异步重新获取号段了。这样会造成号段丢失,但是想到如果服务停掉,也会造成号段丢失,就暂时没有处理。
,美团使用的是双buffer模式,就是使用两个缓存,大致思想就是号段没有用完时但达到了某个值去加载新的号段放入另一个缓存中,当号段用完时将另一个缓存中的号段加载进来。使用双buffer模式需要两个实体map
package com.newland.common.idgenerate;
import java.util.Date;
import java.util.concurrent.ConcurrentHashMap;
import com.newland.common.idgenerate.thread.pool.UpdateGenerateIdThreadPool;
import com.newland.common.utils.UniqueNumberGenerator;
import com.newland.ecommerce.model.entity.GenerateId;
import com.newland.ecommerce.service.GenerateIdService;
public class GenerateIdManager {
/**
* 通过构造函数启动项目时加载号段
* @param generateIdService
*/
public GenerateIdManager(GenerateIdService generateIdService) {
String[] arr = UniqueNumberGenerator.IDTYPE; //保存需要初始化的号段类型
for(String type : arr) {
GenerateId gd = generateIdService.queryByType(type);
int i = 0;
if(gd == null) { //新的号段类型 需要保存到数据库
gd = new GenerateId(null,type,0,10000,0,new Date(),new Date());
gd = generateIdService.save(gd);
i = generateIdService.updateMaxId(type, gd.getVersion());
}else { //已有的号段类型 需要更新数据库
//启动时超过最大号段值 号段归零
if(gd.getMaxId() > 920000) {
gd.setMaxId(0);
gd.setVersion(0);
generateIdService.save(gd);
}
i = generateIdService.updateMaxId(type, gd.getVersion());
//更新不成功 线程睡眠3s后继续执行
while(i != 1) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
gd = generateIdService.queryByType(type);
i = generateIdService.updateMaxId(type, gd.getVersion());
}
}
//将号段缓存到本地服务
generateId.put(type, gd);
nowId.put(type,gd.getMaxId());
maxId.put(type, gd.getMaxId() + gd.getStep() );
}
}
/**
* 保存当前号段的值,每次服务获取唯一标识需要自增
*/
private static ConcurrentHashMap<String,Integer> nowId = new ConcurrentHashMap<String,Integer>();
public ConcurrentHashMap<String,Integer> getNowId(){
return nowId;
}
/**
*保存当前获取的最大号段值
*/
private static ConcurrentHashMap<String,Integer> maxId = new ConcurrentHashMap<String,Integer>();
public ConcurrentHashMap<String,Integer> getMaxId(){
return maxId;
}
/**
* 保存当前获取的号段对象
*/
private static ConcurrentHashMap<String,GenerateId> generateId = new ConcurrentHashMap<String,GenerateId>();
public ConcurrentHashMap<String,GenerateId> getGenerateId(){
return generateId;
}
/**
* 用户通过此方法获取唯一标识
* @param type
* @return
*/
public static String getId(String type) {
GenerateId gd = generateId.get(type);
Integer now = nowId.get(type);
now ++;
nowId.put(type,now);
if(now == (gd.getMaxId()+gd.getStep()*0.7)) {
UpdateGenerateIdThreadPool.insertMsg(type);
}
String str = String.format("%06d", now);
return str;
}
}
由于我使用的是springboot,将缓存类交给spring初始化,需要添加此类IdAutoConfiguration.java
这里需要依赖注入GenerateIdService 完成查询号段,与修改号段的操作。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.newland.common.idgenerate.GenerateIdManager;
import com.newland.ecommerce.service.GenerateIdService;
@Configuration
public class IdAutoConfiguration {
@Bean
public GenerateIdManager GenerateIdManager(GenerateIdService generateIdService) {
GenerateIdManager generateIdManager = new GenerateIdManager(generateIdService );
return generateIdManager;
}
}
GenerateIdService接口提供三个个方法保存 查询与修改,因为我使用的jpa这里我就不贴出来具体的内容了,把关键点贴出来,具体的Service自己去实现
@Repository
@Transactional
public interface GenerateIdRepository extends JpaRepository<GenerateId, Integer> {
GenerateId findByType(String type);
@Modifying
@Query(nativeQuery = true,value = "UPDATE t_ecommerce_id SET max_id = max_id + step,version = version + 1 " +
" WHERE type = :type AND version = :version")
Integer updateMaxId(@Param("type")String type, @Param("version")Integer version);
}
到这里号段法的初始化加载,与获取id的方法都实现了,现在需要完成异步更新号段,与到达某个最大值时重新归0
这里我使用的是自定义线程工厂实现的,也可以自己开启一个线程实现,这里我就不把所有的类贴出来了,贴出两个主要的类,如果要源码可以留邮箱。
UpdateGenerateIdThreadPool.java 类
import com.newland.common.idgenerate.thread.ThreadPool;
import com.newland.common.idgenerate.thread.ThreadPoolFactory;
import com.newland.common.idgenerate.thread.thread.UpdateGenerateIdThread;
public class UpdateGenerateIdThreadPool {
//, PublishUtil.getQueueSize() QUEUE_SIZE=1000000
private static ThreadPool exec = ThreadPoolFactory.createSignleThreadPool(UpdateGenerateIdThreadPool.class);
public static synchronized void insertMsg(String type) {
UpdateGenerateIdThread t1 = new UpdateGenerateIdThread(type);
exec.execute(t1);
}
}
UpdateGenerateIdThread.java 类,当大于最大峰值是置零,更新缓存
import java.util.Date;
import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import com.newland.common.idgenerate.GenerateIdManager;
import com.newland.common.wrapper.SpringContextHolder;
import com.newland.ecommerce.model.entity.GenerateId;
import com.newland.ecommerce.service.impl.GenerateIdServiceImpl;
public class UpdateGenerateIdThread extends Thread {
private static Logger log = LoggerFactory.getLogger(UpdateGenerateIdThread.class);
private String type = null;
GenerateIdServiceImpl generateIdServiceImpl = SpringContextHolder.getBean(GenerateIdServiceImpl.class);
GenerateIdManager generateIdManager = SpringContextHolder.getBean(GenerateIdManager.class);
public UpdateGenerateIdThread(String type) {
this.type = type;
}
@Override
public void run() {
ConcurrentHashMap<String,Integer> nowId = generateIdManager.getNowId();
ConcurrentHashMap<String,Integer> maxId = generateIdManager.getMaxId();
ConcurrentHashMap<String,GenerateId> generateId = generateIdManager.getGenerateId();
//设置峰值
if(maxId.get(type) > 920000) {
GenerateId initId = generateId.get(type);
initId.setMaxId(0);
initId.setVersion(0);
generateIdServiceImpl.save(initId);
}
GenerateId gd = generateIdServiceImpl.queryByType(type);
int i = generateIdServiceImpl.updateMaxId(type, gd.getVersion());
//更新不成功 线程睡眠3s后继续执行
while(i != 1) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
gd = generateIdServiceImpl.queryByType(type);
i = generateIdServiceImpl.updateMaxId(type, gd.getVersion());
}
generateId.put(type, gd);
nowId.put(type,gd.getMaxId());
maxId.put(type, gd.getMaxId() + gd.getStep());
}
}
在这个类里组合订单id,同时添加type类型 类型+当前时间+6位自增数字
public class UniqueNumberGenerator {
public static final String PREFIX_BALANCE_CODE = "BL";
public static final String PREFIX_SYSTEM_CODE = "SY";
public static final String PREFIX_USER_CODE = "UC";
public static final String PREFIX_SUPPLIER_CODE = "SC";
public static final String PREFIX_CONSUMER_CODE = "CSC";
public static final String PREFIX_CHANNEL_CODE = "CN";
public static final String PREFIX_STORE_CODE = "ST";
public static final String PREFIX_SKU_CODE = "SK";
public static final String PREFIX_SPU_CODE = "SP";
public static final String PREFIX_MODSPU_CODE = "MSP";
public static final String PREFIX_AREA_CODE = "AR";
public static final String PREFIX_AFTER_SALE_CODE = "AF";
public static final String DEFAULT_PATTERN="yyyyMMddHHmmss";
/**
* 初始化id
*/
public static final String[] IDTYPE = {PREFIX_BALANCE_CODE,PREFIX_SUPPLIER_CODE,PREFIX_AFTER_SALE_CODE};
public static String format(Date date){
return format(date,DEFAULT_PATTERN);
}
public static String format(Date date,String pattern){
DateFormat dateFormat=new SimpleDateFormat(pattern);
return dateFormat.format(date);
}
/**
* 通过type生成相应的id
* @param type
* @return
*/
public static String getId(String type){
String currentTime= format(new Date(),DEFAULT_PATTERN);
String randomNumber = GenerateIdManager.getId(type);
return type +currentTime+randomNumber;
}
}
测试:
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.newland.common.utils.UniqueNumberGenerator;
import com.newland.ecommerce.model.entity.GenerateId;
@RestController
@RequestMapping("/api")
public class TestController {
@PostMapping("/getId")
public String getId(@RequestBody GenerateId generateId){
String id = UniqueNumberGenerator.getId(generateId.getType());
return id;
}
}
postman 访问结果: