redis - 实现分布式锁
常用的实现分布式锁的技术:redis、zookeeper、数据库,各种锁的比较如下:
1,锁的基本概念
1.1 超卖案例:
1.2 超卖的根源:
1.3线程安全:
什么是线程安全:
如何实现线程安全:
加锁:
如果在方法上增加同步关键字,增加了锁,代码就会变成串行执行,但是代码效率变低了。
1.4 锁的性能优化:
1,把同步方法改成同步代码块
2,
3,读锁,写锁分离
1.4 锁的种类:(JVM锁,进程锁)
独享锁也叫排他锁
2,分布式锁
2.1 分布式环境:
分布式锁的注意事项:
redis 分布式锁流程图:
步骤:
1,竞争锁,判断锁是否存在
如果存在:等待 或者 退出
如果不存在:创建锁,设置锁的有效期,获取锁的线程分配唯一标识
加锁分为三步:
1,判断锁是否存在
2,如果不存在,加锁
3,设置有效时间
按照以上步骤代码如下:
if(setnx(key,1) == 1){ //此处挂掉了.....
expire(key,30)
try {
do something ......
}catch(){
} finally {
del(key)
}
}
setnx和expire的非原子性
设想一个极端场景,当某线程执行setnx,成功得到了锁:
setnx刚执行成功,还未来得及执行expire指令,节点1 Duang的一声挂掉了。
这样一来,这把锁就没有设置过期时间,变得“长生不老”,别的线程再也无法获得锁了。
怎么解决呢?setnx指令本身是不支持传入超时时间的,Redis 2.6.12以上版本为set指令增加了可选参数,伪代码如下:set(key,1,30,NX),这样就可以取代setnx指令。
setnx() 命令只能保证前两个步骤是原子操作。所以在加锁时还是使用set 命令
源码如图所示:set命令有一个nxxx的参数,可以实现setnx() 的功能,又能保证加锁的三步操作都是原子操作。
解锁分为两步:
解锁注意事项:超时后使用del 导致误删其他线程的锁
又是一个极端场景,假如某线程成功得到了锁,并且设置的超时时间是30秒。
如果某些原因导致线程B执行的很慢很慢,过了30秒都没执行完,这时候锁过期自动释放,线程B得到了锁。
随后,线程A执行完了任务,线程A接着执行del指令来释放锁。但这时候线程B还没执行完,线程A实际上删除的是线程B加的锁。
怎么避免这种情况呢?可以在del释放锁之前做一个判断,验证当前的锁是不是自己加的锁。
至于具体的实现,可以在加锁的时候把当前的线程ID当做value,并在删除之前验证key对应的value是不是自己线程的ID。
加锁:
String threadId = Thread.currentThread().getId()
set(key,threadId ,30,NX)
doSomething.....
解锁:
if(threadId .equals(redisClient.get(key))){
del(key)
}
但是,这样做又隐含了一个新的问题,if判断和释放锁是两个独立操作,不是原子性。
因为删除没有原子命令,所以需要借助luau脚本来实现原子删除:
String luaScript = ‘if redis.call(‘get’, KEYS[1]) == ARGV[1] then return redis.call(‘del’, KEYS[1]) else return 0 end’;
redisClient.eval(luaScript , Collections.singletonList(key), Collections.singletonList(threadId));
这样一来,验证和删除过程就是原子操作了。
3,使用redis实现分布式锁,具体代码:
3.1 引入 redis 依赖:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
3.2 解锁的 lua 脚本(可以事先把luau脚本加载到内存中)
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
3.3 RedisClient:通过redis连接池,获取redis连接
package com.example.redis.lock;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
/**
* @author h
* @Description redis 客户端连接池
* @createTime 2019年09月28日 15:29
*/
public class RedisClient {
private static final JedisPool POOL;
static {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(500);
config.setMaxIdle(5);
config.setMaxWaitMillis(1000 * 10);
config.setTestOnBorrow(true);
POOL = new JedisPool(config, "localhost");
}
public static Jedis getClient() {
return POOL.getResource();
}
}
3.4 RedisDistributeLock:实现redis分布式锁
package com.example.redis.yunxi_lock;
import redis.clients.jedis.Jedis;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* @author h
* @Description 使用redis 实现分布式锁
* @createTime 2019年09月28日 15:14
*/
public class RedisDistributeLock implements Lock {
// 锁消息的上下文,保存当前锁的持有人ID
private ThreadLocal<String> lockContext = new ThreadLocal<>();
// 默认锁的超时时间
private long time = 100L;
private Thread exclusiveOwnerThread;
@Override
public void lock() {
// 类似自旋锁的方式获取锁
while (!tryLock()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
if (Thread.interrupted()) {
throw new InterruptedException();
}
while (!tryLock()) {
Thread.sleep(100);
}
}
@Override
public boolean tryLock() {
return tryLock(time, TimeUnit.MILLISECONDS);
}
@Override
public boolean tryLock(long time, TimeUnit unit) {
// 为每个锁的持有人分配唯一的id
String id = UUID.randomUUID().toString();
Thread thread = Thread.currentThread();
Jedis jedis = null;
try {
jedis = RedisClient.getClient();
// 加锁并设置有效期
boolean getLockSuccess = "OK".equals(jedis.set("yunxi_lock", id, "NX", "PX", unit.toMillis(time)));
if (getLockSuccess) {
// 记录锁的持有人id
lockContext.set(id);
// 记录当前线程
setExclusiveOwnerThread(thread);
return true;
// 当前线程已经获得了锁,可重入
} else if (exclusiveOwnerThread == thread) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != jedis) {
jedis.close();
}
}
return false;
}
private final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
@Override
public void unlock() {
String script = null;
Jedis jedis = RedisClient.getClient();
try {
jedis = RedisClient.getClient();
// 读取lua脚本内容
script = inputStreamToString(getClass().getResourceAsStream("/redis.script"));
if (null == lockContext.get()) {
return;
}
// 使用lua脚本,删除锁,通过使用lockContext.get()来确保,谁加的锁,只能有谁来删除
jedis.eval(script, Arrays.asList("redis_lock"), Arrays.asList(lockContext.get()));
lockContext.remove();
}catch (Exception e) {
e.printStackTrace();
} finally {
if (null != jedis) {
jedis.close();
}
}
}
private String inputStreamToString(InputStream in) throws IOException {
StringBuffer out = new StringBuffer();
byte[] b = new byte[4096];
for (int n; (n = in.read(b)) != -1 ; ) {
out.append(new String(b, 0, n));
}
return out.toString();
}
@Override
public Condition newCondition() {
return null;
}
}
3.5 测试类
package com.example.redis.lock;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author h
* @Description TODO
* @createTime 2019年09月28日 17:44
*/
public class RedisLockTest {
private RedisDistributeLock lock = new RedisDistributeLock();
// 初始库存数量
private Integer stock = 8;
/**
* 买票减库存操作:未使用锁
* @param num 购买票数
*/
private void reduce(int num) {
if (stock - num >= 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
stock = stock - num;
System.out.println(Thread.currentThread().getName()
+ "成功:卖出" + num + "张,库存剩余" + stock + "张票");
// 后续增加积分的操作
} else {
System.out.println(Thread.currentThread().getName()
+ "失败:库存不足" + num + "张,库存剩余" + stock + "张票");
}
}
/**
* 买票减库存操作:未使用锁
* @param num 购买票数
*/
private void reduceByRedisLock(int num){
boolean flag = false;
try {
lock.lock();
if (stock - num >= 0) {
stock -= num;
flag = true;
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
if (flag){
System.out.println(Thread.currentThread().getName()
+ "成功:卖出" + num + "张,库存剩余" + stock + "张票");
// 后续增加积分的操作
} else {
System.out.println(Thread.currentThread().getName()
+ "失败:库存不足" + num + "张,库存剩余" + stock + "张票");
}
}
public static void main(String[] args) throws InterruptedException {
RedisLockTest redisLockTest = new RedisLockTest();
// 未加锁
// for (int i = 0; i < 10; i++) {
// new Thread(() -> redisLockTest.reduce(1), "用户" + (i+1)).start();
// }
// 加锁
for (int i = 0; i < 10; i++) {
new Thread(() -> redisLockTest.reduceByRedisLock(1), "用户" + (i+1)).start();
}
Thread.sleep(1000L);
}
}
4,使用redis实现分布式锁的注意事项
4.1 出现并发的可能性
还是刚才第二点所描述的场景,虽然我们避免了线程A误删掉key的情况,但是同一时间有A,B两个线程在访问代码块,仍然是不完美的。
怎么办呢?我们可以让获得锁的线程开启一个守护线程,用来给快要过期的锁“续航”。
当过去了29秒,线程A还没执行完,这时候守护线程会执行expire指令,为这把锁“续命”20秒。守护线程从第29秒开始执行,每20秒执行一次。
当线程A执行完任务,会显式关掉守护线程。
另一种情况,如果节点1 忽然断电,由于线程A和守护线程在同一个进程,守护线程也会停下。这把锁到了超时的时候,没人给它续命,也就自动释放了。
5,基于数据库实现分布式锁
5,基于zookeeper实现分布式锁
准确性高,但性能不如 redis,用于与资金相关的对准确性高要求场景。