一、锁的发展
系统结构由传统的“单应用服务--》SOA --》微服务 --》无服务器” 的演进过程中,场景越来越复杂,由单体应用的但进程中多线程并发的内存锁,随着互联网场景越来越复杂,在复杂的系统交互过程中存在大量的并发。分布式并发锁概念就营运而生
二、锁的介绍
1、进程下单线程模式,更改为多钱成并发模式,于是就产生了线程锁,也就是常说的内存锁,是基于单线程中多线程的锁控制,基于这种方式又演变出来文件锁等,但是都是基于控制多线程并发的锁机制
2、分布式以及微服务的兴起,程序逐步从单进程演变为多进程。进程之间就会产生需要处理并发业务,需要实现业务锁,所以就产生了分布式锁的解决方案。
目前市面上解决分布式锁的方案主要有以下几种:
(1)基于数据库表做乐观锁(乐观锁和悲观锁定义请参照-锁实例介绍找那个单进程内并发锁的博客),用于分布式锁。
(2)使用memcached的add()方法,用于分布式锁。
(3)基于redisson实现分布式锁(redis官方推荐)
不常用但是可以用于技术方案探讨的:
(1)使用memcached的cas()方法,用于分布式锁。
(2)使用redis的setnx()、get()、getset()方法,使用redis的setnx()、expire()方法,用于分布式锁,使用watch、multi、exec命令,用于分布式锁。这几种方法都能根据redis的特性实现分布式锁,但是在集群情况下,很多极端场景会出现问题
(3)使用zookeeper,用于分布式锁。
三、锁实例介绍
1、单进程多线程并发锁。
请参看博客:http://www.cnblogs.com/dennyzhangdd/p/6925473.html
http://www.cnblogs.com/zhimingyang/p/5702752.html(ReenTrantLock源码解析)
注意: 由于1.5将Synchronized优化后,使得性能大大提升,那么什么时候使用什么情况下使用ReenTrantLock:
答案是,如果你需要实现ReenTrantLock的三个独有功能时。
ReenTrantLock独有的能力:
(1) ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
(2)ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
(3) ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。
2、分布式并发锁
(1)数据库实现乐观锁(新手推荐)
乐观锁上文说了是一种思想,相对悲观锁而言,乐观锁假设认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。
具体实现描述:
- 增加数据版本控制标识:数据库表中增加一个version属性,属性由1递增
- 数据更新:每条数据在操作之后会增加一个version版本,比如每次操作增加1
- 数据校验:操作每条数据之前,先查询数据库中记录的版本号,并与传入的操作版本号比对是否一致。如果发生冲突,则返回让开发者自己决定如何操作
(2)memcached实现分布式锁(没有实现过,不做过多评价)
- 实现原理:
memcached带有add函数,利用add函数的特性即可实现分布式锁。add和set的区别在于:如果多线程并发set,则每个set都会成功,但最后存储的值以最后的set的线程为准。而add的话则相反,add会添加第一个到达的值,并返回true,后续的添加则都会返回false。利用该点即可很轻松地实现分布式锁。
- 优点
并发高效。
- 缺点
--》memcached采用列入LRU置换策略,所以如果内存不够,可能导致缓存中的锁信息丢失。
--》memcached无法持久化,一旦重启,将导致信息丢失。
(3)redis实现分布式锁方法(老死机推荐)
在讲解redis锁时,这里只讲解在集群情况下的分布式锁实现(单点redis和集群redis的实现原理是不一样的)
如下是一篇分布式锁的官方翻译微博:http://ifeve.com/redis-lock/,目前redis官方推荐使用的Redisson就提供了分布式锁和相关服务。
- maven引入redisson组件包
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>2.7.0</version> </dependency>
- 使用redisson,最好采用redis 2.6.0以上版本,因为redosson一些后台命令采用eval的命令
import org.redisson.Redisson; import org.redisson.api.RAtomicLong; import org.redisson.config.Config; public class RedissonManager { private static final String RAtomicName = "genId_"; private static Config config = new Config(); private static Redisson redisson = null; public static void init(String key,String value){ try { /* config.useClusterServers() //这是用的集群server .setScanInterval(2000) //设置集群状态扫描时间 .setMasterConnectionPoolSize(10000) //设置连接数 .setSlaveConnectionPoolSize(10000) .addNodeAddress("127.0.0.1:6379");*/ if(key==null || "".equals(key)){ key=RAtomicName; } config.useSingleServer().setAddress("127.0.0.1:6379"); redisson = (Redisson) Redisson.create(config); //清空自增的ID数字 RAtomicLong atomicLong = redisson.getAtomicLong(key); long pValue=1; if(value!=null && !"".equals(value)){ pValue = Long.parseLong(value); } atomicLong.set(pValue); }catch (Exception e){ e.printStackTrace(); } } public static Redisson getRedisson(){ return redisson; } /** 获取redis中的原子ID */ public static Long nextID(){ RAtomicLong atomicLong = getRedisson().getAtomicLong(RAtomicName); //原子性的获取下一个ID,递增1 atomicLong.incrementAndGet(); return atomicLong.get(); } }
- 加锁和释放锁的方法,设置超时
public class DistributedRedisLock { private static Redisson redisson = RedissonManager.getRedisson(); private static final String LOCK_TITLE = "redisLock_"; public static boolean acquire(String lockName){ String key = LOCK_TITLE + lockName; RLock mylock = redisson.getLock(key); mylock.lock(2, TimeUnit.MINUTES); //lock提供带timeout参数,timeout结束强制解锁,防止死锁 System.err.println("======lock======"+Thread.currentThread().getName()); return true; } public static void release(String lockName){ String key = LOCK_TITLE + lockName; RLock mylock = redisson.getLock(key); mylock.unlock(); System.err.println("======unlock======"+Thread.currentThread().getName()); } }
- 在web端,controller中
@RequestMapping("/redder") @ResponseBody public String redder() throws IOException{ String key = "test123"; DistributedRedisLock.acquire(key); Long result = RedissonManager.nextID(); DistributedRedisLock.release(key); return ""+result; }
四、总结
目前对于分布式锁的场景非常多,但是现在目前主要的这几种中redis分布式锁的方式目前市场上用的最多。同时笔者也更推荐redisson的方式。
比较总结
1、单JVM锁
(1)synchronized同步锁(基于JVM源生synchronized关键字实现,新手推荐)
适用于低并发的情况,性能稳定。
(2)ReentrantLock可重入锁(基于JDK实现,需显示获取锁,释放锁,需要指定公平、非公平或condition时使用。)
适用于低、高并发的情况,性能较高
(3)ReentrantReadWriteLock可重入读写锁(基于JDK实现,需显示获取锁,释放锁。老司机推荐)
适用于读多写少的情况。性能高。
(4)StampedLock戳锁(基于JDK实现,需显示获取锁,释放锁。老司机推荐)
JDK8才有,适用于高并发且读远大于写时,支持乐观读,票据校验失败后可升级悲观读锁,性能极高!
2、分布式锁
(1)悲观锁:select for update(基于数据库锁实现,不推荐)
sql直接使用,但水很深。涉及数据库ACID原理+隔离级别+不同数据库规范
(2)乐观锁:版本控制(基于数据库锁实现,新手推荐)
自己实现字段版本控制
(3)redisson(基于redis缓存实现,老司机推荐)
性能极高,支持除了分布式锁外还实现了分布式对象、分布式集合等极端强大的功能
(4)zookeeper(基于zookeeper实现,老司机推荐)
性能较高,除支持分布式锁外,还实现了master选举、节点监听()、分布式队列、Barrier、AtomicLong等计数器
(5)memcached(基于memcached实现,老司机推荐)
有明显的缺点,重启会丢失锁,性能很高,但是有弊端,根据不同的场景选择使用