1、互斥性:在任何时刻,对于同一条数据,只有一台应用可以获取到分布式锁;
2、高可用性:在分布式场景下,一小部分服务器宕机不影响正常使用,这种情况就需要将提供分布式锁的服务以集群的方式部署;
3、防止锁超时:如果客户端没有主动释放锁,服务器会在一段时间之后自动释放锁,防止客户端宕机或者网络不可达时产生死锁;
4、独占性:加锁解锁必须由同一台服务器进行,也就是锁的持有者才可以释放锁,不能出现你加的锁,别人给你解锁了;
package com.zengqingfa.springboot.mybatis.demo.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
@RestController
public class IndexController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/deduct_stock")
public String deductStock() {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock + "");
} else {
System.out.println("扣减失败,库存不足");
}
return "end";
}
}
这种情况下如果有两个线程同时进来,会出现超卖,日志打印结果如下:
@RequestMapping("/deduct_stock")
public String deductStock() {
synchronized (this){
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock + "");
} else {
System.out.println("扣减失败,库存不足");
}
}
return "end";
}
如果是分布式的情况下,synchronized关键字是无效的。
搭建如下的架构,来验证synchronized是无效的,只是jvm进程级别的锁
F:\software\Nginx\nginx-1.11.1>start nginx
F:\software\Nginx\nginx-1.11.1>nginx -v
nginx version: nginx/1.11.1
F:\software\Nginx\nginx-1.11.1>tasklist /fi "imagename eq nginx.exe"
映像名称 PID 会话名 会话# 内存使用
========================= ======== ================ =========== ============
nginx.exe 57040 Console 1 8,136 K
nginx.exe 56612 Console 1 8,576 K
//重新加载配置文件
F:\software\Nginx\nginx-1.11.1>nginx.exe -s reload
//停止nginx
F:\software\Nginx\nginx-1.11.1>nginx.exe -s stop
配置负载均衡规则,F:\software\Nginx\nginx-1.11.1\conf\nginx.conf
server
{
listen 80;
server_name localhost;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
location / {
proxy_pass http://redislock;
proxy_connect_timeout 600;
proxy_read_timeout 600;
}
}
upstream redislock {
server 127.0.0.1:8085 weight=1;
server 127.0.0.1:8086 weight=1;
}
开启jemter压测工具,压测1000个请求
F:\software\apache-jmeter-5.0\bin\jmeter.bat
@RequestMapping("/deduct_stock")
public String deductStock() {
String lockKey = "lockKey";
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "shenlongfeixian");
if (!result) {
return "稍后重试";
}
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock + "");
} else {
System.out.println("扣减失败,库存不足");
}
stringRedisTemplate.delete(lockKey);
return "end";
}
代码如果抛出异常,stringRedisTemplate.delete(lockKey)执行不到,key一直存在redis中,会出现死锁
改进版本2:
@RequestMapping("/deduct_stock")
public String deductStock() {
String lockKey = "lockKey";
try {
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "shenlongfeixian");
if (!result) {
return "稍后重试";
}
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock + "");
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
stringRedisTemplate.delete(lockKey);
}
return "end";
}
如果代码并非执行过程的抛出异常,而且服务突然的在执行到某一行后服务宕机,代码依然会执行不到。
改进版本3:增加超时时间
@RequestMapping("/deduct_stock")
public String deductStock() {
String lockKey = "lockKey";
try {
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "shenlongfeixian");
//增加超时时间
stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);
if (!result) {
return "稍后重试";
}
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock + "");
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
stringRedisTemplate.delete(lockKey);
}
return "end";
}
如果宕机发生的情况在设置key和设置key的超时时间之间呢?
改进版本4:redis中提供了设置key和设置key的超时时间是一条原子命令
org.springframework.data.redis.core.ValueOperations#setIfAbsent(K, V, long, java.util.concurrent.TimeUnit)
/**
* Set {@code key} to hold the string {@code value} and expiration {@code timeout} if {@code key} is absent.
*
* @param key must not be {@literal null}.
* @param value must not be {@literal null}.
* @param timeout the key expiration timeout.
* @param unit must not be {@literal null}.
* @return {@literal null} when used in pipeline / transaction.
* @since 2.1
* @see Redis Documentation: SET
*/
@Nullable
Boolean setIfAbsent(K key, V value, long timeout, TimeUnit unit);
改进代码如下:
@RequestMapping("/deduct_stock")
public String deductStock() {
String lockKey = "lockKey";
try {
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "shenlongfeixian", 10, TimeUnit.SECONDS);
if (!result) {
return "稍后重试";
}
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock + "");
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
stringRedisTemplate.delete(lockKey);
}
return "end";
}
锁失效问题:假设业务执行15s,锁失效的时间是10s,在线程1还没执行完,超过10s,锁失效,线程2可以加锁成功的。线程1执行结束的时候,把线程2加的锁删除了。后续第二个线程把第三个线程的锁释放了,如果高并发的情况下一直持续,锁会一直失效。
改进版本5:自己的加的锁自己释放
@RequestMapping("/deduct_stock")
public String deductStock() {
String lockKey = "lockKey";
String value = UUID.randomUUID().toString();
try {
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, value, 30, TimeUnit.SECONDS);
if (!result) {
return "稍后重试";
}
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock + "");
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
if (value.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
stringRedisTemplate.delete(lockKey);
}
}
return "end";
}
如果在获取value.equals(stringRedisTemplate.opsForValue().get(lockKey))的时候宕机呢?最多10s的超时时间,锁才会释放。
如果要完全实现100%的没问题,需要考虑的成本很高,代价也很高。
锁超时的问题:锁10s,但是业务代码是执行15s,业务代码没执行完,锁已经超时了
锁的超时时间不是很好设置,设置任何一个值都不是很合适。业务的执行时间不可预估。
锁续命:key的超时时间设置30s,启动一个定时器,每过10s,判断一下当前线程加的这个锁是否存在,如果存在,则给这个锁续命30s。如果锁不存在,下一个定时不执行。
github地址:https://github.com/redisson/redisson
引入依赖
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.8.2version>
dependency>
注入
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient getClient() {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
return redisson;
}
}
使用分布式锁
@RequestMapping("/deduct_stock2")
public String deductStock2() throws InterruptedException {
String lockKey = "product_001";
RLock redissonLock = redisson.getLock(lockKey);
try {
// 加锁,实现锁续命功能
redissonLock.lock();
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock + "");
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
redissonLock.unlock();
}
return "end";
}
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
if (leaseTime != -1) {
return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.addListener(new FutureListener<Long>() {
@Override
public void operationComplete(Future<Long> future) throws Exception {
if (!future.isSuccess()) {
return;
}
Long ttlRemaining = future.getNow();
// lock acquired
if (ttlRemaining == null) {
scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
}
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
//KEYS[1] Collections.
//ARGV[1] internalLockLeaseTime
//ARGV[2] getLockName(threadId)
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
//判断key是否存在
"if (redis.call('exists', KEYS[1]) == 0) then " +
// 如果不存在,使用hash结构存储,大key,小key为线程id,value为
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
// 设置key的过期时间
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
// 判断key是否是当前线程加的锁
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
// 如果是,增加1,支持锁重入
"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.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
//定时任务续约
private void scheduleExpirationRenewal(final long threadId) {
if (expirationRenewalMap.containsKey(getEntryName())) {
return;
}
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.addListener(new FutureListener<Boolean>() {
@Override
public void operationComplete(Future<Boolean> future) throws Exception {
expirationRenewalMap.remove(getEntryName());
if (!future.isSuccess()) {
log.error("Can't update lock " + getName() + " expiration", future.cause());
return;
}
if (future.getNow()) {
// reschedule itself
scheduleExpirationRenewal(threadId);
}
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
if (expirationRenewalMap.putIfAbsent(getEntryName(), new ExpirationEntry(threadId, task)) != null) {
task.cancel();
}
}
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
//判断key是是否是当前主线程的id,如果是的话,进行续约internalLockLeaseTime
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;",
Collections.<Object>singletonList(getName()),
internalLockLeaseTime, getLockName(threadId));
}
getName()中获取的name的值是传入的lockKey: RLock redissonLock = redisson.getLock(lockKey);
lockKey最后会赋值给name变量
public abstract class RedissonObject implements RObject {
protected final CommandAsyncExecutor commandExecutor;
private final String name;
protected final Codec codec;
public RedissonObject(Codec codec, CommandAsyncExecutor commandExecutor, String name) {
this.codec = codec;
this.name = name;
this.commandExecutor = commandExecutor;
}
...
@Override
public String getName() {
return name;
}
...
}
internalLockLeaseTime:默认是30s
入参是通过:commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout()方法传递的
org.redisson.config.Config#Config(org.redisson.config.Config),构造函数中有设置值的操作
private long lockWatchdogTimeout = 30 * 1000;
public long getLockWatchdogTimeout() {
return lockWatchdogTimeout;
}
getLockName(threadId):org.redisson.RedissonLock#getLockName,线程的名称
protected String getLockName(long threadId) {
return id + ":" + threadId;
}
其他线程如果加锁失败,会自旋操作:
org.redisson.RedissonLock#lockInterruptibly(long, java.util.concurrent.TimeUnit)
@Override
public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired 如果加锁成功,直接返回,如果是其他线程加锁失败,ttl会返回剩余的时间
if (ttl == null) {
return;
}
RFuture<RedissonLockEntry> future = subscribe(threadId);
commandExecutor.syncSubscription(future);
try {
while (true) {
//尝试自旋加锁
ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
break;
}
// waiting for message
if (ttl >= 0) {
getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
getEntry(threadId).getLatch().acquire();
}
}
} finally {
unsubscribe(future, threadId);
}
// get(lockAsync(leaseTime, unit));
}
因为redis使用的是主从架构,存在同步的问题,master宕机,但是没有同步到slave,哨兵架构会把slave升级为master,此时其他线程会可以加锁成功,相当于线程1和线程2同时拥有了锁。怎么解决?
可以使用zk的强一致性分布式架构,如果使用redis,可以容忍一部分这样的小bug。
为什么不使用zk?zk的性能没有redis性能高,没有必要为了分布式锁引入zk。
其他方案:RedLock解决主从失效的问题,原理类似Zookeeper
@RequestMapping("/redlock")
public String redlock() throws InterruptedException {
String lockKey = "product_001";
//这里需要自己实例化不同redis实例的redisson客户端连接,这里只是伪代码用一个redisson客户端简化了
RLock lock1 = redisson.getLock(lockKey);
RLock lock2 = redisson.getLock(lockKey);
RLock lock3 = redisson.getLock(lockKey);
/**
* 根据多个 RLock 对象构建 RedissonRedLock (最核心的差别就在这里)
*/
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
/**
* 4.尝试获取锁
* waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败
* leaseTime 锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完)
*/
boolean res = redLock.tryLock(10, 30, TimeUnit.SECONDS);
if (res) {
//成功获得锁,在这里处理业务
}
} catch (Exception e) {
throw new RuntimeException("lock fail");
} finally {
//无论如何, 最后都要解锁
redLock.unlock();
}
return "end";
}
RedLock存在性能问题,如果存在网络问题,还涉及到事务回滚问题,存在很多的问题,不推荐使用。
性能和安全不能完全兼顾,如果你一定要保证锁的安全性的话,可以用其他的中间件如db、zookeeper来做控制
客户端1得到了锁,因为网络问题或者GC等原因导致长时间阻塞,然后业务程序还没执行完锁就过期了,这时候客户端2也能正常拿到锁,可能会导致线程安全的问题。
如果redis服务器的机器时钟发生了向前跳跃,就会导致这个key过早超时失效,比如说客户端1拿到锁后,key的过期时间是12:02分,但redis服务器本身的时钟比客户端快了2分钟,导致key在12:00的时候就失效了,这时候,如果客户端1还没有释放锁的话,就可能导致多个客户端同时持有同一把锁的问题。
基础设施和运维保证时间的正确
可以分段,采取类似ConcurrentHashMap的分段锁实现
举例:分段库存锁,通过hash的手段,hash到不同的节点
eper来做控制
客户端1得到了锁,因为网络问题或者GC等原因导致长时间阻塞,然后业务程序还没执行完锁就过期了,这时候客户端2也能正常拿到锁,可能会导致线程安全的问题。
[外链图片转存中…(img-oB8LeL6t-1666506764455)]
如果redis服务器的机器时钟发生了向前跳跃,就会导致这个key过早超时失效,比如说客户端1拿到锁后,key的过期时间是12:02分,但redis服务器本身的时钟比客户端快了2分钟,导致key在12:00的时候就失效了,这时候,如果客户端1还没有释放锁的话,就可能导致多个客户端同时持有同一把锁的问题。
基础设施和运维保证时间的正确
可以分段,采取类似ConcurrentHashMap的分段锁实现
举例:分段库存锁,通过hash的手段,hash到不同的节点