没有标题
小明在一家电商平台上班,开发下单业务。公司只有一台服务器,部署了一个服务,他的代码这么写的:
synchronized (this){
// 业务操作 :操作数据库减库存
}
在多线程环境下,这段代码运行良好,没有问题。
后来,公司发展迅速,服务器变成了集群,部署了多个服务。之前的代码就会出现问题,因为synchronized,只能保证在一个jvm里是线程安全的。在多个jvm中,这段扣减库存会有安全问题。
小明想到分布式环境下可以使用reids来解决,他的代码这么写的:
private void process(){
String prodKey = "prod-001";
try(Jedis jedis = getJedis()){
Long setnx = jedis.setnx(prodKey, "1111");
if(setnx == 0){
// 没有设置成功,说明占用
return;
}
try {
// 业务操作 :操作数据库减库存
} finally {
jedis.del(prodKey);
}
}
}
乍一看没问题,利用setnx,设置一个key,当他存在设置不成功,说明占用了,不存在设置成功,扣减库存,最后finally不忘把key删掉。
问题是假设某台服务器在程序执行到业务一半时挂了,此时这个锁依旧无法释放,造成死锁。
小明知道后,简单,再来一版如下:
private void process(){
String prodKey = "prod-001";
try(Jedis jedis = getJedis()){
Long setnx = jedis.setnx(prodKey, "1111");
// 加上过期时间,自动失效
jedis.expire(prodKey, 30);
if(setnx == 0){
// 没有设置成功,说明占用
return;
}
try {
// 业务操作 :操作数据库减库存
} finally {
jedis.del(prodKey);
}
}
}
我加上失效时间不就行了,又有问题了;假设a线程执行业务超过30s了,锁释放,b线程过来加锁成功,a线程执行完执行了jedis.del(prodKey),结果把b线程的锁释放了,这是c线程过来,加锁成功,b执行完把c锁释放。。。。
小明知道后,简单,再来一版如下:
private void process(){
String prodKey = "prod-001";
String clientId = getMachineNum() + UUID.randomUUID().toString();
try(Jedis jedis = getJedis()){
Long setnx = jedis.setnx(prodKey, clientId);
if(setnx == 0){
// 没有设置成功,说明占用
return;
}
// 加上过期时间,自动失效
jedis.expire(prodKey, 30);
try {
// 操作数据库减库存
} finally {
if(clientId.equals(jedis.get("prodKey"))){
// 只删除自己的key
jedis.del(prodKey);
}
}
}
}
我加了一个本机唯一标识clientId,在删除时判断一下是否一致,一致就删掉,这样就不会误删。
那之前那个还有一个问题,假设a线程执行业务超过了设定的过期时间怎么办?锁超时时间自动释放,此时还是和没锁一样,做了这么多没用的功。
小明知道后,简单,再来一版如下:
private void process(){
String prodKey = "prod-001";
String clientId = getMachineNum() + UUID.randomUUID().toString();
try(Jedis jedis = getJedis()){
Long setnx = jedis.setnx(prodKey, clientId);
if(setnx == 0){
// 没有设置成功,说明占用
return;
}
// 加上过期时间,自动失效
jedis.expire(prodKey, 30);
// 看门狗,给锁续命
new Thread(new RenewalThread(jedis, prodKey)).start();
try {
// 操作数据库减库存
} finally {
if(clientId.equals(jedis.get("prodKey"))){
// 只删除自己的key
jedis.del(prodKey);
}
}
}
}
class RenewalThread implements Runnable{
Jedis jedis;
String prodKey;
boolean exist;
public RenewalThread(Jedis jedis, String prodKey) {
this.jedis = jedis;
this.prodKey = prodKey;
this.exist = true;
}
@Override
public void run() {
while(exist){
if(jedis.exists(prodKey)){
jedis.expire(prodKey, 30);
}else{
exist = false;
}
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
现在大部分问题都解决了,但还有一个致命的问题;多个redis命令是非原子操作。在代码中有先setnx,再expire;有先查再删等。这些都有可能出现安全问题。在redis2.6版本新增了lua脚本解析。这个lua是支持多个命令执行,并且支持事务。我们只要在上面那些操作上换成lua脚本。至此reids分布式锁已有小成。
当你做完上面那些步骤的时候,你看下redisson,立马觉得redisson真香。
他帮我们把上面那些步骤都做好了,并且实现的更好。
使用redisson之后的上述代码:
RedissonClient redissonClient = Redisson.create(new Config());
String prodKey = "prod-001";
// 获取锁
RLock lock = redissonClient.getLock(prodKey);
// 加锁
lock.lock();
try {
// 操作数据库减库存
} finally {
// 解锁
lock.unlock();
}
实际代码只有3步,综上所述,大家去用redisson吧。