一、缓存实例
1.读数据
public ProductSellDetailsModel getProductSellInfo(Long productId) {
ProductSellDetailsModel productSellDetailsModel;
// 查询缓存
Object productSell =
stringRedisTemplate.opsForHash().get("redis:productSellDetail", productId + "");
if (StringUtils.isEmpty(productSell)) {
// 缓存未找到 查db
productSellDetailsModel = productSellDaoMapper.queryByProductId(productId);
if(!StringUtils.isEmpty(productSellDetailsModel)){
stringRedisTemplate.opsForHash().put("redis:productSellDetail",
productId + "", JSON.toJSONString(productSellDetailsModel));
}
} else {
productSellDetailsModel =
JSONObject.parseObject(String.valueOf(productSell),ProductSellDetailsModel.class);
}
return productSellDetailsModel;
}
二、分布式锁
在部署了多台Tomcat服务的情况下,为了解决并发场景下的超卖问题,synchronized本地锁已经无法解决问题。在此情况下我们可以考虑Redis分布式锁。
方案一:
Boolean lock =stringRedisTemplate.opsForValue().setIfAbsent("redisLock"+productId,"lock");
if(lock){
// 拿到锁后 执行逻辑
}else{
// 未拿到锁
}
// 释放锁
stringRedisTemplate.delete("redisLock"+productId);
方案一,大致逻辑没有问题,但是方案还有很多缺点。
比如:在执行业务逻辑抛异常后、或者在释放锁操作时宕机,导致释放锁操作失败,从而产生死锁问题。
针对这两个问题做优化:
1.业务逻辑抛异常。可以使用try finally
2.在释放锁操作前宕机。可以给锁添加一个失效时间
方案二:
try{
// 先设置锁,再设置超时时间,两条命令,没有保证命令的原子性
//Boolean lock =stringRedisTemplate.opsForValue().setIfAbsent("redisLock"+productId,"lock");
//stringRedisTemplate.expire("redisLock"+productId,10,TimeUnit.SECONDS);
Boolean lock = stringRedisTemplate.opsForValue()
.setIfAbsent("redisLock"+productId, "lock",10, TimeUnit.SECONDS);
if(lock){
// 拿到锁后 执行逻辑
}else{
// 未拿到锁
}
}finally{
// 释放锁
stringRedisTemplate.delete("redisLock"+productId);
}
方案二相对于方案一而言,代码更加健壮一些,但还是存在一些问题。
例如:有三个线程同时执行这段代码
1.线程一先拿到锁,开始执行业务逻辑,假设业务逻辑执行时间需要12秒,超过了设置的锁过期时间10秒,导致锁失效,而此时线程一还在执行业务逻辑
2.线程二和三一直在等待锁释放,当锁失效后,线程二拿到了锁后,执行了2秒,而线程一已经执行12秒,逻辑已经执行完成,释放锁,但是此时线程二还在执行业务逻辑。
3.线程二加的锁被线程一释放掉,而线程二还在执行业务逻辑,线程三拿到锁后,也开始执行业务逻辑…
4.如此逻辑下去,不难发现分布式锁的效果并没有实现。
针对上面情景进行问题分析:
a.锁的失效时间不应该小于业务逻辑的执行时间。业务逻辑的执行时间受多重因素影响,是不确定的,因此锁的失效时间设置多少都是不合理的。那我们可不可以新开启一个线程,定时去获取锁的状态,如果有线程在使用,则更新一下过期时间,以此来延迟锁失效。
b.上面的场景可以看到,线程二加的锁被线程一给释放了。我们则可以进行限制,自己加的锁只能自己进行释放。
方案三:
// 生成唯一id
String uuid = UUID.randomUUID().toString();
try{
// 先设置锁,再设置超时时间,两条命令,没有保证命令的原子性
//Boolean lock =stringRedisTemplate.opsForValue().setIfAbsent("redisLock"+productId,"lock");
//stringRedisTemplate.expire("redisLock"+productId,10,TimeUnit.SECONDS);
Boolean lock = stringRedisTemplate.opsForValue()
.setIfAbsent("redisLock"+productId, uuid,10, TimeUnit.SECONDS);
if(lock){
// 拿到锁后 执行逻辑
}else{
// 未拿到锁
}
}finally{
// 释放锁
// 查询和删除没有原子性操作,故舍弃,采用Lua脚本的方式,保证操作的原子性
// String redisLock = stringRedisTemplate.opsForValue().get("redisLock");
// if(uuid.equals(redisLock)){
// // 清理锁
// log.info("清理锁==>"+redisLock);
// stringRedisTemplate.delete("redisLock");
// }
// 脚本解锁
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
stringRedisTemplate.execute(
new DefaultRedisScript(script, Long.class), Arrays.asList("redisLock"+productId), uuid);
}
上面的代码并没有加入锁续命的逻辑,当然代码还是有些地方需要优化。从上面的整个思维来看,除了业务逻辑外,还需要添加很多分布式锁相关代码,整个逻辑相对更加复杂。而redisson框架,帮我们解决了这些问题。
redisson的使用可浏览下个章节