【原创】SSM框架(5):使用一致性hash算法实现动态设置数据源 和 主从读写分离

 一、【转载】一致性哈希算法的原理

  一致性哈希算法,是分布式系统中常用的算法,比如有N台缓存服务器,你需要将数据缓存到这N台服务器上。一致性哈希算法可以将数据尽可能平均的存储到N台缓存服务器上,提高系统的负载均衡,并且当有缓存服务器加入或退出集群时,尽可能少的影响现有缓存服务器的命中率,减少数据对后台服务的大量冲击。 

   一致性哈希算法的基本原理,把数据通过hash函数映射到一个很大的环形空间里,如下图所示:

  A、B、C、D 4台缓存服务器通过hash函数映射环形空间上,数据的存储时,先得到一个hash值,对应到这个环中的每个位置,如缓存数据:K1对应到了图中所示的位置,然后沿顺时针找到一个机器节点A,将K1存储到A节点上,K2存储到A节点上,K3、K4存储到B节点上。

   如果B节点宕机了,则B上的数据就会落到C节点上,如下图所示:

  这样,只会影响C节点,对其他的节点A,D的数据不会造成影响。然而,这又会造成一个“雪崩”的情况,即C节点由于承担了B节点的数据,所以C节点的负载会变高,C节点很容易也宕机,这样依次下去,这样造成整个集群都挂了。

   为此,引入了“虚拟节点”的概念:即把想象在这个环上有很多“虚拟节点”,数据的存储是沿着环的顺时针方向找一个虚拟节点,每个虚拟节点都会关联到一个真实节点,如下图所使用:

  引入“虚拟节点”后,映射关系就从 {对象 ->节点 }转换到了 {对象 ->虚拟节点 }。图中的A1、A2、B1、B2、C1、C2、D1、D2都是虚拟节点,机器A负载存储A1、A2的数据,机器B负载存储B1、B2的数据,机器C负载存储C1、C2的数据。由于这些虚拟节点数量很多,均匀分布,提高了平衡性,因此不会造成“雪崩”现象。

            一致性哈希算法的Java实现(以上内容转载自本链接资源)

            漫画:什么是一致性哈希?

            详解一致性hash算法、hash环

二、项目实战

1、故事描述:

  数据库集群中,有三台主库master1、master2、master3和三台从库slave1、slave2、slave3(主从备份关系)。

  现有一业务需求,要求将客户信息Customr(idcardNo:身份证号,name:姓名)保存到数据库中,由于客户数据量特别大,所以要采用分库进行存储。分库的逻辑是:使用客户的身份证号,进行一致性hash运算后,分别存储在master1/2/3中的其中一台;同时要求,为提高服务器查询性能,要进行主从读写分离处理。

2、实现步骤: 

  1. 在本地创建六个主从数据库,采用触发器模拟实现主从数据同步功能
  2. 修改数据库配置文件,增加以上六个数据源节点,增加动态数据源路由配置
  3. 实现一致性hash算法工具类:HashFunction、ConsistentHash<T>
  4. 重写DynamicDataSourceHolder、DynamicDataSource两个类,使用一致性hash算法,动态计算要查询的目标数据库,实现动态数据源切换:
  5. 编写Service层的业务代码
  6. 编写Controller和客户端代码
  7. 运行代码,测试结果

3、实现详情:

步骤一:在本地创建六个主从数据库,采用触发器模拟实现主从数据同步功能

  (1)新建数据库:nb_master_01、nb_master_02、nb_master_03、nb_slave_01、nb_slave_02、nb_slave_03

  (2)在六个数据库中,新建客户信息表customer

# 人员信息表
DROP TABLE IF EXISTS customer;
CREATE TABLE customer(
   id varchar(25) not null comment '主键id',
    idcard_no VARCHAR(18) not null comment '身份证号',
    name VARCHAR(50)  comment '姓名',
    sex CHAR(2) comment '性别',
   remark varchar(50) default '主库-master-01' comment '信息备注',
);

  (3)在三台主库中,分别创建“insert、update、delete”操作对应的触发器

    触发器增加成功后,当主库通过insert/update/delete语句进行操作时,操作结果会同步更新到从库之中,从而实现从库自动同步主库数据的功能。

################################ 增加数据同步触发器
#######注意根据主从对应关系,修改sql中的主库和从库名称
use nb_master_01;
create TRIGGER tr_insert_customer after INSERT on customer for EACH ROW BEGIN -- 向从库nb_slave_01中的customer表,插入一条数据 insert into nb_slave_01.customer (id,idcard_no,name,sex) values (new.id,new.idcard_no,new.name,new.sex); END; use nb_master_01; create TRIGGER tr_update_customer after UPDATE on customer for EACH ROW BEGIN -- 向从库中同步数据 update nb_slave_01.customer set id=new.id,idcard_no=new.idcard_no,name=new.name,sex=new.sex where id=new.id; END; use nb_master_01; create TRIGGER tr_delete_customer before DELETE on customer for EACH ROW BEGIN -- 向从库中同步数据 delete from nb_slave_01.customer where id=old.id; END;

步骤二:修改数据库配置文件,增加以上六个数据库节点

  (1)修改db.properties,增加数据库的连接地址

#配置主从数据库的链接地址
#主库1
master1.jdbc.url=jdbc:mysql://localhost:3306/nb_master_01?useUnicode=true&characterEncoding=utf8
#主库
master2.jdbc.url=jdbc:mysql://localhost:3306/nb_master_02?useUnicode=true&characterEncoding=utf8
#主库
master3.jdbc.url=jdbc:mysql://localhost:3306/nb_master_03?useUnicode=true&characterEncoding=utf8

#从库1
slave1.jdbc.url=jdbc:mysql://localhost:3306/nb_slave_01?useUnicode=true&characterEncoding=utf8
#从库2
slave2.jdbc.url=jdbc:mysql://localhost:3306/nb_slave_02?useUnicode=true&characterEncoding=utf8
#从库
slave3.jdbc.url=jdbc:mysql://localhost:3306/nb_slave_03?useUnicode=true&characterEncoding=utf8

    (2)在spring-mybatis.xml配置文件中,增加以上六个数据库的bean

    <!--读取静态配置文件,获取相关数据库连接参数 -->
    <context:property-placeholder location="classpath:db.properties"/>
    <!-- 配置数据源 -->
    <bean id="abstractDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
          abstract="true" destroy-method="close">
        <property name="driverClass" value="${jdbc.driver}"/>
        <property name="user" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
        <property name="maxPoolSize" value="${c3p0.maxPoolSize}"/>
        <property name="minPoolSize" value="${c3p0.minPoolSize}"/>
        <property name="autoCommitOnClose" value="${c3p0.autoCommitOnClose}"/>
        <property name="checkoutTimeout" value="${c3p0.checkoutTimeout}"/>
        <property name="acquireRetryAttempts" value="${c3p0.acquireRetryAttempts}"/>
    </bean>
   <!-- 配置数据源:主库 --> <bean id="master1" parent="abstractDataSource"> <property name="jdbcUrl" value="${master1.jdbc.url}"/> </bean> <!-- 配置数据源:主库 --> <bean id="master2" parent="abstractDataSource"> <property name="jdbcUrl" value="${master2.jdbc.url}"/> </bean> <!-- 配置数据源:主库 --> <bean id="master3" parent="abstractDataSource"> <property name="jdbcUrl" value="${master3.jdbc.url}"/> </bean> <!-- 配置数据源:从库1 --> <bean id="slave1" parent="abstractDataSource"> <property name="jdbcUrl" value="${slave1.jdbc.url}"/> </bean> <!-- 配置数据源:从库2 --> <bean id="slave2" parent="abstractDataSource"> <property name="jdbcUrl" value="${slave2.jdbc.url}"/> </bean> <!-- 配置数据源:从库3 --> <bean id="slave3" parent="abstractDataSource"> <property name="jdbcUrl" value="${slave3.jdbc.url}"/> </bean>
<!-- 动态数据源配置,这个class要完成实例化 --> <bean id="dynamicDataSource" class="com.newbie.util.dbMultipleRouting.DynamicDataSource"> <property name="targetDataSources"> <!-- 指定lookupKey和与之对应的数据源,切换时使用的为key --> <map key-type="java.lang.String"> <entry key="master1" value-ref="master1"/> <entry key="master2" value-ref="master2"/> <entry key="master3" value-ref="master3"/> <entry key="slave1" value-ref="slave1"/> <entry key="slave2" value-ref="slave2"/> <entry key="slave3" value-ref="slave3"/> </map> </property> <!-- 这里可以指定默认的数据源 --> <property name="defaultTargetDataSource" ref="master1"/> </bean> <!-- 配置MyBatis创建数据库连接的工厂类 --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <!--数据源指定为:动态数据源DynamicDataSource --> <property name="dataSource" ref="dynamicDataSource"/> <!-- mapper配置文件 --> <property name="mapperLocations" value="classpath:com/newbie/dao/*.xml"/> </bean> <!-- 配置自动扫描DAO接口包,动态实现DAO接口实例,注入到Spring容器中进行管理 --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <!-- 注入SqlSession工厂对象:SqlSessionFactoryBean --> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> <!-- 指定要扫描的DAO接口所在包 --> <property name="basePackage" value="com.newbie.dao"/> </bean>

步骤三:实现一致性hash算法工具类:HashFunction、ConsistentHash<T>

  (1)新建工具类HashFunction,使用MurMurHash算法,实现key的hash值计算。

    一致性hash算法要计算数据库节点的hash值、客户身份证号的hash值,统一使用 HashFunction.hash(key)方法,来计算hash值。

           Murmurhash介绍与实现

           一致性哈希算法的Java实现(推荐阅读)  

package com.newbie.util.consistentHash;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
 * 使用MurMurHash算法,实现key的hash值计算
 * MurmurHash是一种非加密型哈希函数,性能很高,碰撞率低,适用于一般的哈希检索操作。
 */
public class HashFunction {
    public Long hash(String key) {
        ByteBuffer buf = ByteBuffer.wrap(key.getBytes());
        int seed = 0x1234ABCD;

        ByteOrder byteOrder = buf.order();
        buf.order(ByteOrder.LITTLE_ENDIAN);

        long m = 0xc6a4a7935bd1e995L;
        int r = 47;

        long h = seed ^ (buf.remaining() * m);

        long k;
        while (buf.remaining() >= 8) {
            k = buf.getLong();

            k *= m;
            k ^= k >>> r;
            k *= m;

            h ^= k;
            h *= m;
        }

        if (buf.remaining() > 0) {
            ByteBuffer finish = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN);
            finish.put(buf).rewind();
            h ^= finish.getLong();
            h *= m;
        }

        h ^= h >>> r;
        h *= m;
        h ^= h >>> r;

        buf.order(byteOrder);
        return h;
    }
}

  (2)新建一致性hash算法工具类:ConsistentHash

     a. 定义构造方法,接收数据库节点对象,然后根据节点对象计算生成数据存储的节点;

     b. 定义“public T get(String key)”方法,接收客户的身份证号,计算得出该客户信息存储的目标数据节点。

package com.newbie.util.consistentHash;

import java.util.Collection;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* 实现一致性hash计算:实现保存数据库节点方法、实现根据身份证号key来查找存储的节点方法
*
@param <T> : 数据库节点对象(此处使用数据源对应的key:master1、master2、master3) */ public class ConsistentHash<T> { //hash函数接口,调用该接口的hash(key)方法,计算hash值 private final HashFunction hashFunction; //每个机器节点,关联的虚拟节点个数 private final int numberOfReplicas; //环形虚拟节点 private final SortedMap<Long, T> circle = new TreeMap<Long, T>(); /** * @param hashFunction:hash 函数接口 * @param numberOfReplicas;每个机器节点关联的虚拟节点个数 * @param nodes: 真实机器节点 */ public ConsistentHash(HashFunction hashFunction, int numberOfReplicas, Collection<T> nodes) { this.hashFunction = hashFunction; this.numberOfReplicas = numberOfReplicas; //遍历真实节点,生成对应的虚拟节点 for (T node : nodes) { add(node); } } /** * 增加真实机器节点 * 由真实节点,计算生成虚拟节点 * @param node */ public void add(T node) { for (int i = 0; i < this.numberOfReplicas; i++) { long hashcode = this.hashFunction.hash(node.toString() + "-" + i); circle.put(hashcode, node); } } /** * 删除真实机器节点 * * @param node */ public void remove(T node) { for (int i = 0; i < this.numberOfReplicas; i++) { long hashcode = this.hashFunction.hash(node.toString() + "-" + i); circle.remove(hashcode); } } /** * * 根据数据的key,计算hash值,然后从虚拟节点中,查询取得真实机器节点对象 * @param key * @return */ public T get(String key) { if (circle.isEmpty()) { return null; } long hash = hashFunction.hash(key); if (!circle.containsKey(hash)) { SortedMap<Long, T> tailMap = circle.tailMap(hash);// 沿环的顺时针找到一个虚拟节点 hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey(); } return circle.get(hash); // 返回该虚拟节点对应的真实机器节点的信息 } }

步骤四:使用一致性hash算法,动态计算要查询的目标数据库,实现动态数据源切换

(1)重写DynamicDataSourceHolder类的实现,实现以下功能:

  • 属性 -- ThreadLocal<String>  DATA_SOURCE_KEY :保存当前主从数据源标识-master/slave,由Service层的方法设置该属性,指定要操作的是主库,还是从库。
  • 属性 -- ThreadLocal<String> CONSISTENT_HASH_KEY :保存要增加(查询)的客户身份证号,用于计算hash值,由Service层的方法设置该属性。
  • 属性 -- ArrayList<String> dataSouceKeyList : 保存所有主库数据源对应的key值集合[master1、master2、master3],用于一致性hash算法中计算生成主机存储节点。在Spring容器实例化DynamicDataSource对象时,通过重写的afterPropertiesSet()方法,主动给本类的dataSouceKeyList、consistentHashDB进行赋值和实例化。
  • 属性 -- ConsistentHash<String> consistentHashDB :一致性hash算法工具对象,用于根据身份证号,计算获取数据的存储节点,获取目标数据源的key。由DynamicDataSource对象的afterPropertiesSet()方法,进行实例化。
  • 方法 -- public static String getDataSourceFinalKey():根据客户的身份证号【CONSISTENT_HASH_KEY】,计算数据存储的主库数据源key;再根据【DATA_SOURCE_KEY】属性的值,判断要操作的是主库还是从库,是否应该修改为从库的key,进行主从读写分离,最终实现数据源的动态切换。
package com.newbie.util.dbMultipleRouting;

import com.newbie.util.consistentHash.ConsistentHash;
import com.newbie.util.consistentHash.HashFunction;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 自定义类:用来保存数据源标识,从而在实例化DynamicDataSource时来指定要使用的数据源实例
 */
public class DynamicDataSourceHolder {
    /**
     * 注意:数据源标识保存在线程变量中,避免多线程操作数据源时互相干扰
     */
    private static final ThreadLocal<String> DATA_SOURCE_KEY = new ThreadLocal<String>();
    /**
     * 分库方案中,用于计算hash码的基值
     */
    private static final ThreadLocal<String> CONSISTENT_HASH_KEY = new ThreadLocal<String>();
    /**
     * 所有主库对应的key值集合
     * 在一致性hash算法中,用于生成主机虚拟节点
     */
    private static List<String> dataSouceKeyList = new ArrayList<String>();

    /**
     * 一致性hash算法工具
     */
    private static ConsistentHash<String> consistentHashDB = null;


    public static String getDataSourceKey(){
        return DATA_SOURCE_KEY.get();
    }

    public static void setDataSourceKey(String dataSourceKey){
        DATA_SOURCE_KEY.set(dataSourceKey);
    }

    public static void clearDataSourceKey(){
        DATA_SOURCE_KEY.remove();
    }

    public static String geConsistentHashKey(){
        return CONSISTENT_HASH_KEY.get();
    }

    public static void setConsistentHashKey(String consistentHashKey){
        CONSISTENT_HASH_KEY.set(consistentHashKey);
    }

    public static void clearConsistentHashKey(){
        CONSISTENT_HASH_KEY.remove();
    }


    public static List<String> getDataSouceKeyList() {
        return dataSouceKeyList;
    }

    public static void setDataSouceKeyList(List<String> dataSouceKeyList) {
        DynamicDataSourceHolder.dataSouceKeyList = dataSouceKeyList;
    }

    /**
     * 向主库集合中,增加一个主库节点
     * @param key
     */
    public static void addDataSourceKey(String key){
        dataSouceKeyList.add(key);
    }
    /**
     * 使用主库集合,实现一致性hash算法
     */
    public static void initConsistentHash(){
        HashFunction hashFunction = new HashFunction();
        consistentHashDB = new ConsistentHash<String>(hashFunction,100,dataSouceKeyList);
    }

    /**
     * 按照一致性hash分表方案,计算真实存储的数据源节点,返回对应的数据源的key
     * @return
     */
    public static String getDataSourceFinalKey(){
        //计算真实节点
        String originalKey = consistentHashDB.get(CONSISTENT_HASH_KEY.get());
        //判断要操作的是主库,还是从库
        if(DATA_SOURCE_KEY.get() == null || DATA_SOURCE_KEY.get().startsWith("master")){
            return originalKey;
        }else{
            return originalKey.replace("master","slave");
        }
    }

}

(2)修改DynamicDataSource类的实现,重写父类的两个方法:

  • public void afterPropertiesSet() :在Spring容器实例化本类对象时,利用反射机制,从父类中拿到数据源对应的key值集合。然后调用DynamicDataSourceHolder类的方法,给属性dataSouceKeyList进行赋值。
  • protected Object determineCurrentLookupKey() :调用DynamicDataSourceHolder类的getDataSourceFinalKey()方法,动态设置数据源标识。

package com.newbie.util.dbMultipleRouting;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.util.ReflectionUtils;
import javax.sql.DataSource;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * DynamicDataSource由自己实现,实现AbstractRoutingDataSource,数据源由自己指定。
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    /**
     * 重写父类的方法
     * 利用反射机制,从父类中拿到数据源的集合resolvedDataSources,以此设置主库的节点(设置 DynamicDataSourceHolder.dataSouceKeyList)
     */
    @Override
    public void afterPropertiesSet() {
        super.afterPropertiesSet();
        //由于父类的resolvedDataSources属性是私有的子类获取不到,需要使用反射获取
        Field field = ReflectionUtils.findField(AbstractRoutingDataSource.class, "resolvedDataSources");
        field.setAccessible(true);
        Map<String, DataSource> resolvedDataSources = null;
        try {
            resolvedDataSources = (Map<String, DataSource>) field.get(this);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        //根据数据源集合resolvedDataSources,来处理主库的集合
        if (resolvedDataSources != null) {
            Set<Map.Entry<String, DataSource>> entrySet = resolvedDataSources.entrySet();
            Iterator<Map.Entry<String, DataSource>> iterator = entrySet.iterator();
            while (iterator.hasNext()) {
                //判断key是否以master开头,如果是则认为是主库,并将该key值保存到:ynamicDataSourceHolder.dataSouceKeyList
                String key = iterator.next().getKey();
                if (key != null && key.startsWith("master")) {
                    //将主库的key,加入到主库集合列表中
                    DynamicDataSourceHolder.addDataSourceKey(key);
                }
            }
            //根据主库集合列表,生成一致性hash算法节点
            DynamicDataSourceHolder.initConsistentHash();
        }
    }

    /**
     * 动态设置数据源标识
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceHolder.getDataSourceFinalKey();
    }

}

步骤五:编写Service层的业务代码

package com.newbie.service.impl;

import com.newbie.dao.CustomerMapper;
import com.newbie.domain.Customer;
import com.newbie.domain.CustomerExample;
import com.newbie.domain.UserAccount;
import com.newbie.service.IConsistentHashService;
import com.newbie.util.dbMultipleRouting.DynamicDataSourceHolder;
import com.newbie.util.dbMultipleRouting.annotation.AnnotationDBSourceKey;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

/**
 * 使用一致性hash的方式,实现分库插入和查询
 */
@Service
public class ConsistentHashService implements IConsistentHashService {
    @Resource
    private CustomerMapper customerMapper;
    /**
     * 用于记录每次操作的客户信息,以方便查询和插入操作
     */
    private List<Customer> customerList = new ArrayList<Customer>();
    private  int countNumber = 10;      //生成10个客户

    /**
     * 批量生成一写用户,保存到List集合,用于插入数据库 和 查询数据库
     * @return
     */
    public void initCustomerList(String name) {
        for (int i = 0; i < countNumber; i++) {
            Customer customer = new Customer();
            String idcardNo = getIdno();    //生成身份证号
            customer.setId(idcardNo);
            customer.setIdcardNo(idcardNo);
            customer.setName(name + "-" + (countNumber - i));
            customerList.add(customer);
        }
    }

    /**
     * 从客户集合中,随机抽取10个客户,用来查询数据库
     */
    public List<Customer> getTenCustomer(){
        List<Customer> customers = new ArrayList<Customer>();
        //设置要随机访问的客户总数
        int tmpCoustomerNum = customerList.size() < countNumber ? customerList.size() : countNumber;
        for (int i = 0; i < tmpCoustomerNum; i++) {
            int randmoIndex = new Random().nextInt(customerList.size());
            Customer customer = customerList.get(randmoIndex);
            customers.add(customer);
        }
        return customers;
    }

    /**
     * 使用随机数,生成身份证号ID
     * @return
     */
    public String getIdno() {
        StringBuilder sb = new StringBuilder("");
        for (int i = 0; i < 18; i++) {
            int random = new Random().nextInt(10);
            if (random == 0) {
                random = 4;
            }
            sb.append(random);
        }
        return sb.toString();
    }

    /**
     * 向主库中,增加客户信息,插入十条数据,默认姓名为"简小六"
     * @return
     */
    @AnnotationDBSourceKey("master")
    public String addConstomer() {
        String message = "<p>插入结束</p>";
        message += "<p>插入信息如下:客户姓名 , 身份证号 , 原始数据源 , 最终数据源</p>";
        //向属性customerlist中,添加客户信息
        String name = "简小七";
        initCustomerList(name);
        int customerListSize = customerList.size();
        //向数据库中插入数据
        Iterator<Customer> iterator = customerList.iterator();
        while (iterator.hasNext()){
            Customer customer = iterator.next();
            DynamicDataSourceHolder.setConsistentHashKey(customer.getIdcardNo());   //设置用于一致性hash计算的key,以此来动态修改操作的数据源
            customerMapper.insertSelective(customer);       //执行数据库插入操作
            message += "<p>"+customer.getName()+" , "+customer.getIdcardNo()+" , "+DynamicDataSourceHolder.getDataSourceKey()+" , "+DynamicDataSourceHolder.getDataSourceFinalKey();
        }
        return message;
    }

    /**
     * 从主库库中查询客户信息
     */
    @AnnotationDBSourceKey("master")
    public String queryCustomerFromMater() {
        return queryData();
    }

    /**
     * 从slave库中查询客户信息
     */
    @AnnotationDBSourceKey("slave")
    public String queryCustomer() {
       return queryData();
    }
/** * 将查询数据的业务抽离成公共方法 */ public String queryData(){ String message = "<p>查询结束:</p>"; message += "<p>查询信息如下:客户姓名 , 身份证号 , 原始数据源 , 最终数据源</p>"; //从数据库中查询数据 Iterator<Customer> iterator = getTenCustomer().iterator(); while (iterator.hasNext()){ Customer customer = iterator.next(); String idcardNo = customer.getIdcardNo(); CustomerExample example = new CustomerExample(); example.createCriteria().andIdcardNoEqualTo(idcardNo); DynamicDataSourceHolder.setConsistentHashKey(idcardNo); //设置用于一致性hash计算的key,以此来动态修改操作的数据源 customerMapper.selectByExample(example); //执行数据库插入操作 message += "<p>"+customer.getName()+" , "+customer.getIdcardNo()+" , "+DynamicDataSourceHolder.getDataSourceKey()+" , "+DynamicDataSourceHolder.getDataSourceFinalKey(); } return message; } }

步骤六:编写Controller和客户端代码

package com.newbie.controller;

import com.newbie.domain.Customer;
import com.newbie.service.IConsistentHashService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.annotation.Resource;

@Controller
public class ConsistentHashController {
    @Resource
    IConsistentHashService consistentHashService;
    /**
     * 插入十条数据,默认姓名为"简小六"
     */
    @RequestMapping("/addCustomer")
    public String addCustomer(Model model) {
        String message = consistentHashService.addConstomer();
        model.addAttribute("message", message);
        return "showQuery";
    }
    /**
     * 从主库库中查询客户信息
     */
    @RequestMapping("queryCustomerFromMater")
    public String queryCustomerFromMater(Model model){
        String message = consistentHashService.queryCustomerFromMater();
        model.addAttribute("message", message);
        return "showQuery";
    }
    /**
     * 从slave库中查询客户数据
     */
    @RequestMapping("queryCustomer")
    public String queryCustomer(Model model){
        String message = consistentHashService.queryCustomer();
        model.addAttribute("message", message);
        return "showQuery";
    }
}
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>测试:分库分表逻辑</title>
</head>
<body>
<h2>练习二:一致性hash算法分库方案</h2>
<h3> </h3>
<a href="addCustomer"> 插入十条数据,姓名为"简小六" </a><br/><br/>
<a href="queryCustomerFromMater">从master库中查询客户数据</a><br/><br/>
<a href="queryCustomer">从slave库中查询客户数据</a>
<br/><hr/><hr><hr/><br/>
</body>
</html>

步骤七:运行代码,测试结果

(1)插入五条数据,姓名为“简小六”,结果如下图所示:

 

(2)从master库中随机查询五条数据,结果如下图所示:

(3)从slave库中随机查询五条数据,结果如下图所示:

原文地址:https://www.cnblogs.com/newbie27/p/10851923.html