蜘蛛网抢购订单表水平拆分的研究

最近回想蜘蛛网,我负责抢购秒杀系统,那时已经遇到瓶颈,即使rabbitmq改成kafka的的消息队列,也无法突破10倍以上的秒杀速度。

原因在于单机数据库,以下是水平拆分订单表的设计。


那么一般设计水平拆分表要注意什么呢?

1、id编号的处理,保证各个DB都不重复,通过ID可以快速定位到具体在哪一个数据库里

2、id某个很小的时间段里也能在各个DB里分布均匀

3、根据另一个维度查列表,分页的处理,分页包括有序和无序等情况

4、扩容处理,就是增加一个节点DB数据库


现在有一个服务,提供订单增加,查询等功能,

订单表有:订单id,产品名称,用户id,创建时间,订单状态等属性,

查询根据订单id,用户id,创建时间等纬度,也是同样要求每秒10万条记录

一、订单号生成规则依旧是时间戳+机器码+序列号

ExecutorService pool=Executors.newFixedThreadPool(8);
	
	final AtomicInteger seq=new AtomicInteger(Integer.MIN_VALUE);//3位  0-999
	
	Map<String, String> uidMap=new ConcurrentHashMap<String, String>();
	@Test
	public void testSort() throws InterruptedException{
		long s=System.currentTimeMillis();
		//时间戳+机器码+seq  (保证同一台机器一毫秒内“seq ”不重复)
		for (int i = 1; i <= 200; i++) {
			pool.execute(new Runnable() {
				@Override
				public void run() {
					String ss=getSeq();
					if(uidMap.containsKey(ss)){
						System.out.println(ss+"异常重复"+uidMap.get(ss)+"-"+Thread.currentThread().getName());
						return;
					}else{
						uidMap.put(ss, Thread.currentThread().getName());
					}
				}
			});
		}
		
		pool.awaitTermination(3, TimeUnit.MILLISECONDS);
		System.out.println("耗时"+(System.currentTimeMillis()-s)+",size="+uidMap.size());
		for (Entry e : uidMap.entrySet()) {
			System.out.println(e.getKey());
		}
	}
	
	private String getSeq(){
		String strSeq=String.valueOf(seq.addAndGet(1)%1000);//999

		if(strSeq.length()<3){
			strSeq="000".substring(0,3-strSeq.length())+strSeq;
		}else if(strSeq.length()>3){
			strSeq=strSeq.substring(1,4);
		}
		return new Date().getTime()+"-"+"3"+"-"+strSeq;
	}

二、如何存

我们把数据切分成虚拟的1000份

机器0包含1-500

机器1包含501-1000


例如订单号是14731681192103623

机器ID=Integer.parseInt("14731681192103623".substring(11))%1000=623,那么属于机器1的。


三、如何扩容

如果现在增加1台机器,编号是2的,不过最好建议2的倍数的,数据份数好分

可以设置

机器0包含1-350

机器1包含501-850

机器2包含351-500,851-1000


配置好后,机器2开始剪切机器0的351-500和机器1的851-1000的数据。

在机器2没有完全复制机器0,1的数据时,如果存在一条取余是360的数据,那么存放到新的机器2中,读是同时判断机器0和机器2是否包含,直到机器2完全复制结束。


四、查询

查询根据订单id,用户id,创建时间等纬度,支持分页

根据订单号这个不说了,直接hash找到

用户id和创建时间

这个需求解决方法有2种,

第一种是我推荐参考阿里巴巴的一个技术总监写的书描述的,查询每一个库的数据然后拼凑列表。


这里再区分  有排序 和   没排序  情况

例如需求:查询用户id=18的,创建时间在今日的订单列表的第二页数据,每一页是10个。那么怎么做?

(1)无需排序情况

横向取分页好处是大部分无需分别连接多个DB,见下图


@Test
	public void testDistPage() throws Exception {
		int[] distDataSize={4,8,9,1,7};//每个数据库记录个数
		
		int pageNum=5;//取第几页
		int pageSize=4;//每页个数
		//需要计算的,每个db的开始位置和取的数量
		DistPage[] dp=new DistPage[distDataSize.length];
		
		//如果直到取pageNum时,每个DB能独立给出页,那么不用计算
		int minCanPageNum=Integer.MAX_VALUE;
 		for (int dbsize : distDataSize) {
			int tmp=dbsize/pageSize;
  			if(tmp<minCanPageNum){
				minCanPageNum=tmp;
			}
		}
		//即每个DB能提供minCanPageNum个页
 		if(minCanPageNum*distDataSize.length>=pageNum){
			int dbi=(pageNum-1)%distDataSize.length;//选中第几个DB
			int pcount=(pageNum-1)/distDataSize.length;//跳过的页
			
			System.out.println("快速db-"+dbi+"查询第"+(pcount*pageSize+1)+"-"+(pageSize+(pcount*pageSize+1)-1));
			return;
		}else{
			if(minCanPageNum>0){
				//初始化dp
 				for (int j = 0; j < dp.length; j++) {
					dp[j]=new DistPage();
					dp[j].distDBID=j;
					dp[j].startIndex=pageSize*(minCanPageNum-1);
 					dp[j].size=pageSize;
					dp[j].remain=distDataSize[j]-pageSize*minCanPageNum;
					dp[j].isNeed=false;
				}
			}else{
				//初始化dp
				for (int j = 0; j < dp.length; j++) {
					dp[j]=new DistPage();
					dp[j].distDBID=j;
					dp[j].startIndex=0;
					dp[j].size=0;
					dp[j].remain=distDataSize[j];
					dp[j].isNeed=false;
				}
			}
		}
		//开始计算部分DB带有提供不足,其他DB补的情况下
		for (int i = minCanPageNum*distDataSize.length+1; i <=pageNum; i++) {
			for (int j = 0; j < dp.length; j++) {
				if(dp[j]!=null){
					dp[j].isNeed=false;
				}
			}
			//当前的头
			int head=(i-1)%distDataSize.length;
			
			//如果当前取整页不足,则下一个去补,直到一个循环
			if(dp[head].remain<pageSize){
				dp[head].startIndex+=dp[head].size;
				dp[head].size=dp[head].remain;
				dp[head].remain=0;
				dp[head].isNeed=true;
				//库存没就不需要此节点提供数据
				if(dp[head].size<=0){
					dp[head].isNeed=false;
				}
				
				int findNext=head;
				int needGet=pageSize-dp[head].size;
				do{
					findNext=(findNext+1)%distDataSize.length;
					if(dp[findNext].remain<=0)continue;//无法提供数据
					
					if(dp[findNext].remain<needGet){
						dp[findNext].startIndex+=dp[findNext].size;
						dp[findNext].size=dp[findNext].remain;
						dp[findNext].remain=0;
						dp[findNext].isNeed=true;
						needGet-=dp[findNext].size;
					}else{
						dp[findNext].startIndex+=dp[findNext].size;
						dp[findNext].size=needGet;
						dp[findNext].remain-=needGet;
						dp[findNext].isNeed=true;
						break;
					}
				}while(head!=findNext);
			}else{
				dp[head].startIndex+=dp[head].size;//加上一个的size得出当前start
				dp[head].size=pageSize;
				dp[head].remain-=pageSize;
				dp[head].isNeed=true;
			}
			
			for (DistPage distPage : dp) {
				if(distPage!=null&&distPage.isNeed)
					System.out.println("查询第  "+i+"页的db-"+distPage.distDBID+"查询第"+distPage.startIndex+"-"+(distPage.size+distPage.startIndex-1));
			}
			System.out.println();
		}
		
		for (DistPage distPage : dp) {
			if(distPage!=null&&distPage.isNeed)
				System.out.println("db-"+distPage.distDBID+"查询第"+distPage.startIndex+"-"+(distPage.size+distPage.startIndex-1));
		}
	}
	
	class DistPage{
		public int distDBID;
		public int startIndex;
		public int size;
		public int remain;
		public boolean isNeed;
	}

(2)有排序情况

例如每页10个,取第11-20条记录,那么每个DB取1-20条,然后组合一起排序,取11-20条记录即可

这里排序可能有多个,  比如按这个排序 id,name desc,orderTime

下边是排序简单处理,逻辑是id相同,判断name,name相同判断orderTime

/**
     * @param list
     * @param ors 
     * added by cruze([email protected]) at 2014-9-24
     */
	private void sort(List<Map<String, Object>> list, String[] ors) {
		// TODO Auto-generated method stub
		for (int i = 0; i < list.size() - 1; i++) {
			for (int j = i + 1; j < list.size(); j++) {
				Map<String, Object> a = list.get(i);
				Map<String, Object> b = list.get(j);
				if (isChange(a, b, ors, 0)) {
					list.set(i, b);
					list.set(j, a);
				}
			}
		}
	}
    /**
     * 指示是否换,递归
     * @param a
     * @param b
     * @param ors
     * @return 
     * added by cruze([email protected]) at 2014-9-24
     * @throws SecurityException 
     * @throws NoSuchMethodException 
     * @throws InvocationTargetException 
     * @throws IllegalArgumentException 
     * @throws IllegalAccessException 
     */
	boolean isChange(Map<String, Object> a, Map<String, Object> b,
			String[] ors, int i) {
		if (i > ors.length - 1)
			return false;
		String o1 = ors[i];
		boolean isDesc = false;
		if (o1.toLowerCase().contains(" desc")) {
			isDesc = true;
		}

		String key = o1.replace(" desc", "").trim().toUpperCase();

		Object aV = a.get(key);
		Object bV = b.get(key);

		int res=0;
		try {
			Method method=aV.getClass().getMethod("compareTo", aV.getClass());
			res = (Integer)method.invoke(aV, bV);
		} catch (Exception e) {
			return false;
		}
		if (res < 0) {
			return isDesc;
		} else if (res == 0) {
			return isChange(a, b, ors, ++i);
		} else {
			return !isDesc;
		}
	}


第二种再创建一个纬度,如果频繁操作的话,但要注意数据同步。


2016-9-20

实践操作下发现问题了,因为订单号最后是seq,所以分布非常不均匀,导致一段时候压力都在同一个数据库上

由于订单号是对1000取余的,所以设计倒数第三位是随机数,然后插在时间中间,把strSeq减少一位。保证订单号长度不变

private String getSeq(String machineId){
		String strSeq=String.valueOf(Math.abs(seq.addAndGet(1))%100);//0-99

		if(strSeq.length()<2){
			strSeq="00".substring(0,2-strSeq.length())+strSeq;
		}
		String time=String.valueOf(System.currentTimeMillis());
		
		return machineId+"-"+strSeq+"-"+time.substring(0,time.length()-2)+"-"+new Random().nextInt(10)+"-"+time.substring(time.length()-2);
	}

结果如下:

   


可以看出来分布比较均匀了。

https://github.com/penkee/fruitstall

猜你喜欢

转载自blog.csdn.net/penkee/article/details/52451503
今日推荐