今天是五一,决定不出去,在家里撸代码,今天学习redis,因而准备写个基于redis的分布式锁。因为本人属于菜鸟级别,在写的过程当中遇到各类问题,功夫不负有心人,终于搞定,若是发现实现有问题,欢迎指导,感谢.git
在单机时代,虽然不须要分布式锁,但也面临过相似的问题,只不过在单机的状况下,若是有多个线程要同时访问某个共享资源的时候,咱们能够采用线程间加锁的机制,即当某个线程获取到这个资源后,就当即对这个资源进行加锁,当使用完资源以后,再释放锁,其它线程就能够接着使用了。JAVA中已提供相关工具类。可是到了分布式系统的时代,这种线程或者进程之间的锁机制,就可能没做用了,系统可能会有多份而且部署在不一样的机器上,这些资源已经不是在线程之间共享了,而是属于进程之间共享的资源。所以,为了解决这个问题,咱们就必须引入「分布式锁」。github
分布式锁,是指在分布式的部署环境下,经过锁机制来让多客户端互斥的对共享资源进行访问。web
通常分布式锁要知足一下几点要求:redis
目前主流的分布式锁实现主要有如下几种数据库
今天主要将基于redis实现分布式锁缓存
缓存过时安全
缓存能够设置过时时间,redis根据时间自动进行清理。并发
setNx命令dom
将 key 的值设为 value ,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不作任何动做。
SETNX 是『SET if Not eXists』(若是不存在,则 SET)的简写。
复制代码
lua脚本编辑器
脚本语言,用于支持redis原子操做。
熟悉以上redis知识,实现redis分布式锁比较容易了。
缓存过时
最好给加锁的key设置缓存过时时间,能够有效的防止死锁,好比某个进程加锁后没来得及释放锁,宕机,说来负责释放锁?
set值
加锁时,在redis中保存在各节点中惟一的值,防止不一样进程误解锁
好比serviceA已经在redis中加锁lock,通常serviceA执行时间为1秒,则设置缓存过时时间2秒,某天因为机器缘由serviceA执行了3秒,那么对应的锁已经失效,此期间B去加锁,并加锁成功, serviceA执行完会释放锁,致使serviceA会将B加的锁释放,因此产生误删锁,采用惟一值,避免这种状况产生。删锁会检查值,若是加锁与解锁的值不相同则不容许解锁。
一、利用SETNX命令加锁
public static String set(String key, String value, long timeout) {
Jedis jedis = getJedis();
try {
String ret = jedis.set(key, value, "NX", "PX", timeout);
return ret;
} finally {
close(jedis);
}
}
复制代码
二、实现阻塞加锁和非阻塞加锁
/** * 阻塞加锁 */
public void lock() {
if (tryLock()) {
return;
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//lock
lock();
} /** * 基于setNx实现非阻塞锁 * * @return */ public boolean tryLock() { String uuid = UUID.randomUUID().toString(); String ret = JedisUtis.set(LOCK_KEY, uuid, DEFAULT_TIME_OUT); if ("OK".equals(ret)) { //lock success LOCAL.set(uuid); return true; } return false; } public boolean tryLock(long time, TimeUnit unit){ String uuid = UUID.randomUUID().toString(); String ret = JedisUtis.set(LOCK_KEY, uuid, unit.toMillis(time)); if ("ok".equals(ret)) { LOCAL.set(uuid); //lock success return true; } return false; } 复制代码
三、解锁
解锁的同时须要去检查值是否与加锁的值相同,不相同则不容许解锁,这里是经过ThreadLocal传加锁产生的uuid
/** * unlock * 执行lua脚本,保证原子性 */
public void unlock() {
release();
}
private void release() { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; JedisUtis.remove(LOCK_KEY, script, LOCAL.get()); } 复制代码
12306售票是并发学习中经典案例,仍是拿这个举例,好比有100 tickets,有多个售票窗口同时售票,怎么保证不被重复卖
/** * 线程不安全示例 * * @author Qi.qingshan * @date 2020/5/1 */
public class SaleTicket implements Runnable {
private int tickets = 100; public void run() { for (; ; ) { sale(); if (tickets < 0) break; } } } /** * 售票 */ private void sale() { if (tickets > 0) { tickets--; System.out.println(Thread.currentThread().getName() + " - 在售第" + (100 - tickets) + "票 :: 剩余" + (tickets)); } try { Thread.sleep(100); } catch (InterruptedException e) { } } } 复制代码
测试类
/** * @author Qi.qingshan * @date 2020/5/1 */
public class SaleTicketTest {
BlockingDeque queue = new LinkedBlockingDeque(100); private ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 50, 100, TimeUnit.SECONDS, queue); @Test public void testSaleTickets() throws IOException { SaleTicket saleTicket = new SaleTicket(); executor.execute(new Thread(saleTicket, "售票员001")); executor.execute(new Thread(saleTicket, "售票员002")); executor.execute(new Thread(saleTicket, "售票员003")); executor.execute(new Thread(saleTicket, "售票员004")); System.in.read(); } } 复制代码
存在重复售票状况,改用redisLock,调整核心代码
public void run() {
for (; ; ) {
lock.lock();
try {
sale();
if (tickets < 0) break;
} finally {
lock.unlock();
}
}
}
复制代码
执行结果以下
完整代码已上传github.com/qiqsa/distr…