什麼是Redis?
早期很多互聯網產品在面對高併發時經常出現“響應慢”、“卡住”等用戶體驗差的情況,那是因爲用戶的“讀”請求遠遠多於用戶的“寫”請求,頻繁的讀請求在高併發的情況下會增加數據庫的壓力,爲了減少用戶直接與數據庫的交互,許多系統架構引入了緩存中間件,將用戶頻繁需要讀取的數據放入緩存中,可以有效降低數據庫的壓力。
Redis就是緩存中間件的一種,它是一種基於內存的、採用鍵值對結構化存儲的NoSQL數據庫,其底層採用單線程和多路I/O複用模型,所以Redis的查詢速度很快。
Redis的應用非常廣泛,主要有以下四種應用場景:
- 熱點數據的存儲於展示
“熱點數據”可理解爲大部分用戶頻繁訪問的數據,而且這些數據在某一時刻是相同的,比如:微博熱搜,新聞頭條等等。如果採用查詢數據庫的方法獲取熱點數據,將大大增加數據庫的壓力。 - 最近訪問的數據
“最近訪問的數據”在數據庫中的存儲通常以“時間字段”作爲標記,採用時間字段與當前時間的“時間差”作比較進行查詢,這種方式十分耗時。將“最近訪問的數據”存儲在Redis的列表List中,將大大減少對數據庫的查詢壓力。 - 併發訪問
將某些數據預先存儲在Redis中,每次並發過來的請求可以直接從Redis中獲取,減少高併發訪問給數據庫帶來的壓力。 - 排名
Redis的有序集合zset可以存儲需要排序的數據,避免了數據庫中Order By等查詢方式帶來的性能問題。
Redis的基本命令
本文只介紹Redis的基本命令,主要有四種:
- 查看Redis緩存中所有的key:keys *
- 在Redis中創建一個鍵值對: set key value
- 查看Redis中指定key的值: get key
- 刪除Redis中指定的key:del key
在Spring Boot 中使用Redis
在實際項目中很少直接用命令行去操作Redis,而是需要結合實際業務以代碼的形式去操作Redis,本文結合Spring Boot去實現操作Redis。
首先,在Spring Boot中加入Redis依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
然後,在application.properties中加入Redis的連接配置:
spring.redis.host=127.0.0.1
spring.redis.port=6379
使用RedisTemplate操作五種數據結構
字符串String
實戰一:將字符串寫入緩存中,並讀取出來打印在控制檯上。
@Test
void TestOne() {
//Redis通用操作組件
ValueOperations vo = redisTemplate.opsForValue();
//把鍵值對寫入Redis中
vo.set("key1", "Redis實戰一");
//根據key獲取值並打印到控制檯
Object object = vo.get("key1");
System.out.println(object);
}
Redis實戰一
實戰二:將對象序列化爲字符串寫入緩存,然後反序列化對象並打印在控制檯上。
@Data
public class User{
private Integer id;
private String username;
public User(Integer id, String username) {
this.id = id;
this.username = username;
}
}
@Test
void TestTwo() {
User user = new User(1, "測試用戶");
//Redis通用操作組件
ValueOperations vo = redisTemplate.opsForValue();
//把鍵值對寫入Redis中
vo.set("key2", objectMapper.writeValueAsString(user));
//根據key獲取值並打印到控制檯
Object object = vo.get("key2");
System.out.println(object);
}
{"id":1,"username":"測試用戶"}
列表List
Redis的列表和Java中List很相似,用於存儲一系列具有相同類型的數據。
實戰三:將一組已經排好序的用戶對象列表存儲在緩存中,按照排名的先後順序獲取出來並輸出打印到控制檯上。
@Test
void TestThree() throws JsonProcessingException {
User user1 = new User(1, "用戶1");
User user2 = new User(2, "用戶2");
User user3 = new User(3, "用戶3");
ListOperations lo = redisTemplate.opsForList();
//把List寫入Redis中
lo.leftPush("key3", objectMapper.writeValueAsString(user1));
lo.leftPush("key3", objectMapper.writeValueAsString(user2));
lo.leftPush("key3", objectMapper.writeValueAsString(user3));
//根據key獲取值並打印到控制檯
int count = Math.toIntExact(lo.size("key3"));
for (int i = 0; i < count; i++) {
Object object = lo.rightPop("key3");
System.out.println(object);
}
}
{"id":1,"username":"用戶1"}
{"id":2,"username":"用戶2"}
{"id":3,"username":"用戶3"}
在實際應用場景中,List類型特別適合“排名”、“排行榜”、“近期訪問數據列表”等業務場景。
集合Set
Set用於存儲具有相同類型的不重複的數據,即Set中的數據都是唯一的,其底層的數據結構是通過哈希表來實現的,所以其增刪改查的複雜度均爲O(1)。
實戰三:給定一組用戶姓名列表,要求剔除具有相同姓名的人員並組成新的集合,存放到緩存中並取出打印到控制檯。
void TestFour() {
List<String> list = new ArrayList<>();
list.add("小紅");
list.add("小明");
list.add("小王");
list.add("小明");
SetOperations so = redisTemplate.opsForSet();
for (String username: list) {
so.add("key4", username);
}
int count = Math.toIntExact(so.size("key4"));
for (int i = 0; i < count; i++) {
System.out.println(so.pop("key4"));
}
}
小王
小紅
小明
從結果可以看出,Set類型可以保證存儲的數據唯一但是無序。在實際開發中,Set類型常常用於解決重複提交、剔除重複ID等業務場景。
有序集合Zset
Zset和Set具有某些相同的特性,即存儲的數據不重複、無序、唯一的,而這個無序是指集合的存儲順序並不是按照插入順序來的。而有序集合的有序是指我們可以指定一個屬性來排序。
實戰5:將手機用戶按照充值金額從小到大排序,從大到小排序。
@Test
void TestFive() throws JsonProcessingException {
final String key = "key5";
redisTemplate.delete(key);
List<PhoneUser> phoneUsers = new ArrayList<>();
phoneUsers.add(new PhoneUser(101, 504.0));
phoneUsers.add(new PhoneUser(102, 503.0));
phoneUsers.add(new PhoneUser(103, 502.0));
phoneUsers.add(new PhoneUser(104, 501.0));
ZSetOperations zo = redisTemplate.opsForZSet();
for (PhoneUser phoneUser: phoneUsers) {
zo.add(key, objectMapper.writeValueAsString(phoneUser), phoneUser.getMoney());
}
int count = Math.toIntExact(zo.size(key));
//從小到大排序
System.out.println(zo.range(key, 0L, count));
//從大到小排序
System.out.println(zo.reverseRange(key, 0L, count));
}
[{"phoneNumber":104,"money":501.0}, {"phoneNumber":103,"money":502.0}, {"phoneNumber":102,"money":503.0}, {"phoneNumber":101,"money":504.0}]
[{"phoneNumber":101,"money":504.0}, {"phoneNumber":102,"money":503.0}, {"phoneNumber":103,"money":502.0}, {"phoneNumber":104,"money":501.0}]
默認情況下,Zset會根據充值金額從小到大排序。在實際開發中,Zset充值排行榜、積分排行榜以及成績排行榜等。
哈希Hash
Hash類型和Java 的HashMap類型有點類似,由key-value鍵值對組成,而value也有另一個key-value鍵值對組成,相當於Redis中有個小Redis。
實戰六:將學生對象以Hash類型存儲,並指定打印某一名學生的信息。
@Data
public class Student implements Serializable {
private Integer id;
private String name;
private Integer age;
public Student() {
}
public Student(Integer id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}
}
@Test
void TestSix() {
final String key = "student";
List<Student> students = new ArrayList<>();
students.add(new Student(1, "小紅", 10));
students.add(new Student(2, "小名", 11));
students.add(new Student(3, "小網", 12));
HashOperations ho = redisTemplate.opsForHash();
for (Student student: students) {
ho.put(key, student.getId(), student);
}
Student s1 = (Student) ho.get(key, 3);
System.out.println(s1);
}
Student(id=3, name=小網, age=12)
Hash類型適合存儲具有映射關係的類型。在實際開發中,爲了減少Key的數量,可以考慮採用Hash存儲。
Key失效與判斷是否存在
@Test
void TestSeven() throws InterruptedException {
final String key = "seven";
ValueOperations vo = redisTemplate.opsForValue();
vo.set(key, "測試失效", 2, TimeUnit.SECONDS);
System.out.println("是否存在:" + redisTemplate.hasKey(key) + "\t值爲:" + vo.get(key));
Thread.sleep(3 * 1000);
System.out.println("是否存在:" + redisTemplate.hasKey(key) + "\t值爲:" + vo.get(key));
}
是否存在:true 值爲:測試失效
是否存在:false 值爲:null
在實際開發中,常見的業務場景包括:
- 將數據庫查詢到的數據緩存一定的時間TTL,在這段時間內,會從緩存中查詢數據;
- 將數據壓入緩存隊列中,並設置TTL,當時間到,將觸發監聽事件,從而處理相應的邏輯