自学内容网 自学内容网

Redis--延时双删策略

延时双删策略的核心目的是解决在高并发环境下可能出现的短暂不一致性问题。让我们来详细看一下在极端并发情况下,为什么需要延时双删。

更复杂的并发场景

假设我们有如下更复杂的并发场景:

  1. 用户A将库存从100更新为50,并删除缓存。
  2. 用户B在用户A删除缓存之后读取缓存,发现缓存为空,然后从数据库中读取数据并写入缓存。
  3. 用户C在用户A删除缓存之前,读取缓存并写入缓存旧数据100。
步骤1:用户A更新数据库并第一次删除缓存
// 用户A更新数据库
String sqlUpdateQuery = "UPDATE inventory SET stock = 50 WHERE product_id = 1";
updateDatabase(sqlUpdateQuery);

// 第一次删除缓存
redis.del("product_1_stock");

步骤2:用户B读取缓存(缓存为空)并加载数据

// 用户B读取缓存,发现缓存为空,从数据库加载数据
String stock = redis.get("product_1_stock");
if (stock == null) {
    stock = loadStockFromDatabase(1); // 从数据库加载数据
    redis.set("product_1_stock", stock); // 将数据写入缓存
}
步骤3:用户C在用户A删除缓存之前读取缓存
  1. 用户A将库存从100更新为50,并删除缓存。
  2. 用户C在缓存删除之前读取缓存中的旧数据100,并在某个时刻将其重新写入缓存。

问题

用户C在缓存删除之前读取到了旧数据100,并将其写入缓存。如果用户C的写入操作晚于用户A的第一次删除操作,那么缓存中会重新出现旧数据100。

示例代码

Java代码示例
import redis.clients.jedis.Jedis;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class DelayedDoubleDeletionExample {

    private static Jedis redis = new Jedis("localhost", 6379);
    private static String dbUrl = "jdbc:mysql://localhost:3306/mydb";
    private static String dbUser = "user";
    private static String dbPassword = "password";

    // 更新数据库
    public static void updateDatabase(String sqlUpdateQuery) {
        try (Connection conn = DriverManager.getConnection(dbUrl, dbUser, dbPassword);
             PreparedStatement pstmt = conn.prepareStatement(sqlUpdateQuery)) {
            pstmt.executeUpdate();
            conn.commit();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    // 从数据库加载库存数据
    public static String loadStockFromDatabase(int productId) {
        String stock = null;
        String query = "SELECT stock FROM inventory WHERE product_id = ?";
        try (Connection conn = DriverManager.getConnection(dbUrl, dbUser, dbPassword);
             PreparedStatement pstmt = conn.prepareStatement(query)) {
            pstmt.setInt(1, productId);
            ResultSet rs = pstmt.executeQuery();
            if (rs.next()) {
                stock = rs.getString("stock");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return stock;
    }

    // 更新数据库并处理缓存(包含延时双删)
    public static void updateDatabaseAndCache(String sqlUpdateQuery, String cacheKey, long delayInMillis) {
        // 更新数据库
        updateDatabase(sqlUpdateQuery);

        // 第一次删除缓存
        redis.del(cacheKey);

        // 延时
        try {
            Thread.sleep(delayInMillis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 第二次删除缓存
        redis.del(cacheKey);
    }

    public static void main(String[] args) {
        String sqlUpdateQuery = "UPDATE inventory SET stock = 50 WHERE product_id = 1";
        String cacheKey = "product_1_stock";
        long delayInMillis = 2000; // 2秒延时

        // 模拟用户A更新数据库并执行延时双删
        new Thread(() -> {
            updateDatabaseAndCache(sqlUpdateQuery, cacheKey, delayInMillis);
        }).start();

        // 模拟用户B读取缓存
        new Thread(() -> {
            // 延时模拟用户B读取数据的操作延时
            try {
                Thread.sleep(1000); // 延时1秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            String stock = redis.get(cacheKey);
            if (stock == null) {
                stock = loadStockFromDatabase(1); // 从数据库加载数据
                redis.set(cacheKey, stock); // 将数据写入缓存
            }
            System.out.println("User B read stock: " + stock);
        }).start();

        // 模拟用户C读取缓存并写入旧数据
        new Thread(() -> {
            // 延时模拟用户C在用户A删除缓存之前读取数据的操作延时
            try {
                Thread.sleep(500); // 延时0.5秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            String stock = redis.get(cacheKey);
            if (stock == null) {
                // 假设此时用户C读取到了旧数据100(模拟旧数据的写入)
                redis.set(cacheKey, "100"); // 将旧数据写入缓存
            }
            System.out.println("User C read stock and wrote back old data: 100");
        }).start();
    }
}

解释

  1. 用户A更新数据库并第一次删除缓存

    • updateDatabaseAndCache方法更新数据库中的库存数据并删除缓存。
  2. 用户B读取缓存并加载数据

    • 用户B在缓存为空时,从数据库加载数据(正确的50)并写入缓存。
  3. 用户C在缓存删除之前读取缓存并写入旧数据

    • 用户C读取缓存时缓存为空,然后假设用户C读取到的是旧数据100,并将其写入缓存。

结果

  • 数据库中的数据:50
  • 缓存中的数据:100

延时双删的作用

为了避免用户C在用户A第一次删除缓存后写入的旧数据造成缓存和数据库数据不一致,用户A在延时后再次删除缓存:

// 第二次删除缓存
redis.del(cacheKey);

通过第二次删除缓存,可以确保即使有其他线程在用户A第一次删除缓存后写入旧数据,延时后缓存仍会被清除,确保缓存和数据库数据最终一致。

总结

延时双删策略在高并发环境下,通过在第一次删除缓存后延时一段时间进行第二次删除缓存,确保缓存和数据库中的数据一致,避免并发操作带来的数据不一致问题。


原文地址:https://blog.csdn.net/weixin_46520767/article/details/139858231

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!