分佈式服務主鍵策略

        最近有點工作說不忙吧也忙,說忙吧也不忙,對接各種卡,你能想象一下你平常開車能開到八十邁卻因爲堵車只能慢慢挪的節奏,蛋蛋的憂傷。

  • 原因:

        十一月份進項目起就發現各種表的主鍵都TM,UUID,真是各種騷操作都有了。因爲種種原因,忍到現在趁着堵車的時間,搞了一套主鍵生成策略(PS:我以前老總監的東西,改改就拿來用了)。

  • 思路:

        因爲這套主鍵策略是用於分佈式場景業務(例如我售後訂單業務,我希望售後業務ID格式位prefix + yyyy-MM-dd + 9位數自增數值,不滿足0補位。生成結果:SH20190116000000001 ),所以我們需要考慮它的唯一性或者說分佈式鎖機制,大家都知道redis 針對操作來說是單線程的,所以這邊我利用了它的自增特性來解決了這個核心的問題。關於redis 自增存取的key值 key = tableName+yyyy-MM-dd格式,例如:我庫表有一百張表,每天生產的key 一百個,一年就是365 X 100=3.65W個key,隨着時間的遞增key會越來越多,所以建議這部分數據最好存取到指定區域(分片、hash取模)方便管理,後期也方便用來做統計報表。當然你只想做單純分佈式主鍵key = tableName就可以了。

  • 代碼片段:

一、DATE工具類

package com.honghu.cloud.utils;

import java.sql.Timestamp;
import java.text.Format;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import org.apache.commons.lang.time.DateUtils;
import org.apache.commons.lang.time.FastDateFormat;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Date Utility Class. And please use Apache common DateUtils to add, judge
 * equality, round, truncate date.
 * @Date 2019年1月14日 - 下午6:14:07
 * @Info 初始版本
 * @Version 1.0
 * @see org.apache.commons.lang.time.DateUtils
 */
public class DateUtil
{

	protected static final Log log = LogFactory.getLog(DateUtil.class);

	/**
	 * FastDateFormat cache map
	 */
	protected static Map<String, FastDateFormat> dateFormatMap = new HashMap<String, FastDateFormat>();

	/**
	 * Date format pattern
	 */
	public final static String FORMAT_DATE_DEFAULT = "yyyy-MM-dd";
	public final static String FORMAT_DATE_YYYYMMDD = "yyyyMMdd";
	public final static String FORMAT_DATE_YYYY_MM_DD = "yyyy-MM-dd";
	public final static String FORMAT_DATE_SLASH_YYYY_MM_DD = "yyyy/MM/dd";
	public final static String FORMAT_DATE_SLASH_YYYY_M_DD = "yyyy/M/dd";
	public final static String FORMAT_DATE_SLASH_YYYY_MM_D = "yyyy/MM/d";
	public final static String FORMAT_DATE_SLASH_YYYY_M_D = "yyyy/M/d";
	public final static String FORMAT_DATE_YYYYMMDDHHMMSS = "yyyyMMddHHmmss";

	/**
	 * DateTime format pattern
	 */
	public final static String FORMAT_DATETIME_DEFAULT = "yyyy-MM-dd HH:mm";
	public final static String FORMAT_DATETIME_YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss";
	public final static String FORMAT_DATETIME_YYYY_MM_DD_HH_MM_SS_SSS = "yyyy-MM-dd HH:mm:ss.SSS";
	public final static String FORMAT_DATETIME_YYYY_MM_DD_HHMM = "yyyy-MM-dd HHmm";
	public final static String FORMAT_DATETIME_YYYY_MM_DD_HH_MM = "yyyy-MM-dd HH:mm";
	public final static String FORMAT_DATETIME_YYYY_MM_DD_HHMMSS = "yyyy-MM-dd HHmmss";
	public final static String FORMAT_DATETIME_YYYYMMDDHHMMSS = "yyyyMMddHHmmss";
	public final static String FORMAT_DATETIME_EXIF = "yyyy:MM:dd HH:mm:ss";
	public final static String FORMAT_DATETIME_YYYY_MM_DDHH_MM = "yyyy-MM-ddHH:mm";
	/**
	 * Time format pattern
	 */
	public final static String FORMAT_TIME_DEFAULT = "HH:mm";
	public final static String FORMAT_TIME_HH_MM = "HH:mm";
	public final static String FORMAT_TIME_HHMM = "HHmm";
	public final static String FORMAT_TIME_HH_MM_SS = "HH:mm:ss";
	public final static String FORMAT_TIME_HHMMSS = "HHmmss";

	/**
	 * Returns FastDateFormat according to given pattern which is cached.<br/>
	 * This method is not public due to that the FastDateFormat instance may be
	 * altered by outside.
	 * 
	 * @param formatPattern date format pattern string
	 * @return Cached FastDateFormat instance which should not be altered in any
	 * way.
	 */
	protected static Format getCachedDateFormat(String formatPattern)
	{
		// Due to simpledateformat's non thread-local, return a new pattern
		// every time.
		FastDateFormat dateFormat = dateFormatMap.get(formatPattern);

		if (dateFormat == null)
		{

			dateFormat = FastDateFormat.getInstance(formatPattern);
			dateFormatMap.put(formatPattern, dateFormat);

		}

		return dateFormat;

	}

	/**
	 * 日期轉換成時間
	 * 
	 * @param date
	 * @param formatStr
	 * @return
	 */
	public static String dateToStr(Date date, String formatStr)
	{
		String str = "";
		SimpleDateFormat format = null;
		if (formatStr == null || formatStr.equals(""))
		{
			format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		}
		else
		{
			format = new SimpleDateFormat(formatStr);
		}
		try
		{
			str = format.format(date);
		}
		catch (Exception e)
		{
			// System.out.print(e.toString());
		}
		return str;
	}

	/**
	 * Format the date according to the pattern.
	 * 
	 * @param date Date to format. If it's null, the result will be null.
	 * @param formatPattern Date format pattern string. You can find common ones
	 * in DateUtils class.
	 * @return Formatted date in String
	 * @see DateUtil
	 */
	public static String formatDate(Date date, String formatPattern)
	{
		if (date == null)
		{
			return null;
		}
		return getCachedDateFormat(formatPattern).format(date);
	}

	/**
	 * Format the date in default pattern: yyyy-MM-dd.
	 * 
	 * @param date Date to format. If it's null, the result will be null.
	 * @return Formatted date in String
	 * @see DateUtil#FORMAT_DATE_DEFAULT
	 * @see DateUtil#formatDate(Date, String)
	 */
	public static String formatDate(Date date)
	{
		return formatDate(date, FORMAT_DATE_DEFAULT);
	}

	/**
	 * Format the date in default date-time pattern: yyyy-MM-dd HH:mm:ss
	 * 
	 * @param date Date to format. If it's null, the result will be null.
	 * @return Formatted date-time in String
	 * @see DateUtil#FORMAT_DATETIME_DEFAULT
	 * @see DateUtil#formatDate(Date, String)
	 */
	public static String formatDateTime(Date date)
	{
		return formatDate(date, FORMAT_DATETIME_DEFAULT);
	}

	/**
	 * Format the date in default time pattern: HH:mm:ss
	 * 
	 * @param date Date to format. If it's null, the result will be null.
	 * @return Formatted time in String
	 * @see DateUtil#FORMAT_TIME_DEFAULT
	 * @see DateUtil#formatDate(Date, String)
	 */
	public static String formatTime(Date date)
	{
		return formatDate(date, FORMAT_TIME_DEFAULT);
	}

	/**
	 * Same as javascript library's Wkz.date.formatTimeRange logic.
	 * 
	 * @param start
	 * @param end
	 * @return
	 */
	public static String formatTimeRange(Date start, Date end)
	{
		if (start == null) return null;
		if (end == null) return formatDateTime(start);
		if (DateUtils.isSameDay(start, end))
		{
			// same day
			return formatDateTime(start) + '~' + formatTime(end);
		}
		else
		{
			// not same day
			return formatDateTime(start) + '~' + formatDateTime(end);
		}
	}

	/**
	 * Returns current system date.
	 * 
	 * @return current system date.
	 * @see Date#Date()
	 */
	public static Date getCurrentDate()
	{
		return new Date();
	}

	/**
	 * Parse string value to Date by given format pattern. If parse failed, null
	 * would be returned.
	 * 
	 * @param stringValue date value as string.
	 * @param formatPattern format pattern.
	 * @return Date represents stringValue, null while parse exception occurred.
	 */
	public static Date parse(String stringValue, String formatPattern)
	{

		try
		{
			return new SimpleDateFormat(formatPattern).parse(stringValue);
		}
		catch (ParseException e)
		{
			log.warn("parse error:" + e.toString());
			return null;
		}

	}
	/**
	 * 轉換時間對象  匹配格式 yyyy-MM-dd; yyyy-MM-dd HH:mm; yyyy-MM-dd HH:mm:ss; EEE MMM dd HH:mm:ss 'CST' yyyy;
	 * @param stringValue
	 * @return
	 */
	public static Date parse(String stringValue)
	{
		Date date = null;
		SimpleDateFormat format = new SimpleDateFormat(DateUtil.FORMAT_DATE_DEFAULT);

		try
		{
			date = format.parse(stringValue);
		}
		catch (ParseException e)
		{
		}

		if (date == null)
		{
			try
			{
				format.applyPattern(FORMAT_DATETIME_YYYY_MM_DD_HH_MM_SS);
				date = format.parse(stringValue);
			}
			catch (ParseException e)
			{
			}
		}
		if (date == null)
		{
			try
			{
				format.applyPattern(FORMAT_DATETIME_YYYY_MM_DD_HH_MM);
				date = format.parse(stringValue);
			}
			catch (ParseException e)
			{
			}
		}
		
		if (date == null)
		{
			try
			{
				format = new SimpleDateFormat("EEE MMM dd HH:mm:ss 'CST' yyyy", Locale.US);
				date = format.parse(stringValue);
			}
			catch (ParseException e)
			{
			}
		}

		return date;
	}

	/**
	 * Parse string value to Date by given format pattern. If string value is
	 * null or parse failed, null would be returned.
	 */
	public static Date parseAvoidNull(String stringValue, String formatPattern) throws ParseException
	{
		if (stringValue == null || stringValue.trim().equals(""))
		{
			return null;
		}

		int index = stringValue.indexOf("-");
		if (index > 0)
		{
			String str = stringValue.substring(0, index);
			if (str.length() == 2)
			{
				stringValue = "20" + stringValue;
			}
		}

		return new SimpleDateFormat(formatPattern).parse(stringValue);
	}

	/**
	 * Parses given Date to a Timestamp instance with same date in milliseconds
	 * precision.
	 * 
	 * @param date Date to parse.
	 * @return Timestamp in milliseconds precision
	 */
	public static Timestamp parseTimestamp(Date date)
	{
		return new Timestamp(date.getTime());
	}

	/**
	 * Compares two dates based on the specified calendar field, and ignores
	 * those fields more trivial. Neither date could be null.<br/>
	 * For example, if calendarField is Calendar.Month, then result will be 0 if
	 * 2008-8-2 and 2008-8-10 is compared. But the result will be -1 if
	 * 
	 * @param date1
	 * @param date2
	 * @param calenderField
	 * @return date1 < date2 : <0<br/>
	 * date1 = date2 : 0<br/>
	 * date1 > date2 : >0
	 * @throws IllegalArgumentException If either date is null.
	 * @see Calendar
	 */
	public static int compareDateToField(final Date date1, final Date date2, int calendarField)
	{
		if (date1 == null || date2 == null)
		{
			throw new IllegalArgumentException("Neither date could be null");
		}
		Date truncatedDate1 = DateUtils.truncate(date1, calendarField);
		Date truncatedDate2 = DateUtils.truncate(date2, calendarField);
		return truncatedDate1.compareTo(truncatedDate2);

	}

	/**
	 * 獲取兩日期相差天數
	 * 
	 * @param date1
	 * @param date2
	 * @return 日期爲空 返回0 否則返回正負整數
	 */
	public static int compareDaysBetween(final Date date1, final Date date2)
	{
		if (date1 == null || date2 == null) return 0;
		Calendar calendar = Calendar.getInstance();
		calendar.setTime(date1);
		calendar.set(Calendar.HOUR, 0);
		calendar.set(Calendar.MINUTE, 0);
		calendar.set(Calendar.SECOND, 0);
		calendar.set(Calendar.MILLISECOND, 0);
		long begin = calendar.getTimeInMillis();
		calendar.setTime(date2);
		calendar.set(Calendar.HOUR, 0);
		calendar.set(Calendar.MINUTE, 0);
		calendar.set(Calendar.SECOND, 0);
		calendar.set(Calendar.MILLISECOND, 0);
		long end = calendar.getTimeInMillis();
		int days = (int) ((begin - end) / 1000 / 3600 / 24);
		return days;
	}

	/**
	 * 系統常用java日期格式轉化爲extjs對應的日期格式
	 * 
	 * @param java2FastDateFormat
	 * @return
	 */
	public static String extjsFastDateFormat(String java2FastDateFormat)
	{
		Map<String, String> extFastDateFormatMap = new HashMap<String, String>();
		extFastDateFormatMap.put(DateUtil.FORMAT_DATE_DEFAULT, "Y-m-d");
		extFastDateFormatMap.put(DateUtil.FORMAT_DATE_YYYYMMDD, "Ymd");
		extFastDateFormatMap.put(DateUtil.FORMAT_DATE_SLASH_YYYY_M_D, "Y/m/d");
		extFastDateFormatMap.put(DateUtil.FORMAT_DATETIME_DEFAULT, "Y-m-d H:i:s");
		extFastDateFormatMap.put(DateUtil.FORMAT_DATETIME_YYYYMMDDHHMMSS, "YmdHis");
		return extFastDateFormatMap.get(java2FastDateFormat);
	}

	public static Date append(Date start, Integer appendDays)
	{
		Calendar calendar = Calendar.getInstance();
		calendar.setTime(start);
		calendar.add(Calendar.DAY_OF_YEAR, appendDays);

		return calendar.getTime();
	}

	public static void main(String[] args)
	{
		Date d = DateUtil.parse("2009-01-25", FORMAT_DATE_DEFAULT);
		// System.out.println(Calendar.getInstance(Locale.GERMANY).getMinimalDaysInFirstWeek());
		// System.out.println(Calendar.getInstance(Locale.GERMANY).getFirstDayOfWeek());
		Calendar cal = Calendar.getInstance();
		// System.out.println(cal.getMinimalDaysInFirstWeek());
		cal.setTime(d);
		// System.out.println(cal.get(Calendar.WEEK_OF_YEAR));
		// System.out.println(cal.getTime());
		cal.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
		// System.out.println(cal.getTime());

		// test speed
		Date start = new Date();
		for (int i = 0; i < 500000; i++)
		{
			DateUtil.formatDate(start);
		}
		// System.out.println(new Date().getTime() - start.getTime());

		// final SimpleDateFormat df = new SimpleDateFormat("dd-MMM-yyyy",
		// Locale.ENGLISH);
		final String testdata[] = { "1999-04-21", "2001-01-05", "2007-12-30" };

		// test Thread-safe
		Runnable r[] = new Runnable[testdata.length];
		for (int i = 0; i < r.length; i++)
		{
			final int i2 = i;
			r[i] = new Runnable()
			{
				public void run()
				{

					for (int j = 0; j < 1000; j++)
					{
						String str = testdata[i2];
						String str2 = null;
						/* synchronized(df) */
						{
							Date d = DateUtil.parse(str, DateUtil.FORMAT_DATE_YYYY_MM_DD);
							str2 = DateUtil.formatDate(d, DateUtil.FORMAT_DATE_YYYY_MM_DD);
						}
						if (!str.equals(str2))
						{
							throw new RuntimeException("date conversion failed after " + j + " iterations. Expected " + str + " but got " + str2);
						}
					}

				}
			};
			new Thread(r[i]).start();
		}
	}
}

二、REDIS 工具類   

package com.honghu.cloud.common.component.redis.utils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;


/**
 * redis通用工具類
 * 
 */
@SuppressWarnings("rawtypes")
@Component
public class RedisUtil {

	/**
	 * 日誌對象
	 */
	protected Logger logger = LoggerFactory.getLogger(getClass());
	@Autowired
	private RedisTemplate redisTemplate;

	/**
	 * 獲取自增ID序號
	 * @param tableName
	 * @param growthLength
	 * @return
	 */
	public Long getSeq(String tableName, Long growthLength) {
		Long seq = null;
		try {
			seq = redisTemplate.opsForValue().increment(tableName, growthLength);
			//超過理論上限置0
			if (seq > 999999999) {
				redisTemplate.opsForValue().set(tableName, "0");
			}
		} catch (Exception e) {
			logger.error("redis is incr val Error!tableName:" + tableName);
		}
		return seq;
	}
}

三、抽象接口   

package com.honghu.cloud.sequence;

/**
 * 
 * @author YouCai.Xiong
 * @Date 2019年1月14日 - 下午6:15:01
 * @Info 初始版本 
 * @Version 1.0
 */
public interface SequenceService {


	public String getUpdateQuerySeq(String prefix, String tableName);
}
package com.honghu.cloud.sequence.impl;

import java.util.Calendar;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.honghu.cloud.common.component.redis.utils.RedisUtil;
import com.honghu.cloud.sequence.SequenceService;
import com.honghu.cloud.utils.DateUtil;
import com.honghu.cloud.utils.SequenceUtil;

/**
 * 
 * @author YouCai.Xiong
 * @Date 2019年1月14日 - 下午6:15:14
 * @Info 初始版本
 * @Version 1.0
 */
@Service
public class SequenceServiceImpl implements SequenceService {

	@Autowired
	private RedisUtil redisUtil;

	/**
	 * 生產分佈式主鍵ID
	 */
	@Override
	public String getUpdateQuerySeq(String prefix, String tableName) {
		String journalCode = null;
		try {
			Long seq = redisUtil.getSeq(tableName, 1l);
			journalCode = prefix + DateUtil.formatDate(Calendar.getInstance().getTime(), DateUtil.FORMAT_DATE_YYYYMMDD) + SequenceUtil.addLeftZero(seq + "", 9);
		} catch (Exception e) {
			throw e;
		}
		return journalCode;
	}

}

 四、測試

package com.glz.cloud.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.honghu.cloud.sequence.SequenceService;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;

/**
 * 
 * @author YouCai.Xiong
 * @Date 2018年12月20日 - 下午1:28:33
 * @Info 初始版本 測試模塊
 * @Version 1.0
 */
@Api(description = "測試模塊")
@RestController
@RequestMapping("test")
public class SysTestController{
	
	 @Autowired
	 private SequenceService sequenceService;

	@ApiOperation(value = "獲取唯一主鍵")
	@PostMapping("getUpdateQuerySeq")
	public String getUpdateQuerySeq(
			@ApiParam(name = "prefix", value = "前綴") @RequestParam(name = "prefix") String prefix,
			@ApiParam(name = "tableName", value = "表名") @RequestParam(name = "tableName")  String tableName) {
		return sequenceService.getUpdateQuerySeq(prefix,tableName);
	}
	
}

五、生成結果

  • 建議:

        關於理論數值和補0的位數問題,建議大家可以根據自己業務量的實際情況來設置。主鍵生成策略也建議大家抽成一個基礎模塊,所有tableName維護成一個常量類也放在基礎模塊裏面,這樣也方便其他模塊,簡單pom引用一下就可以了。方便快捷、即插即用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章