分布式锁redisson的使用(并发场景下)
1.基于缓存,对热点数据进行刷新过期时间,以实现“冷热数据分离”。
2.可以对“热点数据进行缓存重建”(双层获取)
3.使用分布式读写锁,可解决“数据库与缓存双写不一致”的场景
4.分布式读写锁的机制(读读共享,读写互斥,写写互斥)读写锁底层实现的是读写操作都是抢同一把锁,已控制拿锁的顺序。
若是读读操作,则底层实现的是可重入锁机制,在进行读读操作是,多个线程拿的都是同一把读锁,通过对信号量state进行+1,来计算重入的次数及释放锁的量。
若是写写,或写读场景,则其他未获取到锁的线程,则会处于等待阻塞状态。
@Service
public class ProductService {@Autowiredprivate ProductDao productDao;@Autowiredprivate RedisUtil redisUtil;@Autowiredprivate 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<String, Product> productMap = new ConcurrentHashMap<>();@Transactionalpublic Product create(Product product) {Product productResult = productDao.create(product);redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult),genProductCacheTimeout(), TimeUnit.SECONDS);return productResult;}@Transactionalpublic 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缓存中获取,以减少磁盘IOif (!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.可对某些不重要的服务可进行做熔断降级处理。