项目如何解决线程安全问题,如何保证性能?
秒杀项目
我们需要怎么样的分布式锁?
可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。
这把锁要是一把可重入锁(避免死锁)
这把锁最好是一把阻塞锁(根据业务需求考虑要不要这条)需要考虑
这把锁最好是一把公平锁(根据业务需求考虑要不要这条)不需要
有高可用的获取锁和释放锁功能
获取锁和释放锁的性能要好
死锁
可重入锁最大的作用是避免死锁
可重入锁,也叫递归锁。
“重入”将获取锁的粒度由“调用”转变为“线程”,即当一个线程请求一个未持有的锁,该线程将顺利得到锁并被记录,且将计数值从0变为1;当下次同一个线程再次请求这把锁,该线程无需排队,而是直接得到锁,且计数值由1变为2,仅仅是数量上的累加;每次退出一个线程,计数值-1,直到计数值为0,这把锁将被释放。
如下:两个方法同时被加锁,如果首先执行method1,则该线程拥有Test对象的锁,但如果synchronized不是可重入锁,当method1方法调用method2,发现method2需要等待method1锁释放,但是method1的执行又必须依赖method2,于是循环等待,形成死锁。
1 基于数据库实现的分布式锁
基于表实现的分布式锁[非阻塞]
秒杀下单操作的方法名,备注信息,保存数据的时间,newdate自动生成
每个主机进行下单操作之前都需要去插入这个表,因为对这个表中方法名字段,做了唯一约束,因此只能有一个主机进程插入成功,所以实现了线程安全
当我们想要锁住某个方法时,执行以下SQL: insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)
因为我们对method_name做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。
当方法执行完毕之后,想要释放锁的话,需要执行以下Sql: delete from methodLock where method_name =’method_name’
上面这种简单的实现有以下几个问题:
这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。问题?如何设置主从数据库
这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。
这把锁是非公平锁,所有等待锁的线程凭运气去争夺锁。
当然,我们也可以有其他方式解决上面的问题。
数据库是单点?搞两个数据库,数据之前双向同步。一旦挂掉快速切换到备库上。
没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
非阻塞的?搞一个while循环,直到insert成功再返回成功。
非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。
非公平的?再建一张中间表,将等待锁的线程全记录下来,并根据创建时间排序,只有最先创建的允许获取锁
缺点
会有各种各样的问题,在解决问题的过程中会使整个方案变得越来越复杂。
操作数据库需要一定的开销,性能问题需要考虑。
二、基于缓存的分布式锁
相比较于基于数据库实现分布式锁的方案来说,基于缓存来实现在性能方面会表现的更好一点。
目前有很多成熟的缓存产品,包括Redis,memcached等。这里以Redis为例来分析下使用缓存实现分布式锁的方案。
基于Redis实现分布式锁在网上有很多相关文章,其中主要的实现方式是使用Jedis.setNX方法来实现。
SETNX key value
含义(setnx = SET if Not eXists):
将 key 的值设为 value ,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。
SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。
返回值:
设置成功,返回 1 。
设置失败,返回 0 。
该命令相当于将下面两行操作合并为一个原子操作
SET key value
EXPIRE key seconds # 设置生存时间
这样就可以直接保证解锁过程的原子性
具体实现代码
public boolean trylock(String key) {
ResultCode code = jedis.setNX(key, “This is a Lock.”);
if (ResultCode.SUCCESS.equals(code))
return true;
else
return false;
}
以上实现方式同样存在几个问题:
1、单点问题。
2、这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在redis中,其他线程无法再获得到锁。
3、这把锁只能是非阻塞的,无论成功还是失败都直接返回。
4、这把锁是非重入的,一个线程获得锁之后,在释放锁之前,无法再次获得该锁,因为使用到的key在redis中已经存在。无法再执行setNX操作。
5、这把锁是非公平的,所有等待的线程同时去发起setNX操作,运气好的线程能获取锁。
当然,同样有方式可以解决。
现在主流的缓存服务都支持集群部署,通过集群来解决单点问题。
没有失效时间?redis的setExpire方法支持传入失效时间,到达时间之后数据会自动删除。
非阻塞?while重复执行。
非可重入?在一个线程获取到锁之后,把当前主机信息和线程信息保存起来,下次再获取之前先检查自己是不是当前锁的拥有者。
非公平?在线程获取锁之前先把所有等待的线程放入一个队列中,然后按先进先出原则获取锁。
共性问题
失效时间我设置多长时间为好?**如何设置的失效时间太短,方法没等执行完,锁就自动释放了,那么就会产生并发问题。**如果设置的时间太长,其他获取锁的线程就可能要平白的多等一段时间。
这个问题使用数据库实现分布式锁同样存在。
对于这个问题目前主流的做法是每获得一个锁时,只设置一个很短的超时时间,同时起一个线程在每次快要到超时时间时去刷新锁的超时时间。在释放锁的同时结束这个线程。如redis官方的分布式锁组件redisson,就是用的这种方案。利用看门狗机制去监控,当快过期的时候去监控线程,如果没执行完就刷新过期时间,
性能好
2 基于Zookeeper实现的分布式锁
特性: 基于上面的图片,每一个节点是不能够重复的,如果谁创建了节点,那么谁就获取到了锁
基于zookeeper临时有序节点可以实现的分布式锁。
大致思想即为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。
来看下Zookeeper能不能解决前面提到的问题。
锁无法释放?使用Zookeeper可以有效的解决锁无法释放的问题,因为在创建锁的时候,客户端会在ZK中创建一个临时节点,一旦客户端获取到锁之后突然挂掉(Session连接断开),那么这个临时节点就会自动删除掉。其他客户端就可以再次获得锁。
非阻塞锁?使用Zookeeper可以实现阻塞的锁,客户端可以通过在ZK中创建顺序节点,并且在节点上绑定监听器,一旦节点有变化,Zookeeper会通知客户端,客户端可以检查自己创建的节点是不是当前所有节点中序号最小的,如果是,那么自己就获取到锁,便可以执行业务逻辑了。
不可重入?使用Zookeeper也可以有效的解决不可重入的问题,客户端在创建节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的时候和当前最小的节点中的数据比对一下就可以了。如果和自己的信息一样,那么自己直接获取到锁,如果不一样就再创建一个临时的顺序节点,参与排队。
单点问题?使用Zookeeper可以有效的解决单点问题,ZK是集群部署的,只要集群中有半数以上的机器存活,就可以对外提供服务。
公平问题?使用Zookeeper可以解决公平锁问题,客户端在ZK中创建的临时节点是有序的,每次锁被释放时,ZK可以通知最小节点来获取锁,保证了公平。
优点
有效的解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题。实现起来较为简单。
缺点
性能上不如使用缓存实现分布式锁。 需要对ZK的原理有所了解。