我们可以同时去一个地方“占坑”,如果占到,就执行逻辑。否则就必须等待,直到释放锁。占坑可以去Redis,也可以去数据库,可以去任何大家都可以访问到的地方。等待可以自旋的方式。
Redis官网NX参数说明
NX – 只有键key不存在的时候才会设置key的值。
解读:每个线程在访问时,先会去判断这个“坑位”有没有加锁,如果没有加锁,则自己占住加锁(然后获取数据,从缓存/数据库),别人来的时候看到加锁就会等待,当从坑位离开时,会删除锁,然后其他的才会继续站位。
进入redis:
> docker exec -it redis redis-cli
# 可以多开几个窗口进行测试
127.0.0.1:6379> set lock 1 NX
OK
阶段一示例代码:
String uuid = UUID.randomUUID().toString();
ValueOperations ops = stringRedisTemplate.opsForValue();
Boolean lock = ops.setIfAbsent("lock", uuid,500, TimeUnit.SECONDS);
if (lock) {
//加锁成功执行业务
Map> categoriesDb = getCategoryMap();
stringRedisTemplate.delete("lock");//删除锁
return categoriesDb;
}else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 睡眠0.1s后,重新调用 //自旋
return getCatalogJsonDbWithRedisLock();
}
阶段二示例代码:
Boolean lock = ops.setIfAbsent("lock", uuid,500, TimeUnit.SECONDS);//设置过期时间和占位原子性
阶段三示例代码:
String uuid = UUID.randomUUID().toString();
ValueOperations ops = stringRedisTemplate.opsForValue();
Boolean lock = ops.setIfAbsent("lock", uuid,500, TimeUnit.SECONDS);
if (lock) {
Map> categoriesDb = getCategoryMap();
//删除锁
//先去redis查询下保证当前的锁是自己的
//获取值对比,对比成功删除= 丢失原子性--->需要解决
String lockValue = stringRedisTemplate.opsForValue().get("lock");
if (uuid.equals(lockValue)) {
//删除我自己的锁
stringRedisTemplate.delete("lock");
}
return categoriesDb;
}else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 睡眠0.1s后,重新调用 //自旋
return getCatalogJsonDbWithRedisLock();
}
阶段四示例代码:
String uuid = UUID.randomUUID().toString();
ValueOperations ops = stringRedisTemplate.opsForValue();
Boolean lock = ops.setIfAbsent("lock", uuid,500, TimeUnit.SECONDS);
if (lock) {
Map> categoriesDb = getCategoryMap();
String lockValue = ops.get("lock");
// get和delete原子操作
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
stringRedisTemplate.execute(
new DefaultRedisScript(script, Long.class), // 脚本和返回类型
Arrays.asList("lock"), // 参数
lockValue); // 参数值,锁的值
return categoriesDb;
}else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 睡眠0.1s后,重新调用 //自旋
return getCatalogJsonDbWithRedisLock();
}
//分布式锁三级分类查询
@Override
public HashMap> getCatalogJsonFromDbWithRedisLock(){
//先去缓存查
String catalogListCache = stringRedisTemplate.opsForValue().get("catalogListCache");
//缓存有 直接返回
if(catalogListCache!= null){
HashMap> stringListHashMap = JSON.parseObject(catalogListCache, new TypeReference>>() {
});
System.out.println("缓存命中");
return stringListHashMap;
}else {
//1去redis占锁
String uuid = UUID.randomUUID().toString();
ValueOperations ops = stringRedisTemplate.opsForValue();
//占坑 给时间 原子性操作
Boolean lock = ops.setIfAbsent("lock",uuid, 30, TimeUnit.SECONDS);
if (lock) { //获取锁成功
System.out.println("获取分布式锁成功...");
try {
//执行业务
//redis没有去数据库查
HashMap> dbmap = catalogList();
String s = JSON.toJSONString(dbmap);
//如果数据库查到也是null
if(s == null || StringUtils.isEmpty(s)){
//缓存穿透 短暂缓存null值
stringRedisTemplate.opsForValue().set("catalogListCache",s,30, TimeUnit.SECONDS);
}
//加入缓存时间 随机值 解决雪崩问题
Random random = new Random();
int i = random.nextInt(300);
//查完放入缓存
stringRedisTemplate.opsForValue().set("catalogListCache",s,300+i, TimeUnit.SECONDS);
return dbmap;
}finally {
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("lock"), uuid);
}
}else{ //没有获取锁 自旋等待
System.out.println("获取分布式锁失败...等待重试...");
//加锁失败...重试机制
//休眠一百毫秒
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJsonFromDbWithRedisLock();
}
}
}