成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

SpringBoot 搶券活動:Redis 熱點 Key 三大防護

數據庫 Redis
本文將從緩存擊穿、分片、異步化等角度,探討如何在項目中優化?Redis?和數據庫的性能,以應對搶券活動中的熱點?Key?問題。

引言

在電商系統的搶券活動中,經常會出現某張熱門優惠券被大量用戶同時訪問的情況,這就是典型的熱點 Key 問題。這類問題會導致 Redis 負載過高,甚至可能引發緩存擊穿,大量請求直接打到數據庫,造成系統崩潰。

本文將從緩存擊穿、分片、異步化等角度,探討如何在項目中優化 Redis 和數據庫的性能,以應對搶券活動中的熱點 Key 問題。

熱點 Key 問題分析

在搶券場景中,熱點 Key 問題主要表現為:

  • 當該熱點 Key 在 Redis 中過期時,大量請求會同時穿透到數據庫,造成緩存擊穿
  • 某張熱門優惠券的訪問量遠超其他優惠券,導致 Redis 單節點負載過高
  • 數據庫瞬時承受巨大壓力,可能導致查詢超時甚至服務不可用

?

  • 緩存擊穿:是指當某一key的緩存過期時大并發量的請求同時訪問此key,瞬間擊穿緩存服務器直接訪問數據庫,讓數據庫處于負載的情況。
  • 緩存穿透:是指緩存服務器中沒有緩存數據,數據庫中也沒有符合條件的數據,導致業務系統每次都繞過緩存服務器查詢下游的數據庫,緩存服務器完全失去了其應有的作用。
  • 緩存雪崩:是指當大量緩存同時過期或緩存服務宕機,所有請求的都直接訪問數據庫,造成數據庫高負載,影響性能,甚至數據庫宕機。

緩存擊穿的解決方案

分布式鎖
// 使用Redisson實現分布式鎖防止緩存擊穿
@Service
public class CouponService {
    
    @Autowired
    private RedissonClient redissonClient;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private CouponDao couponDao;
    
    public Coupon getCoupon(String couponId) {
        String key = "coupon:" + couponId;
        Coupon coupon = (Coupon) redisTemplate.opsForValue().get(key);
        
        if (coupon == null) {
            // 獲取分布式鎖
            RLock lock = redissonClient.getLock("lock:coupon:" + couponId);
            try {
                // 嘗試加鎖,最多等待100秒,鎖持有時間為10秒
                boolean isLocked = lock.tryLock(100, 10, TimeUnit.SECONDS);
                if (isLocked) {
                    try {
                        // 再次檢查Redis中是否有值
                        coupon = (Coupon) redisTemplate.opsForValue().get(key);
                        if (coupon == null) {
                            // 從數據庫中查詢
                            coupon = couponDao.getCouponById(couponId);
                            if (coupon != null) {
                                // 設置帶過期時間的緩存
                                redisTemplate.opsForValue().set(key, coupon, 30, TimeUnit.MINUTES);
                            }
                        }
                    } finally {
                        // 釋放鎖
                        lock.unlock();
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return coupon;
    }
}

熱點 Key 分片處理

當單個熱點 Key 的訪問量極高時,可以采用分片策略將請求分散到多個 Redis 節點上:

// 熱點Key分片處理實現
@Service
public class CouponService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private CouponDao couponDao;
    
    // 分片數量
    private static final int SHARD_COUNT = 16;
    
    // 獲取分片后的Key
    private String getShardedKey(String couponId, int shardIndex) {
        return"coupon:" + couponId + ":shard" + shardIndex;
    }
    
    // 初始化分片緩存
    public void initCouponShards(String couponId, int stock) {
        // 計算每個分片的庫存
        int stockPerShard = stock / SHARD_COUNT;
        int remaining = stock % SHARD_COUNT;
        
        for (int i = 0; i < SHARD_COUNT; i++) {
            int currentStock = stockPerShard + (i < remaining ? 1 : 0);
            String key = getShardedKey(couponId, i);
            redisTemplate.opsForValue().set(key, currentStock);
        }
    }
    
    // 扣減庫存(嘗試從隨機分片獲取)
    public boolean deductStock(String couponId) {
        // 隨機選擇一個分片
        int shardIndex = new Random().nextInt(SHARD_COUNT);
        String key = getShardedKey(couponId, shardIndex);
        
        // 使用Lua腳本原子性地扣減庫存
        String script = 
            "local stock = tonumber(redis.call('get', KEYS[1])) " +
            "if stock and stock > 0 then " +
            "  redis.call('decr', KEYS[1]) " +
            "  return 1 " +
            "else " +
            "  return 0 " +
            "end";
        
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(script);
        redisScript.setResultType(Long.class);
        
        Long result = redisTemplate.execute(redisScript, Collections.singletonList(key));
        return result != null && result == 1;
    }
}
根據分片負載動態選擇
// 動態分片選擇(根據剩余庫存)
public boolean deductStockByDynamicShard(String couponId) {
    // 獲取所有分片的庫存
    List<String> keys = new ArrayList<>();
    for (int i = 0; i < SHARD_COUNT; i++) {
        keys.add(getShardedKey(couponId, i));
    }
    
    // 使用MGET批量獲取所有分片庫存
    List<Object> results = redisTemplate.opsForValue().multiGet(keys);
    
    // 選擇庫存最多的分片
    int maxStockIndex = -1;
    int maxStock = 0;
    
    for (int i = 0; i < results.size(); i++) {
        if (results.get(i) != null) {
            int stock = Integer.parseInt(results.get(i).toString());
            if (stock > maxStock) {
                maxStock = stock;
                maxStockIndex = i;
            }
        }
    }
    
    if (maxStockIndex >= 0) {
        // 對選中的分片進行扣減
        String key = getShardedKey(couponId, maxStockIndex);
        // 執行Lua腳本扣減庫存...
    }
    
    returnfalse;
}

異步化處理

// 異步化處理搶券請求
@Service
public class CouponService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private CouponDao couponDao;
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    // 搶券接口 - 快速返回,異步處理
    public boolean grabCoupon(String userId, String couponId) {
        // 先快速檢查Redis中是否有庫存
        String stockKey = "coupon:" + couponId + ":stock";
        Long stock = (Long) redisTemplate.opsForValue().get(stockKey);
        
        if (stock == null || stock <= 0) {
            returnfalse;
        }
        
        // 使用Lua腳本原子性地扣減庫存
        String script = 
            "local stock = tonumber(redis.call('get', KEYS[1])) " +
            "if stock and stock > 0 then " +
            "  redis.call('decr', KEYS[1]) " +
            "  return 1 " +
            "else " +
            "  return 0 " +
            "end";
        
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(script);
        redisScript.setResultType(Long.class);
        
        Long result = redisTemplate.execute(redisScript, Collections.singletonList(stockKey));
        
        if (result != null && result == 1) {
            // 庫存扣減成功,發送消息到MQ異步處理
            CouponGrabMessage message = new CouponGrabMessage(userId, couponId);
            rabbitTemplate.convertAndSend("coupon.exchange", "coupon.grab", message);
            returntrue;
        }
        
        returnfalse;
    }
    
    // 異步處理搶券結果
    @RabbitListener(queues = "coupon.grab.queue")
    public void handleCouponGrab(CouponGrabMessage message) {
        try {
            // 在數據庫中記錄用戶領取優惠券的信息
            couponDao.recordUserCoupon(message.getUserId(), message.getCouponId());
            
            // 可以在這里添加其他業務邏輯,如發送通知等
        } catch (Exception e) {
            // 處理失敗,可以記錄日志或進行補償操作
            log.error("Failed to handle coupon grab for user: {}, coupon: {}", 
                    message.getUserId(), message.getCouponId(), e);
            
            // 回滾Redis中的庫存(這里簡化處理,實際中可能需要更復雜的補償機制)
            String stockKey = "coupon:" + message.getCouponId() + ":stock";
            redisTemplate.opsForValue().increment(stockKey);
        }
    }
}

其他優化策略

本地緩存
// 使用Caffeine實現本地緩存
@Service
public class CouponService {
    
    // 本地緩存,最大容量100,過期時間5分鐘
    private LoadingCache<String, Coupon> localCache = Caffeine.newBuilder()
            .maximumSize(100)
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .build(this::loadCouponFromRedis);
    
    // 從Redis加載優惠券信息
    private Coupon loadCouponFromRedis(String couponId) {
        String key = "coupon:" + couponId;
        return (Coupon) redisTemplate.opsForValue().get(key);
    }
    
    // 獲取優惠券信息
    public Coupon getCoupon(String couponId) {
        try {
            return localCache.get(couponId);
        } catch (ExecutionException e) {
            // 處理異常,從其他地方獲取數據
            return loadCouponFromRedis(couponId);
        }
    }
}
限流
// 使用Sentinel實現熱點參數限流
@Service
public class CouponService {
    
    // 定義熱點參數限流規則
    static {
        initFlowRules();
    }
    
    private static void initFlowRules() {
        List<ParamFlowRule> rules = new ArrayList<>();
        ParamFlowRule rule = new ParamFlowRule();
        rule.setResource("getCoupon");
        rule.setParamIdx(0); // 第一個參數作為限流參數
        rule.setCount(1000); // 每秒允許的請求數
        
        // 針對特定值的限流設置
        ParamFlowItem item = new ParamFlowItem();
        item.setObject("hotCouponId1");
        item.setClassType(String.class.getName());
        item.setCount(500); // 針對熱點優惠券ID的特殊限流
        rule.getParamFlowItemList().add(item);
        
        rules.add(rule);
        ParamFlowRuleManager.loadRules(rules);
    }
    
    // 帶限流的獲取優惠券方法
    public Coupon getCoupon(String couponId) {
        Entry entry = null;
        try {
            // 資源名可使用方法名
            entry = SphU.entry("getCoupon", EntryType.IN, 1, couponId);
            
            // 業務邏輯
            return getCouponFromRedis(couponId);
        } catch (BlockException ex) {
            // 資源訪問阻止,被限流或降級
            // 進行相應的處理操作
            return getDefaultCoupon();
        } finally {
            if (entry != null) {
                entry.exit();
            }
        }
    }
}

實施建議

  • 對優惠券系統進行分層設計,將熱點數據與普通數據分離處理
  • 監控 Redis 的性能指標,及時發現和處理熱點 Key
  • 提前對可能的熱點 Key 進行預判和預熱
  • 設計完善的降級和熔斷策略,保障系統在極端情況下的可用性
  • 定期進行全鏈路壓測,發現系統瓶頸并持續優化

總結

在搶券活動等高并發場景下,熱點 Key 問題是 Redis 和數據庫面臨的主要挑戰之一。通過采用緩存擊穿預防、熱點 Key 分片、異步化處理、本地緩存和限流等多種優化策略,可以有效提升系統的性能和穩定性。

在實際應用中,應根據具體業務場景選擇合適的優化方案,并進行充分的性能測試和壓力測試,確保系統在高并發情況下依然能夠穩定運行。

責任編輯:武曉燕 來源: 一安未來
相關推薦

2021-03-28 21:33:07

Redis熱點key

2025-05-28 03:10:00

2011-03-11 15:07:24

2020-02-11 16:10:44

Redis分布式鎖Java

2023-04-26 01:07:03

2019-01-09 09:35:41

搶票Python軟件

2019-10-30 16:54:08

golangredis數據庫

2010-09-03 10:05:00

安全防護

2018-07-13 05:31:13

2018-02-23 17:16:03

態牛

2022-11-03 08:56:43

RediskeyBitmap

2014-05-13 09:05:09

2014-05-13 11:45:38

2021-06-21 18:23:39

戴爾

2024-11-26 08:09:58

2017-07-19 16:09:27

華碩

2024-12-02 01:16:53

2024-11-21 16:47:55

2023-04-17 08:04:15

Redis性能內存

2025-02-10 09:22:40

點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产精品毛片一区二区三区 | 人人鲁人人莫人人爱精品 | 久干网 | 完全免费av在线 | 欧美国产日韩在线观看 | 成人国产免费视频 | 国产在线一区二区 | 日本男人天堂 | 91久久北条麻妃一区二区三区 | 夜夜草| 国产午夜精品一区二区三区四区 | 日韩毛片免费看 | 亚洲欧洲成人av每日更新 | 国产成人精品在线 | 欧美精品久久久久 | 丁香一区二区 | 成人在线免费观看 | 亚洲欧洲综合av | 亚洲精品一区二区三区在线 | h片在线观看免费 | 欧美视频第三页 | 国产精品久久久爽爽爽麻豆色哟哟 | 久久国产激情视频 | 国产精品99久久久久久动医院 | 欧美一级α片 | www国产精品 | 91九色麻豆 | 午夜国产一区 | 欧洲免费毛片 | 久久成人国产 | 欧美女优在线观看 | 天天综合网天天综合色 | 久久久久亚洲精品 | 国产乱码久久久久久一区二区 | 在线播放一区二区三区 | 黄色一级电影在线观看 | 国产在线中文 | 亚洲国产一区视频 | 国产精品久久久久久影视 | 国产成人精品一区二区三区在线 | 久久婷婷国产麻豆91 |