.im/post/626d
今天,我不自量力的面試了某大廠的java開發崗位,迎面走來一位風塵仆仆的中年男子,手里拿著屏幕還亮著的mac,他沖著我禮貌的笑了笑,然后說了句“不好意思,讓你久等了”,然后示意我坐下,說:“我們開始吧。看了你的簡歷,覺得你對redis應該掌握的不錯,我們今天就來討論下redis......”。我想:“來就來,兵來將擋水來土掩”。
Redis是什么五種數據類型
我:我之前總結了一張圖,關于數據類型的應用場景,如果您感興趣,可以去我的掘金看。。
數據類型應用場景總結類型簡介特性場景
(字符串)
二進制安全
可以包含任何數據,比如jpg圖片或者序列化對象
---
Hash(字典)
鍵值對集合,即編程語言中的map類型
適合存儲對象,并且可以像數據庫中的一個屬性一樣只修改某一項屬性值
存儲、讀取、修改用戶屬性
List(列表)
鏈表(雙向鏈表)
增刪快,提供了操作某一元素的api
最新消息排行;消息隊列
set(集合)
hash表實現,元素不重復
添加、刪除、查找的復雜度都是O(1),提供了求交集、并集、差集的操作
共同好友;利用唯一性,統計訪問網站的所有Ip
set(有序集合)
將set中的元素增加一個權重參數score,元素按score有序排列
數據插入集合時,已經進行了天然排序
排行榜;帶權重的消息隊列
Redis緩存
org.springframework.boot
spring-boot-starter-data-redis
org.apache.commons
commons-pool2
org.springframework.boot
spring-boot-starter-web
org.springframework.session
spring-session-data-redis
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
server:
port: 8082
servlet:
session:
timeout: 30ms
spring:
cache:
type: redis
redis:
host: 127.0.0.1
port: 6379
password:
# redis默認情況下有16個分片,這里配置具體使用的分片,默認為0
database: 0
lettuce:
pool:
# 連接池最大連接數(使用負數表示沒有限制),默認8
max-active: 100
創建實體類User.java
public class User implements Serializable{
private static final long serialVersionUID = 662692455422902539L;
private Integer id;
private String name;
private Integer age;
public User() {
}
public User(Integer id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
的使用方式
默認情況下的模板只能支持,也就是只能存入字符串,所以自定義模板很有必要。添加配置類.java
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisCacheConfig {
@Bean
public RedisTemplate redisCacheTemplate(LettuceConnectionFactory connectionFactory) {
RedisTemplate template = new RedisTemplate<>();
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setConnectionFactory(connectionFactory);
return template;
}
}
測試類
@RestController
@RequestMapping("/user")
public class UserController {
public static Logger logger = LogManager.getLogger(UserController.class);
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedisTemplate redisCacheTemplate;
@RequestMapping("/test")
public void test() {
redisCacheTemplate.opsForValue().set("userkey", new User(1, "張三", 25));
User user = (User) redisCacheTemplate.opsForValue().get("userkey");
logger.info("當前獲取對象:{}", user.toString());
}
然后在瀏覽器訪問,觀察后臺日志 :8082/user/test
使用 cache集成redis
cache具備很好的靈活性,不僅能夠使用SPEL( )來定義緩存的key和各種,還提供了開箱即用的緩存臨時存儲方案,也支持和主流的專業緩存如、Redis、Guava的集成。定義接口.java
public interface UserService {
User save(User user);
void delete(int id);
User get(Integer id);
}
接口實現類.java
@Service
public class UserServiceImpl implements UserService{
public static Logger logger = LogManager.getLogger(UserServiceImpl.class);
private static Map userMap = new HashMap<>();
static {
userMap.put(1, new User(1, "肖戰", 25));
userMap.put(2, new User(2, "王一博", 26));
userMap.put(3, new User(3, "楊紫", 24));
}
@CachePut(value ="user", key = "#user.id")
@Override
public User save(User user) {
userMap.put(user.getId(), user);
logger.info("進入save方法,當前存儲對象:{}", user.toString());
return user;
}
@CacheEvict(value="user", key = "#id")
@Override
public void delete(int id) {
userMap.remove(id);
logger.info("進入delete方法,刪除成功");
}
@Cacheable(value = "user", key = "#id")
@Override
public User get(Integer id) {
logger.info("進入get方法,當前獲取對象:{}", userMap.get(id)==null?null:userMap.get(id).toString());
return userMap.get(id);
}
}
為了方便演示數據庫的操作,這里直接定義了一個Map ,這里的核心是三個注解@、@和@。測試類:
@RestController
@RequestMapping("/user")
public class UserController {
public static Logger logger = LogManager.getLogger(UserController.class);
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedisTemplate redisCacheTemplate;
@Autowired
private UserService userService;
@RequestMapping("/test")
public void test() {
redisCacheTemplate.opsForValue().set("userkey", new User(1, "張三", 25));
User user = (User) redisCacheTemplate.opsForValue().get("userkey");
logger.info("當前獲取對象:{}", user.toString());
}
@RequestMapping("/add")
public void add() {
User user = userService.save(new User(4, "李現", 30));
logger.info("添加的用戶信息:{}",user.toString());
}
@RequestMapping("/delete")
public void delete() {
userService.delete(4);
}
@RequestMapping("/get/{id}")
public void get(@PathVariable("id") String idStr) throws Exception{
if (StringUtils.isBlank(idStr)) {
throw new Exception("id為空");
}
Integer id = Integer.parseInt(idStr);
User user = userService.get(id);
logger.info("獲取的用戶信息:{}",user.toString());
}
}
用緩存要注意,啟動類要加上一個注解開啟緩存
@SpringBootApplication(exclude=DataSourceAutoConfiguration.class)
@EnableCaching
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
1、先調用添加接口::8082/user/add
2、再調用查詢接口,查詢id=4的用戶信息:
可以看出,這里已經從緩存中獲取數據了,因為上一步add方法已經把id=4的用戶數據放入了redis緩存 3、調用刪除方法,刪除id=4的用戶信息,同時清除緩存
4、再次調用查詢接口,查詢id=4的用戶信息:
沒有了緩存,所以進入了get方法,從中獲取。
緩存注解
1、@ 根據方法的請求參數對其結果進行緩存
2、@根據方法的請求參數對其結果進行緩存,和@不同的是,它每次都會觸發真實方法的調用。參數描述見上。
3、@根據條件對緩存進行清空
緩存問題
setRedis(key, value, time+Math.random()*10000);
如果Redis是集群部署,將熱點數據均勻分布在不同的Redis庫中也能避免全部失效。或者設置熱點數據永不過期,有更新操作就更新緩存就好了(比如運維更新了首頁商品,那你刷下緩存就好了,不要設置過期時間),電商首頁的數據也可以用這個操作,保險。
public static String getData(String key) throws InterruptedException {
//從Redis查詢數據
String result = getDataByKV(key);
//參數校驗
if (StringUtils.isBlank(result)) {
try {
//獲得鎖
if (reenLock.tryLock()) {
//去數據庫查詢
result = getDataByDB(key);
//校驗
if (StringUtils.isNotBlank(result)) {
//插進緩存
setDataToKV(key, result);
}
} else {
//睡一會再拿
Thread.sleep(100L);
result = getData(key);
}
} finally {
//釋放鎖
reenLock.unlock();
}
}
return result;
}
Redis為何這么快Redis和的區別淘汰策略策略描述
-lru
從已設置過期時間的KV集中優先對最近最少使用(less used)的數據淘汰
-ttl
從已設置過期時間的KV集中優先對剩余時間短(time to live)的數據淘汰
-
從已設置過期時間的KV集中隨機選擇數據淘汰
-lru
從所有KV集中優先對最近最少使用(less used)的數據淘汰
-
從所有KV集中隨機選擇數據淘汰
不淘汰策略,若超過最大內存,返回錯誤信息
補充一下:.0加入了LFU(least use)淘汰策略,包括-lfu和-lfu,通過統計訪問頻率,將訪問頻率最少,即最不經常使用的KV淘汰。
持久化
appendfsync yes
appendfsync always #每次有數據修改發生時都會寫入AOF文件。
appendfsync everysec #每秒鐘同步一次,該策略為AOF的缺省策略。
AOF可以做到全程持久化,只需要在配置中開啟 yes。這樣redis每執行一個修改數據的命令,都會把它添加到AOF文件中,當redis重啟時,將會讀取AOF文件進行重放,恢復到redis關閉前的最后時刻。
主從復制
上面是psync的執行流程:從節點發送psync[runId][]命令,主節點有三種響應:(1):第一次連接,進行全量復制(2):進行部分復制(3)ERR:不支持psync命令,進行全量復制
關于部分復制有以下幾點說明:1、部分復制主要是Redis針對全量復制的過高開銷做出的一種優化措施,使用psync[runId][]命令實現。當從節點正在復制主節點時,如果出現網絡閃斷或者命令丟失等異常情況時,從節點會向主節點要求補發丟失的命令數據,主節點的復制積壓緩沖區將這部分數據直接發送給從節點,這樣就可以保持主從節點復制的一致性。補發的這部分數據一般遠遠小于全量數據。2、主從連接中斷期間主節點依然響應命令,但因復制連接中斷命令無法發送給從節點,不過主節點內的復制積壓緩沖區依然可以保存最近一段時間的寫命令數據。3、當主從連接恢復后,由于從節點之前保存了自身已復制的偏移量和主節點的運行ID。因此會把它們當做psync參數發送給主節點,要求進行部分復制。4、主節點接收到psync命令后首先核對參數runId是否與自身一致,如果一致,說明之前復制的是當前主節點;之后根據參數在復制積壓緩沖區中查找,如果之后的數據存在,則對從節點發送+命令,表示可以進行部分復制。因為緩沖區大小固定對進程之間信息的復制,若發生緩沖溢出,則進行全量復制。5、主節點根據偏移量把復制積壓緩沖區里的數據發送給從節點,保證主從復制進入正常狀態。
哨兵
1、每個節點都需要定期執行以下任務:每個以每秒一次的頻率,向它所知的主服務器、從服務器以及其他的實例發送一個PING命令。(如上圖)
2、如果一個實例距離最后一次有效回復PING命令的時間超過down-after-所指定的值,那么這個實例會被標記為主觀下線。(如上圖)
3、如果一個主服務器被標記為主觀下線,那么正在監視這個服務器的所有節點,要以每秒一次的頻率確認主服務器的確進入了主觀下線狀態。
4、如果一個主服務器被標記為主觀下線,并且有足夠數量的(至少要達到配置文件指定的數量)在指定的時間范圍內同意這一判斷,那么這個主服務器被標記為客觀下線。
5、一般情況下,每個會以每10秒一次的頻率向它已知的所有主服務器和從服務器發送INFO命令,當一個主服務器被標記為客觀下線時,向下線主服務器的所有從服務器發送INFO命令的頻率,會從10秒一次改為每秒一次。
6、和其他協商客觀下線的主節點的狀態,如果處于SDOWN狀態,則投票自動選出新的主節點,將剩余從節點指向新的主節點進行數據復制。
7、當沒有足夠數量的同意主服務器下線時,主服務器的客觀下線狀態就會被移除。當主服務器重新向的PING命令返回有效回復時,主服務器的主觀下線狀態就會被移除。
總結
本文在一次面試的過程中講述了Redis是什么,Redis的特點和功能,Redis緩存的使用,Redis為什么能這么快,Redis緩存的淘汰策略,持久化的兩種方式,Redis高可用部分的主從復制和哨兵的基本原理。只要功夫深,鐵杵磨成針,平時準備好,面試不用慌。雖然面試不一定是這樣問的,但萬變不離其“宗”。(筆者覺得這種問答形式的博客很不錯,可讀性強而且讀后記的比較深刻)
歡迎加入我的知識星球,一起探討架構,交流源碼。加入方式,長按下方二維碼噢:
已在知識星球更新源碼解析如下:
最近更新《芋道 2.X 入門》系列,已經 20 余篇對進程之間信息的復制,覆蓋了、Redis、、ES、分庫分表、讀寫分離、、、權限、、Dubbo、、、Kafka、性能測試等等內容。
提供近 3W 行代碼的 示例,以及超 4W 行代碼的電商微服務項目。