分库分表时一般有必要自定义生成uuid,大企业一般有自己的uuid生成服务,其他它的实现很简单。我们以订单号为例,组成可以是"业务标识号+年月日+当日自增数字格式化",如0001201608140000020。当然,如果我们用"业务标识号+用户唯一标识+当前时间"也是可以达到uuid的目的的,但用户唯一标识是敏感信息且可能不太方便处理为数字,所以弄一套uuid生成服务是很有必要的。本文就来研究下怎么实现自增数字,且性能能满足企业中的多方业务调用。起初,我想的是DB+Redis,后来想想用Redis不仅会相对降低稳定性,更是一种舍近求远的做法,所以,我最终的做法是DB+本地缓存(内存)。不说了,直接上代码。
public class UuidModel implements Serializable { private static final long serialVersionUID = 972714740313784893L; private String name; private long start; private long end; // above is DB column private long oldStart; private long oldEnd; private long now;
package com.itlong.bjxizhan.uuid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** * Created by shenhongxi on 2016/8/12. */ public class UuidContext { private static final Logger log = LoggerFactory.getLogger(UuidContext.class); // 缓存DB中的截止数 public static ConcurrentMap<String, Long> endCache = new ConcurrentHashMap<String,Long>(); // 缓存当前增加到的数值 public static ConcurrentMap<String, Long> nowCache = new ConcurrentHashMap<String,Long>(); // 缓存共享对象 public static ConcurrentMap<String, UuidModel> uuidCache = new ConcurrentHashMap<String, UuidModel>(); // 缓存配置 public static ConcurrentMap<String, Config> configCache = new ConcurrentHashMap<String, Config>(); static UuidDao uuidDao; /** * 根据名称更新号段 直至成功 * @param um * @return */ public static UuidModel updateUuid(UuidModel um, int length){ boolean updated = false; do{ UuidModel _um = uuidDao.findByName(um.getName()); int cacheSize = 1000; Config config = getConfig(um.getName()); if (config != null) { cacheSize = config.getCacheSize(); } // 判断是否需要重置 条件为:1.配置的重置数<新段的截止数 则需要重置 // 2.新段的截止数大于需要获取的位数 则需要重置 long resetNum = config.getResetNum(); // 取得新段的截止数 long newEnd = _um.getEnd() + cacheSize; um.setOldEnd(_um.getEnd()); um.setOldStart(_um.getStart()); if ((resetNum < newEnd) || (String.valueOf(newEnd).length() > length)) { // 需要重置为0开始段 um.setStart(0); um.setEnd(cacheSize); } else { // 取新段 um.setStart(_um.getEnd()); um.setEnd(_um.getEnd() + cacheSize); } // 最终的更新成功保证了多实例部署时,各实例持有的号段不同 updated = uuidDao.update(um); } while (!updated); return um; } /** * 载入内存 * @param um */ public static void loadMemory(UuidModel um){ endCache.put(um.getName(), um.getEnd()); nowCache.put(um.getName(), um.getStart()); uuidCache.put(um.getName(), um); } public static Config getConfig(String name) { Config config = configCache.get(name); if (config == null) { config = configCache.get("default"); } return config; } }
package com.itlong.bjxizhan.uuid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.text.SimpleDateFormat; import java.util.Date; /** * Created by shenhongxi on 2016/8/12. */ public class UuidServiceImpl implements UuidService { private static final Logger log = LoggerFactory.getLogger(UuidServiceImpl.class); private UuidDao uuidDao; @Override public String nextUuid(String name) { // 日期 + format(nextUuid(name, cacheSize, length)) } private synchronized long nextUuid(String name, int cacheSize, int length) { UuidModel um = UuidContext.uuidCache.get(name); Long nowUuid = null; try { if (um != null) { synchronized (um) { nowUuid = UuidContext.nowCache.get(name); Config cm = UuidContext.getConfig(name); // 判断是否到达预警值 if (UuidContext.nowCache.get(name).intValue() == cm.getWarnNum()) { log.warn("警告:" + name + "号段已达到预警值."); } log.info("dbNum:" + UuidContext.endCache.get(name) + ",nowNum:" + UuidContext.nowCache.get(name)); // 判断内存中号段是否用完 if (UuidContext.nowCache.get(name).compareTo(UuidContext.endCache.get(name)) >= 0) { // 更新号段 UuidContext.updateUuid(um, length); nowUuid = um.getStart() + 1; UuidContext.endCache.put(name, um.getEnd()); UuidContext.nowCache.put(name, nowUuid); } else { nowUuid += 1; // 是否需要重置 判断自增号位数是否大于length参数 if (String.valueOf(nowUuid).length() > length) { // 更新号段,需要重置 nowUuid = 1l; UuidContext.updateUuid(um, 0); UuidContext.endCache.put(name, um.getEnd()); UuidContext.nowCache.put(name, nowUuid); UuidContext.uuidCache.put(name, um); } else { // 直接修改缓存的值就可以了 UuidContext.nowCache.put(name, nowUuid); } } } } else { synchronized (this) { um = UuidContext.uuidCache.get(name); if (um != null) { return nextUuid(name, cacheSize, length); } nowUuid = 1l; // 如果缓存不存在,那么就新增到数据库 UuidModel um2 = new UuidModel(); um2.setName(name); um2.setStart(0); um2.setEnd(cacheSize); uuidDao.insert(um2); // 还要同时在缓存的map中加入 UuidContext.endCache.put(name, um2.getEnd()); UuidContext.nowCache.put(name, nowUuid); UuidContext.uuidCache.put(name, um2); } } } catch (Exception e) { log.error("生成uuid error", e); if (e.getMessage() != null && (e.getMessage().indexOf("UNIQUE KEY") >= 0 || e.getMessage().indexOf("PRIMARY KEY") >= 0)) { UuidModel _um = new UuidModel(); _um.setName(name); // 更新号段 UuidContext.updateUuid(_um, length); // 载入缓存 UuidContext.loadMemory(_um); // 继续获取 return nextUuid(name, cacheSize, length); } throw new RuntimeException("生成uuid error"); } return nowUuid; } }
值得一提的是,DB+本地缓存的思路同样可以用于抢购时的库存计算。