spring+mybatis分库分表

上一篇介绍了读写分离,这一篇来说一下分库分表,废话不多说直接上代码。
1.首先配置文件,这里配置三个库

driver=com.mysql.jdbc.Driver
#定义初始连接数
initialSize=0
#定义最大连接数
maxActive=20
#定义最大空闲
maxIdle=20
#定义最小空闲
minIdle=1
#定义最长等待时间
maxWait=60000

jdbc.mysql.url0=jdbc:mysql://localhost:3306/test_02?createDatabaseIfNotExist=true&characterEncoding=utf-8&useUnicode=true
jdbc.mysql.username0=root
jdbc.mysql.password0=root

jdbc.mysql.url1=jdbc:mysql://localhost:3306/test_00?createDatabaseIfNotExist=true&characterEncoding=utf-8&useUnicode=true
jdbc.mysql.username1=root
jdbc.mysql.password1=root

jdbc.mysql.url2=jdbc:mysql://localhost:3306/test_01?createDatabaseIfNotExist=true&characterEncoding=utf-8&useUnicode=true
jdbc.mysql.username2=root
jdbc.mysql.password2=root

在每个库下边建立三个表如下图

Paste_Image.png

2.配置文件写好之后接下来配置数据源

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 引入配置文件 -->
    <bean id="propertyConfigurer"
          class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="location" value="classpath:props/jdbc.properties"/>
    </bean>


    <bean id="dataSource0" class="org.apache.commons.dbcp.BasicDataSource">
        <property name="driverClassName" value="${driver}"/>
        <property name="url" value="${jdbc.mysql.url0}"/>
        <property name="username" value="${jdbc.mysql.username0}"/>
        <property name="password" value="${jdbc.mysql.password0}"/>
        <!-- 初始化连接大小 -->
        <property name="initialSize" value="${initialSize}"></property>
        <!-- 连接池最大数量 -->
        <property name="maxActive" value="${maxActive}"></property>
        <!-- 连接池最大空闲 -->
        <property name="maxIdle" value="${maxIdle}"></property>
        <!-- 连接池最小空闲 -->
        <property name="minIdle" value="${minIdle}"></property>
        <!-- 获取连接最大等待时间 -->
        <property name="maxWait" value="${maxWait}"></property>
    </bean>


    <bean id="dataSource1" class="org.apache.commons.dbcp.BasicDataSource">
        <property name="driverClassName" value="${driver}"/>
        <property name="url" value="${jdbc.mysql.url1}"/>
        <property name="username" value="${jdbc.mysql.username1}"/>
        <property name="password" value="${jdbc.mysql.password1}"/>
        <!-- 初始化连接大小 -->
        <property name="initialSize" value="${initialSize}"></property>
        <!-- 连接池最大数量 -->
        <property name="maxActive" value="${maxActive}"></property>
        <!-- 连接池最大空闲 -->
        <property name="maxIdle" value="${maxIdle}"></property>
        <!-- 连接池最小空闲 -->
        <property name="minIdle" value="${minIdle}"></property>
        <!-- 获取连接最大等待时间 -->
        <property name="maxWait" value="${maxWait}"></property>
    </bean>


    <bean id="dataSource2" class="org.apache.commons.dbcp.BasicDataSource">
        <property name="driverClassName" value="${driver}"/>
        <property name="url" value="${jdbc.mysql.url2}"/>
        <property name="username" value="${jdbc.mysql.username2}"/>
        <property name="password" value="${jdbc.mysql.password2}"/>
        <!-- 初始化连接大小 -->
        <property name="initialSize" value="${initialSize}"></property>
        <!-- 连接池最大数量 -->
        <property name="maxActive" value="${maxActive}"></property>
        <!-- 连接池最大空闲 -->
        <property name="maxIdle" value="${maxIdle}"></property>
        <!-- 连接池最小空闲 -->
        <property name="minIdle" value="${minIdle}"></property>
        <!-- 获取连接最大等待时间 -->
        <property name="maxWait" value="${maxWait}"></property>
    </bean>
    <!-- 动态获取数据源 -->
    <bean id="mysqlDynamicDataSource" class="com.wz.dbRouting.db.DynamicDataSource">
        <property name="targetDataSources">
            <!-- 标识符类型 -->
            <map>
                <entry key="db0" value-ref="dataSource0"/>
                <entry key="db1" value-ref="dataSource1"/>
                <entry key="db2" value-ref="dataSource2"/>
            </map>
        </property>
    </bean>

    <bean id="dbRuleSet" class="com.wz.dbRouting.bean.RouterSet">
        <property name="routeFieldStart" value="0"></property>
        <property name="routeFieldEnd" value="9200000000000000000"></property>
        <property name="dbNumber" value="3"></property>
        <property name="routeType" value="2"></property>
        <property name="ruleType" value="3"></property>
        <property name="tableNumber" value="5"></property>
        <property name="dbKeyArray">
            <list>
                <value>db0</value>
                <value>db1</value>
                <value>db2</value>
            </list>
        </property>
    </bean>

    <!--事务-->
    <bean id="baiTiaoTransactionManager"
          class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="mysqlDynamicDataSource"></property>
    </bean>

    <bean id="btTransactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
        <property name="transactionManager" ref="baiTiaoTransactionManager"></property>
        <property name="propagationBehaviorName" value="PROPAGATION_REQUIRED"></property>
    </bean>

    <!-- spring和MyBatis完美整合,不需要mybatis的配置映射文件 -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="mysqlDynamicDataSource"/>
        <!-- 自动扫描mapping.xml文件 -->
        <property name="mapperLocations" value="classpath:com/wz/mapping/*.xml"></property>
    </bean>

    <!-- DAO接口所在包名,Spring会自动查找其下的类 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.wz.dao"/>
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
    </bean>

</beans>

动态获取数据源,这个和上篇读写分离一样,也是用treadLocal保证线程安全

package com.wz.dbRouting;

/**
 * 动态数据源实现中KEY的存放工具类
 * 动态数据源实现中KEY的存放工具类:使用treadLocal的方式来保证线程安全
 */
public class DbContextHolder {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
    private static final ThreadLocal<String> tableIndexHolder= new ThreadLocal<String>();
    
    
    public static void setDbKey(String dbKey) {
        contextHolder.set(dbKey);
    }

    public static String getDbKey() {
        return (String) contextHolder.get();
    }

    public static void clearDbKey() {
        contextHolder.remove();
    }
    
    public static void setTableIndex(String tableIndex){
        tableIndexHolder.set(tableIndex);
    }
    
    public static String getTableIndex(){
        return (String) tableIndexHolder.get();
    }
    public static void clearTableIndex(){
        tableIndexHolder.remove();
    }
    
    
}
package com.wz.dbRouting.db;


import com.wz.dbRouting.DbContextHolder;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import java.util.logging.Logger;

/**
 * @Description Spring 的动态数据源的实现
 * @Autohr wz
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    public static final Logger logger = Logger.getLogger(DynamicDataSource.class.toString());
    @Override
    protected Object determineCurrentLookupKey() {
        return DbContextHolder.getDbKey();//获取当前数据源
    }

}

设置一些分库分表需要的参数

package com.wz.dbRouting.bean;

import java.util.List;

/**
 * @Description
 * @Autohr wz
 */
public class RouterSet {

    /**根据字符串*/
    public final static int RULE_TYPE_STR=3;

    public final static int ROUTER_TYPE_DB=0;

    public final static int ROUTER_TYPE_TABLE =1;

    public final static int ROUTER_TYPE_DBANDTABLE=2;

    /**数据库表的逻辑KEY,与数据源MAP配置中的key一致*/
    private List<String> dbKeyArray;

    /**数据库数量*/
    private int dbNumber;
    /**数据表数量*/
    private int tableNumber;
    /**数据表index样式*/
    private String tableIndexStyle;
    /**Id开始*/
    private String routeFieldStart;
    /**Id结束*/
    private String routeFieldEnd;
    /**规则类型*/
    private int ruleType;
    /**路由类型类型*/
    private int routeType;

    public static int getRULE_TYPE_STR() {
        return RULE_TYPE_STR;
    }

    public static int getROUTER_TYPE_DB() {
        return ROUTER_TYPE_DB;
    }

    public static int getROUTER_TYPE_TABLE() {
        return ROUTER_TYPE_TABLE;
    }

    public static int getROUTER_TYPE_DBANDTABLE() {
        return ROUTER_TYPE_DBANDTABLE;
    }

    public List<String> getDbKeyArray() {
        return dbKeyArray;
    }

    public void setDbKeyArray(List<String> dbKeyArray) {
        this.dbKeyArray = dbKeyArray;
    }

    public int getDbNumber() {
        return dbNumber;
    }

    public void setDbNumber(int dbNumber) {
        this.dbNumber = dbNumber;
    }

    public int getTableNumber() {
        return tableNumber;
    }

    public void setTableNumber(int tableNumber) {
        this.tableNumber = tableNumber;
    }

    public String getTableIndexStyle() {
        return tableIndexStyle;
    }

    public void setTableIndexStyle(String tableIndexStyle) {
        this.tableIndexStyle = tableIndexStyle;
    }

    public String getRouteFieldStart() {
        return routeFieldStart;
    }

    public void setRouteFieldStart(String routeFieldStart) {
        this.routeFieldStart = routeFieldStart;
    }

    public String getRouteFieldEnd() {
        return routeFieldEnd;
    }

    public void setRouteFieldEnd(String routeFieldEnd) {
        this.routeFieldEnd = routeFieldEnd;
    }

    public int getRuleType() {
        return ruleType;
    }

    public void setRuleType(int ruleType) {
        this.ruleType = ruleType;
    }

    public int getRouteType() {
        return routeType;
    }

    public void setRouteType(int routeType) {
        this.routeType = routeType;
    }
}

3.下面开始是自定义注解
设置一个路由常量按照userNum来分表

package com.wz.dbRouting.annotation;

/**
 * @Description
 */
public class RouterConstants {

    public static final String ROUTER_FIELD_DEFAULT = "userNum";

    public static final String ROUTER_TABLE_SUFFIX_DEFAULT = "_0000";

}

package com.wz.dbRouting.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @Description
 * @Autohr wz
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Router {

    String routerField() default RouterConstants.ROUTER_FIELD_DEFAULT;

    String tableStyle() default RouterConstants.ROUTER_TABLE_SUFFIX_DEFAULT;
}

4.接下来是拦截器以及分库分表的规则。
主要思路是:通过拦截器来拦截方法名称以及方法参数,根据参数中的userNum字段,对该字段进行hashcode求余的方式来判断这条记录是存在几库几表。具体规则见代码
拦截器代码

package com.wz.dbRouting;

import com.wz.dbRouting.annotation.Router;
import com.wz.dbRouting.annotation.RouterConstants;
import com.wz.dbRouting.router.RouterUtils;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * @Description 切面切点 在Router注解的方法执行前执行 切点织入
 * @Autohr wz
 */
@Aspect
@Component
public class DBRouterInterceptor {

    private static final Logger log = LoggerFactory.getLogger(DBRouterInterceptor.class);

    @Autowired
    private DBRouter dBRouter;

    @Pointcut("@annotation( com.wz.dbRouting.annotation.Router)")
    public void aopPoint() {
    }

    @Before("aopPoint()")
    public Object doRoute(JoinPoint jp) throws Throwable {
        
        long t1 = System.currentTimeMillis();
        boolean result = true;
        //根据JoinPoint jp 获取方法名称和参数
        Method method = getMethod(jp);
        Router router = method.getAnnotation(Router.class);
        String routeField = router.routerField();
        Object[] args = jp.getArgs();
        if (args != null && args.length > 0) {
            for (int i = 0; i < args.length; i++) {
                long t2 = System.currentTimeMillis();
                //通过反射得到对象args[i] 的 routeField 字段的值
                String routeFieldValue = BeanUtils.getProperty(args[i],
                        routeField);
                log.debug("routeFieldValue{}" + (System.currentTimeMillis() - t2));
                if (StringUtils.isNotEmpty(routeFieldValue)) {
                    //看这个值是否为默认的分库分表字段,如果是设置库和表的名称
                    if (RouterConstants.ROUTER_FIELD_DEFAULT.equals(routeField)) {
                        //根据hashcode取%
                        dBRouter.doRouteByResource("" + RouterUtils.getResourceCode(routeFieldValue));
                        break;
                    } 
                }
            }
        }
        log.debug("doRouteTime{}" + (System.currentTimeMillis() - t1));
        return result;
    }

    private Method getMethod(JoinPoint jp) throws NoSuchMethodException {
        Signature sig = jp.getSignature();
        MethodSignature msig = (MethodSignature) sig;
        return getClass(jp).getMethod(msig.getName(), msig.getParameterTypes());
    }

    private Class<? extends Object> getClass(JoinPoint jp)
            throws NoSuchMethodException {
        return jp.getTarget().getClass();
    }

}

具体规则代码

package com.wz.dbRouting;

/**
 * @Description DB路由接口  DB路由器接口,通过调用该接口来自动判断数据位于哪个服务器
 * @Autohr wz
 */
public interface DBRouter {
    /**
     * 进行路由
     * @param fieldId
     * @return
     * @throws
     */
    public String doRoute(String fieldId);


    public String doRouteByResource(String resourceCode);
}

package com.wz.dbRouting.router;

import com.wz.dbRouting.DBRouter;
import com.wz.dbRouting.DbContextHolder;
import com.wz.dbRouting.annotation.RouterConstants;
import com.wz.dbRouting.bean.RouterSet;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.text.DecimalFormat;
import java.util.List;

/**
 * @Description 根据指定变量动态切 库和表
 * @Autohr supers【weChat:13031016567】
 */
@Service("dBRouter")
public class DBRouterImpl implements DBRouter {

    private static final Logger log = LoggerFactory.getLogger(DBRouterImpl.class);

    /**
     * 配置列表
     */
    @Autowired
    private List<RouterSet> routerSetList;

    public String doRoute(String fieldId) {
        if (StringUtils.isEmpty(fieldId)) {
            throw new IllegalArgumentException("dbsCount and tablesCount must be both positive!");
        }
        int routeFieldInt = RouterUtils.getResourceCode(fieldId);
        String dbKey = getDbKey(routerSetList, routeFieldInt);
        return dbKey;
    }

    public String doRouteByResource(String resourceCode) {
        if (StringUtils.isEmpty(resourceCode)) {
            throw new IllegalArgumentException("dbsCount and tablesCount must be both positive!");
        }
        int routeFieldInt = Integer.valueOf(resourceCode);
        String dbKey = getDbKey(routerSetList, routeFieldInt);
        return dbKey;
    }


    /**
     * @Description 根据数据字段来判断属于哪个段的规则,获得数据库key
     * @Autohr wz
     */
    private String getDbKey(List<RouterSet> routerSets, int routeFieldInt) {
        RouterSet routerSet = null;
        if (routerSets == null || routerSets.size() <= 0) {
            throw new IllegalArgumentException("dbsCount and tablesCount must be both positive!");
        }
        String dbKey = null;
        for (RouterSet item : routerSets) {
            if (item.getRuleType() == routerSet.RULE_TYPE_STR) {
                routerSet = item;
                if (routerSet.getDbKeyArray() != null && routerSet.getDbNumber() != 0) {
                    long dbIndex = 0;
                    long tbIndex = 0;
                    //默认按照分库进行计算
                    long mode = routerSet.getDbNumber();
                    //如果是按照分库分表的话,计算
                    if (item.getRouteType() == RouterSet.ROUTER_TYPE_DBANDTABLE && item.getTableNumber() != 0) {
                        mode = routerSet.getDbNumber() * item.getTableNumber();
                        dbIndex = routeFieldInt % mode / item.getTableNumber();
                        tbIndex = routeFieldInt % item.getTableNumber();
                        String tableIndex = getFormateTableIndex(item.getTableIndexStyle(), tbIndex);
                        DbContextHolder.setTableIndex(tableIndex);
                    } else if (item.getRouteType() == RouterSet.ROUTER_TYPE_DB) {
                        mode = routerSet.getDbNumber();
                        dbIndex = routeFieldInt % mode;
                    } else if (item.getRouteType() == RouterSet.ROUTER_TYPE_TABLE) {
                        tbIndex = routeFieldInt % item.getTableNumber();
                        String tableIndex = getFormateTableIndex(item.getTableIndexStyle(), tbIndex);
                        DbContextHolder.setTableIndex(tableIndex);
                    }
                    dbKey = routerSet.getDbKeyArray().get(Long.valueOf(dbIndex).intValue());
                    log.debug("getDbKey resource:{}------->dbkey:{},tableIndex:{},", new Object[]{routeFieldInt, dbKey, tbIndex});
                    DbContextHolder.setDbKey(dbKey);
                }
                break;
            }
        }
        return dbKey;
    }


    /**
     * @Description 此方法是将例如+++0000根式的字符串替换成传参数字例如44 变成+++0044
     * @Autohr supers【weChat:13031016567】
     */
    private static String getFormateTableIndex(String style, long tbIndex) {
        String tableIndex = null;
        DecimalFormat df = new DecimalFormat();
        if (StringUtils.isEmpty(style)) {
            style = RouterConstants.ROUTER_TABLE_SUFFIX_DEFAULT;//在格式后添加诸如单位等字符
        }
        df.applyPattern(style);
        tableIndex = df.format(tbIndex);
        return tableIndex;
    }

    public List<RouterSet> getRouterSetList() {
        return routerSetList;
    }

    public void setRouterSetList(List<RouterSet> routerSetList) {
        this.routerSetList = routerSetList;
    }
}

现在分库分表的规则就告一段落了,接下来进行单元测试。
5.控制台打印:

log4j:WARN No appenders could be found for logger (org.springframework.test.context.junit4.SpringJUnit4ClassRunner).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
db2库 _0003表 的插入结果:1

分库分表成功!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,113评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,644评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,340评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,449评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,445评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,166评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,442评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,105评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,601评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,066评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,161评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,792评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,351评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,352评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,584评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,618评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,916评论 2 344

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,601评论 18 139
  • 自定义注解 路由类型 上下文工具类 应用上下文 规则接口 路由 接口 实现 AOP拦截器 动态数据源 测试 规则实...
    donglq阅读 681评论 0 2
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,869评论 6 13
  • 1. 简介 1.1 什么是 MyBatis ? MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的...
    笨鸟慢飞阅读 5,429评论 0 4
  • 简书交友,看到交友,想起了手QQ交友,微信摇一摇,觉得很无聊,在我的思想里,交友必是对对方有所帮助的,否则现代人都...
    一埝阅读 270评论 0 0