首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

千万级流量并发解决方案

当程序在高并发的情况下,对共享资源进行读写操作,如果不进行并发控制,就必然会带来数据不一致的线程安全性问题。

针对这种高并发的情况,就需要引入锁的机制来保证数据的安全性。

那么什么情况下需要用到锁:

1、多任务环境中

2、任务需要对同一共享资源进行读写操作

3、对资源的访问是互斥的

我举个经典栗子:

车站卖票,一共100张票(共享资源),4个窗口进行卖票(多任务),假设分别叫abcd窗口,a窗口卖了座位号1的票后,就不能在有座位号1的票被卖了(互斥)。

这种情况下,若不使用锁进行并发控制,那么a卖的座位号1的票不能及时通知到abc,就必然会产生一票多卖的问题,也就是我们所说的并发问题。

代码模拟这种卖票行为:

然后我们开始卖票:

看下运行结果:

从程序的运行结果,我们不难看出,出现了一票多卖的情况,第95张票被卖了2次。针对这种情况最简单的方式就是使用synchronized,

除此除此之外jdk还为我们提供了一把很好用的锁:Lock。ReentrantLock类实现了Lock接口,我们可以直接拿来用:

看上去好像比synchronized关键字用起来复杂了,不但要加锁,还要解锁,如果忘记解锁还会出现死锁的情况,Synchronized虽然使用简单,只需要对自己的方法或者关注的同步对象或类使用synchronized关键字即可,但是对于锁的粒度控制比较粗,同时对于实现一些锁的状态的转移比较困难,而lock更加灵活。

看下lock接口的方法:

Lock:阻塞式加锁,(不加成功就一直加,知道加锁成功为止)

lockInterruptibly:可被中断的阻塞式加锁

trylock:尝试加一次锁

tryLock(long time, TimeUnit unit):在指定的时长中尝试加锁

unlock:解锁

newCondition:根据一些条件进行加锁

总结下lock和synchronized的区别:

1.lock是jdk 1.5后新增的

2.synchronized是修饰整个方法,整个代码块。lock可以在任何地方调用lock方法,再在想要结束的地方调用unlock()方法

3.synchronized是java的底层关键字,是在JVM层面上实现,在代码执行异常时,jvm可以自动释放锁定。lock是java类,是通过代码实现来处理异常,所以在finanlly里面一定要调用unlock释放锁。

4.使用synchronized关键字,如果一个线程不释放锁,另一个会一致等待下去。使用lock,如果一个线程不释放锁,在等待很长时间后,可以中断等待去做其他事情。

局限性:

以上synchronized,和lock方法也有其局限性:这2种锁机制只能在进程中的多线程间有效,无法解决分布式环境的多进程间的资源问题。

引入分布式锁:

为了解决以上问题,在高并发的多进程的情况下我们就需要引入分布式锁。目前分布式锁的实现方式主要有3种:

一、利用数据库自身提供的锁机制来实现分布式锁

实现思路:现在的数据库基本都支持行级锁,基于行级锁,我们可以在数据库中建一个锁的表,只有一个id字段,设置主键,那么当需要加锁时,我们只需要向这张表里插入一条id=1的数据即可,其他进程在想加锁,也就是插入id=1是加不了的,这是加锁,解锁只要delete这条id=1的数据即可。利用数据库自身提供的锁机制来实现分布式锁

分析一下数据库实现分布式锁的优缺点:优点很明显:实现简单,稳定可靠,应为是用数据库的行级锁来变相的帮助我们实现分布式锁的,可就是说只要数据库的行级锁没问题,我们的分布式锁就没问题。缺点:1性能差,由于依靠数据库实现,那么其性能也受限于数据库的读写能力,在高并发场景下显然不适用2易出现死锁,一旦加锁成功后没有进行解锁操作或者这个应用挂了,那么在数据库中永远都有一条id=1的记录,其他所有进程都无法进行加锁操作,即出现死锁。

二、利用redis实现分布式锁方案

实现思路:

redis的特性:

由于redis本身就是单进程单线程的,所以即使在高并发场景下也不会存在竞争关系。

redis中的数据设置生存时间后,当key过期时会被自动删除。

setnx key value,当key不存在时将key的值设置为value。

基于以上redis特性,当我们需要加锁:使用setnx向特定的key写入一个随机值,并同时设置失效时间(避免死锁),写值成功即加锁成功,其他进程或线程在进行加锁操作显然就会失败。

以上加锁操作有3点我们需要关注:

写入随机值时要设置失效时间,这样可以有效避免死锁问题,到期自动解锁;

加锁时每个产生一个随机字符串,当解锁时为了避免锁误删,需要对这个随机值和写入redis的值进行比对,如果一致就认为是原加锁线程来进行了解锁,可以执行解锁操作,如果不一致,就拒绝解锁;

写入随机值与设置失效时间必须是同时的(保证加锁是原子的)

解锁:解锁实际需要执行3个逻辑:

1、获取数据

2、判断是否一致

3、删除数据。

这里我们使用lua脚本进行这3步操作,如不使用lua脚本执行解锁操作,则无法保证操作的原子性,lua脚本实现解锁脚本(保证操作原子性,下面3个步骤要么全部执行,要么全部不执行,不存在执行部分的情况):

If redis.call(“get”,keys[1])==argv[1] then

Return redis.call(“del”,keys[1])

Else

Return 0

End

同样,分析一下优缺点:优点也很明细:基于redis的实现方式,同样继承了redis的高性能的优点;缺点:1相对比数据库实现,redis的实现相对复杂2key有效期的设置需要基于个人经验,有出现死锁的可能性3无法优雅的实现阻塞

三、利用zookeeper实现分布式锁方案

实现思路:

Zookeeper特性:

1、Zookeeper会在内存中维护一个具有层次结构的数据结构,类似于文件系统

2、数据结构中的每个节点都可以存数据,还有各种属性信息;数据节点类型有:持久节点、持久顺序节点、临时节点、临时顺序节点,临时顺序节点是实现分布式锁的基础

3、事件监听器watcher:客户端可以在节点注册watcher,当节点发生特定的变化,服务器会将事件通知到客户端

利用以上zk特性,我们先在zk上维护一个lock的持久节点,任何业务需要加锁就在zk的lock节点下维护一个临时顺序节点,临时顺序节点是有序的,并且一旦链接断开临时节点就会自动删除,(这样就保证了不会死锁),当创建完成临时顺序节点后我们获取当前节点的序号值L,然后获取lock节点的所有子节点,比较当前节点的L值是否是所有节点中最小的,如果是最小的就获得锁,如果不是最小的,就向当前节点的前一个节点添加监听器,那么一旦前一个节点被删除,就会重新走一下上面的判断是否最小节点流程流程,直到获取锁。

同样分析一下优缺点:优点:zookeeper和redis一样,数据保存在内存中,这就意味着它同redis一样有高吞吐量和低延迟的性能优势;能避免出现死锁(基于临时节点的特性,一旦客户端挂掉,临时节点即删除,也就是解锁了)能优雅的实现阻塞式锁(基于zk的watcher机制,前2种方法都无法实现阻塞式锁);缺点:缺点很明显,实现方式较复杂

以上介绍了分布式锁的3种实现方式的实现思路,具体的实现代码太占篇幅就不贴上来了

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20190108G14TR800?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券