基于redis分布式锁实现的多线程并发程序(原创)


前两个版本的代码 都或多或少存在一定的问题,虽然可能微乎其微,但是程序需要严谨再严谨,
第一个版本问题: 局限于单机版,依赖于 Jvm的锁
第二个版本问题: 极端情况下,解锁逻辑的问题,线程B的锁,可能会被线程A解掉,这种情况实际上是不合理的。
1. 由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。
2. 当锁过期的时候,如果多个客户端同时执行jedis.getSet()方法,那么虽然最终只有一个客户端可以加锁,
但是这个客户端的锁的过期时间可能被其他客户端覆盖。
3. 锁不具备拥有者标识,即任何客户端都可以解锁。
版本一: http://www.cnblogs.com/xifenglou/p/8807323.html
版本二: http://www.cnblogs.com/xifenglou/p/8883717.html

所以基于以上问题,第三个版本出来了,
Talk is cheap, show me the code!

import org.springframework.util.StopWatch;
import redis.clients.jedis.Jedis;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
* 使用RedisTool.tryGetDistributedLock
* 实现 分布式锁
* 终极版本
*/
public class TicketRunnable3 implements Runnable {
private CountDownLatch count;
private CyclicBarrier barrier;
private static final Integer Lock_Timeout = 10000;
private static final String lockKey = "LockKey";
private volatile static boolean working = true;

public TicketRunnable3(CountDownLatch count, CyclicBarrier barrier) {
this.count = count;
this.barrier = barrier;
}

private int num = 20; // 总票数 此处可随意 写一个数,保证线程能运行起来,真正的共享变量不应该写死在程序中, 应该从redis中获取,这样模拟多进程多线程的并发访问

public void sellTicket(Jedis jedis) {
String name = Thread.currentThread().getName();
try{
boolean getLock = RedisTool.tryGetDistributedLock(jedis,lockKey, name,Lock_Timeout);
if( getLock){
if(!working)
return;
// Do your job
num = Integer.parseInt(jedis.get("ticket"));
if (num > 0) {
num--;
jedis.set("ticket",num+"");
if(num!=0)
System.out.println("================"+Thread.currentThread().getName()+"================= 售出票号" + (num+1)+",还剩" + num + "张票--" );
else {
System.out.println("================"+Thread.currentThread().getName()+"================= 售出票号" + (num+1)+",票已经票完!--");
working = false;
}
}
}else{
//System.out.println();
if(!working)
return;
System.out.println(Thread.currentThread().getName()+" Try to get the Lock,and wait 20 millisecond....");
Thread.sleep(10);
}
}catch(Exception e){
System.out.println(e);
}finally {
try {
if(RedisTool.releaseDistributedLock(jedis,lockKey,name)){
Thread.sleep(30);
}
}catch (Exception e ) {
e.printStackTrace();
}
}
}


@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"到达,等待中...");
Jedis jedis = new Jedis("localhost", 6379);

try{
barrier.await(); // 此处阻塞 等所有线程都到位后 一起进行抢票
if(Thread.currentThread().getName().equals("pool-1-thread-1")){
System.out.println("-----------------全部线程准备就绪,开始抢票------------------");
}else {
Thread.sleep(5);
}
while (working) {
sellTicket(jedis);
}
count.countDown(); //当前线程结束后,计数器-1
}catch (Exception e){e.printStackTrace();}
}

/**
*
* @param args
*/
public static void main(String[] args) {
int threadNum = 5; //模拟多个窗口 进行售票
final CyclicBarrier barrier = new CyclicBarrier(threadNum);
final CountDownLatch count = new CountDownLatch(threadNum); // 用于统计 执行时长

StopWatch watch = new StopWatch();
watch.start();
TicketRunnable3 tickets = new TicketRunnable3(count,barrier);
ExecutorService executorService = Executors.newFixedThreadPool(threadNum);
//ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < threadNum; i++) { //此处 设置数值 受限于 线程池中的数量
executorService.submit(tickets);
}
try {
count.await();
executorService.shutdown();
watch.stop();
System.out.println("耗 时:" + watch.getTotalTimeSeconds() + "秒");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
import redis.clients.jedis.Jedis;
import java.util.Collections;

public class RedisTool {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
private static final Long RELEASE_SUCCESS = 1L;
/**
* 尝试获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/

public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
System.out.println("=============="+Thread.currentThread().getName()+"=============== 获取到锁,开始工作!");
return true;
}
return false;
}

/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
System.out.println("=============="+Thread.currentThread().getName()+"=============== 解锁成功!");
return true;
}
return false;
}

}

解锁部分,我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。

那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的。源于Redis的特性,下面是官网对eval命令的部分解释:

简单来说,就是在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。



 

 

运行结果如下:

 

 欢迎留言,期待更深层次的探讨!

 

针对 上述代码,使用两个类 运行,

TicketRunnable3  TicketRunnable4 模拟多进程  多线程场景 ,

场景1: 运行时长 > 过期时长    

此时: 锁自动失效, 线程均不用解锁,即使解锁也是失败!

代码及运行结果如下:

import org.springframework.util.StopWatch;
import redis.clients.jedis.Jedis;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
* 使用RedisTool.tryGetDistributedLock
* 实现 分布式锁
* 终极版本
*/
public class TicketRunnable4 implements Runnable {
private CountDownLatch count;
private CyclicBarrier barrier;
private static final Integer Lock_Timeout = 3000; // 过期时间 代表 3秒后过期
private static final Integer ExecuteTime = 5000;
private static final Integer RetryInterval = 20;
private static final String lockKey = "LockKey";
private volatile static boolean working = true;

public TicketRunnable4(CountDownLatch count, CyclicBarrier barrier) {
this.count = count;
this.barrier = barrier;
}

private int num = 20; // 总票数

public void sellTicket(Jedis jedis) {
String name = Thread.currentThread().getName();
boolean gotLock = false;
try{
gotLock = RedisTool.tryGetDistributedLock(jedis,lockKey, name,Lock_Timeout);
if( gotLock && working){
// Do your job
num = Integer.parseInt(jedis.get("ticket"));
if (num > 0) {
num--;
jedis.set("ticket",num+"");
if(num!=0)
System.out.println("=============="+name+"=============== 售出票号" + (num+1)+",还剩" + num + "张票--" );
else {
System.out.println("=============="+name+"=============== 售出票号" + (num+1)+",票已经票完!--");
return;
}
}
if(num == 0){
System.out.println("=============="+name+"============票已经被抢空啦");
working = false;
}
Thread.sleep(ExecuteTime);
}else{
//System.out.println();
//System.out.println(name+" Try to get the Lock,and wait "+RetryInterval+" millisecond....");
Thread.sleep(RetryInterval);
}
}catch(Exception e){
System.out.println(e);
}finally {
try {
if(!gotLock||!working) //未获取到锁的线程不用解锁
return;
/**
* 解锁成功后 sleep, 尝试让出cpu给其他线程机会
* 解锁失败 说明锁已经失效 被其他线程获取到
*/
if(RedisTool.releaseDistributedLock(jedis,lockKey,name)){
Thread.sleep(100);
}
}catch (Exception e ) {
e.printStackTrace();
}
}
}


@Override
public void run() {
String prefix = "#";
String threadName = Thread.currentThread().getName();
Thread.currentThread().setName(prefix+threadName);
System.out.println(Thread.currentThread().getName()+"到达,等待中...");
Jedis jedis = new Jedis("localhost", 6379);

try{
barrier.await(); // 此处阻塞 等所有线程都到位后 一起进行抢票
if(Thread.currentThread().getName().equals(prefix+"pool-1-thread-2")){
System.out.println("-----------------全部线程准备就绪,开始抢票------------------");
}else {
Thread.sleep(5);
}
while (working) {
sellTicket(jedis);
}
count.countDown(); //当前线程结束后,计数器-1
}catch (Exception e){e.printStackTrace();}
}

/**
*
* @param args
*/
public static void main(String[] args) {
int threadNum = 3; //模拟多个窗口 进行售票
final CyclicBarrier barrier = new CyclicBarrier(threadNum);
final CountDownLatch count = new CountDownLatch(threadNum); // 用于统计 执行时长

StopWatch watch = new StopWatch();
watch.start();
TicketRunnable4 tickets = new TicketRunnable4(count,barrier);
ExecutorService executorService = Executors.newFixedThreadPool(threadNum);
//ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < threadNum; i++) { //此处 设置数值 受限于 线程池中的数量
executorService.submit(tickets);
}
try {
count.await();
executorService.shutdown();
watch.stop();
System.out.println("耗 时:" + watch.getTotalTimeSeconds() + "秒");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

TicketRunnable3 售出 10 9 8 6 5 3 2 1 票号。

TicketRunnable4售出  7 4  两个票号

合计10张票,模拟结束!

场景2: 运行时长 < 过期时长    

此时: 此时需要有锁线程去释放锁,这样多线程再去竞争获取锁。

 

修改代码:

 

private static final Integer  Lock_Timeout = 5000;    // 将时间从3秒改为5秒
private static final Integer ExecuteTime = 3000; // 将执行时间5秒改为3秒

运行结果如下:

 

一个进程售出 10  9 8 6 4 3 1 票号
另一进程售出 7 5 2 票号
此时 每个线程完成任务后,均需要释放锁,这样本地线程或是异地线程 才能获取到锁,这样才能有机会进行任务的执行!

 

 


posted @ 2018-04-20 10:24  西凤楼  阅读(8214)  评论(0编辑  收藏  举报
如果,您认为阅读这篇博客让您有些收获, 如果,您希望更容易地发现我的新博客,不妨关注一下。因为,我的写作热情也离不开您的肯定支持。 感谢您的阅读,如果您对我的博客所讲述的内容有兴趣,请继续关注我的后续博客。 因为有小孩,兼职卖书,路过的朋友有需要低价购买图书、点读笔、纸尿裤等资源的,可扫最上方二维码,质量有保证,价格很美丽,欢迎咨询!