redis分布式锁实现,使用redis实现分布式的优点:
- redis基于内存操作,加锁、加锁非常快
- redis中间件非常常用,基于redis实现分布式锁不需要再引入其他中间件
- 实现很简单,redis本身就是单线程,不存在数据同步问题
最简单的方式,使用setnx命令实现,缺点是不支持可重入。
使用lua脚本实现分布式锁,lua脚本执行是原子性的,使用hash数据结构实现,不同的线程生成不同的hash表的key,实现锁机制。
lua脚本
加锁的lua代码:
local key=KEYS[1]
local lockKey=ARGV[1]
local expireSecond=ARGV[2]
local exists=redis.call('exists',key)
if exists==0 then
redis.call('hset',key,lockKey,1)
redis.call('expire',key,expireSecond)
return 1
end
local value=redis.call('hget',key,lockKey)
if value then
redis.call('hincrby',key,lockKey,1)
redis.call('expire',key,expireSecond)
return 1
end
return 0
解锁lua脚本:
local key=KEYS[1]
local lockKey=ARGV[1]
local value=redis.call('hget',key,lockKey)
if value then
if tonumber(value)>1 then
redis.call('hincrby',key,lockKey,-1)
else
redis.call('del',key)
end
end
return 1
spring中实现
定义锁接口
package top.mybiao.redis.core;
import java.util.concurrent.TimeUnit;
public interface DistributeLock {
boolean tryLock();
boolean tryLock( long time, TimeUnit timeUnit);
void lock();
void unlock();
}
实现接口:
package top.mybiao.redis.core;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import java.util.Collections;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
public class RedisLockImpl implements DistributeLock{
private final StringRedisTemplate redisTemplate;
private final String key;
private final String lockKey;
//过期时间30秒
private static final int TIMEOUT = 30;
public RedisLockImpl(StringRedisTemplate redisTemplate, String key,String lockKey){
this.redisTemplate = redisTemplate;
this.key = key;
this.lockKey = lockKey;
System.out.println("初始化lockKey:"+lockKey);
}
private static final String lockScript = "" +
"local key=KEYS[1]\n" +
"local lockKey=ARGV[1]\n" +
"local expireSecond=ARGV[2]\n" +
"local exists=redis.call('exists',key)\n" +
"if exists==0 then\n" +
" redis.call('hset',key,lockKey,1)\n" +
" redis.call('expire',key,expireSecond)\n" +
" return 1\n" +
"end\n" +
"local value=redis.call('hget',key,lockKey)\n" +
"if value then \n" +
" redis.call('hincrby',key,lockKey,1)\n" +
" redis.call('expire',key,expireSecond)\n" +
" return 1\n" +
"end\n" +
"return 0\n";
private static final String unlockScript = "" +
"local key=KEYS[1]\n" +
"local lockKey=ARGV[1]\n" +
"local value=redis.call('hget',key,lockKey)\n" +
"if value then\n" +
" if tonumber(value)>1 then\n" +
" redis.call('hincrby',key,lockKey,-1)\n" +
" else\n" +
" redis.call('del',key)\n" +
" end\n" +
"end\n" +
"return 1";
private final RedisScript<Long> lockRedisScript = new DefaultRedisScript<>(lockScript,Long.class);
private final RedisScript<Long> unlockRedisScript = new DefaultRedisScript<>(unlockScript,Long.class);
@Override
public boolean tryLock() {
Long code = redisTemplate.execute(lockRedisScript, Collections.singletonList(key),lockKey,String.valueOf(TIMEOUT));
Objects.requireNonNull(code);
return code==1;
}
@Override
public boolean tryLock(long time, TimeUnit timeUnit) {
if (time<=0) throw new IllegalArgumentException("time must greater than zero");
long start = System.currentTimeMillis();
long end = start+timeUnit.toMillis(time);
try {
while (start < end) {
if(tryLock()) return true;
Thread.sleep(10);
start = System.currentTimeMillis();
}
}catch (InterruptedException e){
e.printStackTrace();
Thread.currentThread().interrupt();
}
return false;
}
@Override
public void lock() {
try {
while (!tryLock()) {
Thread.sleep(30);
}
System.out.println("获取到锁");
}catch (InterruptedException e){
e.printStackTrace();
Thread.currentThread().interrupt();
}
}
@Override
public void unlock() {
System.out.println("释放锁");
redisTemplate.execute(unlockRedisScript,Collections.singletonList(key),lockKey);
}
}
获取锁对象接口:
package top.mybiao.redis.core;
public interface RedisLockFactory {
DistributeLock getLock(String key);
}
接口实现:
package top.mybiao.redis.core;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.UUID;
@Component
public class RedisLockFactoryImpl implements RedisLockFactory{
private final StringRedisTemplate redisTemplate;
public RedisLockFactoryImpl(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public DistributeLock getLock(String key) {
return new RedisLockImpl(redisTemplate,key, UUID.randomUUID().toString());
}
}
每个锁都要有不同的值,对应redis的key,每个线程对用不同的lock key,表示不同的线程。
使用方法
package top.mybiao.redis;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import top.mybiao.redis.core.DistributeLock;
import top.mybiao.redis.core.RedisLockFactory;
import javax.annotation.PostConstruct;
import java.util.Objects;
@SpringBootApplication
@RestController
public class RedisApplication {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private RedisLockFactory redisLockFactory;
private static final Logger log = LoggerFactory.getLogger(RedisApplication.class);
public static void main(String[] args) {
SpringApplication.run(RedisApplication.class, args);
}
@PostMapping("/seal")
public String seal(String name){
DistributeLock lock = redisLockFactory.getLock("seal");
try{
lock.lock();
log.info("name={},want seal 1 iphone",name);
String num = redisTemplate.opsForValue().get("iphone");
Thread.sleep(1000);
if (Objects.nonNull(num) && Integer.parseInt(num)>0) {
redisTemplate.opsForValue().increment("iphone", -1);
log.info("buy one iphone success");
return "success";
}
log.info("库存不足");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return "系统繁忙";
}
@PostConstruct
public void init(){
redisTemplate.opsForValue().set("iphone","100");
}
}
总结
之前写的redis分布式锁有明显bug,就是每个线程的lock key相同,同时多个线程获取到锁,达不到锁的要求。本文中这样改进的实现解决了这个bug,并且接口更加易于使用,每次加锁解锁不需要传额外的参数。
这样实现基本满足需求,不会发生死锁,解锁其他线程的锁的情况,但没有实现锁续期,当锁超时时,会有多个线程同时获得锁,进一步可以通过redis键空间通知实现。锁是非公平锁,会发生线程饥饿情况,长时间获取不到锁。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。