全局id生成器
在redis新建一个自增长的key,利用时间戳(31bit)+序列号(32bit)拼接实现唯一id。此key还可以记录数量。
库存超卖问题
悲观锁
比较适合插入数据。
悲观锁可以实现对于数据的串行化执行,synchronization,和lock都是悲观锁的代表,同时,悲观锁中又可以再细分为公平锁,非公平锁,可重入锁,等等。
乐观锁
在更改数据时判断之前查询到的数据是否有被修改过。
比较适合更新数据。
版本号法
有一个版本号,每次操作数据会对版本号+1,再提交回数据时,会去校验是否比之前的版本大1 ,如果大1 ,则进行操作成功,这套机制的核心逻辑在于,如果在操作过程中,版本号只比原来大1 ,那么就意味着操作过程中没有人对他进行过修改,他的操作就是安全的,如果不大1,则数据被修改过。
CAS法
利用cas进行无锁化机制加锁,通过判断要更改的数据和查询到的数据是否达到预估的结果。可以看成是版本号法的mini版。
一人一单
单机模式下的线程安全问题
插入数据,使用悲观锁。
控制锁粒度
利用用户id作为锁,锁的范围尽可能的小。
小tips
toString()是new出来的对象,要想保证拿到同一把String锁,得使用intern()方法,她是从常量池拿数据的。
先提交事务再释放锁
避免事务还未提交,锁就释放了导致出现线程安全问题。
小tips
spring中,在方法A通过对象调用方法B,B具有事务,此时要想让B的事务也生效,这个调用对象得是bean(代理对象)。
集群模式下的线程安全问题
分布式锁(手写)
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁,核心思想就是让大家都使用同一把锁。
误删问题
原因:持有锁的线程还没执行完业务就超时释放了锁,然后删除了别的线程的锁。
解决:线程只能删自己的锁,给每个锁加唯一value。
锁的原子性问题
原因:判断唯一value和释放锁不是同一条语句,中间可能会阻塞,导致别的线程趁虚而入。
解决:Lua脚本把判断和释放锁当成一个命令,保证原子性。
分布式锁(Redisson)
流程图:
可重入锁
在hash中用value记录重入次数,获取同把锁锁加1,释放同把锁减1。要删除前会先判断value是否等于0。
KEYS[1] : 锁名称
ARGV[1]: 锁失效时间
ARGV[2]: id + ":" + threadId; 锁的小key (field,对应value是重入次数)
获取锁:
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + //大key+小key判断
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);"
获取锁成功返回的是null,失败则返回锁的剩余毫秒数。
释放锁:
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end; " +
"return nil;"
锁重试
利用信号量(Semaphore,并发工具类)和PubSub(发布订阅模式,一种设计模式)功能实现等待,唤醒,获取锁失败的重试机制。
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
long time = unit.toMillis(waitTime);
long current = System.currentTimeMillis();
long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return true;
}
time -= System.currentTimeMillis() - current; //最大等待时间-获取锁消耗的时间
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
current = System.currentTimeMillis();
//subscribe订阅释放锁的消息(publish)
RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
//await:当方法在指定的时间内完成是为true
if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
if (!subscribeFuture.cancel(false)) {
subscribeFuture.onComplete((res, e) -> {
if (e == null) {
//超时取消订阅
unsubscribe(subscribeFuture, threadId);
}
});
}
acquireFailed(waitTime, unit, threadId);
return false;
}
try {
//剩余等待时间 - 等待订阅消耗的时间
time -= System.currentTimeMillis() - current;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
while (true) {
long currentTime = System.currentTimeMillis();
ttl = tryAcquire(waitTime, leaseTime, unit, threadId);//重试
// lock acquired
if (ttl == null) {
return true;
}
//再次判断剩余等待时间
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// waiting for message
currentTime = System.currentTimeMillis();
if (ttl >= 0 && ttl < time) {//判断过期时间是否小于剩余等待时间
subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
subscribeFuture.getNow().getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
}
} finally {
unsubscribe(subscribeFuture, threadId);
}
}
超时续约
利用watchDog,每隔一段时间,重置超时时间。只有在未指定锁超时时间时才会使用看门狗,每隔一段时间:releaseTime(默认30s) / 3,重置超时时间,确保锁是因为业务执行完释放,而不是因为阻塞释放。
具体源码就不放了,我还没弄懂,以后有需要再来看。ps:P67
MutiLock
使用multiLock锁代替主从,每个节点的地位都是一样的,这把锁加锁的逻辑需要写入到每一个节点上,只有所有的服务器都写入成功,此时才是加锁成功,假设现在某个节点挂了,那么他去获得锁的时候,只要有一个节点拿不到,都不能算是加锁成功,就保证了加锁的可靠性。
API:创建联锁:redissonClient.getMultiLock(锁1, 锁2, 锁3...)
三种分布式锁对比
- 手写分布式锁
- 原理:利用setnx的互斥性;利用ex避免死锁;释放锁时判断唯一value。
- 缺点:不可重入,无法重试,锁超时失效。
- Redisson的分布式锁
- 原理:利用hash结构,记录线程标识和重入次数;利用信号量和PubSub控制锁重试等待;利用watchDog延续锁时间。
- 缺点:redis宕机引起锁失效问题。
- Redisson的multiLock
- 原理:多个独立的Redis节点,必须在所有节点都获取锁,才算获取锁成功。
秒杀优化
先利用redis完成判断是否具有购买资格(代替锁),再把耗时较久的数据库写操作放入阻塞队列,用独立线程异步下单。