对于我们开发的网站,如果网站的访问量非常大的话,我们就需要考虑相关的并发访问问题了。而且并发问题也是中高级工程师面试中必问的问题,今天我们就来系统学习一下。
为了更好的理解并发和同步,我们先学习两个重要的概念:同步和异步。
1、同步和异步的区别和联系
所谓同步,可以理解为在执行完一个函数或方法之后,一直等待系统返回值或消息,这时程序是处于阻塞状态的,只有接受到返回值或消息之后才往下执行其他的命令。
所谓异步,执行外函数或方法之后,不必阻塞性地等待返回值或消息,只需要向系统委托一个异步过程,那么当系统接收到返回值或消息时,系统会自动触发委托的异步过程,从而完成一个完整的流程。
同步在一定程度上可以看作是单线程,这个线程请求一个方法之后就等待这个方法给他回复,否则就不往下执行。
异步在一定程度上可以看作多线程,请求一个方法之后就不管了,继续执行其他的方法。
同步就是一件事,一件事一件事的做。
异步就是,做一件事,不影响做其他的事情。
对于java程序而言,我们会经常听到同步关键字synchronized,假如这个同步的监事对象是类,那么当一个对象访问这个类里面的同步方法的时候,其他的对象也想访问这个同步方法就会进入阻塞状态,只有等前一个对象执行完该同步方法,当前对象才能继续执行该方法,这就是同步。相反,如果方法前没有同步关键字修饰的话,那么不同的对象就可以在同一时间访问同一个方法,这就是异步。再补充一下脏数据和不可重复读的概念。
2、脏数据和不可重复读
脏数据:
脏读就是指,当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务访问并使用了这个数据。因为这个数据还没有提交,那么另外一个事务读到的数据就是脏数据(dirty data),依据脏数据所做的操作可能是不正确的。
不可重复读:
指在数据库访问中,一个事务范围内两个相同的查询却返回了不同的数据。这是由于查询时系统中其它事务修改的提交引起的。一种更容易理解的说法是:在一个事务内,多次读取同一数据。在这个事务还没结束时,另一个事务也访问该数据。那么,在第一个事务的两次读数据之间,由于第二个事务的修改,导致第一个事务两次读到的数据可能不同,因此成为不可重复读,即原始读取不可重复。
3、如何处理并发和同步
今天学习的处理并发和同步问题主要是通过锁机制。
锁机制有两个层面:
一种是代码层面上的,如java中的同步锁,典型的就是同步关键字synchronized和lock。
http://www.cnblogs.com/xiohao/p/4151408.html
http://www.cnblogs.com/xiohao/p/4151924.html
http://blog.csdn.net/lmb55/article/details/46279155
另一种是数据库层面上的,比较典型的就是悲观锁和乐观锁。
有关悲观锁和乐观锁的内容请参看:http://blog.csdn.net/lmb55/article/details/78266667
4、常见java并发同步案例分析
案例一:订票系统案例,某航班只有一张机票,假定有1W个人打开你的网站来订票,问你如何解决并发问题(可扩展到任何高并发网站要考虑的并发读写问题)
问题,1W个人来访问,要求票没出去前必须保证大家都能看到票,不可能一个人在看到票的时候别人就不能看到。但是最后到底谁能抢到,那得要看这个人的“运气”了(网络快慢等)
首先我们容易想到和并发相关的几个方案:
锁同步更多指的是应用程序的层面,多个线程进来,只能一个一个的访问。除了Java中的对象锁,还有另外一个层面—数据库锁。如果是分布式系统,显然只能使用数据库端的锁来实现。
假定我们采用了同步机制或者数据库物理锁机制,如果保证1W个人还能同时看到有票,这显然会牺牲性能,在高并发网站中是不可取的。
由此来看,采用乐观锁即可解决此问题。乐观锁是在不锁表的情况下,利用业务的控制来解决并发问题,这样既保证数据的并发可读性又保证数据的排他性,保证性能的同时也解决了并发带来的脏数据问题。
hibernate如何实现乐观锁:
前提:在现有表中增加一个冗余字段,long类型的version版本号。
原理:
1)只有当前版本号>=数据库表的版本号才能提交
2)提价成功后,版本号version++
实现很简单:在orgmapping增加一个属性optimistic-lock=”version”即可,以下是样例片段:
<hibernate-mapping>
<class name="com.stock.ABC" optimistic-lock="version" table="T_Stock" schema="STOCK">
</hibernate-mapping>
案例二:股票交易系统、银行系统,大数据量你是如何考虑的
首先,股票交易系统的行情表,每几秒钟就有一个行情记录产生,一天下来就有(假定行情3s一个)股票数量*20*60*6条记录,一个月下来这个表的记录数量多大?Oracle中一张表的记录数超过100W之后查询性能就很差了,如何保证系统性能?
再比如,中国移动有上亿的用户量,表如何设计?把所有数据存到一张表?
所以,大数据量的系统,必须考虑表拆分(表名不一样,但是结构完全一样),通常用到的有以下几种方式(视情况而定):
1)按业务分。比如手机号的表,我们可以考虑130开头的一个表,131开头的另一个表,以此类推。
2)利用Oracle的表拆分机制做分表
3)如果是交易系统,我们可以考虑按时间轴拆分,当日数据一个表,历史数据弄到其它表。这里历史数据的报表和查询不会影响当日交易。
当然,表拆分后我们的应用要做相应的适配。单纯的orgmapping就得改动了。比如部分业务要通过存储过程等。
此外,我们还要考虑缓存:
这里的缓存,指的不仅是hibernate,hibernate本身提供了一级二级缓存。这里的缓存独立于应用,依然是内存的读取。假如我们能减少数据库频繁的访问,那对系统肯定大大有利。比如一个电子商务系统的商品搜索,如果某个关键字的商品经常被搜,那就可以考虑把这部分商品列表存放到缓存(内存)中去,这样不用每次访问数据库,性能大大增加。
简单的缓存我们可以理解为自己做一个HashMap,把经常访问的数据做一个key,value是第一次从数据库搜索出来的值,下次访问就可以从map中读取,而不需要读数据库。目前专业些的有独立缓存框架比如memcached等,可独立部署成一个缓存服务器。
redis实现分布式锁:
http://blog.csdn.net/lmb55/article/details/78235768
Redis实现秒杀:
http://blog.csdn.net/lmb55/article/details/78266905
5、常见的提高高并发下访问效率的手段
首先要了解高并发的瓶颈在哪里?
1)可能是服务器网络带宽不够
2)可能是web线程连接数不够
3)可能是数据库连接查询上不去
根据不同的情况,解决思路也不同
1)像第一种情况可以增加网络带宽,DNS域名解析分发多台服务器。
2)负载均衡,前置代理服务器nginx、apache等等。
3)数据库查询优化,读写分离,分表等等。
最后复制一些在高并发下面常常需要处理的内容:
1)尽量使用缓存,包括用户缓存、信息缓存等,多花点内存来做缓存。可以大量减少与数据库的交互,提高性能。
2)用jprofiler等工具找出性能瓶颈,减少额外开销。
3)优化数据库查询语句,减少直接使用hibernate等工具的直接生成语句(仅耗时较长的查询做优化)
4)优化数据库结构,多做索引,提高查询效率
5)统计的功能尽量做缓存,或按每天统计或者定时统计相关报表,避免需要时进行统计的功能。
6)能使用静态页面的地方尽量使用静态页面,减少容器的解析(尽量将动态内容内容生成静态html显示)。
7)解决以上问题后,使用服务器集群来解决单台的瓶颈问题。