常用數(shù)據(jù)結(jié)構(gòu)
String類型
Hash類型
List類型
List可以看做是一個雙向鏈表結(jié)構(gòu),既可以支持正向檢索,也可以支持反向檢索。
鏈表的特點是插入和刪除效率快,常用來存儲有序的、需頻繁插入和刪除的數(shù)據(jù),例如朋友圈點贊列表,評論列表等。
Set類型
注意:這里的set是無序的!
# 存放數(shù)據(jù)
sadd zs lisi wangwu zhaoliu
sadd ls wangwu mazi ergou
# 查詢張三好友數(shù)
scard zs
# 計算張三和李四有哪些共同好友
sinter zs ls
# 是張三好友不是李四好友
sdiff zs ls
# 查詢張三和李四的好友共有哪些人
sunion zs ls
# 判斷李四是否為張三好友
sismember zs ls
# 將李四從張三的好友列表中刪除
srem zs ls
SortedSet 類型
通用命令
- keys:查看符合模板的所有key
- del:刪除一個key,可以空格跟多個key,批量刪除
- exists:判斷key是否存在
- expire:給一個key設置有效期(單位為秒),有效期到期時,該key會自動刪除
- TTL:查看一個key的剩余有效期,-1表示永久有效。
key的層級結(jié)構(gòu)
Redis的key允許有多個單詞形成的層級結(jié)構(gòu),多個單詞之間用:
隔開。
Spring Data Redis
spring data redis 是redis的Java客戶端的一種實現(xiàn),它整合Lettuce和Jedis。
快速入門
1、 引入依賴
2、配置文件
3、注入RedisTemplate
在依賴的啟動器中會自動地依賴自動配置,自動配種會將需要的類注冊到IoC容器中。
4、編寫測試案例
RedisTemplate的序列化方式
RedisTemplate的默認序列化方式會將存儲內(nèi)容轉(zhuǎn)為字節(jié)碼,如果我們想要實現(xiàn)所見即所存,可以自定義序列化方法。
經(jīng)過配置,我們不僅可以存中文字符串,亦可以存入對象,RedisTemplate會自動幫助我們把對象序列化為json,并在使用時反序列化為對象。
StringRedisTemplate
從上邊存放的對象可以看出,為了實現(xiàn)能夠自動反序列化對象,在redis中不僅存入了對象的json序列,還存放了對象的字節(jié)碼類型名。這就帶來了額外的存儲開銷?。?!
為了節(jié)省內(nèi)存空間,我們并不會使用JSON序列化器來處理value,而是統(tǒng)一使用String序列化器,要求只能存儲String類型的key和value,當需要存儲Java對象時,手動完成對象的序列化和反序列化。
RedisTemplate的Hash類型操作
opsForHash是針對Hash類型的數(shù)據(jù)操作的API函數(shù)。
實戰(zhàn)操作
短信登錄
發(fā)送驗證碼
controller層:
/**
* 發(fā)送手機驗證碼
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
return userService.sedCode(phone,session);
}
service層:
將驗證碼code保存在Redis中。
@Override
public Result sedCode(String phone, HttpSession session) {
//1. 校驗手機號
if (RegexUtils.isPhoneInvalid(phone)) {
//2.如果不符合,返回錯誤信息
return Result.fail("手機號格式錯誤");
}
//3. 符合,生成驗證碼
String code = RandomUtil.randomNumbers(6);
//4. 保存驗證碼到Redis,并設置有效期
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
//5. 發(fā)送驗證碼
log.debug("發(fā)送短信驗證碼成功,驗證碼:{}",code);
//返回ok
return Result.ok();
}
校驗登錄信息
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
//1. 校驗手機號
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手機號格式錯誤");
}
//2. 從redis中獲取并校驗驗證碼
Object cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
if (cacheCode == null || !cacheCode.toString().equals(code)){
//3. 不一致,報錯
return Result.fail("驗證碼錯誤");
}
//4.一致,根據(jù)手機號查詢用戶
User user = query().eq("phone", phone).one();
//5. 判斷用戶是否存在
if (user == null){
//6. 不存在,創(chuàng)建新用戶
user = createUserWithPhone(phone);
}
//7.保存用戶信息到Redis,并隨機生成token令牌,將User對象轉(zhuǎn)為Hash存儲,并設置token有效期
String token = UUID.randomUUID().toString(true);
// 只保存user中非敏感信息
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
// 使用hash存儲,因此把對象轉(zhuǎn)為map,由于id字段為Long類型,而redis存放字符串類型,因此需要類型轉(zhuǎn)換
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions.create()
.setIgnoreNullValue(true).setFieldValueEditor((fileName, fileValue) -> fileValue.toString()));
// 存放在redis中的user對象的key
String tokenKey = LOGIN_USER_KEY + token;
// 存入redis
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
// 設置有效期
stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
return Result.ok();
}
校驗登錄狀態(tài)
基于springmvc的攔截器校驗登錄狀態(tài),如果檢測到登錄狀態(tài),需要刷新token有效期。這些操作使用攔截器來實現(xiàn)。
多個攔截器的執(zhí)行順序:
攔截器接口中的三個方法:
- preHandle:controller層之前
- postHandle:controller方法執(zhí)行之后,視圖渲染返回前
- afterComplete:視圖渲染完畢
刷新攔截器放在前邊:
public class RefreshTokenInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.獲取請求頭中的token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
return true;
}
// 2.基于TOKEN獲取redis中的用戶
String key = LOGIN_USER_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
// 3.判斷用戶是否存在
if (userMap.isEmpty()) {
return true;
}
// 5.將查詢到的hash數(shù)據(jù)轉(zhuǎn)為UserDTO
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// 6.存在,保存用戶信息到 ThreadLocal
UserHolder.saveUser(userDTO);
// 7.刷新token有效期
stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用戶
UserHolder.removeUser();
}
}
校驗登錄狀態(tài)攔截器:
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1. 從threadLocal中判斷是否有user,有就放行,沒有就攔截
if (UserHolder.getUser() == null) {
// 沒有需要攔截
response.setStatus(401);
return false;
}
//2. 有user放行
return true;
}
}
攔截器注冊:
order越小,優(yōu)先級越高
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 登錄攔截器
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
).order(1);
// token刷新攔截器
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
}
}
商家查詢緩存
使用緩存,加速查詢過程。
緩存更新策略
緩存穿透
緩存穿透是指客戶端請求的數(shù)據(jù)在緩沖中和數(shù)據(jù)庫中都不存在,這樣緩存永遠不會生效,這些請求都會打到數(shù)據(jù)庫中。
基于緩存null,可能會導致存放了一些無用信息,造成額外內(nèi)存開銷,可以通過給緩存設置較短的TTL,緩解這種情況。
緩存雪崩
緩存雪崩是指在同一時間段,大量的緩存key同時失效或者Redis服務宕機,導致大量請求到達數(shù)據(jù)庫,帶來巨大訪問壓力。
解決方案:
- 給不同的key的TTL添加隨機值,避免同時大量緩存過期
- 利用Redis集群,提高服務的可用性
- 給緩存業(yè)務添加降級限流策略,當出現(xiàn)大量緩存失效時,做服務降級限流,犧牲部分服務
- 給業(yè)務添加多級緩存
緩存擊穿
緩存擊穿問題也稱為熱點key問題,就是一個被高并發(fā)訪問并且緩存重建業(yè)務較復雜的key突然失效了(熱點key失效),無數(shù)的請求訪問會在瞬間給數(shù)據(jù)庫帶來巨大的沖擊。
解決方法主要有兩種:
1、互斥鎖:
重建緩存的過程上鎖,其他沒有拿到鎖的線程會阻塞,相互等待。這種做法簡單粗暴,多個線程相互等待鎖釋放。
互斥鎖的代碼實現(xiàn)上,可以使用redis的setnx
指令:
setnx:set if not exist,僅當不存在時才設置。
這樣,假如我們指定一個字段key,lock
,那么多個線程想要設置這個key時,只有第一個線程會設置成功。
這樣就模擬了鎖的獲取。
如果要釋放鎖,那么即可將該字段刪除。
2、邏輯過期:
在緩存中保存一個邏輯上的有效期字段“expire”。
第一個發(fā)現(xiàn)緩存過期,獲取到鎖的線程,會開啟一個新的線程來執(zhí)行緩存重建過程,而自己直接返回過期的緩存。其余沒有拿到鎖的線程,在緩存重建成功之前,都會拿到過期緩存,他們不阻塞等待,直接返回過期緩存。
邏輯過期策略解決緩存擊穿問題,可能會返回過期緩存,適用于弱一致性緩存策略。
緩存擊穿一般出現(xiàn)在活動期間,出現(xiàn)突然的訪問高峰時,因此邏輯緩存字段“expire”可以在活動結(jié)束后移除
互斥鎖線程需要等待,性能受影響,但數(shù)據(jù)是強一致性的。
邏輯過期,線程無需等待,性能更好,但可能返回過期緩存,屬于弱一致性。
緩存工具封裝
首先構(gòu)建這樣一個CacheClient,作為緩存工具類:
設置需要的線程池(緩存擊穿,執(zhí)行邏輯過期策略時需要),StringRedisTemplate(redis操作需要)。
在構(gòu)造函數(shù)時,需要傳入stringRedisTemplate。
@Component
public class CacheClient {
private final StringRedisTemplate stringRedisTemplate;
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
}
方法1:將java對象序列化為json并存放在string類型的key中,并可以設置TTL時間:
主要需要注意的點在于,對象的序列化,使用hutool工具包的JSONUtil.toJsonStr(value)
將對象轉(zhuǎn)為JsonStr。
public void set(String key, Object value, Long time, TimeUnit unit) {
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
}
方法2:將java對象序列化為json并存放在string類型的key中,并可以設置邏輯過期時間:
邏輯過期時間,需要在原對象的基礎上加一個邏輯過期屬性字段expire
,表示邏輯過期時間,具體實現(xiàn)可以構(gòu)建一個RedisData
類,該類中封裝有一個對象成員,和一個邏輯過期屬性成員。
@Data
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}
構(gòu)建好了RedisData類,那么方法2實現(xiàn)邏輯就與方法1一致了,只是邏輯過期對象是永久存在的。
public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
// 設置邏輯過期
RedisData redisData = new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
// 寫入Redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
}
方法3:根據(jù)指定的key查詢緩存,使用緩存空值的方式來應對緩存穿透問題。
使用泛型來封裝函數(shù),指定兩個泛型類型,R表示查詢的對象類型,ID表示存儲對象的id類型。另外,假如緩存未命中,需要查詢數(shù)據(jù)庫,這個查詢數(shù)據(jù)庫的過程也需要調(diào)用者提供,因此,函數(shù)參數(shù)中有一個函數(shù)接口類型,
Function<ID, R> dbFallback,表示一個入?yún)镮D類型,返回值類型為R的函數(shù)。
可以直接使用dbFallback的apply()方法,執(zhí)行。
關于緩存穿透,如果數(shù)據(jù)庫中查詢結(jié)果為null,將null值緩存到redis,并返回錯誤信息。如果數(shù)據(jù)庫查詢結(jié)果非null,則將其緩存到redis。
public <R,ID> R queryWithPassThrough(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){
String key = keyPrefix + id;
// 1.從redis查詢商鋪緩存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判斷是否存在
if (StrUtil.isNotBlank(json)) {
// 3.存在,直接返回
return JSONUtil.toBean(json, type);
}
// 判斷命中的是否是空值
if (json != null) {
// 返回一個錯誤信息
return null;
}
// 4.不存在,根據(jù)id查詢數(shù)據(jù)庫
R r = dbFallback.apply(id);
// 5.不存在,返回錯誤
if (r == null) {
// 將空值寫入redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回錯誤信息
return null;
}
// 6.存在,寫入redis
this.set(key, r, time, unit);
return r;
}
方法4:根據(jù)指定的key查詢緩存,使用邏輯過期的策略來應對緩存擊穿問題。
首先,引入基于redis的setnx(如果沒有則設置)模擬的上鎖和解鎖過程。
專門在redis中保存一個鍵值對來模擬鎖對象,使用setnx,對應于stringRedisTemplate的selfIfAbsent()
方法,僅在緩存中不存在該鍵值對才能設置成功,相當于獲取到鎖對象。
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
private void unlock(String key) {
stringRedisTemplate.delete(key);
}
有了lock函數(shù)和unlock函數(shù),方法4與方法3相比,多了如下操作:
- 獲取到緩存對象時,需要去除邏輯過期“expire”字段,再返回
- 獲取到緩存對象,需要判斷是否邏輯上過期。
- 若緩存過期,在重建時,利用lock上鎖函數(shù),獲取到鎖,開啟新的獨立線程,從數(shù)據(jù)庫中查詢結(jié)果,重建緩存。
- 線程是異步執(zhí)行的,非阻塞,直接返回過期的結(jié)果。
- 沒有獲取到鎖的請求,直接返回過期緩存。
public <R, ID> R queryWithLogicalExpire(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyPrefix + id;
// 1.從redis查詢商鋪緩存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判斷是否存在
if (StrUtil.isBlank(json)) {
// 3.存在,直接返回
return null;
}
// 4.命中,需要先把json反序列化為對象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
LocalDateTime expireTime = redisData.getExpireTime();
// 5.判斷是否過期
if(expireTime.isAfter(LocalDateTime.now())) {
// 5.1.未過期,直接返回店鋪信息
return r;
}
// 5.2.已過期,需要緩存重建
// 6.緩存重建
// 6.1.獲取互斥鎖
String lockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
// 6.2.判斷是否獲取鎖成功
if (isLock){
// 6.3.成功,開啟獨立線程,實現(xiàn)緩存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// 查詢數(shù)據(jù)庫
R newR = dbFallback.apply(id);
// 重建緩存
this.setWithLogicalExpire(key, newR, time, unit);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
// 釋放鎖
unlock(lockKey);
}
});
}
// 6.4.返回過期的商鋪信息
return r;
}
附件商鋪 - GEO數(shù)據(jù)結(jié)構(gòu)
示例
1、添加北京的三個火車站坐標,key為g1
GEOADD g1 116.378248 39.865275 bjn 116.42803 39.903738 bjz 116.322287 39.893729 bjx
在Redis中保存的數(shù)據(jù)結(jié)構(gòu)為ZSET
,經(jīng)緯度被編碼為score哈希值
2、計算北京西到北京南的距離:
GEODIST g1 bjx bjn km
3、搜索天安門(116.397904 39.909005)附件10km內(nèi)所有的火車站,并且按照距離升序排序。
GEODIST g1 FROMLONLAT 116.397904 39.909005 BYRADIUS 10 km
附件商鋪
需求:搜索某個類型的店鋪,按照距離返回店鋪列表:
數(shù)據(jù)庫中的店鋪信息:
Redis存儲思路:
文章來源:http://www.zghlxwxcb.cn/news/detail-534819.html
controller層接口:
service層實現(xiàn):文章來源地址http://www.zghlxwxcb.cn/news/detail-534819.html
到了這里,關于Redis學習(一)數(shù)據(jù)類型、Java中使用redis、緩存概念的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!