Loading

分布式锁

分布式锁

1.只适用于单线程

@RestController
@RequestMapping("/stock")
public class StockController {

    final StringRedisTemplate stringRedisTemplate;

    public StockController(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @GetMapping("/sell")
    public String sell() {
        String stockKey = "stock";
        int stock = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get("stock")));
        if (stock > 0) {
            int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set(stockKey, realStock + "");
            System.out.println("减库存成功,剩余库存:" + realStock);
        } else {
            System.out.println("库存不足!");
        }
        return "end";
    }

}

一旦进入多个线程进入出现超卖问题

使用jmeter压力测试下,出现重复打印库存,即超卖

减库存成功,剩余库存:99
减库存成功,剩余库存:99
减库存成功,剩余库存:98
减库存成功,剩余库存:98
减库存成功,剩余库存:97
减库存成功,剩余库存:97
减库存成功,剩余库存:96
减库存成功,剩余库存:96
减库存成功,剩余库存:95
减库存成功,剩余库存:94
减库存成功,剩余库存:94
减库存成功,剩余库存:93
减库存成功,剩余库存:92
减库存成功,剩余库存:92
减库存成功,剩余库存:91
减库存成功,剩余库存:91
减库存成功,剩余库存:91
减库存成功,剩余库存:90

2.单机部署下

package top.wangjf.springbootredis.controller;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Objects;

@RestController
@RequestMapping("/stock")
public class StockController {

    final StringRedisTemplate stringRedisTemplate;

    public StockController(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @GetMapping("/sell")
    public String sell() {
        synchronized (this) {
            String stockKey = "stock";
            int stock = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get("stock")));
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set(stockKey, realStock + "");
                System.out.println("减库存成功,剩余库存:" + realStock);
            } else {
                System.out.println("库存不足!");
            }
            return "end";
        }

    }

}

集群部署(两个服务server1, server2),使用nginx反向代理负载均衡,

upstream redislock {
    server localhost:8080 weight=1;
    server localhost:8090 weight=1;
}
server {
        listen       80;
        location / {
            root   html;
            index  index.html index.htm;
            proxy_pass http://redislock; #访问nginx根目录,反向代理到redis_lock
        }
}

使用jmeter压力测试下,出现重复打印库存,即超卖

server1 server2
减库存成功,剩余库存:99
减库存成功,剩余库存:98
减库存成功,剩余库存:97
减库存成功,剩余库存:95
减库存成功,剩余库存:94
减库存成功,剩余库存:93
减库存成功,剩余库存:91
减库存成功,剩余库存:90
减库存成功,剩余库存:97
减库存成功,剩余库存:96
减库存成功,剩余库存:94
减库存成功,剩余库存:93
减库存成功,剩余库存:92
减库存成功,剩余库存:91

3.redis分布式锁(setnx)

package top.wangjf.springbootredis.controller;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Objects;

@RestController
@RequestMapping("/stock")
public class StockController {

    final StringRedisTemplate stringRedisTemplate;

    public StockController(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @GetMapping("/sell")
    public String sell() {
        String lockKey = "lockKey";
        //redis实现分布式锁 类似 jedis.setnx(key,value);
        final Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "lock");
        if (!lock) { //未获得锁,但是库存还有
            return "error_code";
        }
        String stockKey = "stock";
        int stock = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get("stock")));
        if (stock > 0) {
            int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set(stockKey, realStock + "");
            System.out.println("减库存成功,剩余库存:" + realStock);
        } else {
            System.out.println("库存不足!");
        }

        //redis实现分布式锁
        stringRedisTemplate.delete(lockKey);
        return "end";
    }

}

3.1解决死锁问题

可能出现的问题:

  • 死锁
    • 运行中抛出异常导致最后的删除操作没有执行
      • 解决方法:使用try{}catch(){}finall{} 在finally代码块中删除
    • 运行中进程终止导致最后的删除操作没有执行
      • 为分布式锁设置过期时间(可能存在原子性问题)
package top.wangjf.springbootredis.controller;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Objects;
import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("/stock")
public class StockController {

    final StringRedisTemplate stringRedisTemplate;

    public StockController(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @GetMapping("/sell")
    public String sell() {
        String lockKey = "lockKey";
        try {
            //redis实现分布式锁 类似 jedis.setnx(key,value);
            //final Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "lock");
            //进程中止导致导致死锁优化
            //stringRedisTemplate.expire(lockKey,10, TimeUnit.MILLISECONDS);

            //进程中止导致导致死锁优化(解决原子性问题)
            final Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "lock", 10, TimeUnit.MILLISECONDS);
            if (!lock) { //未获得锁,但是库存还有
                return "error_code";
            }
            String stockKey = "stock";
            int stock = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get("stock")));
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set(stockKey, realStock + "");
                System.out.println("减库存成功,剩余库存:" + realStock);
            } else {
                System.out.println("库存不足!");
            }
        } finally {
            //解决异常导致死锁问题
            stringRedisTemplate.delete(lockKey);
        }
        return "end";
    }
}

3.2解决高并发下锁失效问题

可能存在的问题:

自己的锁被别的线程释放:

假如秒杀功能执行需要100ms,但是分布式锁只有10ms的过期时间,

A线程在执行中分布式锁过期但是还未执行完,B进程获得分布式锁,

此时A进程执行结束释放锁,但是释放得是B进程的锁。导致分布式锁失效。

解决方案:

​ 保证加的锁和释放的锁是同一个。

package top.wangjf.springbootredis.controller;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("/stock")
public class StockController {

    final StringRedisTemplate stringRedisTemplate;

    public StockController(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @GetMapping("/sell")
    public String sell() {
        String uuid = UUID.randomUUID().toString(); //解决高并发下分布式锁失效问题
        String lockKey = "lockKey";
        try {
            //redis实现分布式锁 类似 jedis.setnx(key,value);
            //final Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "lock");
            //进程中止导致导致死锁优化
            //stringRedisTemplate.expire(lockKey,10, TimeUnit.MILLISECONDS);

            //进程中止导致导致死锁优化(解决原子性问题)
            final Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, uuid, 10, TimeUnit.MILLISECONDS);
            if (!lock) { //未获得锁,但是库存还有
                return "error_code";
            }
            String stockKey = "stock";
            int stock = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get("stock")));
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set(stockKey, realStock + "");
                System.out.println("减库存成功,剩余库存:" + realStock);
            } else {
                System.out.println("库存不足!");
            }
        } finally {
            //解决高并发下分布式锁失效问题
            if (uuid.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
                stringRedisTemplate.delete(lockKey);
            }
        }

        return "end";

    }
}

依然会存在超卖问题,在并发量不是非常非常高的情况下可以接受

优化方案:

每8ms判断以下是否仍然未释放锁,如果没有释放则,适当延长过期时间

4.分布式锁 redisson

 <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        this.internalLockLeaseTime = unit.toMillis(leaseTime);
        return this.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command,
                        "if (redis.call('exists', KEYS[1]) == 0) " +
                        "then redis.call('hincrby', 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 redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                        "redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; " +
                        "return redis.call('pttl', KEYS[1]);",
                Collections.singletonList(this.getName()), this.internalLockLeaseTime, this.getLockName(threadId));
    }
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
        if (leaseTime != -1L) {
            return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        } else {
            RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
            ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
                if (e == null) {
                    if (ttlRemaining == null) {
                        this.scheduleExpirationRenewal(threadId); //定时任务
                    }

                }
            });
            return ttlRemainingFuture;
        }
    }
private void scheduleExpirationRenewal(long threadId) {
        RedissonLock.ExpirationEntry entry = new RedissonLock.ExpirationEntry();
        RedissonLock.ExpirationEntry oldEntry = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry);
        if (oldEntry != null) {
            oldEntry.addThreadId(threadId);
        } else {
            entry.addThreadId(threadId);
            this.renewExpiration();
        }

    }
private void renewExpiration() {
        RedissonLock.ExpirationEntry ee = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
        if (ee != null) {
            Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
                public void run(Timeout timeout) throws Exception {
                    RedissonLock.ExpirationEntry ent = (RedissonLock.ExpirationEntry)RedissonLock.EXPIRATION_RENEWAL_MAP.get(RedissonLock.this.getEntryName());
                    if (ent != null) {
                        Long threadId = ent.getFirstThreadId();
                        if (threadId != null) {
                            RFuture<Boolean> future = RedissonLock.this.renewExpirationAsync(threadId);
                            future.onComplete((res, e) -> {
                                if (e != null) {
                                    RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", e);
                                } else {
                                    if (res) {
                                        RedissonLock.this.renewExpiration();
                                    }

                                }
                            });
                        }
                    }
                }
            }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS); 
            ee.setTimeout(task);
        }
    }
public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
        super(commandExecutor, name);
        this.commandExecutor = commandExecutor;
        this.id = commandExecutor.getConnectionManager().getId();
        this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
        this.entryName = this.id + ":" + name;
        this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub();
    }
public Config() {
        this.transportMode = TransportMode.NIO;
        this.lockWatchdogTimeout = 30000L; //默认是30s
        this.keepPubSubOrder = true;
        this.decodeInExecutor = false;
        this.useScriptCache = false;
        this.minCleanUpDelay = 5;
        this.maxCleanUpDelay = 1800;
        this.cleanUpKeysAmount = 100;
        this.nettyHook = new DefaultNettyHook();
        this.useThreadClassLoader = true;
        this.addressResolverGroupFactory = new DnsAddressResolverGroupFactory();
    }

5.分布式锁zookeeper

6.redis zookeeper分布式锁区别

posted @ 2022-04-17 14:50  左手画圆右手画方  阅读(54)  评论(0)    收藏  举报