分庫、分表
用戶量大的時候必須去分庫分表,分庫分表也需要自動化
分庫
前置工作
/**
* Describe:修改註解,增加主鍵標識
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DbField {
String value();//表列名
boolean isPrimary() default false;//是否是主鍵
}
public class BaseDao<T> implements IBaseDao<T> {
//獲得當前表的唯一標識,一般爲主鍵
//這裏查詢到當前表主鍵對應的值,傳遞進來的entityClass是帶有唯一標識的,這裏將它找出來
public String getPrimary(T entity) {
for (Map.Entry<String, Field> entry : cacheMap.entrySet()) {
Field cacheField = entry.getValue();
DbField dbField = cacheField.getAnnotation(DbField.class);
//當前表列名是主鍵
if (dbField != null && dbField.isPrimary()) {
try {
//得到主鍵值
return cacheField.get(entity).toString();
} catch (Exception e) {
e.printStackTrace();
}
}
}
return "";
}
}
/**
* Describe:數據庫分庫的工廠類
*/
public class BaseDaoSubFactory extends BaseDaoFactory {
//單例
private static final BaseDaoSubFactory instance = new BaseDaoSubFactory();
public static BaseDaoSubFactory getInstance() {
return instance;
}
//數據庫文件統一存放路徑
private static final String def_db_path = "data/data/" + BuildConfig.APPLICATION_ID;
//數據庫連接池,緩存分庫的連接池
private Map<String, BaseDao> dbGroup = Collections.synchronizedMap(new HashMap<String, BaseDao>());
/**
* 獲得分庫的操作dao
*
* @param daoClass dao的class對象
* @param entityClass 對應表的class對象
* @param <T> 繼承basedao的dao對象的類型
* @param <M> 傳遞進dao的表對應的java實體類的類型
* @param m 傳遞進dao的表對應的java實體類,必須包含唯一約束的主鍵
* @return 數據庫分庫操作dao對象
*/
public <T extends BaseDao<M>, M> T getSubDao(Class<T> daoClass, Class<M> entityClass, M m) {
//在主數據庫裏先獲取到basedao對象
T baseDao = BaseDaoSubFactory.getInstance().getBaseDao(daoClass, entityClass);
//在主數據庫裏獲取到了對象才能創建分庫
if (baseDao != null) {
//創建分庫數據庫
String dbPath = getSeparateTablePath(baseDao, m);
//如果緩存有,則直接取緩存中的數據庫操作對象
if (dbGroup.get(dbPath) != null) {
return (T) dbGroup.get(dbPath);
}
//沒有緩存,則創建
SQLiteDatabase sqLiteDatabase = SQLiteDatabase.openOrCreateDatabase(dbPath, null);
try {
//獲得dao的對象,並初始化
baseDao = daoClass.newInstance();
baseDao.init(sqLiteDatabase, entityClass);
//添加緩存
dbGroup.put(dbPath, baseDao);
} catch (Exception e) {
e.printStackTrace();
}
return baseDao;
}
return null;
}
//創建當前用戶的分數據庫路徑
public <M> String getSeparateTablePath(BaseDao<M> baseDao, M m) {
String separateTablePath = "";
//創建分庫的唯一標識
String primary = baseDao.getPrimary(m);
//獲取到了唯一標識,創建數據庫文件
if (!TextUtils.isEmpty(primary)) {
File file = new File(def_db_path);
boolean iscreate = true;
//不存在默認的數據庫則創建
if (!file.exists()) {
//創建成功
if (!file.mkdirs()) {
iscreate = false;
}
}
if (iscreate) {
//如果數據庫根目錄存在,則返回分庫路徑
//這樣,每一個表都有一個單獨的數據庫。例如,用戶表,根據用戶id將每個用戶分到獨立的數據庫中,這個數據庫中只存在當前用戶的一些列表信息
separateTablePath = file.getAbsolutePath() + "/" + baseDao.getTableName() + "_" + primary + ".db";
}
}
return separateTablePath;
}
}
注意:
對於前端應用來說,最好的方式就是一個用戶一個數據庫,以爲前端用戶少,一個用戶一個庫便於操作。
對於服務器來說,則需要定製一套規則。例如,一千萬用戶一個庫,1-10000000id的用戶分爲庫0,以此類推。那麼這一千萬用戶就有兩個id,主庫id(隨着用戶量的增長而增長,0~+∞)、分庫id(0~10000000,以此類推),並且主庫id和分庫id不可以一起查,因爲他們之間是沒有關聯的。
分表
當我們的數據量上升到一定大小的時候,操作速度就會明顯受影響,所以要根據有規律的唯一標識符按一定規則來分表,規則是靈活的。
例如:
某一個用戶操作表,需要根據用戶id來查到該用戶在這張表中的操作記錄。
如果這張操作表記錄了非常多的用戶操作,那麼我們就可以根據一定的特性來進行分表。
分表的本質和分庫一致。
例如,操作表中,用戶id是唯一的或者操作id是唯一的。那麼我們就可以根據唯一標識符來進行分表。
比如我們根據用戶id結尾的數字就可以分出十張表。0-9的十張操作表。這十張表分別存用戶id結尾爲0-9的操作記錄。
如果十張表不夠用,則我們可以將0-9的id轉成16進制,這樣就可以有十六張表。
如果十六張表不夠用,則我們可以根據用戶id的後兩位,也就是00-99分成10張表。
以此類推。
這,就是分表。
/**
* Describe:用戶頭像表,有一百張,根據用戶id的後兩位數來分
*/
@DbTable("tb_user_img")
public class UserImg {
private Long imgId;
@DbField("_id")
private Long userId;//用戶id,更嚴格的說,這裏應該指向user表的id,它是一個外鍵
private String time;//創建時間
private String imgPath;//頭像路徑
//獲得用戶id的後兩位
public int getUserIdSuffix() {
if (userId < 100) {
return userId.intValue();
} else {
String idStr = userId + "";
return Integer.parseInt(idStr.substring(idStr.length() + 2));
}
}
/**
* Describe:用戶頭像,分了一百張,規則是用戶id的後兩位00-99
*/
public class PhotoDao extends BaseDao<UserImg> {
//重寫初始化方法,在初始化數據庫操作對象的時候創建一百張表
@Override
public void init(SQLiteDatabase sqLiteDatabase, Class<UserImg> entityClass) {
// super.init(sqLiteDatabase,entityClass);
this.sqLiteDatabase = sqLiteDatabase;
this.entityClass = entityClass;
if (!isInit) {
//通過反射和註解獲得表名
//自動建表
//如果沒有添加註解,則通過反射去拿表明
DbTable dbTable = entityClass.getAnnotation(DbTable.class);
if (dbTable != null) {
//註解拿表名
this.tableName = dbTable.value();
} else {
//反射拿表名
this.tableName = entityClass.getSimpleName();
}
//這裏開始建一百張表
for (int i = 0; i < 100; i++) {
//創建表的sql語句
String createTableSql = getCreateTableSql(tableName + getTableSuffix(i));
this.sqLiteDatabase.execSQL(createTableSql);
}
initCacheMap();
isInit = true;
}
}
//實際查詢,根據用戶id先定位到哪張表再查詢
@Override
public List<UserImg> queryByWhere(UserImg where) {
//準備好條件語句
Map<String, String> values = getValues(where);
Condition condition = new Condition(values);
String photoName = tableName + getTableSuffix(where.getUserIdSuffix());
Cursor cursor = sqLiteDatabase.query(photoName, null, condition.whereClause, condition.whereArgs, null, null, null, null);
List<UserImg> resultList = getResult(cursor, where);
cursor.close();
return resultList;
}
//處理分表的後綴
private String getTableSuffix(int i) {
return (i < 10 ? "0" : "") + i;
}
}