前言
Redis是開源的,內存數據結構存儲的健值數據庫。即Key: Value形式來存儲每一個數據,以Redis String、Hash、List類型爲例:
- String 相當於 Map<String, String>
- Hash 相當於 Map<String, Map<String, String>>
- List 相當於 Map<String, List<String>>
每一個記錄都相當於一個Map鍵值對,並根據不同的值類型,使用不同類型的Map。
如何用Redis存儲一個對象實體
那麼如何使用這種結構來存儲一個對象記錄呢,例如User {id: "user1", name: "sundial dreams", age: 21, sex: "man"},其實存儲方式有很多,我們挑一種簡單的存儲方式,即直接用Hash類型來存儲,以user:id當成一個鍵,如下:
user:user1=> {name: "sundial dreams", age: 21, sex: "man"}
但是我們存儲了一堆的User,然後我們需要獲取所有的User咋辦呢,最暴力的方式是遍歷所用的鍵,然匹配出user:爲前綴的鍵,但是效率上肯定不高,因此我們可以使用一個List來存儲當前的用戶Id,並以user:list爲鍵,如下
user:list => ["user1"]
那如果我們需要根據用戶名來查詢一條用戶的信息呢,最暴力的方式還是枚舉出所有的用戶,然後再匹配姓名,考慮到效率問題,我們使用反向索引的方式來實現,即以user:username爲鍵值爲userid的List類型,比如
user:sundial dreams => ["user1"]
如果還想根據年齡來查找用戶,也可以爲年齡構建一個反向索引,即
user:21 => ["user1"]
有了這個思路,其他的都是同樣的道理。
基於Java反射的Dao類
可以看到,User實體的存儲其實也是有固定模式的(其實其他實體也可以用類似的存儲模式),因此可以寫一個UserDao類來存儲每一個User實體,基本方法包括增刪改查,但是,如果我們的實體類一換,即不是User,而是其他類,如Article,那麼我們不得不寫一個ArticleDao類,並重新實現跟UserDao類似的方法,這不符合代碼複用原則,即同一份代碼,在UserDao的實現類裏要寫一遍,而在ArticleDao的實現類裏也得寫一遍,所以我使用Java的反射和泛型來實現了一個公共的Dao類,並且以Dao<User> userDao = new Dao()的方式來產生操作User實體類的Dao對象,同理Dao<Article> articleDao = new Dao()來產生操作Article實體類的Dao對象,這樣同一個方法只需實現一次即可,提高代碼複用率。
IDao接口的定義
基於上面的思路我們定義如下接口:
/**
* Dao 接口
*
* @param <E>
*/
interface IDao<E> {
//查詢所有 返回一個Map類型的值,其中鍵爲Id,值爲泛型類對象(實體類對象,如User類的對象)
Map<String, E> queryAll() throws Exception;
//基於域查詢 返回值與queryAll一致, 根據對應屬性的值來查詢,以User類爲例,queryByField("name", "sundial dreams") => 查詢叫做sundial dreams的用戶ID
Map<String, E> queryByField(String field, String value) throws Exception;
//基於Id查詢
Tuple2<String, E> queryById(String id) throws Exception;
//插入操作
String insert(E ele) throws Exception;
//更新操作
String update(String id, E ele) throws Exception;
//刪除操作
void delete(String id) throws Exception;
}
定義實體,以User實體爲例
/**
* User實體類
* 因爲我實現的Dao是通用的Dao,而且在構建反向索引時不可能爲類每一個屬性都構建,
* 比如可以通過name查詢對應用戶,但沒有根據密碼來查詢用戶的說法。
* 因此,我只會爲設置爲public的屬性構建反向索引,並且定義的實體類名,最後會對應上Redis數據庫上存儲的名字
*/
class User {
public String name; // 用戶名 會構建反向索引
public String birthday; // 生日 會構建反向索引
public String area; // 所在地區 會構建反向索引
private String sex; // 性別 不會構建反向索引
private String password; // 密碼 不會構建反向索引
public User() {
}
public User(String name, String birthday, String password, String area, String sex) {
this.name = name;
this.birthday = birthday;
this.password = password;
this.area = area;
this.sex = sex;
}
static public User onlyPassword(String password) {
return new User(null, null, password, null, null);
}
@Override
public String toString() {
return String.format("{ name: %s, birthday: %s, password: %s, sex: %s, area: %s}", name, birthday, password, sex, area);
}
}
對於上面的User實體,存儲到Redis中的結構如下
IDao接口的實現
接下來我們基於lettuce(Redis數據庫驅動,官網:https://lettuce.io/)來實現IDao接口,每一個方法的實現思路如下
- insert(T ele) :
根據上面的redis存儲模式,我們除了要存儲一個Hash類型外,還要存儲Set, Zset類型來做輔助查詢(反向索引),上面設計實體類(User)的時候提到,對public屬性設置反向索引,對private的屬性不設置反向索引,所以我們就需要用反射來獲取一個對象的public屬性,和private 屬性,另一方面,使用incr函數來構建一個自增的Id,並由此產生一個用來存儲實體類的屬性的鍵(pKey),使用hmset方法來存儲對象的屬性和值,遍歷每個public屬性,獲取屬性名和值,由此拼接一個用來做反向索引的鍵,使用sadd方法存儲,其值就是上面提到的pKey,最後在使用zadd方法來存儲當前的pKey。需要注意的是,由於我使用的是lettuce的異步命令,每次操作都返回一個RedisFuture對象(hmset, sadd, zadd),所以等使用LettuceFutures.awaitAll來等待所有的異步操作完成。
- queryById(String id):
這個方法實現比較簡單,利用id構建一個鍵,首先判斷鍵是否存在,存在就使用hgetall來獲取對於的值,然後利用反射構建一個泛型對象併爲屬性賦值,然後返回一個二元組對象,即(id, object)。
- queryByField(String field, String value):
通過域來查詢,首先判斷域是否爲公有域(只爲公有域構建了反向索引),然後通過field, value來構建sKey,再使用smembers方法來獲取對應的存儲對象屬性和值的id,調用queryById方法獲取特定的對象。
- queryAll():
從存儲主鍵的有序集合裏獲取所有值,對獲取的值使用並行流進行map處理,map函數的返回值爲queryById的結果。
- update(String id, T obj):
由於之前設計時使用了反向索引,所以在對實體對象更新時,需要對反向索引進行更新。步驟就是,先根據id從數據庫中查找出對應的hash值,依據hash構建對象即爲newObj,讓他和obj相對比,找出需要更新的字段,然後更新該字段在hash裏面的值,以及更新該字段的反向索引。
- delete(String id):
刪除操作也是,不僅僅是刪除數據庫hash類型的實體,還要刪除對應的反向索引。根據id找出對應hash,然後根據hash裏面的 key找對應的反向索引,由於反向索引都是集合類型,所以刪除對應id的值即可。
(注:使用的是lettuce,並且使用異步命令)
完整的Java代碼如下:
/**
* Dao類
* JDK 1.8
* 使用lettuce 5.1.3 且使用異步命令
* 數據庫存儲結構
* 以User爲例
* User: 類型String 記錄user id
* User:id 類型Hash 記錄User信息
* User:fields:attr:value 類型SET 反向索引 屬性與鍵
* User:index 記錄所有user 的key 類型ZSET
*
* @param <T>
*/
public class Dao<T> implements IDao<T> {
private Class<T> _class;
protected String dbName;
protected final String SEP = ":";
private final String NS_FIELDS = "fields";
private final String NS_INDEX = "index";
// 使用lettuce異步命令
private RedisClient redisClient = RedisClient.create(Utils.createRedisURI());
protected RedisClusterAsyncCommands<String, String> async = redisClient.connect().async();
public Dao<T> withAsync(RedisClusterAsyncCommands<String, String> async) {
this.async = async;
return this;
}
public Dao(Class<T> oClass) {
_class = oClass;
dbName = _class.getName().toLowerCase();
}
private String makePrimaryKey(String id) {
return (dbName + SEP + id).trim();
}
private String makeIndexKey() {
return (dbName + SEP + NS_INDEX).trim();
}
private String makeSetKey(String field, String value) {
return (dbName + SEP + NS_FIELDS + SEP + field + SEP + value).trim();
}
/**
* @return Map ,{id, {...}}
* @throws Exception
*/
public Map<String, T> queryAll() throws Exception {
String indexKey = makeIndexKey();
if (async.exists(indexKey).get(1, TimeUnit.MINUTES) == null) throw new Exception("key is not exist");
return async.zrange(indexKey, 0, -1).get(1, TimeUnit.MINUTES)// user:1 user:2 ........
.parallelStream() // 使用Jdk1.8的並行流
.map(key -> {
String[] k = key.split(SEP);
try {
return queryById(k[1]);
} catch (Exception e) {
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toMap(Tuple2::getT1, Tuple2::getT2));
}
/**
* 基於Field查詢,即queryByField("name", "sundial dreams")
*
* @param field
* @param value
* @return Map, {id, {...}}
* @throws Exception
*/
public Map<String, T> queryByField(String field, String value) throws Exception {
if (_class.getField(field) == null) throw new Exception("field is not exist");
String sKey = makeSetKey(field, value);
if (async.exists(sKey).get(1, TimeUnit.MINUTES) == null) throw new Exception("key is not exist");
return async.smembers(sKey).get(1, TimeUnit.MINUTES) // user:1 ,user:2 ....
.parallelStream()
.map(key -> {
String id = key.split(SEP)[1];
try {
return queryById(id);
} catch (Exception e) {
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toMap(Tuple2::getT1, Tuple2::getT2));
}
/**
* Id查詢
*
* @param id
* @return Tuple2, 即(id, {...})
* @throws Exception
*/
public Tuple2<String, T> queryById(String id) throws Exception {
String queryKey = makePrimaryKey(id);
if (async.exists(queryKey).get(1, TimeUnit.MINUTES) == null) throw new Exception("key is not exist");
Map<String, String> map = async.hgetall(queryKey).get(1, TimeUnit.MINUTES);
T object = _class.getDeclaredConstructor().newInstance();
for (Map.Entry<String, String> entry : map.entrySet()) {
String k = entry.getKey(), v = entry.getValue();
Field field = _class.getDeclaredField(k);
if (!field.isAccessible()) field.setAccessible(true);
field.set(object, v);
}
return new Tuple2<>(id, object);
}
/**
* 插入
* 這裏利用反射,對實體類的公有屬性設置反向索引
*
* @param ele
* @throws Exception
*/
public String insert(T ele) throws Exception {
Field[] publicFields = _class.getFields(),
fields = _class.getDeclaredFields();
Map<String, String> map = new TreeMap<>();
List<RedisFuture> futures = new LinkedList<>();
Long id = async.incr(dbName + SEP).get(1, TimeUnit.MINUTES);
String queryKey = makePrimaryKey(id + "");
String indexKey = makeIndexKey();
for (Field field : publicFields) {
String fieldName = field.getName(),
value = String.valueOf(field.get(ele)),
sKey = makeSetKey(fieldName, value);
futures.add(async.sadd(sKey, queryKey));
}
for (Field field : fields) {
if (!field.isAccessible()) field.setAccessible(true);
String fieldName = field.getName(),
value = String.valueOf(field.get(ele));
map.put(fieldName, value);
}
futures.add(async.hmset(queryKey, map));
futures.add(async.zadd(indexKey, id, queryKey));
// 等待所有Future對象完成
LettuceFutures.awaitAll(Duration.ofSeconds(5), futures.toArray(new RedisFuture[0]));
return queryKey;
}
/**
* 更新
* 主要維護兩種鍵
* 以User爲例
* User:1 => 類型Hash,存儲實體類User的屬性,也是更新操作的主要目標
* User:fields:name:sundial dreams => 類型Set,存儲姓名的反向索引,用於快速查詢對應名字的鍵
* 以更新名字爲例,我們不僅需要改Hash類型的名字,而且還需要對應名字的更新反向索引
*
* @param id
* @param obj
* @throws Exception
*/
public String update(String id, T obj) throws Exception {
String queryKey = makePrimaryKey(id);
if (async.exists(queryKey).get(1, TimeUnit.MINUTES) == null) throw new Exception("error");
T object = queryById(id).getT2();
Field[] fields = _class.getDeclaredFields(),
publicFields = _class.getFields();
for (Field field : fields) if (!field.isAccessible()) field.setAccessible(true);
List<RedisFuture> futures = new LinkedList<>();
for (Field field : publicFields) {
if (field.get(obj) != null && !(field.get(obj)).equals("")) {
String oldKey = makeSetKey(field.getName(), String.valueOf(field.get(object))),
newKey = makeSetKey(field.getName(), String.valueOf(field.get(obj)));
if (!field.get(object).equals(field.get(obj))) {
futures.add(async.srem(oldKey, queryKey));
futures.add(async.sadd(newKey, queryKey));
}
}
}
for (Field field : fields)
if (field.get(obj) != null && !(field.get(obj).equals("")))
futures.add(async.hset(queryKey, field.getName(), String.valueOf(field.get(obj))));
for (Field field : publicFields) {
if (field.get(obj) != null && !(field.get(obj).equals(""))) {
String sKey = makeSetKey(field.getName(), String.valueOf(field.get(obj)));
futures.add(async.sadd(sKey, queryKey));
}
}
// 等待所有Future對象完成
LettuceFutures.awaitAll(Duration.ofSeconds(5), futures.toArray(new RedisFuture[0]));
return queryKey;
}
/**
* 刪除對應的鍵
*
* @param id
* @throws Exception
*/
public void delete(String id) throws Exception {
String queryKey = makePrimaryKey(id);
if (async.exists(queryKey).get(1, TimeUnit.MINUTES) == null) throw new Exception("has error");
T object = queryById(id).getT2();
List<RedisFuture> futures = new LinkedList<>();
futures.add(async.del(queryKey));
futures.add(async.zrem(makeIndexKey(), queryKey));
Field[] publicFields = _class.getFields();
for (Field field : publicFields) {
String sKey = makeSetKey(field.getName(), String.valueOf(field.get(object)));
futures.add(async.srem(sKey, queryKey));
}
// 等待所有Future對象完成
LettuceFutures.awaitAll(Duration.ofSeconds(5), futures.toArray(new RedisFuture[0]));
}
}
輔助類
/**
* 輔助類
* 提供數據庫連接URI
* Redis版本 5.1
*
*/
public class Utils {
static private final String HOST = "localhost"; // 主機
static private final int PORT = 6379; // 端口
static private final String AUTH = ""; //redis 數據庫密碼
static private final int DB = 2; // 選擇的數據庫
static public RedisURI createRedisURI() {
return RedisURI.Builder.redis(HOST, PORT)
.withPassword(AUTH).withDatabase(DB).withTimeout(Duration.ofSeconds(5)).build();
}
}
/**
* 工具類 二元組
* @param <T1>
* @param <T2>
*/
class Tuple2<T1, T2> {
private final T1 t1;
private final T2 t2;
public Tuple2(T1 t1, T2 t2) {
this.t1 = t1;
this.t2 = t2;
}
public T1 getT1() {
return t1;
}
public T2 getT2() {
return t2;
}
@Override
public String toString() {
return "(" + t1 + ", " + t2 + ")";
}
}
Dao類的基本使用
insert方法
List<User> users = new ArrayList<>();
users.add(new User("sundial dreams", "1998-11-04", "12345678", "US", "women"));
users.add(new User("daydream", "1998-12-04", "12345678", "US", "women"));
Dao<User> userDao = new Dao<>(User.class);
for (User user: users) {
userDao.insert(user);
}
query方法
System.out.println(userDao.queryAll());
System.out.println(userDao.queryById("1"));
System.out.println(userDao.queryByField("name", "sundial dreams"));
update方法
userDao.update("1", User.onlyPassword("new password"));
delete方法
userDao.delete("1");