記錄一下Redis的學(xué)習(xí)筆記。
一、Redis是什么?
??Redis 是一個(gè)開源(BSD許可)的,內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)系統(tǒng),它可以用作數(shù)據(jù)庫(kù)、緩存和消息中間件。 它支持多種類型的數(shù)據(jù)結(jié)構(gòu),如 字符串(strings),散列(hashes),列表(lists),集合(sets),有序集合(sorted sets)與范圍查詢,bitmaps,hyperloglogs和地理空間(geospatial)索引半徑查詢。Redis 內(nèi)置了復(fù)制(replication),LUA腳本(Lua scripting),LRU驅(qū)動(dòng)事件(LRU eviction),事務(wù)(transactions)和不同級(jí)別的磁盤持久化(persistence),并通過Redis哨兵(Sentinel)和自動(dòng)分區(qū)(Cluster)提供高可用性(highavailability)。
??性能
??下面是官方的bench-mark數(shù)據(jù):
??測(cè)試完成了50個(gè)并發(fā)執(zhí)行100000個(gè)請(qǐng)求。
??設(shè)置和獲取的值是一個(gè)256字節(jié)字符串。
??結(jié)果:讀的速度是110000次/s,寫的速度是81000次/s
??Redis歷史簡(jiǎn)介
??2008年,意大利一家創(chuàng)業(yè)公司Merzia的創(chuàng)始人Salvatore Sanfilippo為了避免MySQL的低性能,親自定做一個(gè)數(shù)據(jù)庫(kù),并于2009年開發(fā)完成,這個(gè)就是Redis。
??從2010年3月15日起,Redis的開發(fā)工作由VMware主持。
??從2013年5月開始,Redis的開發(fā)由Pivotal贊助。
??說明:Pivotal公司是由EMC和VMware聯(lián)合成立的一家新公司。Pivotal希望為新一代的應(yīng)用提供一個(gè)原生的基礎(chǔ),建立在具有領(lǐng)導(dǎo)力的云和網(wǎng)絡(luò)公司不斷轉(zhuǎn)型的IT特性之上。Pivotal的使命是推行這些創(chuàng)新,提供給企業(yè)IT架構(gòu)師和獨(dú)立軟件提供商。
??支持語言

??支持的數(shù)據(jù)類型
??string 、hash 、 list 、set 、sorted set
二、關(guān)系型數(shù)據(jù)庫(kù)與非關(guān)系型數(shù)據(jù)庫(kù)
??關(guān)系型數(shù)據(jù)庫(kù)
??采用關(guān)系模型來組織數(shù)據(jù)的數(shù)據(jù)庫(kù),關(guān)系模型就是二維表格模型。一張二維表的表名就是關(guān)系,二維表中的一行就是一條記錄,二維表中的一列就是一個(gè)字段。
優(yōu)點(diǎn)
- 容易理解
- 方便使用,通用的sql語言
- 易于維護(hù),豐富的完整性(實(shí)體完整性、參照完整性和用戶定義的完整性)大大降低了數(shù)據(jù)冗余和數(shù)據(jù)不一致的概率
缺點(diǎn)
- 磁盤I/O是并發(fā)的瓶頸
- 海量數(shù)據(jù)查詢效率低
- 橫向擴(kuò)展困難,無法簡(jiǎn)單的通過添加硬件和服務(wù)節(jié)點(diǎn)來擴(kuò)展性能和負(fù)載能力,當(dāng)需要對(duì)數(shù)據(jù)庫(kù)進(jìn)行升級(jí)和擴(kuò)展時(shí),需要停機(jī)維護(hù)和數(shù)據(jù)遷移
- 多表的關(guān)聯(lián)查詢以及復(fù)雜的數(shù)據(jù)分析類型的復(fù)雜sql查詢,性能欠佳。因?yàn)橐WCacid,必須按照三范式設(shè)計(jì)
數(shù)據(jù)庫(kù)
??Orcale,Sql Server,MySql,DB2
??非關(guān)系型數(shù)據(jù)庫(kù)
??非關(guān)系型,分布式,一般不保證遵循ACID原則的數(shù)據(jù)存儲(chǔ)系統(tǒng)。鍵值對(duì)存儲(chǔ),結(jié)構(gòu)不固定。
優(yōu)點(diǎn)
- 根據(jù)需要添加字段,不需要多表聯(lián)查。僅需id取出對(duì)應(yīng)的value
- 適用于SNS(社會(huì)化網(wǎng)絡(luò)服務(wù)軟件。比如facebook,微博)
- 嚴(yán)格上講不是一種數(shù)據(jù)庫(kù),而是一種數(shù)據(jù)結(jié)構(gòu)化存儲(chǔ)方法的集合
缺點(diǎn)
- 只適合存儲(chǔ)一些較為簡(jiǎn)單的數(shù)據(jù)
- 不適合復(fù)雜查詢的數(shù)據(jù)
- 不適合持久存儲(chǔ)海量數(shù)據(jù)
數(shù)據(jù)庫(kù)
- K-V:Redis,Memcache
- 文檔:MongoDB
- 搜索:Elasticsearch,Solr
- 可擴(kuò)展性分布式:HBase
??比較
| 內(nèi)容 | 關(guān)系型數(shù)據(jù)庫(kù) | 非關(guān)系型數(shù)據(jù)庫(kù) |
|---|---|---|
| 成本 | 有些需要收費(fèi)(Orcale) | 基本都是開源 |
| 查詢數(shù)據(jù) | 存儲(chǔ)存于硬盤中,速度慢 | 數(shù)據(jù)存于緩存中,速度快 |
| 存儲(chǔ)格式 | 只支持基礎(chǔ)類型 | K-V,文檔,圖片等 |
| 擴(kuò)展性 | 有多表查詢機(jī)制,擴(kuò)展困難 | 數(shù)據(jù)之間沒有耦合,容易擴(kuò)展 |
| 持久性 | 適用持久存儲(chǔ),海量存儲(chǔ) | 不適用持久存儲(chǔ),海量存儲(chǔ) |
| 數(shù)據(jù)一致性 | 事務(wù)能力強(qiáng),強(qiáng)調(diào)數(shù)據(jù)的強(qiáng)一致性 | 事務(wù)能力弱,強(qiáng)調(diào)數(shù)據(jù)的最終一致性 |
三、Redis-cli操作Redis
操作string
- set :添加一條String類型數(shù)據(jù)
- get :獲取一條String類型數(shù)據(jù)
- mset :添加多條String類型數(shù)據(jù)
- mget :獲取多條String類型數(shù)據(jù)

操作hash
- hset :添加一條hash類型數(shù)據(jù)
- hget :獲取一條hash類型數(shù)據(jù)
- hmset :添加多條hash類型數(shù)據(jù)
- hmget :獲取多條hash類型數(shù)據(jù)
- hgetAll :獲取指定所有hash類型數(shù)據(jù)
- hdel :刪除指定hash類型數(shù)據(jù)(一條或多條)

操作list
- lpush :左添加(頭)list類型數(shù)據(jù)
- rpush :右添加(尾)類型數(shù)據(jù)
- lrange : 獲取list類型數(shù)據(jù)start起始下標(biāo) end結(jié)束下標(biāo) 包含關(guān)系
- llen :獲取條數(shù)
- lrem :刪除列表中幾個(gè)指定list類型數(shù)據(jù)

操作set
- sadd :添加set類型數(shù)據(jù)
- smembers :獲取set類型數(shù)據(jù)
- scard :獲取條數(shù)
- srem :刪除數(shù)據(jù)

操作sorted set
sorted set是通過分?jǐn)?shù)值來進(jìn)行排序的,分?jǐn)?shù)值越大,越靠后
- zadd :添加sorted set類型數(shù)據(jù)
- zrange :獲取sorted set類型數(shù)據(jù)
- zcard :獲取條數(shù)
- zrem :刪除數(shù)據(jù)
zadd需要將Float或者Double類型分?jǐn)?shù)值參數(shù),放置在值參數(shù)之前

Redis中以層級(jí)關(guān)系、目錄形式存儲(chǔ)數(shù)據(jù)


設(shè)置key的失效時(shí)間
Redis 有四個(gè)不同的命令可以用于設(shè)置鍵的生存時(shí)間(鍵可以存在多久)或過期時(shí)間(鍵什么時(shí)候會(huì)被刪除) :
- EXPlRE <key> <ttl> :用于將鍵 key 的生存時(shí)間設(shè)置為 ttl 秒。
- PEXPIRE <key> <ttl> :用于將鍵 key 的生存時(shí)間設(shè)置為 ttl 毫秒。
- EXPIREAT <key> < timestamp> :用于將鍵 key 的過期時(shí)間設(shè)置為 timestamp 所指定的秒數(shù)時(shí)間戳。
- PEXPIREAT <key> < timestamp > :用于將鍵 key 的過期時(shí)間設(shè)置為 timestamp 所指定的毫秒數(shù)時(shí)間戳。
TTL :獲取的值為-1說明此 key 沒有設(shè)置有效期,當(dāng)值為-2時(shí)證明過了有效期。
方法一:

方法二:

方法三:
- 第一個(gè)參數(shù): key
- 第二個(gè)參數(shù): value
- 第三個(gè)參數(shù): NX 是不存在時(shí)才set, XX 是存在時(shí)才set
- 第四個(gè)參數(shù): EX 是秒, PX 是毫秒

刪除
- del :用于刪除數(shù)據(jù)(通用,適用于所有數(shù)據(jù)類型)
- hdel :用于刪除hash類型數(shù)據(jù)

??tips:命令為java中方法名,參數(shù):去除括號(hào),引號(hào),將逗號(hào)變空格即可


??zadd需要將Float或者Double類型參數(shù),放置在值參數(shù)之前,在java中則相反。


四、Java操作Redis
創(chuàng)建項(xiàng)目




添加依賴
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.xxxx</groupId>
<artifactId>redisdemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>redisdemo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- spring data redis 組件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<!--
1.x 的版本默認(rèn)采用的連接池技術(shù)是 Jedis
2.0 以上版本默認(rèn)連接池是 Lettuce,
如果采用 Jedis,需要排除 Lettuce 的依賴。
-->
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- jedis 依賴 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!-- web 組件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- test 組件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
配置文件
spring:
redis:
# Redis服務(wù)器地址
host: 192.168.10.100
# Redis服務(wù)器端口
port: 6379
# Redis服務(wù)器密碼
password: root
# 選擇哪個(gè)庫(kù),默認(rèn)0庫(kù)
database: 0
# 連接超時(shí)時(shí)間
timeout: 10000ms
jedis:
pool:
# 最大連接數(shù),默認(rèn)8
max-active: 1024
# 最大連接阻塞等待時(shí)間,單位毫秒,默認(rèn)-1ms
max-wait: 10000ms
# 最大空閑連接,默認(rèn)8
max-idle: 200
# 最小空閑連接,默認(rèn)0
min-idle: 5
Java如何連接Redis?
/**
* 連接Redis
*/
@Test
public void initConn01() {
// 創(chuàng)建jedis對(duì)象,連接redis服務(wù)
Jedis jedis = new Jedis("192.168.10.100", 6379);
// 設(shè)置認(rèn)證密碼
jedis.auth("root");
// 指定數(shù)據(jù)庫(kù) 默認(rèn)是0
jedis.select(1);
// 使用ping命令,測(cè)試連接是否成功
String result = jedis.ping();
System.out.println(result);// 返回PONG
// 添加一條數(shù)據(jù)
jedis.set("username", "zhangsan");
// 獲取一條數(shù)據(jù)
String username = jedis.get("username");
System.out.println(username);
// 釋放資源
if (jedis != null)
jedis.close();
}
通過Redis連接池獲取連接對(duì)象并操作服務(wù)器
/**
* 通過Redis連接池獲取連接對(duì)象
*/
@Test
public void initConn02() {
// 初始化redis客戶端連接池
JedisPool jedisPool = new JedisPool(new JedisPoolConfig(), "192.168.10.100", 6379,
10000, "root");
// 從連接池獲取連接
Jedis jedis = jedisPool.getResource();
// 指定數(shù)據(jù)庫(kù) 默認(rèn)是0
jedis.select(2);
// 使用ping命令,測(cè)試連接是否成功
String result = jedis.ping();
System.out.println(result);// 返回PONG
// 添加一條數(shù)據(jù)
jedis.set("username", "zhangsan");
// 獲取一條數(shù)據(jù)
String username = jedis.get("username");
System.out.println(username);
// 釋放資源
if (jedis != null)
jedis.close();
}
封裝JedisUtil對(duì)外提供連接對(duì)象獲取方法
@Configuration
public class RedisConfig {
//服務(wù)器地址
@Value("${spring.redis.host}")
private String host;
//端口
@Value("${spring.redis.port}")
private int port;
//密碼
@Value("${spring.redis.password}")
private String password;
//超時(shí)時(shí)間
@Value("${spring.redis.timeout}")
private String timeout;
//最大連接數(shù)
@Value("${spring.redis.jedis.pool.max-active}")
private int maxTotal;
//最大連接阻塞等待時(shí)間
@Value("${spring.redis.jedis.pool.max-wait}")
private String maxWaitMillis;
//最大空閑連接
@Value("${spring.redis.jedis.pool.max-idle}")
private int maxIdle;
//最小空閑連接
@Value("${spring.redis.jedis.pool.min-idle}")
private int minIdle;
@Bean
public JedisPool redisPoolFactory(){
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
//注意值的轉(zhuǎn)變
jedisPoolConfig.setMaxWaitMillis(Long.parseLong(maxWaitMillis.substring(0,maxWaitMillis.length()-2)));
//注意屬性名
jedisPoolConfig.setMaxTotal(maxTotal);
jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMinIdle(minIdle);
JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port,
Integer.parseInt(timeout.substring(0,
timeout.length() - 2)), password);
return jedisPool;
}
}
Java操作Redis五種數(shù)據(jù)類型
連接與釋放
@Autowired
private JedisPool jedisPool;
private Jedis jedis = null;
//初始化jedis對(duì)象實(shí)例
@Before
public void initConn(){
jedis = jedisPool.getResource();
}
//釋放資源
@After
public void closeConn(){
if (jedis!=null){
jedis.close();
}
}
操作string
// 1.操作String
@Test
public void testString() {
// 添加一條數(shù)據(jù)
jedis.set("username", "zhangsan");
jedis.set("age", "18");
// 添加多條數(shù)據(jù) 參數(shù)奇數(shù)為key 參數(shù)偶數(shù)為value
jedis.mset("address", "bj", "sex", "1");
// 獲取一條數(shù)據(jù)
String username = jedis.get("username");
System.out.println(username);
// 獲取多條數(shù)據(jù)
List<String> list = jedis.mget("username", "age", "address", "sex");
for (String str : list) {
System.out.println(str);
}
// 刪除
//jedis.del("username");
}
操作hash
// 2.操作hash
@Test
public void testHash() {
/*
* 添加一條數(shù)據(jù)
* 參數(shù)一:redis的key
* 參數(shù)二:hash的key
* 參數(shù)三:hash的value
*/
jedis.hset("userInfo", "name", "lisi");
// 添加多條數(shù)據(jù)
Map<String, String> map = new HashMap<>();
map.put("age", "20");
map.put("sex", "1");
jedis.hmset("userInfo", map);
// 獲取一條數(shù)據(jù)
String name = jedis.hget("userInfo", "name");
System.out.println(name);
// 獲取多條數(shù)據(jù)
List<String> list = jedis.hmget("userInfo", "age", "sex");
for (String str : list) {
System.out.println(str);
}
// 獲取Hash類型所有的數(shù)據(jù)
Map<String, String> userMap = jedis.hgetAll("userInfo");
for (Entry<String, String> userInfo : userMap.entrySet()) {
System.out.println(userInfo.getKey() + "--" + userInfo.getValue());
}
// 刪除 用于刪除hash類型數(shù)據(jù)
//jedis.hdel("userInfo", "name");
}
操作list
// 3.操作list
@Test
public void testList() {
// 左添加(上)
// jedis.lpush("students", "Wang Wu", "Li Si");
// 右添加(下)
// jedis.rpush("students", "Zhao Liu");
// 獲取 start起始下標(biāo) end結(jié)束下標(biāo) 包含關(guān)系
List<String> students = jedis.lrange("students", 0, 2);
for (String stu : students) {
System.out.println(stu);
}
// 獲取總條數(shù)
Long total = jedis.llen("students");
System.out.println("總條數(shù):" + total);
// 刪除單條 刪除列表中第一次出現(xiàn)的Li Si
// jedis.lrem("students", 1, "Li Si");
// 刪除多條
// jedis.del("students");
}
操作set
// 4.操作set-無序
@Test
public void testSet() {
// 添加數(shù)據(jù)
jedis.sadd("letters", "aaa", "bbb", "ccc", "ddd", "eee");
// 獲取數(shù)據(jù)
Set<String> letters = jedis.smembers("letters");
for (String letter: letters) {
System.out.println(letter);
}
//獲取總條數(shù)
Long total = jedis.scard("letters");
System.out.println(total);
// 刪除
//jedis.srem("letters", "aaa", "bbb");
}
操作sorted set
// 5.操作sorted set-有序
@Test
public void testSortedSet() {
Map<String, Double> scoreMembers = new HashMap<>();
scoreMembers.put("zhangsan", 7D);
scoreMembers.put("lisi", 3D);
scoreMembers.put("wangwu", 5D);
scoreMembers.put("zhaoliu", 6D);
scoreMembers.put("tianqi", 2D);
// 添加數(shù)據(jù)
jedis.zadd("score", scoreMembers);
// 獲取數(shù)據(jù)
Set<String> scores = jedis.zrange("score", 0, 4);
for (String score: scores) {
System.out.println(score);
}
// 獲取總條數(shù)
Long total = jedis.zcard("score");
System.out.println("總條數(shù):" + total);
// 刪除
//jedis.zrem("score", "zhangsan", "lisi");
}
Redis中以層級(jí)關(guān)系、目錄形式存儲(chǔ)數(shù)據(jù)
// Redis中以層級(jí)關(guān)系、目錄形式存儲(chǔ)數(shù)據(jù)
@Test
public void testdir(){
jedis.set("user:01", "user_zhangsan");
System.out.println(jedis.get("user:01"));
}
設(shè)置key的失效時(shí)間
Redis 有四個(gè)不同的命令可以用于設(shè)置鍵的生存時(shí)間(鍵可以存在多久)或過期時(shí)間(鍵什么時(shí)候會(huì)被刪除) :
- EXPlRE <key> <ttl> :用于將鍵 key 的生存時(shí)間設(shè)置為 ttl 秒。
- PEXPIRE <key> <ttl> :用于將鍵 key 的生存時(shí)間設(shè)置為 ttl 毫秒。
- EXPIREAT <key> < timestamp> :用于將鍵 key 的過期時(shí)間設(shè)置為 timestamp 所指定的秒數(shù)時(shí)間戳。
- PEXPIREAT <key> < timestamp > :用于將鍵 key 的過期時(shí)間設(shè)置為 timestamp 所指定的毫秒數(shù)時(shí)間戳。
TTL :獲取的值為-1說明此 key 沒有設(shè)置有效期,當(dāng)值為-2時(shí)證明過了有效期。
@Test
public void testExpire() {
// 方法一:
jedis.set("code", "test");
jedis.expire("code", 180);// 180秒
jedis.pexpire("code", 180000L);// 180000毫秒
jedis.ttl("code");// 獲取秒
// 方法二:
jedis.setex("code", 180, "test");// 180秒
jedis.psetex("code", 180000L, "test");// 180000毫秒
jedis.pttl("code");// 獲取毫秒
// 方法三:
SetParams setParams = new SetParams();
//不存在的時(shí)候才能設(shè)置成功
// setParams.nx();
// 存在的時(shí)候才能設(shè)置成功
setParams.xx();
//設(shè)置失效時(shí)間,單位秒
// setParams.ex(30);
//查看失效時(shí)間,單位毫秒
setParams.px(30000);
jedis.set("code","test",setParams);
}
獲取所有key&事務(wù)&刪除
// 獲取所有key
@Test
public void testAllKeys() {
// 當(dāng)前庫(kù)key的數(shù)量
System.out.println(jedis.dbSize());
// 當(dāng)前庫(kù)key的名稱
Set<String> keys = jedis.keys("*");
for (String key: keys) {
System.out.println(key);
}
}
// 操作事務(wù)
@Test
public void testMulti() {
Transaction tx = jedis.multi();
// 開啟事務(wù)
tx.set("tel", "10010");
// 提交事務(wù)
// tx.exec();
// 回滾事務(wù)
tx.discard();
}
// 刪除
@Test
public void testDelete() {
// 刪除 通用 適用于所有數(shù)據(jù)類型
jedis.del("score");
}
操作byte
創(chuàng)建SerializeUtil.java
package com.xxxx.util;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
/**
* 序列化工具類
*/
public class SerializeUtil {
/**
* 將java對(duì)象轉(zhuǎn)換為byte數(shù)組 序列化過程
*/
public static byte[] serialize(Object object) {
ObjectOutputStream oos = null;
ByteArrayOutputStream baos = null;
try {
// 序列化
baos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(baos);
oos.writeObject(object);
byte[] bytes = baos.toByteArray();
return bytes;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 將byte數(shù)組轉(zhuǎn)換為java對(duì)象 反序列化
*/
public static Object unserialize(byte[] bytes) {
if(bytes == null)return null;
ByteArrayInputStream bais = null;
try {
// 反序列化
bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais);
return ois.readObject();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
創(chuàng)建User.java
package com.xxxx.entity;
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 9148937431079191022L;
private Integer id;
private String username;
private String password;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
創(chuàng)建JedisTest.java
// 操作byte
@Test
public void testByte() {
User user = new User();
user.setId(2);
user.setUsername("zhangsan");
user.setPassword("123");
// 序列化
byte[] userKey = SerializeUtil.serialize("user:" + user.getId());
byte[] userValue = SerializeUtil.serialize(user);
jedis.set(userKey, userValue);
// 獲取數(shù)據(jù)
byte[] userResult = jedis.get(userKey);
// 反序列化
User u = (User) SerializeUtil.unserialize(userResult);
System.out.println(u);
}
五、Redis搭建主從復(fù)用
?Redis支持主從復(fù)用。數(shù)據(jù)可以從主服務(wù)器向任意數(shù)量的從服務(wù)器上同步,同步使用的是發(fā)布/訂閱機(jī)制。Master
Slave的模式,從Slave向Master發(fā)起SYNC命令。
?可以是1 Master 多Slave,可以分層,Slave下可以再接Slave,可擴(kuò)展成樹狀結(jié)構(gòu)。
?因?yàn)闆]有兩臺(tái)電腦,所以只能在一臺(tái)機(jī)器上搭建兩個(gè)Redis服務(wù)端。
?這里使用單機(jī)來模擬redis 主從服務(wù)器 ,實(shí)現(xiàn)讀寫分離配置。
讀寫分離
??創(chuàng)建三個(gè)目錄(數(shù)據(jù)文件、日志文件、配置文件)

??復(fù)制redis.conf至/opt/redis/conf目錄下

??修改redis-common.conf公共配置文件
?? 注釋掉bind 127.0.0.1
?? 關(guān)閉保護(hù)模式,修改為no
?? 注釋公共配置端口

?? 修改為后臺(tái)啟動(dòng)

?? 注釋進(jìn)程編號(hào)記錄文件

??注釋公共配置日志文件

??注釋公共配置數(shù)據(jù)文件、修改數(shù)據(jù)文件路徑
??在默認(rèn)情況下,Redis 將數(shù)據(jù)庫(kù)快照保存在名字為 dump.rdb 的二進(jìn)制文件中。當(dāng)然,這里可以通過修改redis.conf 配置文件來對(duì)數(shù)據(jù)存儲(chǔ)條件進(jìn)行定義,規(guī)定在“ N 秒內(nèi)數(shù)據(jù)集至少有 M 個(gè)改動(dòng)”這一條件被滿足時(shí),自動(dòng)保存一次數(shù)據(jù)集。也可以通過調(diào)用save 或bgsave ,手動(dòng)讓Redis進(jìn)行數(shù)據(jù)集保存操作dbfilename和dir組合使用,dbfilename找dir路徑生成數(shù)據(jù)文件。

??添加從服務(wù)器訪問主服務(wù)器認(rèn)證
??添加訪問認(rèn)證

??注釋公共配置追加文件
??根據(jù)需求配置是否打開追加文件選項(xiàng)
??appendonly yes -> 每當(dāng)Redis執(zhí)行一個(gè)改變數(shù)據(jù)集的命令時(shí)(比如 SET),這個(gè)命令就會(huì)被追加到 AOF 文件的
末尾。這樣的話,當(dāng)Redis重新啟時(shí),程序就可以通過重新執(zhí)行 AOF文件中的命令來達(dá)到重建數(shù)據(jù)集的目的

??appendfilename和dir組合使用,找dir(/opt/redis/data)路徑生成數(shù)據(jù)文件

??從服務(wù)器默認(rèn)是只讀不允許寫操作(不用修改)

??添加3個(gè)服務(wù)的私有配置文件
??touch 或者 vi 都可以創(chuàng)建空白文件
??touch 直接創(chuàng)建空白文件, vi 創(chuàng)建并且進(jìn)入編輯模式, :wq 創(chuàng)建成功,否則不創(chuàng)建

??編輯redis-6379.conf
#引用公共配置
include /opt/redis/conf/redis-common.conf
#進(jìn)程編號(hào)記錄文件
pidfile /var/run/redis-6379.pid
#進(jìn)程端口號(hào)
port 6379
#日志記錄文件
logfile "/opt/redis/log/redis-6379.log"
#數(shù)據(jù)記錄文件
dbfilename dump-6379.rdb
#追加文件名稱
appendfilename "appendonly-6379.aof"
#下面的配置無需在6379里配置
#備份服務(wù)器從屬于6379推薦配置配局域網(wǎng)IP
slaveof 192.168.10.100 6379
??復(fù)制redis-6379.conf的內(nèi)容至redis-6380.conf,redis-6381.conf并且修改其內(nèi)容,將6379替換即可。
??運(yùn)行3個(gè)redis進(jìn)程

??查看redis服務(wù)器主從狀態(tài)
??redis-6379

??redis-6380

??redis-6381

??在主服務(wù)器下添加數(shù)據(jù) 并測(cè)試從服務(wù)器數(shù)據(jù)是否正常顯示


??從服務(wù)器只讀,不允許寫操作

主備切換
??主從節(jié)點(diǎn)redis.conf配置
??參照 讀寫分離 的相應(yīng)配置
??修改sentinel-common.conf 哨兵公共配置文件
??從redis解壓目錄下復(fù)制sentinel.conf至/opt/redis/conf/
cp sentinel.conf /opt/redis/conf/sentinel-common.conf

??注釋哨兵監(jiān)聽進(jìn)程端口號(hào)

??指示 Sentinel 去監(jiān)視一個(gè)名為 master 的主服務(wù)器,這個(gè)主服務(wù)器的IP地址為 127.0.0.1,端口號(hào)為6379,而將這個(gè)主服務(wù)器判斷為失效至少需要1個(gè)(一般設(shè)置為2個(gè))。 Sentinel 同意 (只要同意 Sentinel 的數(shù)量不達(dá)標(biāo),自動(dòng)故障遷移就不會(huì)執(zhí)行)。
??這個(gè)要配局域網(wǎng)IP,否則遠(yuǎn)程連不上。

??設(shè)置master和slaves的密碼

??Sentinel 認(rèn)為服務(wù)器已經(jīng)斷線所需的毫秒數(shù)

??若 sentinel 在該配置值內(nèi)未能完成 failover 操作(即故障時(shí)master/slave自動(dòng)切換),則認(rèn)為本次 failover失敗。

??關(guān)閉保護(hù)模式,修改為no

??修改為后臺(tái)啟動(dòng)

??添加3個(gè)哨兵的私有配置文件
??touch 或者 vi 都可以創(chuàng)建空白文件
??touch 直接創(chuàng)建空白文件, vi 創(chuàng)建并且進(jìn)入編輯模式, :wq 創(chuàng)建成功,否則不創(chuàng)建

??編輯sentinel-26379.conf
#引用公共配置
include /opt/redis/conf/sentinel-common.conf
#進(jìn)程端口號(hào)
port 26379
#進(jìn)程編號(hào)記錄文件
pidfile /var/run/sentinel-26379.pid
#日志記錄文件(為了方便查看日志,先注釋掉,搭好環(huán)境后再打開)
logfile "/opt/redis/log/sentinel-26379.log"
復(fù)制 sentinel-26379.conf 的內(nèi)容至 sentinel-26380.conf , sentinel-26381.conf 并且修改其內(nèi)容,將26379
替換即可。
??啟動(dòng)測(cè)試
??啟動(dòng)3個(gè)redis服務(wù)
??/usr/local/redis/bin/redis-server /opt/redis/conf/redis-6379.conf
??/usr/local/redis/bin/redis-server /opt/redis/conf/redis-6380.conf
??/usr/local/redis/bin/redis-server /opt/redis/conf/redis-6381.conf

??啟動(dòng)3個(gè)哨兵服務(wù)
??/usr/local/redis/bin/redis-sentinel /opt/redis/conf/sentinel-26379.conf
??/usr/local/redis/bin/redis-sentinel /opt/redis/conf/sentinel-26380.conf
??/usr/local/redis/bin/redis-sentinel /opt/redis/conf/sentinel-26381.conf

??查看主從狀態(tài)
??redis-6379

??redis-6380

??redis-6381

??檢測(cè)哨兵功能是否配置成功
?? kill -9 終止redis-6379,查看哨兵是否選舉了新的主節(jié)點(diǎn)

??已選舉6380為主節(jié)點(diǎn),從節(jié)點(diǎn)目前只有6381

??重新啟動(dòng)6379節(jié)點(diǎn),再次查看主從狀態(tài)
??發(fā)現(xiàn)6379已被發(fā)現(xiàn)且成為從節(jié)點(diǎn)

??6380之前不可以寫操作,現(xiàn)在可以寫操作,因?yàn)橐殉蔀橹鞴?jié)點(diǎn)。
??最后,公共配置文件修改為后臺(tái)啟動(dòng),私有配置文件打開日志記錄文件,環(huán)境搭建成功
六、SpringDataRedis
創(chuàng)建項(xiàng)目




添加依賴
<dependencies>
<!-- spring data redis 組件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- commons-pool2 對(duì)象池依賴 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- web 組件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- test 組件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
添加application.yml配置文件
spring:
redis:
# Redis服務(wù)器地址
host: 192.168.10.100
# Redis服務(wù)器端口
port: 6379
# Redis服務(wù)器端口
password: root
# Redis服務(wù)器端口
database: 0
# 連接超時(shí)時(shí)間
timeout: 10000ms
lettuce:
pool:
# 最大連接數(shù),默認(rèn)8
max-active: 1024
# 最大連接阻塞等待時(shí)間,單位毫秒,默認(rèn)-1ms
max-wait: 10000ms
# 最大空閑連接,默認(rèn)8
max-idle: 200
# 最小空閑連接,默認(rèn)0
min-idle: 5
Lettuce和Jedis的區(qū)別
??Jedis 是一個(gè)優(yōu)秀的基于 Java 語言的 Redis 客戶端,但是,其不足也很明顯: Jedis 在實(shí)現(xiàn)上是直接連接 RedisServer,在多個(gè)線程間共享一個(gè) Jedis 實(shí)例時(shí)是線程不安全的,如果想要在多線程場(chǎng)景下使用 Jedis ,需要使用連接池,每個(gè)線程都使用自己的 Jedis 實(shí)例,當(dāng)連接數(shù)量增多時(shí),會(huì)消耗較多的物理資源。Lettuce 則完全克服了其線程不安全的缺點(diǎn):Lettuce 是基于 Netty 的連接(StatefulRedisConnection),Lettuce 是一個(gè)可伸縮的線程安全的 Redis 客戶端,支持同步、異步和響應(yīng)式模式。多個(gè)線程可以共享一個(gè)連接實(shí)例,而不必?fù)?dān)心多線程并發(fā)問題。它基于優(yōu)秀 Netty NIO 框架構(gòu)建,支持 Redis 的高級(jí)功能,如 Sentinel,集群,流水線,自動(dòng)重新連接和 Redis 數(shù)據(jù)模型。
測(cè)試環(huán)境是否搭建成功
@RunWith(SpringRunner.class)
@SpringBootTest(classes = SpringDataRedisApplication.class)
public class SpringDataRedisApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
public void initconn() {
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
ops.set("username","lisi");
ValueOperations<String, String> value = redisTemplate.opsForValue();
value.set("name","wangwu");
System.out.println(ops.get("name"));
}
}
自定義模板解決序列化問題
??默認(rèn)情況下的模板 RedisTemplate<Object, Object>,默認(rèn)序列化使用的是JdkSerializationRedisSerializer ,存儲(chǔ)二進(jìn)制字節(jié)碼。這時(shí)需要自定義模板,當(dāng)自定義模板后又想存儲(chǔ)String 字符串時(shí),可以使StringRedisTemplate的方式,他們倆并不沖突。
??序列化問題:要把 domain object 做為 key-value 對(duì)保存在 redis 中,就必須要解決對(duì)象的序列化問題。
??Spring Data Redis給我們提供了一些現(xiàn)成的方案:
??JdkSerializationRedisSerializer 使用JDK提供的序列化功能。 優(yōu)點(diǎn)是反序列化時(shí)不需要提供類型信息(class),但缺點(diǎn)是序列化后的結(jié)果非常龐大,是JSON格式的5倍左右,這樣就會(huì)消耗 Redis 服務(wù)器的大量?jī)?nèi)存。
??Jackson2JsonRedisSerializer 使用 Jackson 庫(kù)將對(duì)象序列化為JSON字符串。優(yōu)點(diǎn)是速度快,序列化后的字符串短小精悍。但缺點(diǎn)也非常致命,那就是此類的構(gòu)造函數(shù)中有一個(gè)類型參數(shù),必須提供要序列化對(duì)象的類型信息(.class對(duì)象)。通過查看源代碼,發(fā)現(xiàn)其只在反序列化過程中用到了類型信息。
??GenericJackson2JsonRedisSerializer 通用型序列化,這種序列化方式不用自己手動(dòng)指定對(duì)象的 Class。
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,Object> redisTemplate(LettuceConnectionFactory
redisConnectionFactory){
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
//為string類型key設(shè)置序列器
redisTemplate.setKeySerializer(new StringRedisSerializer());
//為string類型value設(shè)置序列器
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
//為hash類型key設(shè)置序列器
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
//為hash類型value設(shè)置序列器
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
}
//序列化
@Test
public void testSerial(){
User user = new User();
user.setId(1);
user.setUsername("張三");
user.setPassword("111");
ValueOperations<String, Object> value = redisTemplate.opsForValue();
value.set("userInfo",user);
System.out.println(value.get("userInfo"));
}
操作string
// 1.操作String
@Test
public void testString() {
ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
// 添加一條數(shù)據(jù)
valueOperations.set("username", "zhangsan");
valueOperations.set("age", "18");
// redis中以層級(jí)關(guān)系、目錄形式存儲(chǔ)數(shù)據(jù)
valueOperations.set("user:01", "lisi");
valueOperations.set("user:02", "wangwu");
// 添加多條數(shù)據(jù)
Map<String, String> userMap = new HashMap<>();
userMap.put("address", "bj");
userMap.put("sex", "1");
valueOperations.multiSet(userMap);
// 獲取一條數(shù)據(jù)
Object username = valueOperations.get("username");
System.out.println(username);
// 獲取多條數(shù)據(jù)
List<String> keys = new ArrayList<>();
keys.add("username");
keys.add("age");
keys.add("address");
keys.add("sex");
List<Object> resultList = valueOperations.multiGet(keys);
for (Object str : resultList) {
System.out.println(str);
}
// 刪除
redisTemplate.delete("username");
}
操作hash
// 2.操作Hash
@Test
public void testHash() {
HashOperations<String, String, String> hashOperations = redisTemplate.opsForHash();
/*
* 添加一條數(shù)據(jù)
* 參數(shù)一:redis的key
* 參數(shù)二:hash的key
* 參數(shù)三:hash的value
*/
hashOperations.put("userInfo","name","lisi");
// 添加多條數(shù)據(jù)
Map<String, String> map = new HashMap();
map.put("age", "20");
map.put("sex", "1");
hashOperations.putAll("userInfo", map);
// 獲取一條數(shù)據(jù)
String name = hashOperations.get("userInfo", "name");
System.out.println(name);
// 獲取多條數(shù)據(jù)
List<String> keys = new ArrayList<>();
keys.add("age");
keys.add("sex");
List<String> resultlist =hashOperations.multiGet("userInfo", keys);
for (String str : resultlist) {
System.out.println(str);
}
// 獲取Hash類型所有的數(shù)據(jù)
Map<String, String> userMap = hashOperations.entries("userInfo");
for (Entry<String, String> userInfo : userMap.entrySet()) {
System.out.println(userInfo.getKey() + "--" + userInfo.getValue());
}
// 刪除 用于刪除hash類型數(shù)據(jù)
hashOperations.delete("userInfo", "name");
}
操作list
// 3.操作list
@Test
public void testList() {
ListOperations<String, Object> listOperations = redisTemplate.opsForList();
// 左添加(上)
// listOperations.leftPush("students", "Wang Wu");
// listOperations.leftPush("students", "Li Si");
// 左添加(上) 把value值放到key對(duì)應(yīng)列表中pivot值的左面,如果pivot值存在的話
//listOperations.leftPush("students", "Wang Wu", "Li Si");
// 右添加(下)
// listOperations.rightPush("students", "Zhao Liu");
// 獲取 start起始下標(biāo) end結(jié)束下標(biāo) 包含關(guān)系
List<Object> students = listOperations.range("students", 0,2);
for (Object stu : students) {
System.out.println(stu);
}
// 根據(jù)下標(biāo)獲取
Object stu = listOperations.index("students", 1);
System.out.println(stu);
// 獲取總條數(shù)
Long total = listOperations.size("students");
System.out.println("總條數(shù):" + total);
// 刪除單條 刪除列表中存儲(chǔ)的列表中幾個(gè)出現(xiàn)的Li Si。
listOperations.remove("students", 1, "Li Si");
// 刪除多條
redisTemplate.delete("students");
}
操作set
// 4.操作set-無序
@Test
public void testSet() {
SetOperations<String, Object> setOperations = redisTemplate.opsForSet();
// 添加數(shù)據(jù)
String[] letters = new String[]{"aaa", "bbb", "ccc", "ddd", "eee"};
//setOperations.add("letters", "aaa", "bbb", "ccc", "ddd", "eee");
setOperations.add("letters", letters);
// 獲取數(shù)據(jù)
Set<Object> let = setOperations.members("letters");
for (Object letter: let) {
System.out.println(letter);
}
// 刪除
setOperations.remove("letters", "aaa", "bbb");
}
操作sorted set
// 5.操作sorted set-有序
@Test
public void testSortedSet() {
ZSetOperations<String, Object> zSetOperations = redisTemplate.opsForZSet();
ZSetOperations.TypedTuple<Object> objectTypedTuple1 =
new DefaultTypedTuple<Object>("zhangsan", 7D);
ZSetOperations.TypedTuple<Object> objectTypedTuple2 =
new DefaultTypedTuple<Object>("lisi", 3D);
ZSetOperations.TypedTuple<Object> objectTypedTuple3 =
new DefaultTypedTuple<Object>("wangwu", 5D);
ZSetOperations.TypedTuple<Object> objectTypedTuple4 =
new DefaultTypedTuple<Object>("zhaoliu", 6D);
ZSetOperations.TypedTuple<Object> objectTypedTuple5 =
new DefaultTypedTuple<Object>("tianqi", 2D);
Set<ZSetOperations.TypedTuple<Object>> tuples = new
HashSet<ZSetOperations.TypedTuple<Object>>();
tuples.add(objectTypedTuple1);
tuples.add(objectTypedTuple2);
tuples.add(objectTypedTuple3);
tuples.add(objectTypedTuple4);
tuples.add(objectTypedTuple5);
// 添加數(shù)據(jù)
zSetOperations.add("score", tuples);
// 獲取數(shù)據(jù)
Set<Object> scores = zSetOperations.range("score", 0, 4);
for (Object score: scores) {
System.out.println(score);
}
// 獲取總條數(shù)
Long total = zSetOperations.size("score");
System.out.println("總條數(shù):" + total);
// 刪除
zSetOperations.remove("score", "zhangsan", "lisi");
}
獲取所有key&刪除
// 獲取所有key
@Test
public void testAllKeys() {
// 當(dāng)前庫(kù)key的名稱
Set<String> keys = redisTemplate.keys("*");
for (String key: keys) {
System.out.println(key);
}
}
// 刪除
@Test
public void testDelete() {
// 刪除 通用 適用于所有數(shù)據(jù)類型
redisTemplate.delete("score");
}
設(shè)置key的失效時(shí)間
@Test
public void testEx() {
ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
// 方法一:插入一條數(shù)據(jù)并設(shè)置失效時(shí)間
valueOperations.set("code", "abcd", 180, TimeUnit.SECONDS);
// 方法二:給已存在的key設(shè)置失效時(shí)間
boolean flag = redisTemplate.expire("code", 180, TimeUnit.SECONDS);
// 獲取指定key的失效時(shí)間
Long l = redisTemplate.getExpire("code");
}
SpringDataRedis整合使用哨兵機(jī)制
??編輯application.yml
spring:
redis:
# Redis服務(wù)器地址
host: 192.168.10.100
# Redis服務(wù)器端口
port: 6379
# Redis服務(wù)器端口
password: root
# Redis服務(wù)器端口
database: 0
# 連接超時(shí)時(shí)間
timeout: 10000ms
lettuce:
pool:
# 最大連接數(shù),默認(rèn)8
max-active: 1024
# 最大連接阻塞等待時(shí)間,單位毫秒,默認(rèn)-1ms
max-wait: 10000ms
# 最大空閑連接,默認(rèn)8
max-idle: 200
# 最小空閑連接,默認(rèn)0
min-idle: 5
#哨兵模式
sentinel:
#主節(jié)點(diǎn)名稱
master: mymaster
#節(jié)點(diǎn)
nodes: 192.168.10.100:26379,192.168.10.100:26380,192.168.10.100:26381
??Bean注解配置
@Bean
public RedisSentinelConfiguration redisSentinelConfiguration(){
RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
// 主節(jié)點(diǎn)名稱
.master("mymaster")
// 主從服務(wù)器地址
.sentinel("192.168.10.100", 26379)
.sentinel("192.168.10.100", 26380)
.sentinel("192.168.10.100", 26381);
// 設(shè)置密碼
sentinelConfig.setPassword("root");
return sentinelConfig;
}
七、如何應(yīng)對(duì)緩存穿透、緩存擊穿、緩存雪崩問題
Key的過期淘汰機(jī)制
??Redis可以對(duì)存儲(chǔ)在Redis中的緩存數(shù)據(jù)設(shè)置過期時(shí)間,比如我們獲取的短信驗(yàn)證碼一般十分鐘過期,我們這時(shí)候就需要在驗(yàn)證碼存進(jìn)Redis時(shí)添加一個(gè)key的過期時(shí)間,但是這里有一個(gè)需要格外注意的問題就是:并非key過期時(shí)間到了就一定會(huì)被Redis給刪除。
定期刪除
??Redis 默認(rèn)是每隔 100ms 就隨機(jī)抽取一些設(shè)置了過期時(shí)間的 Key,檢查其是否過期,如果過期就刪除。為什么是隨機(jī)抽取而不是檢查所有key?因?yàn)槟闳绻O(shè)置的key成千上萬,每100毫秒都將所有存在的key檢查一遍,會(huì)給CPU帶來比較大的壓力。
惰性刪除
??定期刪除由于是隨機(jī)抽取可能會(huì)導(dǎo)致很多過期 Key 到了過期時(shí)間并沒有被刪除。所以用戶在從緩存獲取數(shù)據(jù)的時(shí)候,redis會(huì)檢查這個(gè)key是否過期了,如果過期就刪除這個(gè)key。這時(shí)候就會(huì)在查詢的時(shí)候?qū)⑦^期key從緩存中清除。
內(nèi)存淘汰機(jī)制
??僅僅使用定期刪除 + 惰性刪除機(jī)制還是會(huì)留下一個(gè)嚴(yán)重的隱患:如果定期刪除留下了很多已經(jīng)過期的key,而且用戶長(zhǎng)時(shí)間都沒有使用過這些過期key,導(dǎo)致過期key無法被惰性刪除,從而導(dǎo)致過期key一直堆積在內(nèi)存里,最終造成Redis內(nèi)存塊被消耗殆盡。那這個(gè)問題如何解決呢?這個(gè)時(shí)候Redis內(nèi)存淘汰機(jī)制應(yīng)運(yùn)而生了。Redis內(nèi)存淘汰機(jī)制提供了6種數(shù)據(jù)淘汰策略:
- volatile-lru :從已設(shè)置過期時(shí)間的數(shù)據(jù)集中挑選最近最少使用的數(shù)據(jù)淘汰。
- volatile-ttl :從已設(shè)置過期時(shí)間的數(shù)據(jù)集中挑選將要過期的數(shù)據(jù)淘汰。
- volatile-random :從已設(shè)置過期時(shí)間的數(shù)據(jù)集中任意選擇數(shù)據(jù)淘汰。
- allkeys-lru :當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時(shí)移除最近最少使用的key。
- allkeys-random :從數(shù)據(jù)集中任意選擇數(shù)據(jù)淘汰。
- no-enviction(默認(rèn)) :當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時(shí),新寫入操作會(huì)報(bào)錯(cuò)。
??一般情況下,推薦使用 volatile-lru 策略,對(duì)于配置信息等重要數(shù)據(jù),不應(yīng)該設(shè)置過期時(shí)間,這樣Redis就永遠(yuǎn)不會(huì)淘汰這些重要數(shù)據(jù)。對(duì)于一般數(shù)據(jù)可以添加一個(gè)緩存時(shí)間,當(dāng)數(shù)據(jù)失效則請(qǐng)求會(huì)從DB中獲取并重新存入Redis中。
緩存擊穿
??首先我們來看下請(qǐng)求是如何取到數(shù)據(jù)的:當(dāng)接收到用戶請(qǐng)求,首先先嘗試從Redis緩存中獲取到數(shù)據(jù),如果緩存中能取到數(shù)據(jù)則直接返回結(jié)果,當(dāng)緩存中不存在數(shù)據(jù)時(shí)從DB獲取數(shù)據(jù),如果數(shù)據(jù)庫(kù)成功取到數(shù)據(jù),則更新Redis,然后返回?cái)?shù)據(jù)

??定義:高并發(fā)的情況下,某個(gè)熱門key突然過期,導(dǎo)致大量請(qǐng)求在Redis未找到緩存數(shù)據(jù),進(jìn)而全部去訪問DB請(qǐng)求數(shù)據(jù),引起DB壓力瞬間增大。
??解決方案:緩存擊穿的情況下一般不容易造成DB的宕機(jī),只是會(huì)造成對(duì)DB的周期性壓力。對(duì)緩存擊穿的解決方案一般可以這樣:
??Redis中的數(shù)據(jù)不設(shè)置過期時(shí)間,然后在緩存的對(duì)象上添加一個(gè)屬性標(biāo)識(shí)過期時(shí)間,每次獲取到數(shù)據(jù)時(shí),校驗(yàn)對(duì)象中的過期時(shí)間屬性,如果數(shù)據(jù)即將過期,則異步發(fā)起一個(gè)線程主動(dòng)更新緩存中的數(shù)據(jù)。但是這種方案可能會(huì)導(dǎo)致有些請(qǐng)求會(huì)拿到過期的值,就得看業(yè)務(wù)能否可以接受,如果要求數(shù)據(jù)必須是新數(shù)據(jù),則最好的方案則為熱點(diǎn)數(shù)據(jù)設(shè)置為永不過期,然后加一個(gè)互斥鎖保證緩存的單線程寫。
緩存穿透
??定義:緩存穿透是指查詢緩存和DB中都不存在的數(shù)據(jù)。比如通過id查詢商品信息,id一般大于0,攻擊者會(huì)故意傳id為-1去查詢,由于緩存是不命中則從DB中獲取數(shù)據(jù),這將會(huì)導(dǎo)致每次緩存都不命中數(shù)據(jù)導(dǎo)致每個(gè)請(qǐng)求都訪問DB,造成緩存穿透。
??解決方案:
??利用互斥鎖,緩存失效的時(shí)候,先去獲得鎖,得到鎖了,再去請(qǐng)求數(shù)據(jù)庫(kù)。沒得到鎖,則休眠一段時(shí)間重試
??采用異步更新策略,無論key是否取到值,都直接返回。value值中維護(hù)一個(gè)緩存失效時(shí)間,緩存如果過期,異步起一個(gè)線程去讀數(shù)據(jù)庫(kù),更新緩存。需要做緩存預(yù)熱(項(xiàng)目啟動(dòng)前,先加載緩存)操作。提供一個(gè)能迅速判斷請(qǐng)求是否有效的攔截機(jī)制,比如,利用布隆過濾器,內(nèi)部維護(hù)一系列合法有效的key。迅速判斷出,請(qǐng)求所攜帶的Key是否合法有效。如果不合法,則直接返回。
如果從數(shù)據(jù)庫(kù)查詢的對(duì)象為空,也放入緩存,只是設(shè)定的緩存過期時(shí)間較短,比如設(shè)置為60秒。
緩存雪崩
??定義:緩存中如果大量緩存在一段時(shí)間內(nèi)集中過期了,這時(shí)候會(huì)發(fā)生大量的緩存擊穿現(xiàn)象,所有的請(qǐng)求都落在了DB上,由于查詢數(shù)據(jù)量巨大,引起DB壓力過大甚至導(dǎo)致DB宕機(jī)。
??解決方案:
??給緩存的失效時(shí)間,加上一個(gè)隨機(jī)值,避免集體失效。如果Redis是集群部署,將熱點(diǎn)數(shù)據(jù)均勻分布在不同的Redis庫(kù)中也能避免全部失效的問題
??使用互斥鎖,但是該方案吞吐量明顯下降了。
??設(shè)置熱點(diǎn)數(shù)據(jù)永遠(yuǎn)不過期。
??雙緩存。我們有兩個(gè)緩存,緩存A和緩存B。緩存A的失效時(shí)間為20分鐘,緩存B不設(shè)失效時(shí)間。自己做緩存預(yù)熱操作。然后細(xì)分以下幾個(gè)小點(diǎn)
????1. 從緩存A讀數(shù)據(jù)庫(kù),有則直接返回
????2. A沒有數(shù)據(jù),直接從B讀數(shù)據(jù),直接返回,并且異步啟動(dòng)一個(gè)更新線程。
????3. 更新線程同時(shí)更新緩存A和緩存B。