Redis簡介:Redis 是完全開源免費的,是一個高性能的key-value數據庫,目前市面上主流的no-sql數據庫有Redis、Memcache、Tair(淘寶自研發),Redis的官網:https://redis.io/
之前的博客已經寫過redis的搭建,主從複製以及哨兵機制,本篇博客側重於講解底層原理,實戰等...開始吧那就
1. Redis應用場景
相信我們程序員都用過或者聽過Redis,那麼我們首先談下它有哪些應用場景,博主總結了以下幾點:
① Token令牌的生成
② 短信驗證碼的code
③ 可以實現緩存查詢數據,減輕我們的數據庫的訪問壓力 Redis與mysql數據庫不同步的問題
④ 分佈式鎖
⑤ 延遲操作(案例:訂單超時未支付,也可以用RabbitMQ解決)
⑥ 分佈式消息中間件(發佈訂閱,一般用的很少)
2. Redis線程模型
首先Redis官方是沒有windows版本的,只有Linux版本,那麼問題來了,爲什麼我們Windows本地可以安裝? 答案:一些大牛把Linux版的epoll機制改成Select輪訓,從而發佈到github供我們下載,所以我們Windows上的Redis並不是官方的。
Redis的底層採用NIO中的多路IO複用的機制,能夠非常好的支持這樣的併發,從而保證線程安全問題;Redis單線程,也就是底層採用一個線程維護多個不同的客戶端IO操作。但是Nio在不同的操作系統上實現的方式有所不同,在我們Windows操作系統使用Selector實現輪訓時間複雜度是爲O(N),而且還存在空輪訓的情況,效率非常低, 其次是默認對我們輪訓的數據有一定限制,所以支持上萬的TCP連接是非常難。所以在Linux操作系統採用epoll實現事件驅動回調,不會存在空輪訓的情況,只對活躍的 Socket連接實現主動回調這樣在性能上有大大的提升,所以時間複雜度是爲O(1)。
所以爲什麼單線程的Nginx、Redis都能夠非常高支持高併發,最終都是Linux中的IO多路複用機制epoll。
Windows - 空輪訓,相當於在Selector寫了個for循環,一直在輪訓,萬一別人不給我發數據也去輪訓一下,效率非常低;
Linux - epoll,給每一個TCP連接註冊一個事件回調,一旦有人給我發數據,我就走我的事件回調,沒給我發數據,我就不用去輪訓,不會存在空輪訓的情況,只對活躍的 Socket連接實現主動回調這樣在性能上有大大的提升。
3. Redis - Value的五種數據類型
① String類型 - <key,value> 【用的最多】
String是Redis最基本的類型,一個key對應一個value,String類型是二進制安全的。意思是Redis的String可以包含任何數據。比如Jpg圖片或者序列化的對象, String類型是Redis最基本的數據類型,一個鍵最大能存儲512MB。
命令:set name zhangsan,get name
② hash類型 - <key,<key1,value>> 案例:購物車
我們可以將Redis中的Hash類型看成具有<key,<key1,value>>,其中同一個key可以有多個不同key值的<key1,value>,所以該類型非常適合於存儲值對象的信息。如Username、Password和Age等。如果Hash中包含很少的字段,那麼該類型的數據也將僅佔用很少的磁盤空間。
命令:hmset user name zhangsan 【解讀:key爲user,value爲hashmap<name,zhangsan>】,hgetall user
可以支持多個key-hmset user name zhangsan age 22
③ list類型 - 案例:秒殺時一個庫存對應多個令牌桶token
Redis列表是簡單的字符串列表,按照插入順序排序。你可以添加一個元素到列表的頭部(左邊)或者尾部(右邊)
命令:lpush userlist zhangsan lisi wangwu
④ set - 特點:value不允許重複,如果重複則覆蓋 【基本不用】
Redis 的 Set 是 String 類型的無序集合。集合成員是唯一的,這就意味着集合中不能出現重複的數據。
⑤ zset - 【基本不用】
Redis 有序集合和集合一樣也是string類型元素的集合,且不允許重複的成員。不同的是每個元素都會關聯一個double類型的分數。redis正是通過分數來爲集合中的成員進行從小到大的排序。
拓展:Redis如何存放一個java對象?
答案: ① json(即String類型),② 二進制
方式①實現:Redis Desktop Manager工具不會亂碼
@Component
public class RedisUtils {
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void setString(String key, String value) {
setString(key, value, null);
}
public void setString(String key, String value, Long timeOut) {
stringRedisTemplate.opsForValue().set(key, value);
if (timeOut != null) {
stringRedisTemplate.expire(key, timeOut, TimeUnit.SECONDS);
}
}
public String getString(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
}
@RestController
public class RedisController {
@Autowired
private RedisUtils redisUtils;
@GetMapping("/addUser")
public String addUser(UserEntity userEntity) {
String json = JSONObject.toJSONString(userEntity);
redisUtils.setString("userEntity", json);
return "success";
}
@RequestMapping("/getUser")
public UserEntity getUser(String key) {
String json = redisUtils.getString(key);
UserEntity userEntity = JSONObject.parseObject(json, UserEntity.class);
return userEntity;
}
}
方式②實現:(注意:需要序列化的對象(如UserEntity)一定要實現Serializable接口) Redis Desktop Manager會亂碼
@Component
public class RedisTemplateUtils {
@Resource //@Resource通過名稱注入,注意這裏不能用@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void setObject(String key, Object value) {
setObject(key, value, null);
}
public void setObject(String key, Object value, Long timeOut) {
redisTemplate.opsForValue().set(key, value);
if (timeOut != null) {
redisTemplate.expire(key, timeOut, TimeUnit.SECONDS);
}
}
public Object getObject(String key) {
return redisTemplate.opsForValue().get(key);
}
}
@RestController
public class RedisController {
@Autowired
private RedisTemplateUtils redisTemplateUtils;
@GetMapping("/addUser")
public String addUser(UserEntity userEntity) {
redisTemplateUtils.setObject("userEntity", userEntity);
return "success";
}
@RequestMapping("/getUser")
public UserEntity getUser(String key) {
return (UserEntity) redisTemplateUtils.getObject(key);
}
}
兩種方式區別:二進制只適合於Java對象,json是通用的。
4. Mysql與Redis數據同步解決方案
方式①:當數據庫有數據變動時,直接清空Redis對應的該數據,此次或下次再同步到Redis
方式②:直接採用MQ訂閱MySQL binlog日誌文件增量同步到Redis中,整個過程採用最終一致性方案。類似於之前博客的主從複製
方式③:使用阿里巴巴的開源框架Canal(後期單獨拿一篇博客講述)
5. Redis的持久化機制
Redis因爲某種原因的情況下宕機之後,數據是不會丟失的,原理就是持久化機制 ~
同理,我們用的ehcache也有持久化機制,大部分的緩存框架都會有基本功能:淘汰策略、持久機制。
Redis的持久化的機制有兩種:aof、rdb(默認),下面分別講解:
Redis提供了兩種持久化的機制,分別爲RDB、AOF實現,RDB採用定時(全量)持久化機制,但是服務器因爲某種原因宕機後可能數據會丟失,AOF是基於數據日誌操作實現的持久化(增量)。
RDB:(全量同步)
Redis已經幫助我默認開啓了rdb存儲,以快照的形式將數據持久化到磁盤的是一個二進制的文件dump.rdb, 可以在redis.conf中搜索到,生成的rdb文件爲,存放的目錄爲
Redis會將數據集的快照dump到dump.rdb文件中。此外,我們也可以通過配置文件來修改Redis服務器dump快照的頻率,在打開6379.conf文件之後,我們搜索save,可以看到下面的配置信息:
save 900 1 # 在900秒(15分鐘)之後,如果至少有1個key發生變化,則dump內存快照。
save 300 10 # 在300秒(5分鐘)之後,如果至少有10個key發生變化,則dump內存快照。
save 60 10000 # 在60秒(1分鐘)之後,如果至少有10000個key發生變化,則dump內存快照。
AOF:(增量同步)
AOF在Redis的配置文件中存在三種同步策略,它們分別是:
appendfsync always #每次有數據修改發生時都會寫入AOF文件,能夠保證數據不丟失,但是效率非常低。
appendfsync everysec #每秒鐘同步一次,可能會丟失1s內的數據,但是效率非常高。【默認 - 建議使用】
appendfsync no #從不同步。高效但是數據不會被持久化。
Redis默認沒有開啓aof存儲,那麼如何開啓aof? 非常簡單,只需要在redis.conf中把默認的配置改爲yes即可,aof同步的文件爲
原理:aof是以執行命令的形式實現同步。每次執行都會記錄寫的操作,如set name zhangsan等都會被記錄下來,appendfsync everysec一秒後從緩衝區同步到aof文件中去。
拓展:全量同步(rdb)與增量同步(aof)區別:
① 就是每天定時(避開高峯期)或者是採用一種週期的實現將數據拷貝另外一個地方。頻率不是很大,但是可能會造成數據的丟失。
② 增量同步採用行爲操作對數據實現同步,頻率非常高、對服務器同步的壓力也是非常大的、保證數據不丟失。
6. Redis內存淘汰策略
概念:將Redis用作緩存時,如果內存空間用滿,就會自動驅逐老的數據,防止內存撐爆。
Redis默認有六種淘汰策略:
noeviction:當內存使用達到閾值的時候,執行命令直接報錯
allkeys-lru:在所有的key中,優先移除最近未使用的key。(推薦)
volatile-lru:在設置了過期時間的鍵空間中,優先移除最近未使用的key。
allkeys-random:在所有的key中,隨機移除某個key。
volatile-random:在設置了過期時間的鍵空間中,隨機移除某個key。
volatile-ttl:在設置了過期時間的鍵空間中,具有更早過期時間的key優先移除。
在redis.conf文件中,設置Redis 內存大小的限制,比如:
當數據達到限定大小後,會選擇配置的策略淘汰數據,通過配置maxmemory-policy置Redis的淘汰策略,默認也是注掉的,如果自定義內存大小的話,建議使用allkeys-lru,即。
【關於maxmemory的設置,如果redis的應用場景是作爲db使用,那不要設置這個選項,因爲db是不能容忍丟失數據的。
如果作爲cache使用,則可以啓用這個選項(其實既然有淘汰策略,那就是cache了。。。),默認官方沒有對maxmemory做限制,理論上默認最大內存限制爲當前機器可用內存】
7. Redis自動過期機制
在實際開發過程中經常會遇到一些有時效性數據,比如訂單超時功能,30分鐘未支付應該將訂單改爲已失效狀態。在關係型數據庫中一般都要增加一個字段記錄數據的到期時間,然後通過定時任務週期性的進行檢查,這種方式性能太差了。Redis本身就對鍵過期提供了很好的支持。
實現需求:處理訂單過期自動取消,比如下單30分鐘未支付自動更改訂單狀態爲已失效(超時)
解決方案:① 定時器 ② RabbitMQ的死信/延時隊列 ③ 利用Redis的過期機制
Redis過期機制:在Redis中可以使用expire命令設置一個鍵的存活時間(ttl: time to live),過了這段時間,該鍵就會自動被刪除,EXPIRE命令的使用方法如下:
expire key ttl(單位秒) 【命令返回1表示設置ttl成功,返回0表示鍵不存在或者設置失敗。】
key失效監聽實戰:
把RedisMessageListenerContainer注入到Spring容器
@Configuration
public class RedisListenerConfig {
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
}
編寫Redis監聽器
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
@Autowired
private OrderMapper orderMapper;
/**
* 【監聽】 - 當我們key失效的時候執行該方法
*/
@Override
public void onMessage(Message message, byte[] pattern) {
String expireKey = message.toString();
System.out.println(expireKey + "失效啦~");
// 前綴判斷 - 每個服務,前綴事先商量好,防止衝突 eg:orderToken_XXXXXXX // 或者是根據庫區分
OrderEntity orderEntity = orderMapper.getOrderNumber(expireKey);
if (orderEntity == null) {
return;
}
// 獲取訂單狀態
Integer orderStatus = orderEntity.getOrderStatus();
// 如果該訂單狀態爲待支付的情況下,直接將該訂單修改爲已經超時 (0待支付 1已支付 2已失效)
if (orderStatus == 0) {
orderMapper.updateOrderStatus(expireKey, 2);
// 庫存在加上1...此處省略,自行實現
}
// 不設置超時時間,但是可能被淘汰回收策略回收掉,也會走回調方法
}
}
@Data
public class OrderEntity {
private Long id;
private String orderName;
private Integer orderStatus;
private String orderToken;
private String orderNumber;
public OrderEntity(Long id, String orderName, String orderNumber, String orderToken) {
this.id = id;
this.orderName = orderName;
this.orderNumber = orderNumber;
this.orderToken = orderToken;
}
}
@Mapper
public interface OrderMapper {
@Insert("insert into order_table values (null,#{orderName},0,#{orderToken},#{orderNumber})")
int insertOrder(OrderEntity OrderEntity);
@Select("SELECT id,order_name,order_status,order_token,order_number FROM order_table " +
"where order_token=#{orderToken};")
OrderEntity getOrderNumber(String orderToken);
@Update("update order_table set order_status=#{orderStatus} where order_token=#{orderToken}")
int updateOrderStatus(String orderToken, Integer orderStatus);
}
@Component
public class RedisUtils {
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void setString(String key, String value) {
setString(key, value, null);
}
public void setString(String key, String value, Long timeOut) {
stringRedisTemplate.opsForValue().set(key, value);
if (timeOut != null) {
stringRedisTemplate.expire(key, timeOut, TimeUnit.SECONDS);
}
}
public String getString(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
}
@RestController
public class OrderController {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RedisUtils redisUtils;
@RequestMapping("/addOrder")
public String addOrder() {
// 1.提前生成訂單token 臨時且唯一
String orderToken = UUID.randomUUID().toString();
Long orderNumber = System.currentTimeMillis();
// 2.將我們的token存放到redis中
redisUtils.setString(orderToken, orderNumber + "", 60L); // 60秒
OrderEntity orderEntity = new OrderEntity(null, "騰訊視頻年度vip", orderNumber + "", orderToken);
return orderMapper.insertOrder(orderEntity) > 0 ? "success" : "fail";
}
}
啓動項目,調用addOrder接口,會看到數據庫新增一條數據:
60秒後,redis中的該key刪除,進入監聽器,並根據該key判斷訂單狀態,如果爲1,則不操作,如果依舊爲0,則改爲2:
【原理】:
① 創建訂單的時候綁定一個訂單token 存放在redis中(有效期只有30分鐘) key爲token,value爲訂單編號。
② 對該key綁定過期事件回調,判斷狀態從而修改訂單狀態。
8. Redis緩存雪崩&緩存擊穿&緩存穿透
【緩存穿透】
緩存穿透是值指定key不存在的key,頻繁的高併發查詢,導致緩存是無法命中,如上圖所示,當黑客利用for循環,隨機生成orderNumber去訪問getOrder接口,則每次大概率會穿透Redis進入MySQL數據庫,導致大量IO操作,從而會導致數據庫的壓力非常大。
解決方案:
① 接口實現api的限流、防禦ddos(模擬請求)、接口頻率限制、網關實現黑名單(核心)
② 從數據庫和Redis如果都查詢不到數據的情況下,將數據庫的空值寫入到緩存中,加上短時間的有效期 【針對於黑客使用相同的key進行攻擊,一定程度上可以減輕頻繁數據庫IO操作】
③ 布隆過濾器
默認情況下,數組值爲0,數組取值(0或1)
爲什麼要有三個hash函數計算出3個hash值?防止衝突,減少重疊的概率
布隆過濾器:布隆過濾器適用於判斷一個元素在集合中是否存在,但是可能會存在誤判的問題。
Bloom Filter基本實現原理採用位數組與聯合函數一起實現,實現的原理採用二進制向量數組和隨機映射hash函數。
爲什麼存在誤判問題?
因爲有可能在查詢的時候,會根據key計算hash值,該值可能與布隆過濾器存放的其它元素的hash值產生重疊,即上圖中的sex,hash值數組對應的下表都爲1,即代表布隆過濾器中存在該key,而實際上該key是不存在的。
如何減少衝突(誤判)概率?
二進制數組長度設置大一點,代碼直接把fpp調低一點即可:
BloomFilter<Integer> integerBloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, 0.001);
代碼實現&應用場景:
在上圖代碼中我們可以用redis很好的降低數據庫訪問壓力,那麼用戶頻繁訪問redis,也會對redis造成很大壓力,這時候我們就引入布隆過濾器,如果不存在直接不走redis,直接返回null給客戶端即可:
<!--引入布隆過濾器 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>22.0</version>
</dependency>
@Mapper
public interface OrderMapper {
@Insert("insert into order_table values (null,#{orderName},0,#{orderToken},#{orderNumber})")
int insertOrder(OrderEntity OrderEntity);
@Select("select * from order_table where id=#{id}")
OrderEntity getById(Integer orderId);
@Select("select id from order_table")
List<Integer> getOrderIds();
}
@RestController
public class OrderController {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RedisUtils redisUtils;
@Autowired
private RedisTemplateUtils redisTemplateUtils;
BloomFilter<Integer> integerBloomFilter;
@RequestMapping("/addOrder")
public String addOrder() {
// 1.提前生成訂單token 臨時且唯一
String orderToken = UUID.randomUUID().toString();
Long orderNumber = System.currentTimeMillis();
// 2.將我們的token存放到rdis中
redisUtils.setString(orderToken, orderId + "", 10L);
OrderEntity orderEntity = new OrderEntity(null, "QQ黃鑽", orderNumber + "", orderToken);
return orderMapper.insertOrder(orderEntity) > 0 ? "success" : "fail";
}
@RequestMapping("/getOrder")
public OrderEntity getOrder(Integer orderId) {
// 0.判斷我們的布隆過濾器
if (!integerBloomFilter.mightContain(orderId)) {
System.out.println("從布隆過濾器中查詢不存在");
return null;
}
// 如果正好碰巧,orderId在redis和mysql都沒有,也沒關係,
// 就算布隆過濾器產生了誤判的情況,也不會影響整個業務,頂多穿透了布隆而已
// 1.先查詢Redis中數據是否存在
OrderEntity orderRedisEntity = (OrderEntity) redisTemplateUtils.getObject(orderId + "");
if (orderRedisEntity != null) {
System.out.println("直接從Redis中返回數據");
return orderRedisEntity;
}
// 2. 查詢數據庫的內容
System.out.println("從DB查詢數據");
OrderEntity orderDBEntity = orderMapper.getOrderById(orderId);
if (orderDBEntity != null) {
System.out.println("將Db數據放入到Redis中");
redisTemplateUtils.setObject(orderId + "", orderDBEntity);
}
return orderDBEntity;
}
/**
* 從數據庫預熱id到布隆過濾器中(預熱機制)
* @return
*/
@RequestMapping("/dbToBulong")
public String dbToBulong() {
List<Integer> orderIds = orderMapper.getOrderIds();
integerBloomFilter = BloomFilter.create(Funnels.integerFunnel(), orderIds.size(), 0.01);
for (int i = 0; i < orderIds.size(); i++) {
// 添加到我們的布隆過濾器中
integerBloomFilter.put(orderIds.get(i));
}
return "success";
}
}
首先用預熱機制,把所有訂單id存到布隆過濾器
分別請求:http://localhost:8080/getOrder?orderId=1,http://localhost:8080/getOrder?orderId=2,
http://localhost:8080/getOrder?orderId=1,http://localhost:8080/getOrder?orderId=2,會發現控制檯打印信息:
請求一個不存在的id,http://localhost:8080/getOrder?orderId=3,會發現直接返回null,並打印從布隆查詢不存在。
由此可見,使用布隆過濾器可以有效減少redis的壓力。
【緩存擊穿】
在高併發的情況下,當一個熱點key(經常使用到key)過期時,因爲訪問該key請求過多,多個請求同時發現該緩存key過期,這時候同時查詢數據庫,同時向Redis寫入緩存數據,對我們數據庫壓力非常大。
做電商項目的時候,把這個熱點key就稱爲"爆款"。
解決方案:
① 分佈式集羣環境 - 使用分佈式鎖技術:多個請求同時只要誰能夠獲取到鎖,誰就能夠去數據庫查詢將數據查詢的結果放入Redis中(直接在上面代碼的紅色箭頭處上鎖即可)
② 單機環境用Lock鎖或者Synchronized
③ 軟過期 - 對熱點key設置無限有效期或者異步延長時間
【緩存雪崩】
緩存雪崩,是指在某一個時間段,緩存集中過期失效,突然給數據庫產生了巨大的壓力,甚至擊垮數據庫的情況。
產生雪崩的原因之一,比如馬上就要到雙十一零點,很快就會迎來一波搶購,這波商品時間比較集中的放入了緩存,假設緩存一個小時。那麼到了凌晨一點鐘的時候,這批商品的緩存就都過期了。而對這批商品的訪問查詢,都落到了數據庫上,對於數據庫而言,就會產生週期性的壓力波峯。
解決方案:對不用的數據使用不同的失效時間,或者採用失效時間加上隨機因子。
例如:做電商項目的時候,一般是採取不同分類商品,緩存不同週期。在同一分類中的商品,加上一個隨機因子。這樣能儘可能分散緩存過期時間,而且,熱門類目的商品緩存時間長一些,冷門類目的商品緩存時間短一些,也能節省緩存服務的資源。
小結:緩存穿透是指key不存在情況下,緩存擊穿是指擊穿 單個熱點key失效的在併發的查詢的情況下,緩存雪崩是指多個key失效的情況下(在某一個時間段,緩存集中過期失效)。
9. Redis哨兵原理,分片,平滑遷移,縮容擴容...
等待更新ing...