分布式锁是什么
分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现;
如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往通过互斥来防止彼此干扰;
Redis分布式锁的底层实现是利用setnx命令:
当已存在key时,则存储时返回失败,否则返回成功;
下面是我自己写的redis锁,使用的是redis集群:
public class RedisLockUtil {
/**
* 加锁
*
* @param redisTemplate
* @param lockKey
* @param requestId
* @param expireTime
* @return true为加锁成功,false失败
*/
public static boolean tryLock(RedisTemplate redisTemplate, String lockKey, String requestId, int expireTime) {
Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, expireTime, TimeUnit.SECONDS);
//还有一个可用的原子命令redis的原始命令:
//SET key value [expiration EX seconds|PX milliseconds] [NX|XX] //总共有四个参数,第三个参数[expiration EX seconds|PX milliseconds]
//设置过期时间,第四个参数如果传"NX"则表示只有当key不存在是才进行set,“XX”代表如果只有当key已经存在时才进行set;这个命令的方法在redisTemplate中
//没找到,jedis中有
return result;
}
/**
* 释放锁
*
* @param redisTemplate
* @param lockKey
* @param requestId
* @return
*/
public static Boolean unlock(RedisTemplate redisTemplate, String lockKey, String requestId) {
//使用lua是为了
/**
* // 判断加锁与解锁是不是同一个客户端
* if (requestId.equals(jedis.get(lockKey))) {
* // 若在此时,这把锁突然不是这个客户端的,则会误解锁
* jedis.del(lockKey);
* }
*/
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setResultType(Long.class);
redisScript.setScriptText(script);
Object res = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), requestId);
if (Objects.equals(1L, res)) {
return true;
}
return false;
}
}
减库存使用:
@RestController
@Slf4j
public class RedisLockController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/redisLock")
public String testredisLock() throws InterruptedException {
String redisLock = "product_id_10001";
String stockKey = "product:10001";
String uuid = UUID.randomUUID().toString();
try {
boolean b = RedisLockUtil.tryLock(stringRedisTemplate, redisLock, uuid, 30);
if (!b) {
log.info(Thread.currentThread().getName() + "抢锁失败");
}
String value = stringRedisTemplate.opsForValue().get(stockKey);
if (StringUtils.isEmpty(value) || Integer.parseInt(value) < 0) {
return "errorCode:100001,msg:库存不足";
}
//库存减1
stringRedisTemplate.opsForValue().decrement(stockKey);
return "库存扣减成功";
} finally {
RedisLockUtil.unlock(stringRedisTemplate, redisLock, uuid);
}
}
}
上面自己写的分布式锁还是存在一定问题的,比如如果业务代码执行的时间超过了锁时间,则会被别的线程拿到锁执行相同的代码,出现共享资源的线程安全问题;
解决思路:就是创建一个子线程,定期的检查业务代码是否执行完成,如果还没执行完成就进行续锁操作;
自己写这种代码肯定会出现很多bug的,所以我们可以使用开源的Redission锁框架来解决上述问题,里面提供了多种锁,
Redisson的分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口,同时还支持自动过期解锁
Redisson同时还为分布式锁提供了异步执行的相关方法:
lock.lockAsync();
lock.lockAsync(10, TimeUnit.SECONDS);
Future<Boolean> res = lock.tryLockAsync(3, 10, TimeUnit.SECONDS);
if(res.get()){
// do your business
}
公平锁(Fair Lock):RLock fairLock = redisson.getFairLock("anyLock");
联锁(MultiLock)Redisson的RedissonMultiLock对象可以将多个RLock对象关联为一个联锁,每个RLock对象实例可以来自于不同的Redisson实例。同时加锁:lock1 lock2 lock3, 所有的锁都上锁成功才算成功
红锁(RedLock)Redisson的RedissonRedLock对象实现了Redlock介绍的加锁算法。该对象也可以用来将多个RLock对象关联为一个红锁,每个RLock对象实例可以来自于不同的Redisson实例;同时加锁:lock1 lock2 lock3, 红锁在大部分节点上加锁成功就算成功。
读写锁(ReadWriteLock)Redisson的分布式可重入读写锁RReadWriteLock,Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。同时还支持自动过期解锁。该对象允许同时有多个读取锁,但是最多只能有一个写入锁。
信号量(Semaphore):Redisson的分布式信号量(Semaphore)Java对象RSemaphore采用了与java.util.concurrent.Semaphore相似的接口和用法。
可过期性信号量(PermitExpirableSemaphore)Redisson的可过期性信号量(PermitExpirableSemaphore)实在RSemaphore对象的基础上,为每个信号增加了一个过期时间。每个信号可以通过独立的ID来辨识,释放时只能通过提交这个ID才能释放
闭锁(CountDownLatch):Redisson的分布式闭锁(CountDownLatch)Java对象RCountDownLatch采用了与java.util.concurrent.CountDownLatch相似的接口和用法。
可重入锁的用法:
引入的依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>
配置:
@Configuration
public class RedisConfig {
@Bean
public Redisson redission() {
Config config = new Config();
config.useClusterServers().addNodeAddress(
"redis://192.168.244.129:8001", "redis://192.168.244.129:8002",
"redis://192.168.244.129:8003", "redis://192.168.244.129:8004",
"redis://192.168.244.129:8005", "redis://192.168.244.129:8006")
.setPassword("bijian")
.setConnectTimeout(5000)
.setTimeout(3000)
;
return (Redisson) Redisson.create(config);
}
}
使用:
@RequestMapping("/redissonLock")
public String testredissonLock() throws InterruptedException {
String redisLock = "product_id_10001";
String stockKey = "product:10001";
RLock lock = redisson.getLock(redisLock);
try {
lock.lock(30, TimeUnit.SECONDS);//具有看门狗的功能
//boolean b = lock.tryLock();
//lock.tryLock(2, TimeUnit.SECONDS);
boolean b = lock.tryLock(5, 5, TimeUnit.SECONDS);
String value = stringRedisTemplate.opsForValue().get(stockKey);
if (StringUtils.isEmpty(value) || Integer.parseInt(value) < 0) {
return "errorCode:100001,msg:库存不足";
}
//库存减1
stringRedisTemplate.opsForValue().decrement(stockKey);
return "库存扣减成功";
} finally {
lock.unlock();
}
}
其实Redis分布式锁是存在问题的:
比如集群架构:
master存入锁的key后,还没来得及同步给slave,master宕机slave变为新的master,这样导致其它线程可以加锁,因为这个新slave没有这家lockKey;
zk就可以避免这个问题,因为zk必须是大部分几点都同步完成才能返回加锁成功;
我们可以使用RedLock红锁来模仿zk,因为RedLock是大部分redis都同步成功才会返回成功。
但是redis cluster其实也可以模仿zk的,因为有个配置可以表示多少个slave同步成功才返回成功,就避免了这个问题,如果不配置所有salve都同步成功,则有可能没有同步到lockKey的选为新master(概率小,因为redis选举算法中,存储的数据越新,越有可能成为master);
问题:如果想给这次抢购活动提高效率,在分布式锁的基础上如何设计:
可以使用JDK1.7的ConrrentHashMap的分段锁的思想,比如有1000个商品,可以平均分成10份,每100个商品使用一把不同的锁,按ConrrentHashMap的定位下每段锁下标的方法来分配到不同的分段锁,当发现该锁上面的商品已经抢购完之后,再去其它锁抢购;