分布式锁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<String, Product> 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.可对某些不重要的服务可进行做熔断降级处理。