Redis-Redis多级缓存架构(实践)

分布式锁redisson的使用(并发场景下)

1.基于缓存,对热点数据进行刷新过期时间,以实现“冷热数据分离”

2.可以对“热点数据进行缓存重建”(双层获取)

3.使用分布式读写锁,可解决“数据库与缓存双写不一致”的场景

4.分布式读写锁的机制(读读共享,读写互斥,写写互斥)读写锁底层实现的是读写操作都是抢同一把锁,已控制拿锁的顺序。

        若是读读操作,则底层实现的是可重入锁机制,在进行读读操作是,多个线程拿的都是同一把读锁,通过对信号量state进行+1,来计算重入的次数及释放锁的量。

        若是写写,或写读场景,则其他未获取到锁的线程,则会处于等待阻塞状态。

@Service
public class ProductService {

    @Autowired
    private ProductDao productDao;

    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private Redisson redisson;

    public static final Integer PRODUCT_CACHE_TIMEOUT = 60 * 60 * 24;
    public static final String EMPTY_CACHE = "{}";
    public static final String LOCK_PRODUCT_HOT_CACHE_PREFIX = "lock:product:hot_cache:";
    public static final String LOCK_PRODUCT_UPDATE_PREFIX = "lock:product:update:";
    public static Map productMap = new ConcurrentHashMap<>();

    @Transactional
    public Product create(Product product) {
        Product productResult = productDao.create(product);
        redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult),
                genProductCacheTimeout(), TimeUnit.SECONDS);
        return productResult;
    }

    @Transactional
    public Product update(Product product) {
        Product productResult = null;
        //RLock updateProductLock = redisson.getLock(LOCK_PRODUCT_UPDATE_PREFIX + product.getId());
        RReadWriteLock readWriteLock = redisson.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX + product.getId());
        //通过读写锁来控制并发(读写锁 共用了是同一把锁)
        RLock writeLock = readWriteLock.writeLock();
        writeLock.lock();
        //这种不传时间值,当在并发条件下,那些未获取到锁的线程,会在一直处于阻塞状态,等待持有锁的线程执行完毕,再去唤醒需要持有锁的线程
       // writeLock.lock(10,TimeUnit.SECONDS);
        // 这种传入时间值,当在并发条件下,那些未获取到锁的线程,会在在10s时间内阻塞等待,若超过10s还未获取到锁,那么那些需要获取到锁的线程会重新再次去请求获取锁对象
        try {
            productResult = productDao.update(product);
            redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult),
                    genProductCacheTimeout(), TimeUnit.SECONDS); //更新缓存
            productMap.put(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), product);
            //使用map去存储高热点数据(高热点数据比较少,则需要的内存较少,所以本地缓存可以使用),本地缓存可支持百万级别的并发,那么就服务就不会那么容易崩亏
        } finally {
            writeLock.unlock();
        }
        return productResult;
    }

    public Product get(Long productId) throws InterruptedException {
        Product product = null;
        String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;
        //先从本地缓存中获取数据
        product = getProductFromCache(productCacheKey);
        if (product != null) {
            return product;
        }
        //DCL
        //若本地缓存中不存在,则通过分布式锁,进行加锁
        RLock hotCacheLock = redisson.getLock(LOCK_PRODUCT_HOT_CACHE_PREFIX + productId);
        //并发条件下,未获取到锁的线程,在此等待
        hotCacheLock.lock();
        //boolean result = hotCacheLock.tryLock(3, TimeUnit.SECONDS);
        try {
            //当加上分布式锁后再次中本地缓存中去获取数据,此为“热点缓存并发重建”
            //加分布式锁后,再次从本地缓存中去获取数据(考虑到,再加分布式锁过程中,有其他线程已经执行完查询的数据,并进行了缓存)
            product = getProductFromCache(productCacheKey);
            if (product != null) {
                return product;
            }
            //若缓存中未获取到数据,则通过redisson获取一把读写锁,来控制读写顺序(读写锁是共同抢同一把锁,具有互斥性,从而达到顺序执行)
            //同时分布式的读写锁,可以解决“数据库与缓存双写不一致”的问题
            //RLock updateProductLock = redisson.getLock(LOCK_PRODUCT_UPDATE_PREFIX + productId);
            RReadWriteLock readWriteLock = redisson.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX + productId);
            RLock rLock = readWriteLock.readLock();
            rLock.lock();
            try {
                product = productDao.get(productId);//读取数据库数据
                if (product != null) {
                    redisUtil.set(productCacheKey, JSON.toJSONString(product),
                            genProductCacheTimeout(), TimeUnit.SECONDS); //将取到的数据,进行redis缓存 和 本地缓存
                    productMap.put(productCacheKey, product);
                } else {
                    //若数据库中并没有该数据,则将缓存一个空对象,以防止恶意攻击
                    redisUtil.set(productCacheKey, EMPTY_CACHE, genEmptyCacheTimeout(), TimeUnit.SECONDS);
                }
            } finally {
                rLock.unlock();//释放读锁
            }
        } finally {
            hotCacheLock.unlock(); //释放分布式锁
        }
        return product;
    }


    private Integer genProductCacheTimeout() {
        return PRODUCT_CACHE_TIMEOUT + new Random().nextInt(5) * 60 * 60;
        //设置不同的缓存过期时间,防止所有的key在同一时刻失效,造成缓存击穿,从而使服务瘫痪
    }

    private Integer genEmptyCacheTimeout() {
        return 60 + new Random().nextInt(30);
        //对空对象也进行设置不同的过期时间
    }

    private Product getProductFromCache(String productCacheKey) {
        Product product = productMap.get(productCacheKey);//先从本地缓存中获取数据,以提高数据访问量和有效性
        if (product != null) {
            return product;
        }

        String productStr = redisUtil.get(productCacheKey);//若本地缓存不存在,则从reids缓存中获取,以减少磁盘IO
        if (!StringUtils.isEmpty(productStr)) {  //判断需要获取的对象是否为空,若为空,则再次判断是否为空对象,若是空对象,则进行缓存
            if (EMPTY_CACHE.equals(productStr)) { //对获取的对象进行判断,是否为空对象(空对象存在的意义是防止恶意攻击,黑客攻击等),若为空对象,则返回一个新建的对象
                redisUtil.expire(productCacheKey, genEmptyCacheTimeout(), TimeUnit.SECONDS);
                return new Product();
            }
            product = JSON.parseObject(productStr, Product.class);
            redisUtil.expire(productCacheKey, genProductCacheTimeout(), TimeUnit.SECONDS);
            //读延期,已实现“冷热数据分离”(原理是当前被访问的对象,每被查询一次,就刷新一次过期时间)

        }
        return product;
    }

}

对于缓存架构基于热点爆发数据处理:

1.当热点数据突然迸发时,容易出现“缓存雪崩”场景,则可通过前端进行限流处理。

2.前端有处理失效的情况,那么可考虑使用多级缓存架构,如:前端缓存,本地缓存,redis缓存

3.可对某些不重要的服务可进行做熔断降级处理。

你可能感兴趣的:(缓存,redis,架构)