最近的項目需要使用redis。
主要需要實現(xiàn)幾個功能:
1 數(shù)據(jù)獲取
2 數(shù)據(jù)緩存
3 分布式鎖
4 帶鎖的數(shù)據(jù)處理
為了實現(xiàn)這幾個功能,對redis做了一層相對很薄的封裝,過程如下:
1 首先,我們需要定義redis的訪問接口,如下:
//數(shù)據(jù)查詢,更新接口
public interface DataAccess<T> {
void set(String key, T value);
void set(String key, T value, int validTime);
T get(String key);
List<T> popAll(String key);
void push(String key, T value);
//validTime = -1 表示無失效時間
boolean setNX(String key, T value, int validTime);
void delete(String key);
boolean tryLock(String key, T value, int timeout);
void unLock(String key);
}
接口的實現(xiàn)我們基于構(gòu)建匿名類的方式,數(shù)據(jù)訪問基于RedisTemplate, 代碼如下:
public class RedisAccessBuilder<T> {
private RedisTemplate<String, T> redisTemplate;
public RedisAccessBuilder(RedisTemplate<String, T> redisTemplate) {
this.redisTemplate = redisTemplate;
}
//1 設(shè)置json序列化方法
//2 設(shè)置redis訪問器
//3 構(gòu)造匿名類
public DataAccess<T> buildDataAccess(Class<T> clazz, final DbDataGet<T> dbDataGet) {
redisTemplate.setKeySerializer(new StringRedisSerializer());
//1
if (clazz.equals(String.class)) {
redisTemplate.setValueSerializer(new StringRedisSerializer());
} else {
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<T>(clazz));
}
//2
final ValueOperations<String, T> valueOperations= redisTemplate.opsForValue();
final ListOperations<String, T> listOperations = redisTemplate.opsForList();
//3
return new DataAccess<T>() {
@Override
public void set(String key, T value) {
valueOperations.set(key, value);
}
@Override
public void set(String key, T value, int validTime) {
set(key, value);
redisTemplate.expire(key, validTime, TimeUnit.SECONDS);
}
@Override
public T get(String key) {
T data = valueOperations.get(key);
//如果redis中沒有且設(shè)置了數(shù)據(jù)庫源,則從數(shù)據(jù)庫中獲取
if (null == data
&& null != dbDataGet) {
data = dbDataGet.get(key);
if (null != data) {
//默認保存一天
set(key, data, Constants.ONE_DAY);
}
try {
//避免數(shù)據(jù)庫壓力過大
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return data;
}
@Override
public List<T> popAll(String key) {
String listKey = "list#" + key;
List<T> resList = listOperations.range(listKey, 0, -1);
delete(listKey);
return resList;
}
@Override
public void push(String key, T value) {
String listKey = "list#" + key;
listOperations.rightPush(listKey, value);
}
@Override
public boolean setNX(String key, T value, int validTime) {
if (valueOperations.setIfAbsent(key, value)) {
//todo throw exception
if (validTime > 0) {
return redisTemplate.expire(key, validTime, TimeUnit.SECONDS);
} else {
return true;
}
}
return false;
}
@Override
public void delete(String key) {
redisTemplate.delete(key);
}
@Override
public boolean tryLock(String key, T value, int timeout) {
String lockKey = "locked#" + key;
long startTime = System.currentTimeMillis();
int sleepTimes = 0;
while (!setNX(lockKey, value, Constants.MAX_LOCK_TIME)) {
try {
Thread.sleep(1);
sleepTimes++;
if (sleepTimes % 100 == 0) {
//超時判斷
if (System.currentTimeMillis() - startTime > Constants.MAX_TIMEOUT * 1000) {
return false;
}
}
} catch (InterruptedException e) {
return false;
}
}
return true;
}
@Override
public void unLock(String key) {
String lockKey = "locked#" + key;
delete(lockKey);
}
};
}
}
在有了redis訪問接口之后,我們基于它,可以繼續(xù)定制redis緩存
2 redis緩存
我們的redis緩存分為兩種:
a 類似hashset,判斷一個key是否存在,實現(xiàn)如下:
//用redis實現(xiàn)的有時效緩存封裝
public class RedisCache {
private DataAccess<String> dataAccess;
public RedisCache(RedisTemplate<String, String> redisTemplate) {
RedisAccessBuilder<String> redisAccessBuilder = new RedisAccessBuilder<>(redisTemplate);
dataAccess = redisAccessBuilder.buildDataAccess(String.class, null);
}
//如果不存在,則插入并返回true
//否則返回false
public boolean cacheIfAbsent(String key, int validSecond) {
String cacheKey = "cached#" + key;
String cacheValue = "" + System.currentTimeMillis();
return dataAccess.setNX(cacheKey, cacheValue, validSecond);
}
}
b 類似hashtable,通過一個key,查詢value,為了保證數(shù)據(jù)的時效性,在實現(xiàn)中使用異步的方式從數(shù)據(jù)中查詢數(shù)據(jù),保證當前數(shù)據(jù)的有效性,同時為了保證效率,使用一個基于內(nèi)存的hashmap來做第一層緩存,代碼如下:
public class MemCache<T> {
private Map<String, T> map = new ConcurrentHashMap<String, T>();
private DataAccess<T> dataAccess;
private DbDataGet<T> dbDataGet;
public MemCache(RedisTemplate<String, T> redisTemplate, Class<T> clazz, final DbDataGet<T> dbDataGet) {
RedisAccessBuilder<T> redisAccessBuilder = new RedisAccessBuilder<>(redisTemplate);
dataAccess = redisAccessBuilder.buildDataAccess(clazz, dbDataGet);
this.dbDataGet = dbDataGet;
asynUpdateData();
}
//1 檢查內(nèi)存
//2 查詢redis(redis沒有則查詢數(shù)據(jù)庫)
public T get(String key) {
//1
T value = map.get(key);
if (null == value) {
//2
value = dataAccess.get(key);
map.put(key, value);
}
return value;
}
//異步更新數(shù)據(jù),主要針對數(shù)據(jù)庫中的數(shù)據(jù)被修改,造成和redis及內(nèi)存不一致的情況
//1 創(chuàng)建新的map
//2 從原map中遍歷數(shù)據(jù)
//3 從數(shù)據(jù)庫中獲取數(shù)據(jù),判斷是否和內(nèi)存中一致
//4 如果不一致,則更新內(nèi)存及redis中的數(shù)據(jù)
//5 將新的map作為當前的map
private void asynUpdateData() {
ExecutorService service = Executors.newFixedThreadPool(1);
service.execute(new Runnable() {
@Override
public void run() {
while (true) {
int times = 0;
try {
//1
Map<String, T> newMap = new ConcurrentHashMap<>();
//2
for (String key : map.keySet()) {
times++;
//3
if (null != dataAccess.get(key)) {
T value = dbDataGet.get(key);
//4
if (value != map.get(key)) {
newMap.put(key, value);
dataAccess.set(key, value, Constants.ONE_DAY);
}
}
//每100條控制一下時間,避免數(shù)據(jù)庫壓力過大
if (times % 100 == 0) {
Thread.sleep(100);
}
}
//5
map = newMap;
Thread.sleep(Constants.FIVE_MINUTES);
} catch (InterruptedException e) {
log.info("interrupted " + e);
} catch (Exception e) {
log.info("exception " + e);
}
}
}
});
}
}
3&4 數(shù)據(jù)處理和分布式鎖我們放在一起處理,首先定義數(shù)據(jù)處理接口
//數(shù)據(jù)處理接口
public interface DataProcess<T> {
DetailRes process(String key, T data);
}
之后定義帶鎖的數(shù)據(jù)處理接口
//鎖住key后執(zhí)行數(shù)據(jù)處理
public interface DataProcessWithLock<T> {
DetailRes execute(String key, T data);
}
我們需要用戶自己實現(xiàn)DataProcess接口,并在構(gòu)建DataProcessWithLock的時候傳入,構(gòu)建DataProcessWithLock的代碼如下:
//帶鎖的數(shù)據(jù)處理封裝
public class RedisDataProcessWithLockBuilder {
private DataAccess<String> dataAccess;
public RedisDataProcessWithLockBuilder(RedisTemplate<String, String> redisTemplate) {
RedisAccessBuilder<String> redisAccessBuilder = new RedisAccessBuilder<>(redisTemplate);
dataAccess = redisAccessBuilder.buildDataAccess(String.class, null);
}
public <T> DataProcessWithLock<T> buildDataProcessWithLock(final DataProcess<T> dataProcess) {
return new DataProcessWithLock<T>() {
@Override
public DetailRes execute(String key, T data) {
//value為當前時間,方便追查問題
String lockValue = "" + System.currentTimeMillis();
try {
if (dataAccess.tryLock(key, lockValue, Constants.MAX_LOCK_TIME)) {
return dataProcess.process(key, data);
} else {
return new DetailRes(false, key + " locked failed");
}
} catch (Exception e) {
return new DetailRes(false, key + " " + e.toString());
} finally {
dataAccess.unLock(key);
}
}
};
}
}
有了前面的各種準備之后,我們用一個例子做結(jié)。
首先定義一個model:
@AllArgsConstructor
@NoArgsConstructor
@Data
public class User {
long userId;
String name;
}
在對這個model處理前,我們要按userId將其鎖住,之后的處理就是簡單的將這個user打印出來,代碼如下:
@Service
public class RedisExample {
@Autowired
RedisTemplate<String, String> stringRedisTemplate;
@Autowired
RedisTemplate<String, User> userRedisTemplate;
private DataProcessWithLock<User> dataProcessWithLock;
@PostConstruct
public void init() {
RedisDataProcessWithLockBuilder redisDataProcessWithLockBuilder =
new RedisDataProcessWithLockBuilder(stringRedisTemplate);
dataProcessWithLock = redisDataProcessWithLockBuilder.buildDataProcessWithLock(new DataProcess<User>() {
//用戶自己的實現(xiàn)
@Override
public DetailRes process(String key, User user) {
System.out.println(user);
return new DetailRes(true, "");
}
});
}
public void process() {
User user = new User(111, "littlersmall");
String key = "" + user.getUserId();
dataProcessWithLock.execute(key, user);
}
}
github地址如下