Redis 的單執行緒與多執行緒
Redis 單執行緒和多執行緒
到 6.0 以後,整個 redis 來說,是多執行緒的,但 redis 的 工作執行緒 (十大資料型態的操作)是單執行緒,如 RDB,AOF,非同步刪除,叢集資料同步 都是額外的執行緒執行的
最初單執行緒仍然很快的原因
為什麼後來又加入了多執行緒
網路 IO 是多執行緒,真正幹活的執行緒還是單執行緒
多個伺服器連接一個 redis 伺服器進行操作,多個網路 IO,但是只有 redis 主執行緒負責寫入
IO 多路複用入門,真正的主執行緒登場
一般來說 CPU、記憶體都不是問題,網路 IO 是主要的問題
雖然有些命令操作可以用背景執行緒或子行程執行(比如資料刪除、快照生成、AOF 重寫)。但是,從網路 IO 處理到實際的讀寫命令處理,都是由單個執行緒完成的。Redis 的效能瓶頸有時會出現在網路 IO 的處理上,也就是說,單個主執行緒處理網路請求的速度跟不上底層網路硬體的速度,為了應對這個問題:
採用多個IO執行緒來處理網路請求,提高網路請求處理的並行度,Redis6/7就是採用的這種方法
IO 執行緒讀取和解析,比如網路是否通,IP 是否允許,防火牆是否允許
redis 很快的原因是 IO 多路複用和 epoll 函式的使用
Unix 網路程式設計中五種 IO 模型
IO 多路複用入門 : 只用一個服務行程就可以同時處理多個套接字描述符連接

IO 多路複用就是一種同步的 IO 模型,實現一個執行緒監視多個檔案句柄,一旦某個檔案句柄就緒就可以通知到對應程式進行相應的讀寫操作,沒有問及那句柄就緒就會阻塞應用程式,從而釋放電腦資源
IO 多路複用 : 使用單執行緒就能夠處理多個客戶端的連接,無需維護過多的行程
IO 多路複用入門
redis與多路複用
redis 不會出現執行緒安全問題因為主命令執行依舊是由主執行緒執行完成的
由於多執行緒預設關閉,實際應用中,如果發現 redis 實例的 CPU 開銷不大但吞吐量卻沒有提升,可以考慮開啟,80% 的公司單執行緒就夠了
開啟多執行緒
Redis 的多路複用指的是在單執行緒中透過 epoll 或 kqueue 等機制同時監聽多個通訊端的可讀寫狀態,從而實現對多個連線的管理和處理。這種機制使得 Redis 單執行緒能夠處理大量的並行連線而不會阻塞。
即使 Redis 不使用多執行緒,仍然可以利用多路複用機制來管理連線。這是因為多路複用並不依賴於多執行緒,而是利用了作業系統提供的非同步 I/O 機制,透過單個執行緒來監聽和處理多個連線的狀態變化。
BigKey
scan
Redis 的 SCAN 命令是用於迭代集合中的元素的命令。它可以用於走訪集合中的所有元素,而不會阻塞伺服器。
相較於使用 KEYS 命令取得所有元素,SCAN 命令更加高效,因為它可以在不同的迭代器之間平均地分布工作負載,而不會對伺服器產生太大的負載
SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
SCAN 回傳一個包含兩個元素的陣列
第一個元素是用於進行下一次迭代的新游標
第二個元素則是一個 陣列,這個陣列中包含了所有被迭代的元素。如果新游標回傳零表示迭代已結束。
一次回傳的數量不可控, 只能是大概率符合 count 參數
使用方式
- 建立好
scanParams scan中填入scanParams
定義一個scan
ScanParams scanParams = new ScanParams().match(pattern).count(count); ScanResult<String> scanResult = jedis.scan(cursor, scanParams);
//第一個元素
cursor = scanResult.getCursor();
//第二個元素
List<String> keys = scanResult.getResult();
大 key
如何發現大key
刪除
漸進式刪除 , 慢慢刪除部分資料,最後再進行 del
設定調優
redis 有兩種刪除模式,阻塞和非阻塞
快取雙寫一致性
redis 充當快取,分為兩種
- 唯讀快取 (資料只在
redis中存在,不存在mysql) - 讀寫快取 (有寫有讀)
- 同步直寫策略 assets/Redis高級篇/image-20240226220618130.png 即時生效業務要求,採取先更新資料庫,後刪除快取
- 非同步緩寫策略 assets/Redis高級篇/image-20240226220544159.png 物流等,不著急從mysql更新到 redis
取得快取策略 : 雙檢加鎖策略
採用雙重檢查加鎖機制,只保證一個執行緒從 mysql 讀取最新資料,然後寫入 mysql,完成了資料的一致性,其他執行緒在拿到鎖再查時就已經有快取
首先嘗試從 Redis 中取得,沒有之後嘗試取得鎖,
取得機制 : 雙重檢查加鎖策略
從 Redis 取得,Redis 沒有則從 MySQL 取得
@Service
@Slf4j
public class UserService {
public static final String CACHE_KEY_USER = "user:";
@Resource
private UserMapper userMapper;
@Resource
private RedisTemplate redisTemplate;
/**
* Business logic is not written wrong, for small and medium factories(QPS《=1000)it can be used, but for big factories it is not acceptable
* @param id
* @return
*/
public User findUserById(Integer id)
{
User user = null;
String key = CACHE_KEY_USER+id;
//1 First from redis query inside, if there is a result return it directly, if not then query mysql
user = (User) redisTemplate.opsForValue().get(key);
if(user == null)
{
//2 redis nothing inside, continue querying mysql
user = userMapper.selectByPrimaryKey(id);
if(user == null)
{
//3.1 redis+mysql no data in both
//you refine it specifically, to prevent repeated penetration, our business rule is, record the one that causes penetration key write back redis
return user;
}else{
//3.2 mysql there is, need to write the data back to redis,ensure the cache hit rate next time
redisTemplate.opsForValue().set(key,user);
}
}
return user;
}
/**
* strengthen the supplement, avoid sudden key expiration, overwhelming mysql,do some prevention, try not to have breakdown situations.
* @param id
* @return
*/
public User findUserById2(Integer id)
{
User user = null;
String key = CACHE_KEY_USER+id;
//1 First from redis query inside, if there is a result return it directly, if not then query mysql,
// Number 1 query time redis,before locking
user = (User) redisTemplate.opsForValue().get(key);
if(user == null) {
//2 used by big factories, for high QPS optimization, lock first when entering, ensure one request operation, let the outside redis wait a bit, avoid breakdown mysql
synchronized (UserService.class){
//Number 2 query time redis,after locking
user = (User) redisTemplate.opsForValue().get(key);
//3 second check redis still null,can query the mysql it is done(mysql default has data)
if (user == null) {
//4 query mysql get data(mysql default has data)
user = userMapper.selectByPrimaryKey(id);
if (user == null) {
return null;
}else{
//5 mysql there is data inside, need to write back redis,complete the synchronization work of data consistency
redisTemplate.opsForValue().setIfAbsent(key,user,7L,TimeUnit.DAYS);
}
}
}
}
return user;
}
}
更新策略
給快取設定過期時間,定期清理快取並回寫,是保證最終一致性的解決方案
先刪除快取,再更新資料庫(延遲雙刪)
先刪除了reids資料,讓然後mysql進行了更新,mysql沒更新完的時候就存取了資料,存取取得了舊值,還會把舊值寫回了redis,之前就白刪了,就導致新更新的資料沒有同步到redis,redis中資料一直是髒的,採用延時雙刪來解決,也就是在mysql更新後再刪除一次 Redis,延遲刪除時間需要估算
解決方案 : 延時雙刪策略
更新 MySQL 前後兩次進行刪除,負責刪除的 A 執行緒 sleep 時間要大於 B 執行緒寫入 Redis 快取的時間,防止別的執行緒讀取到舊資料寫入 Redis 後沒有被刪除(第二次刪除:將刪除變成非同步的避免吞吐量降低)
- 刪除該休眠多久
- 同步淘汰策略,吞吐量降低怎麼辦
將刪除變成非同步的避免吞吐量降低
前面的方案都不完美,就算解決還要引入新的比如延時雙刪
面試
事實一致性很難做到,一般都是最終一致性
Redis 與 MySQL 資料雙寫一致性工程落地案例
cannel
alibaba/canal:阿里巴巴 MySQL binlog 增量訂閱與消費元件 (github.com)
使用 canal 前 MySQL 修改
啟動
/opt/mycanal/bin路徑下執行 ./startup.sh
日誌
mycanal/logs/canal 中cat canal.log
canal 在 Java 業務中的編寫
pom
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.8.0</version>
</dependency>
RedisUtils
public class RedisUtils
{
public static final String REDIS_IP_ADDR = "192.168.111.185";
public static final String REDIS_pwd = "111111";
public static JedisPool jedisPool;
static {
JedisPoolConfig jedisPoolConfig=new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(20);
jedisPoolConfig.setMaxIdle(10);
jedisPool=new JedisPool(jedisPoolConfig,REDIS_IP_ADDR,6379,10000,REDIS_pwd);
}
public static Jedis getJedis() throws Exception {
if(null!=jedisPool){
return jedisPool.getResource();
}
throw new Exception("Jedispool is not ok");
}
}
canal 設定 RedisCanalClientExample
public class RedisCanalClientExample
{
public static final Integer _60SECONDS = 60;
public static final String REDIS_IP_ADDR = "192.168.111.185";
private static void redisInsert(List<Column> columns)
{
JSONObject jsonObject = new JSONObject();
for (Column column : columns)
{
System.out.println(column.getName() + " : " + column.getValue() + " update=" + column.getUpdated());
jsonObject.put(column.getName(),column.getValue());
}
if(columns.size() > 0)
{
try(Jedis jedis = RedisUtils.getJedis())
{
jedis.set(columns.get(0).getValue(),jsonObject.toJSONString());
}catch (Exception e){
e.printStackTrace();
}
}
}
private static void redisDelete(List<Column> columns)
{
JSONObject jsonObject = new JSONObject();
for (Column column : columns)
{
jsonObject.put(column.getName(),column.getValue());
}
if(columns.size() > 0)
{
try(Jedis jedis = RedisUtils.getJedis())
{
jedis.del(columns.get(0).getValue());
}catch (Exception e){
e.printStackTrace();
}
}
}
private static void redisUpdate(List<Column> columns)
{
JSONObject jsonObject = new JSONObject();
for (Column column : columns)
{
System.out.println(column.getName() + " : " + column.getValue() + " update=" + column.getUpdated());
jsonObject.put(column.getName(),column.getValue());
}
if(columns.size() > 0)
{
try(Jedis jedis = RedisUtils.getJedis())
{
jedis.set(columns.get(0).getValue(),jsonObject.toJSONString());
System.out.println("---------update after: "+jedis.get(columns.get(0).getValue()));
}catch (Exception e){
e.printStackTrace();
}
}
}
public static void printEntry(List<Entry> entrys) {
for (Entry entry : entrys) {
if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND) {
continue;
}
RowChange rowChage = null;
try {
//get changed row data
rowChage = RowChange.parseFrom(entry.getStoreValue());
} catch (Exception e) {
throw new RuntimeException("ERROR ## parser of eromanga-event has an error,data:" + entry.toString(),e);
}
//get change type
EventType eventType = rowChage.getEventType();
System.out.println(String.format("================> binlog[%s:%s] , name[%s,%s] , eventType : %s",
entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(),
entry.getHeader().getSchemaName(), entry.getHeader().getTableName(), eventType));
for (RowData rowData : rowChage.getRowDatasList()) {
if (eventType == EventType.INSERT) {
redisInsert(rowData.getAfterColumnsList());
} else if (eventType == EventType.DELETE) {
redisDelete(rowData.getBeforeColumnsList());
} else {//EventType.UPDATE
redisUpdate(rowData.getAfterColumnsList());
}
}
}
}
public static void main(String[] args)
{
System.out.println("---------O(∩_∩)O哈哈~ initCanal() main方法-----------");
//=================================
// create connection canal server side
CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress(REDIS_IP_ADDR,
11111), "example", "", "");
int batchSize = 1000;
//idle spin counter
int emptyCount = 0;
System.out.println("---------------------canal init OK,开始监听mysql变化------");
try {
connector.connect();
//connector.subscribe(".*\\..*");
//more monitoring,slower write back,only guarantee double write consistency for important data
connector.subscribe("bigdata.t_user");
connector.rollback();
int totalEmptyCount = 10 * _60SECONDS;
while (emptyCount < totalEmptyCount) {
System.out.println("我是canal,每秒一次正在监听:"+ UUID.randomUUID().toString());
Message message = connector.getWithoutAck(batchSize); // get specified amount of data
long batchId = message.getId();
int size = message.getEntries().size();
if (batchId == -1 || size == 0) {
emptyCount++;
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
} else {
//reset counter to zero
emptyCount = 0;
printEntry(message.getEntries());
}
connector.ack(batchId); // submit confirmation
// connector.rollback(batchId); // processing failed, roll back data
}
System.out.println("已经监听了"+totalEmptyCount+"秒,无任何消息,请重启重试......");
} finally {
connector.disconnect();
}
}
}
案例落地實戰 bitmap/hyperloglog/GEO
hyperloglog
UV/PV/DAU/MAU :獨立訪客/頁面瀏覽量/日活躍使用者/月活躍使用者
基數統計:統計集合中不重複的元素個數,就是對集合去重複後剩餘元素的計算,透過機率演算法,並不儲存資料本身,預估基數值,同時保證誤差在一定範圍內
百分之 0.81 誤差
JAVA微服務生態/Redis7/基礎/Redis入門 > redis 基數統計( HyperLogLog )
hyperloglog如何演化出來的
由於bitmap在資料量過大時佔用了過多記憶體,因此演化出了hyperloglog,記憶體減少的効果顯著
GEO
JAVA微服務生態/Redis7/基礎/Redis入門 > 地理空間 GEO
JAVA微服務生態/Redis7/基礎/Redis入門 > 地理空間 GEO
RedisTemplate對GEO的相關操作
public static final String CITY ="city";
@Autowired
private RedisTemplate redisTemplate;
public String geoAdd()
{
Map<String, Point> map = new HashMap<>();
map.put("天安门",new Point(116.403963,39.915119));
map.put("故宫",new Point(116.403414,39.924091));
map.put("长城",new Point(116.024067,40.362639));
redisTemplate.opsForGeo().add(CITY,map);
return map.toString();
}
public Point position(String member) {
//get longitude and latitude coordinates
List<Point> list = redisTemplate.opsForGeo().position(CITY, member);
return list.get(0);
}
public String hash(String member) {
//geohash generated by algorithm base32 encoded value
List<String> list = redisTemplate.opsForGeo().hash(CITY, member);
return list.get(0);
}
public Distance distance(String member1, String member2) {
//get distance between two given locations
Distance distance = redisTemplate.opsForGeo().distance(CITY, member1, member2,
RedisGeoCommands.DistanceUnit.KILOMETERS);
return distance;
}
public GeoResults radiusByxy() {
//find nearby places by longitude and latitude,Beijing Wangfujing location 116.418017,39.914402
//Metrics.KILOMETERS.getMultiplier()radius of circle in kilometers
Circle circle = new Circle(116.418017, 39.914402, Metrics.KILOMETERS.getMultiplier());
// return 50 items
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortDescending().limit(50);
GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults = redisTemplate.opsForGeo().radius(CITY, circle, args);
return geoResults;
}
public GeoResults radiusByMember() {
//find nearby places by location
return null;
}
bitmap
JAVA微服務生態/Redis7/基礎/Redis入門 > redis 位圖( bitmap )
布隆過濾器 BloomFilter
布隆過濾器是一段很長的 binary array+hash algorithm 製作而成的,並不儲存資料而是判斷資料是否存在
布隆過濾器就是利用自身存在則可能存在 (不同資料 hash 值可能一樣),不存在則一定不存在的特性,快速判斷一定一定 data not present in the sample / data present in the sample(has error) ,根據需求進行過濾
高效的插入和查詢,佔用空間少
返回的結果是不確定加不夠完美
由一個初始值為 0 的 bit 陣列和多個雜湊函式構成,用來快速判斷 collection 中是否存在某個元素,判斷一個區間是否有資料
- 根據存在是否 (存在的判斷有誤差)
白名單校驗:存在才允許存取,不存在不允許存取(解決 redis 快取穿透) 黑名單校驗 (垃圾郵件):存在不允許存取,不存在允許存取
原理
我們可以將 redis 中的一個 bitmap 資料製作成布隆過濾器
使用場景
手寫布隆過濾器
一般不用手寫,用谷歌的 guava 即可
Math.abs 計算絕對值,避免負數
手寫布隆過濾器
布隆過濾器, 新增 @component, @PostConstruct
自訂一個 鍵值 如 whitelistCustomer, 資料類型為 bitmap, 這個資料類型的 bitmap 就作為布隆過濾器
將其他鍵的值進行計算, 得出對應的坑位,並設定為true
BloomFilterInit
* 布隆過濾器白名單初始化工具類,一開始就設定一部分資料為白名單所有,
* 白名單業務預設規定:布隆過濾器有,redis是極大可能有。
@Component
public class BloomFilterInit
{
@Resource
private RedisTemplate redisTemplate;
@PostConstruct//初始化白名單資料,暫時註解省的後台列印
public void init()
{
//1 白名單客戶載入到布隆過濾器
String key = "customer:12";
//2 計算hashValue,由於存在計算出來負數的可能,我們取絕對值
int hashValue = Math.abs(key.hashCode());
//3 透過hashValue和2的32次方後取餘,獲得對應的下標坑位
long index = (long)(hashValue % Math.pow(2,32));
log.info(key+" 对应的坑位index:{}",index);
//4 設定redis裡面的bitmap對應類型白名單:whitelistCustomer的坑位,將該值設定為1
redisTemplate.opsForValue().setBit("whitelistCustomer",index,true);
}
}
CheckUtils 同樣計算出坑位的值 , 檢查是否在布隆過濾器中
checkItem 就代表布隆過濾器
CheckUtils
public class CheckUtils
{
@Resource
private RedisTemplate redisTemplate;
public boolean checkWithBloomFilter(String checkItem,String key)
{
int hashValue = Math.abs(key.hashCode());
long index = (long)(hashValue % Math.pow(2,32));
////checkItem就是key值
boolean existOK = redisTemplate.opsForValue().getBit(checkItem,index);
log.info("--->key:"+key+" 对应坑位下标index: "+index+" 是否存在:"+existOK);
return existOK;
}
}
java
public Customer findCustomerByIdWithBloomFilter (Integer customerId)
{
Customer customer = null;
//快取key的名稱
String key = CACHA_KEY_CUSTOMER + customerId;
//布隆過濾器check,無是絕對無,有是可能有
//===============================================
if(!checkUtils.checkWithBloomFilter("whitelistCustomer",key))
{
log.info("白名单无此顾客,不可以访问: "+key);
return null; }
//===============================================
//1 查詢redis
customer = (Customer) redisTemplate.opsForValue().get(key);
//redis無,進一步查詢mysql
if (customer == null) {
//2 從mysql查出來customer
customer = customerMapper.selectByPrimaryKey(customerId);
// mysql有,redis無
if (customer != null) {
//3 把mysql撈到的資料寫入redis,方便下次查詢能redis命中。
redisTemplate.opsForValue().set(key, customer);
}
}
return customer;
}
除了布隆還有布穀鳥過濾器
mapper
abel533/Mapper: Mybatis Common Mapper - Easy to use (github.com)
https://www.bilibili.com/video/BV13R4y1v7sP?p=121&vd_source=17535a06cd4587318e9b67ac64afeab7
快取預熱加快取雪崩加快取擊穿加快取穿透
快取預熱
主動將資料先放到 redis 中
@PostConstruct初始化白名單資料 (也用於初始化布隆過濾器)- 從設定檔
application.yml注入
@Value("${whitelist.ips}") // 注入 IP 白名單
private String ips;
@Value("${whitelist.users}") // 注入 使用者白名單
private String users;
快取雪崩
快取雪崩
redis 掛了,或者 redis 中同時有大量 key 大面積失效
解決:
- 均勻過期
redis中的熱點key設定為永不過期、過期時間錯開redis快取叢集實現高可用:主從加哨兵、叢集,AOF/RDB- 熔斷降級 服務熔斷/服務降級
快取穿透
每次查詢 redis 中都沒有,從頭到尾都沒有,直接達到了 mysql 導致資料庫壓力暴增
解決方案
空物件快取 (使用預設值) 穿透的都進一步快取到
redis中,駭客會用不同key進行更新, 會寫入很多無關緊要的key,越寫越大Google布隆過濾器Guava(權威且建議) Guava 解耦性好 誤判機率很小可以接受 將全部合法的key放入Guava版本布隆過濾器+redis裡面,不然資料返回null
<!--guava Google 開源的 Guava 中自帶的布隆過濾器-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
Guava 白名單實例
誤判率越小:生成的槽位越多,使用的雜湊函式越多
但是,增加雜湊函式的數量也會增加計算開銷。通常情況下,合理的雜湊函式數量可以在保持低誤判率的同時降低計算成本
這10萬資料都不在布隆過濾器中,應該都不存在,但由於過濾器誤判可能判為存在
@Service
@Slf4j
public class GuavaBloomFilterService
{
//1 定義一個常數
public static final int _1W = 10000;
//2 定義我們guava布隆過濾器,初始容量
public static final int SIZE = 100 * _1W;
//3 誤判率,它越小誤判的個數也就越少(Thinking, can it be infinitely small? Would it not be better without false positives?)
public static double fpp = 0.01;//0.01 0.000000000000001
//4 Create guava Bloom filter
private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), SIZE,fpp);
public void guavaBloomFilter()
{
//1 First let bloomFilter add 100W whitelist data
for (int i = 1; i <= SIZE ; i++) {
bloomFilter.put(i);
}
//2 Deliberately take 10W pieces of data outside the valid range,to demonstrate the false positive rate
ArrayList<Integer> list = new ArrayList<>(10 * _1W);
//3 Verify
//The filter only added SIZE data,Deliberately start from SIZE+1 data outside to begin judging,false positives occur
for (int i = SIZE+1; i <= SIZE+(10 * _1W) ; i++)
{
if(bloomFilter.mightContain(i))
{
log.info("被误判了:{}",i);
list.add(i);
}
}
log.info("误判总数量:{}",list.size());
}
}
Cache breakdown
A certain key when heavily requested, this key happens to expire, causing a large number of requests to hit the database, resulting in cache breakdown

Some hackers will, at the instant an event ends, at this instant there will be a batch of redis being deleted, new ones imported redis, hackers will exploit this instant to perform cache breakdown
Solution: double check locking, mutual exclusion update, random backoff, staggered expiration times
Mutual exclusion update: similar to double check locking, but more focused on ensuring only one thread updates the cache, while other threads wait for the update to complete and then directly read the cache.
Random backoff: when the cache expires, different requests apply a random delay before accessing the database, which avoids a large number of requests accessing the database at the same time. It affects user experience.
Cause
- Solution
- Use the double check locking on read described earlier
- Staggered expiration times, or for frequently accessed hot keys, simply do not set an expiration time
- Use two copies of data A expired B as backup, A at deletion time the update is not yet complete, at this time there is B holding up
Summary
Hand writing a Redis distributed lock
(lock name|UUID|counter)``redisLock:uuid:valueNumber
Lock name: all the same, ensuring uniqueness, there is only one lock UUID = factory UUID plus thread UUID: determines whether the lock was created by itself, so as to judge whether it can be deleted (ensuring only the creator of the lock has permission to operate on the lock), and whether reentrant operations can be performed, but the UUID in the lock is bound to the thread
Use Hset to build a distributed lock
Because of multiple distributed virtual machines, while Java the lock in can only manage one virtual machine, the single thread thread lock mechanism no longer works, at this time a lock that all threads can access and acquire is needed (can be through redis or zookeeper to build), to go across processes and services
Using the string data type in redis (which can be improved to hash when the lock is reusable), create a key with a fixed value and only one, as a distributed lock, other threads must compete for this unique redis lock before they can operate, the stand alone version lock (synchronized/lock) can only manage its own current thread
Prevent multi thread spurious wakeup
- Problem 1: prevent multi thread
spurious wakeup,不要使用if判斷,而是使用while,while在wait喚醒後,又進行一次while判斷,而if不會,同樣遞迴在高併發容易出錯,而此時採用自旋來代替方法重試呼叫
搶不到要重試,如果使用 if,則需要 遞迴 重試,太多請求可能爆 StackOverflowError
為了防止多執行緒的 虛假喚醒,不要使用if判斷,而是使用while
while 可以在滿足條件時一直執行,而 if 想要一直執行則需要遞迴本方法
保證過期和建立具有原子性
- 問題 2 : 宕機導致的問題,需要保證過期和建立具有原子性
當拿到鎖後,其中一個 Java 服務 A突然宕機了,而建立的分散式鎖沒有設定時間依然存在,而沒有設定過期時間就會一直存在,需要加上到期時間
這串程式碼不具有 原子性,可能出現搶到鎖了,但是卻掛了沒有設定上過期時間(程式崩潰、伺服器宕機等),確保搶到鎖的同時設定上了過期時間則具有原子性
如何防止到期誤刪 key
- 問題 3 : 如何防止到期誤刪 key,過期時間到了鎖已經失效了但任務還沒有完成,別的執行緒拿到了鎖,之後自己完成後進行刪除,卻刪除了別人執行緒的鎖
鎖也就是 key 透過設定相同的,value 設定為 uuid,則會透過判斷 value 的值來判斷該 key 是否是自己建立的
因為 key 都是相同的,key 存在則不能進行建立,這樣到期後別的執行緒建立了新的鎖,自己的鎖已經到期,也不會刪除別人建立的鎖
現在: 判斷是否是自己的,
lua 腳本: 將刪除具有原子性
Distributed Locks with Redis | Redis
判斷和刪除兩個操作如何具有原子性?
- 問題四 : 刪除不具有原子性,需要
lua 腳本,如果剛判斷完掛掉了就會導致沒有刪除
作用
redis 呼叫 lua 腳本透過 eval 命令保證程式執行的原子性,直接 return 返回腳本執行後的結果值
lua 腳本的作用就是 將多條命令進行多合一執行,如上面三條變成一次性執行

語法格式
nil 代表返回空,等價於 java 中的 false
nil=false
1=true
0=false
- lua語法格式
end" 後的參數數量先依次分給 KEYS, 再分給 ARGV
EVAL 命令中,KEYS 參數的數量之後的所有參數都會被視為 ARGV 參數
java 中 lua 腳本的使用
執行 lua 腳本推薦用 DefaultRedisScript()
stringRedisTemplate.execute(new DefaultRedisScript(luaScript,Boolean.class), Arrays.asList(key),uuidValue);
Arrays.asList(key)==KEYS[]
uuidValue==ARGV[]
nil = null 1 = true 0 = false 返回值有三套值
所以用 Boolean.class 接受,也可以用 Long.class 接受
但 Boolean.class 類型無法直接接受 Redis 返回的 nil 值
Redis Lua 腳本返回值到 Java Boolean 的轉換規則:嘗試將 Redis Lua 腳本的返回值轉換為 Boolean 時,它會遵循一定的規則。通常,Redis 的整數返回值會被這樣轉換:
0 會被轉換為 false,任何非零的整數 (通常是 1 或其他正數或負數) 會被轉換為 true
Lua 語言本身的真值和假值規則: 在 Lua 的邏輯判斷中(例如 if 條件),lua 認為 0 和 空字串 都是 true, 除了 false 和 nil 為假, 其他值都為真。
注意
如果 lua 腳本外面有雙引號了,裡面要改成單引號
Boolean 代表了返回值
可重用鎖
同一個執行緒中的方法拿到鎖後,本身呼叫的方法又需要拿鎖,該如何設計?
redis 中一個業務拿到鎖後,下次還是自己搶到,該如何分配?
一個執行緒中的一個方法拿到鎖後,方法內的方法如果再需要鎖,可以用之前拿到的鎖進行重用
每個鎖物件擁有一個鎖計數器和一個指向持有該鎖的執行緒的指標。
只需要將自己寫的 redis 鎖具有 lock,unlock 功能,以及鎖的計數器 就可以
只有 redis 中 hash 滿足 kkv 結構
(鎖名|UUID|計數器)``redisLock:uuid:valueNumber
鎖名都是相同的,UUID 只是刪除時進行判斷,和可重用鎖時進行判斷
因此應該用 hash 版 redis 鎖代替之前的 string 版本
LOCK
lua 腳本中回傳給 java 的資料,也就是 ruturn 中:0 會被轉換為 false,任何非零的整數 (通常是 1 或其他正數或負數) 會被轉換為 true
lua 腳本中 : call 方法回傳 0 表示不存在,或者 1 表示存在
Lua 語言本身的真值和假值規則:在 Lua 的邏輯判斷中 (例如 if 條件),lua 認為 0 和 空字串 都是 true, 除了 false 和 nil 為假,其他值都為真。
unLOCK
目前 UUID 是在鎖中產生的,重用鎖會導致 UUID 不一樣,後期使用一個工廠造鎖來解決
lua 整合進 java 程式
實作 Lock 鎖,按照 Lock 鎖的規範進行設計
引入工廠模式:就是提前幫你 new 好各種類型,自己選擇使用
- 問題
uuid 在鎖中製造,而透過工廠造鎖,重使用時工廠會重新造鎖,導致 uuid 都不一樣,同一個執行緒出現了兩個 uuid 為了確保同一個執行緒共用一個鎖,需要設定為同一個 UUID
因此鎖工廠引入固定的 uuid,這樣不同執行緒產生相同的 uuid,而在 redis 鎖中 uuid 是執行緒工廠的 uuid 加上對應執行緒
解決:在工廠中製造好 UUID,不在鎖中製作
- 問題
有時業務執行時間長,如何自動續時長
透過 java 的定時器 timer
寫一個自動續時的 lua 腳本,例如過了 10s 還沒完成,自動加鐘
DistributedLockFactory 工廠
@Component
public class DistributedLockFactory
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
private String lockName;
private String uuid;
public DistributedLockFactory()
{
this.uuid = IdUtil.simpleUUID();
}
public Lock getDistributedLock(String lockType)
{
if(lockType == null) return null;
if(lockType.equalsIgnoreCase("REDIS")){
this.lockName = "zzyyRedisLock";
return new RedisDistributedLock(stringRedisTemplate,lockName,uuid);
}else if(lockType.equalsIgnoreCase("ZOOKEEPER")){
this.lockName = "zzyyZookeeperLockNode";
//TODO zookeeper版本的分散式鎖
return null;
}else if(lockType.equalsIgnoreCase("MYSQL")){
//TODO MYSQL版本的分散式鎖
return null;
}
return null;
}
}
timer 是 java 的定時器,用 timer 來實現續期
RedisDistributedLock
lock()會呼叫 tryLock()
tryLock()呼叫tryLock(long time, TimeUnit unit)
import cn.hutool.core.util.IdUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* @auther zzyy
* @create 2023-01-09 16:41
* 我們自行研發的redis分散式鎖,實作了Lock介面
*/
//@Component 引入DistributedLockFactory工廠模式,從工廠取得即可
public class RedisDistributedLock implements Lock
{
private StringRedisTemplate stringRedisTemplate;
private String lockName;//KEYS[1]
private String uuidValue;//ARGV[1]
private long expireTime;//ARGV[2]
public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName, String uuid)
{
this.stringRedisTemplate = stringRedisTemplate;
this.lockName = lockName;
this.uuidValue = uuid+":"+Thread.currentThread().getId();
this.expireTime = 30L;
}
@Override
public void lock()
{
tryLock();
}
@Override
public boolean tryLock()
{
try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
{
if(time == -1L)
{
String script =
"if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
"redis.call('hincrby',KEYS[1],ARGV[1],1) " +
"redis.call('expire',KEYS[1],ARGV[2]) " +
"return 1 " +
"else " +
"return 0 " +
"end";
System.out.println("lockName:"+lockName+"\t"+"uuidValue:"+uuidValue);
//執行腳本為true成功,但while中為false會跳出迴圈,也就是需要取反
while(!stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName), uuidValue,String.valueOf(expireTime)))
{
//暫停60毫秒
try { TimeUnit.MILLISECONDS.sleep(60); } catch (InterruptedException e) { e.printStackTrace(); }
}
//新建一個後台掃描程式,來持續key目前的ttl,是否到我們規定的1/2 1/3來實作續期
renewExpire();
return true; }
return false;
}
//執行解鎖腳本
@Override
public void unlock()
{
System.out.println("unlock(): lockName:"+lockName+"\t"+"uuidValue:"+uuidValue);
String script =
"if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
"return nil " +
"elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " +
"return redis.call('del',KEYS[1]) " +
"else " +
"return 0 " +
"end";
// nil = null 1 = true 0 = false 傳回值有三套值
Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
//如果沒鎖會傳回null,說明逾時,自己的鎖會自動消失,不會影響其他執行緒建立自己的鎖 (key:value,number)
if(null == flag)
{
throw new RuntimeException("this lock doesn't exists,o(╥﹏╥)o");
}
}
//自動續期
private void renewExpire()
{
String script =
"if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then " +
"return redis.call('expire',KEYS[1],ARGV[2]) " +
"else " +
"return 0 " +
"end";
//timer是java的計時器
new Timer().schedule(new TimerTask()
{
@Override
public void run()
{
if (stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime)))
{
renewExpire();
}
}
},(this.expireTime * 1000)/3);
}
//====下面兩個暫時用不到,不再重寫
//不需要考慮中斷,用完就刪就行了
@Override
public void lockInterruptibly() throws InterruptedException
{
}
@Override
public Condition newCondition()
{
return null;
}
}
//V8.0,實作自動續期功能的完善,後台自訂掃描程式,如果規定時間內沒有完成業務邏輯,會呼叫加鐘自動續期的腳本
//V8.0,實作自動續期功能的完善,後台自訂掃描程式,如果規定時間內沒有完成業務邏輯,會呼叫加鐘自動續期的腳本
public String sale()
{
String retMessage = "";
Lock redisLock = distributedLockFactory.getDistributedLock("redis");
redisLock.lock();
try {
//1 查詢庫存資訊
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判斷庫存是否足夠
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣減庫存,每次減少一個
if(inventoryNumber > 0)
{
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余:"+inventoryNumber;
System.out.println(retMessage+"\t"+"服务端口号"+port);
//暫停120秒鐘執行緒,故意的,示範自動續期的功能。。。。。。
try { TimeUnit.SECONDS.sleep(120); } catch (InterruptedException e) { e.printStackTrace(); }
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
}finally {
redisLock.unlock();
}
return retMessage+"\t"+"服务端口号"+port;
}
CAP
面試
如何撰寫?
首先按照 Java 中 juc 中的 LOCK 規範實作方法,
上鎖邏輯 : 使用 Hset , KKV 結構來進行加鎖,透過 while 以一定間隔, 重試,不斷 自旋 來判斷是否能夠達到鎖,透過 lua 腳本來保證原子性,透過續期來保證時間長的業務能夠自動續期順利完成業務

解鎖邏輯 : 確保保證是自己的鎖,只能自己刪除自己的鎖,透過鎖的計數器,為 0 才可以刪除
使用Redis實作分散式鎖 | Redis --- Distributed Locks with Redis | Redis
Redlock 演算法和底層原始碼分析
使用Redis实现分布式锁 _ Redis --- Distributed Locks with Redis _ Redis (03_03_2024 21_53_05) ↗
單點故障
單點故障: 由於之前 redis 中複製是異步的,突然宕機會出現鎖消失的問題,執行緒一拿到鎖後,master 還未將鎖寫入 slave 後就掛了,然後 slave 被選舉為了 master,執行緒二看到新的 master 沒有鎖後就建立了自己的鎖,此時出現兩個以上執行緒拿到了鎖,這種情況是嚴禁的,解決方法就是捨棄 slave 節點,而是建立鎖節點,只能從鎖節點取得鎖
Redlock
8. 分散式鎖與同步器 · redisson/redisson Wiki (github.com)
redisson/redisson: Redisson
使用多個 reids,每個都是 master,沒有使用複製或任何其他隱式協調系統
Redlock
設定
1. 修改pom
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.19.1</version>
</dependency>
2. 在configuration中redisConfig中的設定,或者設定yaml)
@Bean
public Redisson redisson()
{
Config config = new Config();
//新增多台,這裡只是示範
config.useSingleServer().setAddress("redis://192.168.111.185:6379").setDatabase(0).setPassword("111111");
// or read config from file
//config = Config.fromYAML(new File("config-file.yaml"));
return (Redisson) Redisson.create(config);
}
- 問題
redlock 也要判斷是否是自己的鎖
//V9.1,引入 Redisson 對應的官網推薦 RedLock 演算法實作類別
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
@Autowired
private DistributedLockFactory distributedLockFactory;
//V9.1,引入Redisson對應的官網推薦RedLock演算法實作類別
@Autowired
private Redisson redisson;
public String saleByRedisson()
{
String retMessage = "";
RLock redissonLock = redisson.getLock("zzyyRedisLock");
redissonLock.lock();
try{
//1 查詢庫存資訊
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判斷庫存是否足夠
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣減庫存,每次減少一個
if(inventoryNumber > 0)
{
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余:"+inventoryNumber;
System.out.println(retMessage+"\t"+"服务端口号"+port);
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
}finally {
//改進點,只能刪除屬於自己的key,不能刪除別人的
if(redissonLock.isLocked() && redissonLock.isHeldByCurrentThread())
{
redissonLock.unlock();
}
}
return retMessage+"\t"+"服务端口号"+port;
}
原始碼解析
依舊是加鎖,可重入,續命,解鎖
redissonLock 預設過期時間 30 秒,
多機案例
基於容錯移轉實作的 redis 主從無法實現真正的 redlock
java
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.19.1</version>
</dependency>
server.port=9090
spring.application.name=redlock
spring.swagger2.enabled=true
spring.redis.database=0
spring.redis.password=
spring.redis.timeout=3000
spring.redis.mode=single
//池化技術
spring.redis.pool.conn-timeout=3000
spring.redis.pool.so-timeout=3000
spring.redis.pool.size=10
spring.redis.single.address1=192.168.111.185:6381
spring.redis.single.address2=192.168.111.185:6382
spring.redis.single.address3=192.168.111.185:6383
Redis 快取過期淘汰策略
記憶體設定
如果不設定或者設定為 0, 在 64 位元作業系統下不限制記憶體大小,在 32 位元作業系統最多使用 3GB ,一般推薦設定 Redis 記憶體為最大實體記憶體的四分之三
maxmemory
maxmemory 128mb
超出記憶體會爆 OOM
修改 : 設定檔/命令 config set maxmemory xxx
查看 : info memory / config get maxmemory
過期鍵的刪除策略
如果一個鍵是過期的,那它到了過期時間之後是不是馬上就從記憶體中被刪除呢?
立即刪除
對 CPU 不好,用處理器效能換取儲存空間 (時間換空間)
惰性刪除
開啟 : lazyfree-lazy-eviction=yes
對記憶體不友善,用儲存空間換取處理器效能 (空間換時間)
只掃描被存取的 key,沒有被存取的 key 即使過期也不知道,不會被刪除

定期刪除
定期刪除是前兩種檢查策略的折衷
每隔一段時間抽取 一部分key 檢查是否有過期的 key,會產生漏網之魚
快取淘汰策略
lru和lfu演算法的區別是什麼
LRU:最近 最少使用 頁面置換演算法,使用時間上來講
LFU:最近 最不常用 頁面置換演算法,頻率上來講
一共八種
兩個維度 : 過期鍵中篩選/所有鍵中篩選
四個方面 : LRU/LFU/random/ttl
配置
快取配置建議
Redis 經典五大類型原始碼及底層實作
這一章完全是為了面試,沒有太多學習的意義
高效能設計之 epoll 和 IO 多路複用深度解析
總結
資料
- 起步思維: 官網
- 排除故障思維 : 日誌
^u0ikb3

















































































































































































