案列
在查询商品信息时,往往要把商品信息放在缓存中,很容易就想到了redis 。
基本逻辑:先查缓存,缓存中没有,就查数据库,并放到缓存中返回,有就走缓存。
问题
在高并发情况下,这样做的数据是有误的比如你发了一千次查询请求,实际上可能只有几百次请求。
解决
应用redis分布式锁解决 :
-
setnx命令 : 将 key 的值设为 value ,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。高并发下本地锁都是不同的,所有设置分布式锁。 -
应用lua脚本,解决不具备原子性的问题,如 处理完业务需要释放锁,释放时也要判断锁是否是自己的,判断和释放不具备统一的原子操作,所以要引入lua脚本
-
自旋:其他的线程进来没有拿到锁, 就自旋拿锁 while循环
-
引入本地线程 :前面的的锁不具备重入性,自旋出来又会加锁,导致锁上锁,锁死,所以引入本地线程 ,ThreadLocal 底层实际上是map集合, 添加一个map保存拿到锁的状态 把token放到第一轮map中,用完之后删除第一轮的token,
-
代码如下:
在这//根据skuId查询商品的基本信息 先走redis 没有再走数据库
@Override
public SkuInfo getSkuInfo(Long skuId) {
SkuInfo skuInfo = getSkuInfoFromRedis(skuId);
return skuInfo;
}
ThreadLocal<String> threadLocal = new ThreadLocal<>();
private SkuInfo getSkuInfoFromRedis(Long skuId) {
//细粒度锁
String lockKey="lock-"+skuId;
String cacheKey = RedisConst.SKUKEY_PREFIX + skuId + RedisConst.SKUKEY_SUFFIX;
SkuInfo skuInfoRedis = (SkuInfo) redisTemplate.opsForValue().get(cacheKey);
if (skuInfoRedis == null) {
String token = threadLocal.get();
boolean accquireLock=false;
if(!StringUtils.isEmpty(token)){
//锁已经拿到过了
accquireLock=true;
}else{
//做一个标记
token = UUID.randomUUID().toString();
accquireLock = redisTemplate.opsForValue().setIfAbsent(lockKey, token,3, TimeUnit.SECONDS);
}
if(accquireLock){
SkuInfo skuInfoDb = getSkuInfoFromDb(skuId);
//把数据放到redis中
redisTemplate.opsForValue().set(cacheKey, skuInfoDb, RedisConst.SKUKEY_TIMEOUT, TimeUnit.SECONDS);
//添加一个lua脚本
String luaScript="if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
//把脚本放到script对象当中
redisScript.setScriptText(luaScript);
//设置脚本返回类型
redisScript.setResultType(Long.class);
redisTemplate.execute(redisScript, Arrays.asList(lockKey),token);
threadLocal.remove();
return skuInfoDb;
}else{
//自旋 目的是为了去拿锁
while(true){
SleepUtils.millis(50);
boolean retryAccquireLock = redisTemplate.opsForValue().setIfAbsent(lockKey, token,3, TimeUnit.SECONDS);
if(retryAccquireLock){
threadLocal.set(token);
break;
}
}
return getSkuInfoFromRedis(skuId);
}
}
return skuInfoRedis;
/**
* 数据库查询
* @param skuId
* @return
*/
private SkuInfo getSkuInfoFromDb(Long skuId) {
SkuInfo skuInfo = this.getById(skuId);
if (skuInfo!=null){
LambdaQueryWrapper<SkuImage> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SkuImage::getSkuId, skuId);
List<SkuImage> imageList = skuImageService.list(wrapper);
skuInfo.setSkuImageList(imageList);
}
return skuInfo;
}
前面都是不用redisson的依赖情况下,自己设置的分布式锁,引入redisson会更加简单,底层也是lua脚本等实现,可以更加方便,使用也很简单
redisson使用
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.11.2</version>
</dependency>
配置类编写
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress("redis://10.211.55.25:6389");
RedissonClient redisson = Redisson.create(config);
return redisson;
}
}
service代码
@Autowired
private RedissonClient redissonClient;
private SkuInfo getSkuInfoFromRedis(Long skuId) {
//减小锁的粒度 分布式锁过大,加上商品减小锁粒度,
//以便别的商品不受正在查询的商品影响
String lockKey="lock-"+skuId;
String cacheKey = RedisConst.SKUKEY_PREFIX + skuId + RedisConst.SKUKEY_SUFFIX;
SkuInfo skuInfoRedis = (SkuInfo) redisTemplate.opsForValue().get(cacheKey);
RLock lock = redissonClient.getLock(lockKey);
if (skuInfoRedis == null) {
try {
lock.lock();
SkuInfo skuInfoDB = getSkuInfoFromDb(skuId);
//把数据放入redis
redisTemplate.opsForValue().set(cacheKey, skuInfoDB, RedisConst.SKUKEY_TIMEOUT, TimeUnit.SECONDS);
return skuInfoDB;
} finally {
lock.unlock();
}
}
return skuInfoRedis;
}
会发现简单很多。因为其底层都帮我们实现了一些原子性问题等等判断,这样就不需要麻烦自己