redis实战优化案例

业务场景

在项目发展过程中,刚开始业务量不大。对redis的使用也仅仅停留在能用的层面,但是一旦业务流量上来,很多并发性的问题也就随之显现出来,其对应的业务代码也就要根据实际情况进行调整优化。

初始方案

	// 1、最初实现CRUD
	@Override
    @Transactional
    public Product createProduct(Product product) {
        productRepo.saveAndFlush(product);
        jedis.set(SystemConstants.REDIS_KEY_PREFIX + product.getProductId(), gson.toJson(product))
        return product;
    }

    @Override
    @Transactional
    public Product updateProduct(Product product) {
        productRepo.saveAndFlush(product);
        jedis.set(SystemConstants.REDIS_KEY_PREFIX + product.getProductId(), gson.toJson(product))
        return product;
    }

    @Override
    public Product getProduct(Long productId) {
        // 1. 先查redis
        String productRedis = jedis.get(SystemConstants.REDIS_KEY_PREFIX + productId);
        if (!StringUtil.isBlank(productRedis)) {
            return gson.fromJson(productRedis, Product.class);
        }
        // 2. redis没有,再查mysql数据库
        Product productMysql = productRepo.findByProductId(productId);
      	if (productMysql != null) {
            // 3. 数据库有,则更新redis数据
            jedis.set(SystemConstants.REDIS_KEY_PREFIX + productMysql.getProductId(), gson.toJson(productMysql));
        }
        // 4. 返回mysql数据库数据
        return productMysql;
    }

问题剖析:
	1. redis缓存容量小问题:几百G的海量数据不可能一直都放到redis缓存中,会大大降低redis作为内存数据库的效率(一般redis数据量<10G)
		解决方案:缓存时设置固定过期时间,比如说一天,虽然一开始redis数据量很大,但是一天之后,会有大量数据失效,达到冷热数据的分离。
			jedis.set(SystemConstants.REDIS_KEY_PREFIX + product.getProductId(), gson.toJson(product));
			jedis.expire(SystemConstants.REDIS_KEY_PREFIX + product.getProductId(), SystemConstants.REDIS_KEY_EXPIRED_TIME);
			
	2. 缓存击穿问题:虽然设置了过期时间,仍然会出现缓存击穿问题, 即单个热点key失效的瞬间,持续的大并发请求就会击破缓存,直接请求到数据库,好像蛮力击穿一样(缓存无数据/数据库有数据)
   		解决方案:设置随机过期时间,保证key不会同时失效
		jedis.expire(SystemConstants.REDIS_KEY_PREFIX + productId, genRandomExpiredTime(5));
		
	3. 缓存穿透问题:用户访问的数据既不在缓存当中,也不在数据库中,按道理说数据库都没有这个数据,就不能一直来查数据库了,防止黑客恶意攻击。

解决方案初始版1.0

@Override
    public Product getProduct(Long productId) {
        String redisId = SystemConstants.REDIS_KEY_PREFIX + productId;
        // 1. 先查redis
        String productRedis = jedis.get(redisId);
        if (!StringUtil.isBlank(productRedis)) {
            // 判断缓存是否是默认值,避免缓存穿透
            if (productRedis.equals(SystemConstants.REDIS_DEFAULT_CACHE)) {
                jedis.expire(redisId, genRandomExpiredTime(3));
                return null;
            }
            jedis.expire(redisId, genRandomExpiredTime(5));
            return gson.fromJson(productRedis, Product.class);
        }

        // 2. redis没有,再查mysql数据库
        Product productMysql = productRepo.findByProductId(productId);
    	if (productMysql != null) {
            // 3. 数据库有,则更新redis数据
            jedis.set(redisId, gson.toJson(productMysql));
            jedis.expire(redisId, genRandomExpiredTime(5));
        } else {
            // 缓存空或默认值 + 过期时间,避免缓存穿透
            jedis.set(redisId, SystemConstants.REDIS_DEFAULT_CACHE);
            jedis.expire(redisId, genRandomExpiredTime(3));
        }
        return productMysql;
    }

4. 缓存雪崩:在使用缓存时,通常会对缓存设置过期时间,一方面目的是保持缓存与数据库数据的一致性,另一方面是减少冷缓存占用过多的内存空间。但当缓存中大量热点缓存在某一个时刻同时失效据库,从而导致数据库压力骤增,造成系统崩溃等情况,这就是缓存雪崩。
	解决方案:
    1. key均匀失效:   将key的过期时间后面加上一个随机数(比如随机1-5分钟),让key均匀的失效。
		2. 双key策略:		主key设置过期时间,备key不设置过期时间,当主key失效时,直接返回备key值。
		3. 构建缓存高可用集群
            
	5. 突发性热点缓存重建导致数据库系统压力倍增:也就是说某一数据本来是冷数据,存储在数据库中,突然出现大量访问,redis还没缓存该数据,因此需要大量查询数据库并重建缓存,也就是以下代码重复执行,要是只执行一次就好了。
    		if (StringUtil.isNotBlank(productRedis)) {
                // 3. 数据库有,则更新redis数据
                jedis.set(redisId, gson.toJson(productMysql));
                jedis.expire(redisId, genRandomExpiredTime(5));
        	}
	解决方案一:DCL双端检锁机制
		但仍然存在以下问题,一方面synchronized锁住的是单个JVM,若是该web项目集群部署,则在每个JVM都需要锁一次,另一方面,假如productId=101是热点数据会被锁住,但是其他数据productId=202也需要排队等待,效率降低。
  	解决方案二:分布式锁setnx
		但仍然存在redis缓存和mysql数据库数据不一致问题

解决方案一:DCL双端检锁机制

@Override
    public Product getProduct(Long productId) {
        String redisId = SystemConstants.REDIS_KEY_PREFIX + productId;
        // 1. 先查redis
        String productRedis = jedis.get(redisId);
        if (!StringUtil.isBlank(productRedis)) {
            // 判断缓存默认值,避免缓存穿透
            if (productRedis.equals(SystemConstants.REDIS_DEFAULT_CACHE)) {
                jedis.expire(redisId, genRandomExpiredTime(3));
                return null;
            }
            jedis.expire(redisId, genRandomExpiredTime(5));
            return gson.fromJson(productRedis, Product.class);
        }

        Product productMysql = null;
        synchronized (this) {
            // 2. DCL再查redis,因为只要有一次查询数据库操作,redis就已经有缓存数据了
            productRedis = jedis.get(redisId);
            if (!StringUtil.isBlank(productRedis)) {
                if (productRedis.equals(SystemConstants.REDIS_DEFAULT_CACHE)) {
                    jedis.expire(redisId, genRandomExpiredTime(3));
                    return null;
                }
                jedis.expire(redisId, genRandomExpiredTime(5));
                return gson.fromJson(productRedis, Product.class);
            }

            // 3. redis还是没有,再查mysql数据库
            productMysql = productRepo.findByProductId(productId);
            if (productMysql != null) {
                // 4. 数据库有,则更新redis数据【可能出现突发性热点缓存重建导致数据库系统压力倍增】
                jedis.set(redisId, gson.toJson(productMysql));
                jedis.expire(redisId, genRandomExpiredTime(5));
            } else {
                // 缓存空或默认值 + 过期时间,避免缓存穿透
                jedis.set(redisId, SystemConstants.REDIS_DEFAULT_CACHE);
                jedis.expire(redisId, genRandomExpiredTime(3));
            }
        }
        return productMysql;
    }

解决方案二:分布式锁setnx

// 解决方案二:分布式锁setnx
    <dependency>
      <groupId>org.redisson</groupId>
      <artifactId>redisson</artifactId>
      <version>3.20.0</version>
    </dependency>
    
    @Configuration
		public class RedissonConfig {
    @Bean
    public Redisson redisson() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
        return (Redisson) Redisson.create(config);
    	}
		}

 		// 集群部署:分布式锁
    public Product getProduct2(Long productId) {
        String redisId = SystemConstants.REDIS_KEY_PREFIX + productId;
        // 1. 先查redis缓存
        Product product = getProductFromRedis(redisId);
        if (product != null) {
            return product;
        }

        // 分布式锁RLock确保锁住特定的productId,不影响其他productId,解决所有问题
        RLock lock = redisson.getLock(SystemConstants.LOCK_HOT_CACHE_PREFIX + productId);
        lock.lock(); // 等价于setnx(SystemConstants.LOCK_HOT_CACHE_PREFIX + productId, value)
        
        // 2. DCL再查redis,因为只要有一次查询数据库操作,redis就已经有缓存数据了
        Product productMysql = null;
        try {
            product = getProductFromRedis(redisId);
            if (product != null) {
                return product;
            }
            // 3. redis还是没有,最后查mysql数据库
            productMysql = getProductFromMysql(productId);
        } finally {
            lock.unlock();
        }
        return productMysql;
    }

    private Product getProductFromRedis(String redisId) {
        Product product = null;
        String productRedis = jedis.get(redisId);
        if (!StringUtil.isBlank(productRedis)) {
            if (productRedis.equals(SystemConstants.REDIS_DEFAULT_CACHE)) {
                // 缓存中存在,却是缓存默认值,也就是数据库没有数据,设置过期时间,避免缓存穿透
                jedis.expire(redisId, genRandomExpiredTime(3));
                return new Product(); // 特殊情况
            }
            // 缓存中存在,也是正常值
            jedis.expire(redisId, genRandomExpiredTime(5));
            product = gson.fromJson(productRedis, Product.class);
        }
        return product;
    }

    private Product getProductFromMysql(Long productId) {
        String redisId = SystemConstants.REDIS_KEY_PREFIX + productId;
        Product productMysql = productRepo.findByProductId(productId);
        if (productMysql != null) {
            // 数据库有,则同步更新redis缓存数据【但是可能出现突发性热点缓存重建导致数据库系统压力倍增,也就是这段代码大量执行】
            jedis.set(redisId, gson.toJson(productMysql));
            jedis.expire(redisId, genRandomExpiredTime(5));
        } else {
            // 数据库没有,则设置默认值缓存 + 过期时间,避免缓存穿透
            jedis.set(redisId, SystemConstants.REDIS_DEFAULT_CACHE);
            jedis.expire(redisId, genRandomExpiredTime(3));
        }
        return productMysql;
    }

解决方案三:锁优化-读写锁

// 解决方案三:锁优化-读写锁
public Product getProductByReadWriteLock(Long productId) {
        String redisId = SystemConstants.REDIS_KEY_PREFIX + productId;
        // 1. 先查redis缓存
        Product product = getProductFromRedis(redisId);
        if (product != null) {
            return product;
        }
        // 加写锁
        ReadWriteLock readWriteLock = redisson.getReadWriteLock(SystemConstants.LOCK_HOT_UPDATE_PREFIX + productId);
        Lock writeLock = readWriteLock.writeLock();
        writeLock.lock();

        // 2. DCL再查redis,因为只要有一次查询数据库操作,redis就已经有缓存数据了
        Product productMysql;
        try {
            product = getProductFromRedis(redisId);
            if (product != null) {
                return product;
            }
            // 3. 加读锁 读数据库
            ReadWriteLock readWriteLock2 = redisson.getReadWriteLock(SystemConstants.LOCK_HOT_UPDATE_PREFIX + productId);
            Lock readLock = readWriteLock2.readLock();
            readLock.lock();
            productMysql = getProductFromMysql(productId);
            readLock.unlock();
        } finally {
            writeLock.unlock();
        }
        return productMysql;
    }  
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mr朱墨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值