对于咱们开发的网站,若是网站的访问量很是大的话,那么咱们就须要考虑相关的并发访问问题了,然而并发问题是令咱们大多数程序员头疼的问题,但话又说回来了,既然逃避不掉,那咱们就坦然面对吧~今天就让咱们深刻研究一下常见的并发和同步问题吧。html
1、同步和异步的区别和联系java
为了更好的理解同步和并发的问题,咱们须要先掌握两个重要的概念:同步、异步mysql
同步:能够理解为在执行完一个函数或者方法后,一直等待系统返回值或消息,这时程序是处于阻塞的状态,只有接收到系统的返回值或者消息后,才会继续往下执行。程序员
异步:执行完函数或方法后,没必要阻塞性的等待返回值或消息,只须要向系统委托一个异步过程,那么系统接收到返回值或消息时,就会自动触发委托的异步过程,从而完成一个完整的流程。web
同步在必定程度上能够看作是单线程,这个线程请求一个方法后,就等待这个方法给他回复,不然不往下执行(死心眼子)。redis
异步在必定程度上能够看作是多线程(废话,一个线程怎么叫异步),请求一个方法后就无论了,继续执行接下来的其余方法。sql
同步就一件事、一件事、一件事的作。数据库
异步就是作一件事情,不影响作其余的事情。json
例如:吃饭和说话是同步的,只能一件事一件事的来,由于只有一张嘴。吃饭和听音乐是异步的,听音乐不影响咱们吃饭。缓存
对于java程序员来讲,咱们常常见到同步关键字 synchronized,假如这个同步的监视对象是一个类,当一个对象A在访问这个类里面的同步方法,此时另一个对象B也想访问这个类里面的这个同步方法,就会进入阻塞,只有等待前一个对象执行完该同步方法后当前对象才可以继续执行该方法;这就是同步。
相反,若是方法前没有同步关键字修饰的话,那么不一样的对象就能够在同一时间访问同一个方法,这就是异步。
再补充一下,脏数据和不可重复读的概念:
一、脏数据
脏读是指:一个事务正在访问数据,而且对数据进行了修改,而这个修改还有提交到数据库中,这时另一个事务也访问这个数据,而后使用了这个数据。由于这个数据是尚未提交的数据,那么另一个事务读到的数据是脏数据(Dirty Data),脏数据所作的操做多是不正确的。
二、不可重复读
不可重复读是指:在一个事务内,屡次读同一条数据。这个事务尚未结束时,另一个事务也访问了该数据,那么,在第一个事务中两次读数据之间,因为第二个事务的修改,致使第一个事务两次读到数据可能不同。这样就发生了在同一个事务内,两次读到的数据是不同的,所以称为不可重复读。
2、如何处理并发和同步
今天讲的如何处理并发和同步问题主要是经过锁机制去解决。咱们须要明白锁机制有两个层面:
第一是代码层面,如java中的同步锁,典型的就是同步关键字synchronize(还有Lock等)。 感兴趣的能够参考: http://www.cnblogs.com/xiohao/p/4151408.html
第二是数据库层面上,比较典型的就是悲观锁和乐观锁,这里重点研究一下悲观锁(传统的物理锁)和乐观锁,这两个锁:
悲观锁正如其名,它指的是对数据被外界(包括本系统当前的其余事务,以及外部系统的事务)的修改持保守状态,所以,在整个数据处理过程当中,将数据处于锁定状态。
悲观锁的实现,通常是依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,不然即便在本系统中实现了加锁机制,也没法保证外部系统会修改数据)。
一个典型的依赖数据库悲观锁调用:select * from account where name='zhangsan' for update;
整条sql锁定了account表中全部符合检索条件(name='zhangsan')的记录;本次事务提交以前(事务提交会释放事务过程当中的锁),外界没法修改这些记录。
hibernate的悲观锁,也是基于数据库的锁机制实现的,下面代码实现了对查询记录的加锁:
1 String hqlStr ="from TUser as user where user.name='zhangsan' "; 2 Query query = session.createQuery(hqlStr); 3 query.setLockMode("user",LockMode.UPGRADE); // 加锁 4 List userList = query.list();// 执行查询,获取数
query.setLocalMode 对查询语句中特定别名所对应的记录进行加锁(咱们对TUser类制定了一个别名“user”),这也就是对多有返回的记录加锁。
观察容许其的hibernate,生成的sql语句:这里hibernate经过使用数据库的 for update 子句实现了悲观锁机制
1 select tuser0_.id as id, tuser0_.name as name, tuser0_.group_id as group_id, tuser0_.user_type as user_type, tuser0_.sex as sex from t_user tuser0_ where (tuser0_.name='Erica' ) for update
hibernate的加锁机制有:
一、LockMode.NONE:无锁机制
二、LockMode.WRITE:hibernate 在 insert 和 update 的时候记录会自动获取
三、LockMode.READ:hibernate在读取记录的时候会自动获取
以上这三种锁机制通常由heibernate内部使用,如hibernate为保证 update 过程当中对象不会被外界修改,会在 save 方法实现中自动为目标对象加上 WRITE 锁。
四、LockMode.UPGRADE:利用数据的 for update 子句加锁
五、LockMode.UPGRADE_NOWAIT:Oracle 的特定实现,利用 Oracle 的 for update nowait 子句实现加锁
上面这两种锁机制是咱们应用层较为经常使用的,加锁通常经过:Criteria.setLockMode;Query.setLockMode;Session.lock;方法实现。
须要注意点的是:只有在查询开始以前(也就是hibernate生成sql以前)设置加锁,才会真正经过数据的锁机制进行加锁处理,不然,数据已经过不包含 for update 子句的 select sql 加载进来,所谓数据的加锁也就无从谈起。
为了更好的理解 select... for update 的锁表过程,咱们以 mysql 为例,进行研究
要测试锁定的状态,可利用 mysql 的 Command Mode,开两个视窗来测试:
表的基本结构以下:
表中内容以下:
开启两个测试窗口,在其中一个窗口执行 select * from ta for update;
而后再另一个窗口执行 update 操做:
等到第一个窗口 commit 后:
至此,悲观锁的机制,有一些感受了吧,
须要注意的是 for update 要放到 mysql 的事务中,即 begin 和 commit 之间,不然不起做用。
至于锁住整张表仍是锁住选中的行,请参考:http://www.cnblogs.com/xiohao/p/4385768.html
二、乐观锁 (Optimistic Locking):
相对悲观锁而言,乐观锁机制采起了更加宽松的加锁机制。悲观锁大多数状况下依靠数据库的锁机制实现,以保证操做最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事物而言,这样的开销是没法承受的。
如一个金融系统当一个操做员读取用户数,并在读出的数据上进行修改操做(如更改用户帐户余额),若是采用悲观锁机制,也就意味着整个操做过程当中(从读出数据,修改数据,直到提交数据,甚至还包括操做员中途去煮咖啡的时间),数据库的记录始终处于加锁状态,能够想到,若是面对成百上千个并发,这种状况会致使什么样的后果。乐观锁的机制在必定程度上解决了这个问题。
乐观锁,大可能是基于数据版本(version)记录机制实现。数据版本:即为在数据库增长一个版本标识。在基于数据库表的版本解决方案中,通常是经过在数据库表中增长一个 version 字段来实现。读取数据的时候将此版本号一块儿读出,以后更新时候将此版本号加一。此时,将提交数据的版本信息与数据库表对应记录的当前本版信息进行对比,若是提交数据的当前本版号大于数据库表当前版本号,则予以更新,不然认为是过时数据。对于上面修改用户信息的例子而言,假设数据库帐户信息表中有一个 version 字段,当前值为1,当前帐户余额字段 balance 为 100;此时操做员A将此数据读出,并从帐户余额扣除 50。在操做员 A 操做过程当中,操做员 B 也读去了该用户的信息,并从帐户余额中扣除 20。这是操做员 A 完成了修改工做,将数据版本信息加一(version = 2),和帐户扣除后的余额(balance = 50),提交到数据库更新,此时因为提交数据版本大于数据库记录的版本号,数据记录被更新 version 更新为 2。接着,操做员 B 完成了操做,也将版本号加一(version = 2),试图提交数据(balance = 80),但此时对比数据库记录版本时候发现,数据库当前本版也为2,不知足“数据版本必须大于记录当前版本才能执行更新”的乐观锁策略,所以操做员 B 的提交被驳回。这样就避免了操做员 B 使用基于 vsersion=1 的旧数据修改的结果覆盖了操做员 A 的操做结果。从例子能够看出,乐观锁的机制避免了长事务中数据库加锁数据库开销(操做员A和操做员B操做过程当中都没有对数据库数据加锁),大大提高了大并发下系统总体性能的表现,来自外部系统的用户余额更新操做不受咱们系统的控制,所以可能会形成在脏数据被更新到数据中,在系统设计阶段,咱们应该充分考虑到发送这些状况的可能性,并进行相应的调整(如将乐观锁策略在数据库存储过程当中实现,对外只开放基于此存储过程的数据更新途径,而不是直接将数据库表直接对外公开)。
Hibernate在其数据访问引擎中内置了乐观锁的实现。若是不考虑外部系统对数据库的更新操做,利用Hibernate 提供的透明化乐观锁实现,将大大提高咱们的生产力。
User.hbm.xml : 注意 version 节点必须出如今 ID 节点以后。
1 <?xml version="1.0"?> 2 <!DOCTYPE hibernate-mapping PUBLIC 3 "-//Hibernate/Hibernate Mapping DTD 3.0//EN" 4 "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> 5 6 <hibernate-mapping package="com.ayht.test"> 7 <class name="User" table="user" optimistic-lock="version" > 8 <id name="id"> 9 <generator class="native" /> 10 </id> 11 <!--version标签必须跟在id标签后面--> 12 <version column="version" name="version" /> 13 <property name="userName"/> 14 <property name="password"/> 15 </class> 16 </hibernate-mapping>
这里咱们申明了一个 version 属性,用于存放用户的版本信息,保存在User表的 version 中,optimistic-lock 属性有以下取值:
none :无乐观锁
version : 经过版本机制实现乐观锁
dirty : 经过检查发送变更的属性实现乐观锁
all :经过检查全部属性实现乐观锁
其中经过 version 实现乐观锁机制是 Hibernate 官方推荐的乐观锁实现,同时也是 Hibernate 中,目前惟一在数据对象脱离Session 发生修改的状况下依然有效的锁机制。所以通常状况下咱们都选择 version 的方式做为 Hibernate 的乐观锁实现机制。
hibernate.cfg.xml
1 <!DOCTYPE hibernate-configuration PUBLIC 2 "-//Hibernate/Hibernate Configuration DTD 3.0//EN" 3 "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd"> 4 5 <hibernate-configuration> 6 <session-factory> 7 <!-- 指定数据库方言 若是使用jbpm的话,数据库方言只能是InnoDB--> 8 <property name="dialect">org.hibernate.dialect.MySQL5InnoDBDialect</property> 9 <!-- 根据须要自动建立数据表 --> 10 <property name="hbm2ddl.auto">update</property> 11 <!-- 显示Hibernate持久化操做所生成的SQL --> 12 <property name="show_sql">true</property> 13 <!-- 将SQL脚本进行格式化后再输出 --> 14 <property name="format_sql">false</property> 15 <property name="current_session_context_class">thread</property> 16 17 <!-- 导入映射配置 --> 18 <property name="connection.url">jdbc:mysql:///user</property> 19 <property name="connection.username">root</property> 20 <property name="connection.password">123456</property> 21 <property name="connection.driver_class">com.mysql.jdbc.Driver</property> 22 <mapping resource="com/xiaohao/test/User.hbm.xml" /> 23 </session-factory> 24 </hibernate-configuration>
UserTest.java:每次对 TUser 更新的时候,咱们会发现,数据库的 version 都在递增。
1 package com.xiaohao.test; 2 3 import org.hibernate.Session; 4 import org.hibernate.SessionFactory; 5 import org.hibernate.Transaction; 6 import org.hibernate.cfg.Configuration; 7 8 public class UserTest { 9 public static void main(String[] args) { 10 Configuration conf=new Configuration().configure(); 11 SessionFactory sf=conf.buildSessionFactory(); 12 Session session=sf.getCurrentSession(); 13 Transaction tx=session.beginTransaction(); 14 // User user=new User("小浩","英雄"); 15 // session.save(user); 16 // session.createSQLQuery("insert into user(userName,password) value('张英雄16','123')").executeUpdate(); 17 User user=(User) session.get(User.class, 1); 18 user.setUserName("221"); 19 // session.save(user); 20 21 System.out.println("恭喜您,用户的数据插入成功了哦~~"); 22 tx.commit(); 23 } 24 }
如下咱们将要经过乐观锁来实现一下并发和同步的测试用例:咱们准备两个测试类,分别运行在不一样的虚拟机上,以此来模拟多个用户同时操做同一张表,同事一个测试类要模拟一个长事务。
UserTest.java
package com.xiaohao.test; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.Transaction; import org.hibernate.cfg.Configuration; public class UserTest { public static void main(String[] args) { Configuration conf=new Configuration().configure(); SessionFactory sf=conf.buildSessionFactory(); Session session=sf.openSession(); // Session session2=sf.openSession(); User user=(User) session.createQuery(" from User user where user=5").uniqueResult(); // User user2=(User) session.createQuery(" from User user where user=5").uniqueResult(); System.out.println(user.getVersion()); // System.out.println(user2.getVersion()); Transaction tx=session.beginTransaction(); user.setUserName("101"); tx.commit(); System.out.println(user.getVersion()); // System.out.println(user2.getVersion()); // System.out.println(user.getVersion()==user2.getVersion()); // Transaction tx2=session2.beginTransaction(); // user2.setUserName("4468"); // tx2.commit(); } }
UserTest2.java
package com.xiaohao.test; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.Transaction; import org.hibernate.cfg.Configuration; public class UserTest2 { public static void main(String[] args) throws InterruptedException { Configuration conf=new Configuration().configure(); SessionFactory sf=conf.buildSessionFactory(); Session session=sf.openSession(); // Session session2=sf.openSession(); User user=(User) session.createQuery(" from User user where user=5").uniqueResult(); Thread.sleep(10000); // User user2=(User) session.createQuery(" from User user where user=5").uniqueResult(); System.out.println(user.getVersion()); // System.out.println(user2.getVersion()); Transaction tx=session.beginTransaction(); user.setUserName("100"); tx.commit(); System.out.println(user.getVersion()); // System.out.println(user2.getVersion()); // System.out.println(user.getVersion()==user2.getVersion()); // Transaction tx2=session2.beginTransaction(); // user2.setUserName("4468"); // tx2.commit(); } }
这里首先启动 UserTest2.java 测试类,在执行到 Thread.sleep(10000); 这里的时候,当前线程会进入到休眠状态。在10秒内启动 UserTest.java 类,在到达10秒的时候,UserTest.java 将会抛出下面的异常:
1 Exception in thread "main" org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.xiaohao.test.User#5] 2 at org.hibernate.persister.entity.AbstractEntityPersister.check(AbstractEntityPersister.java:1932) 3 at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:2576) 4 at org.hibernate.persister.entity.AbstractEntityPersister.updateOrInsert(AbstractEntityPersister.java:2476) 5 at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:2803) 6 at org.hibernate.action.EntityUpdateAction.execute(EntityUpdateAction.java:113) 7 at org.hibernate.engine.ActionQueue.execute(ActionQueue.java:273) 8 at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:265) 9 at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:185) 10 at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:321) 11 at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:51) 12 at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1216) 13 at org.hibernate.impl.SessionImpl.managedFlush(SessionImpl.java:383) 14 at org.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:133) 15 at com.xiaohao.test.UserTest2.main(UserTest2.java:21)
UserTest.java 将在 tx.commit(); 处抛出:StaleObjectStateException 异常,并指出版本检查失败,当前事务正在准备提交一个过时数据,经过捕捉这个异常,咱们能够在乐观锁效验失败时候进行相应的处理。
3、常见并发同步案例分析:
案例一:订票系统案例:某航班只有一张机票,假定有1W我的打开你的网站来订票,问你如何解决并发问题(可扩展到任何高并发网站要考虑到的并发读写问题)
问题1:1W我的来访问,票没订出去前要保证你们都能看到有票,不可能一我的在看到票的时候别人就不能看了。
问题2:并发1W我的同时点击购买,总共只有一张票,到底谁能买到。
首先咱们容易想到和并发相关的几个方案:
锁同步更多指的是应用程序层面,多线程进来,只能一个一个的访问,java 中使用的是 synchronized 关键字。锁也有两个层面,一个是 java 中谈到的对象锁,用于线程同步;另一个层面是数据库锁;若是是分布式的系统,显然只能利用数据库的锁来实现。
假定咱们采用了同步机制或者数据库物理机制,如何保证1W我的还能同时看到有票,显然会牺牲性能,在高并发网站中不可取。使用 Hibernate 以后咱们提出了另一个概念:乐观锁、悲观锁(传统的物理锁)。
采用乐观锁便可解决此问题,乐观锁的意思是在不锁定表的状况下,利用业务的控制来解决并发问题,这样既保证数据的并发可读性、又保证保存数据的排他性。保证性能的同时,解决了并发带来脏数据的问题。
Hibernate 中实现乐观锁的方法:
注意:在现有表中增长一个冗余字段,version 版本号,long 类型
原理:一、只有当前版本号 >= 数据库表版本号,才能提交
二、提交成功后版本号 version++
实现很简单:在 or-mapping 增长一个属性 optimistic-lock="version" 便可。一下是样例片断:
1 <hibernate-mapping> 2 3 <class name="com.xiaohao.abc.ABC" optimistic-lock="version" table="T_Stock" schema="STOCK">
案例二:股票交易系统、银行系统,大数据量如何考虑?
首先股票交易系统的行情表,每几秒钟就有一个行情记录产生,假定行情3秒一条,一天下来就有 股票数量 * 20 * 60 * 6 条记录,一个月下来这个表数量有多大?数据库中表的记录数超过100W后,查询性能就不好了,如何保证系统性能?
再好比:中国移动有上亿的用户量,全部用户都存在一张表吗?表如何设计?因此大数据量的系统,必须考虑表拆分(表名不同,表结构彻底同样),通用的几种方式:
一、按业务拆分,好比手机号的表,能够考虑130开头的号码存在一张表,131开头的再存一张表,以此类推。
二、利用 oracle 的表拆分机制作分表
三、若是是交易系统,能够考虑按时间轴拆分,当日数据一个表,历史数据保存到其余表。这里的历史数据的报表和查询不会影响当日交易
固然、表拆分后咱们应用程序非作相应的适配,单纯的 or-mapping 就得要改动了,好比业务部分得经过存储过程等。
此外、咱们还要考虑缓存,这个缓存不只仅指的是 Hibernate 提供的1、二级缓存。这个缓存独立于应用,依然是内存的读取,假如咱们能减小数据库的频繁访问,那对系统性能确定大大有利。好比电子商务系统的商品搜索,若是某个关键字的商品常常被搜索,那就能够考虑把这部分的商品列表存放到缓存中(内存中),这样不用每次访问数据库,使系统性能大大增长。
简单的缓存能够理解为一个 hashMap,把常常访问的数据做为一个key,value是第一次从数据库查出的数据,下次访问就能够从map中取值,而不读取数据库。目前可使用memcached,redis,等。可独立部署成一个缓存服务器。
案例三:抢购秒杀业务的解决方案:
以前咱们将高并发解决方案误认为能够用线程或者队列解决,由于高并发的时候有不少用户在访问,致使出现系统数据不正确,丢失数据现象。因此想到的是用队列解决。
其实队列解决的方式也能够处理,好比咱们再竞拍商品、转发评论微博、秒杀商品等,同一时间访问量特别大,队列再次起到特别的做用,将全部请求放入队列,以毫秒为计时单位,有序的进行,从而不回初选数据丢失系统不正确的状况。
4、常见的高并发下提升访问效率的方法:
首先了解高并发的瓶颈在哪里
一、多是服务器网络带宽不够:能够增长网路带宽,DNS域名解析分发多台服务器。
二、多是web线程链接数不够:负载均衡,前置代理服务器 negix、Apache 等等。
三、多是数据库链接查询上不去:数据库查询优化、读写分离,分表等等。
如下列出一些在高并发下常常须要处理的内容:
一、尽可能使用缓存,包括用户信息缓存,经常使用信息缓存,多花点内存来作缓存,能够大量减小与数据库的交互,提升性能。
二、用 jprofile 等工具找出性能瓶颈,减小额外的开销。
三、优化数据库库查询语句,减小直接使用 Hibernate 等工具生成查询语句(只针对耗时长的语句作优化)。
四、数据库结构,使用索引,提升查询效率。
五、统计的功能尽可能做缓存,或按天天一统计或者定时统计相关报表,避免须要时进行统计的功能。
六、能使用静态页面的地方尽可能使用静态页面,减小容器的解析(尽可能将动态的内容生成 html 来显示)。
七、解决以上问题以后,尽可能使用服务器集群来解决单台服务器的瓶颈。
高并发的解决方案有以下两种:
第一:使用缓存;第二:生成静态页面;
还有就是从最基础的地方优化咱们写的代码,减小没必要要的资源浪费:
一、不要频繁 new 对象;对于在整个应用中只须要存在一个实例的类,使用单例模式;对于 String 的拼接操做,使用 StringBuffer 或者 StringBuilder;对于 utility 类型的类经过静态方法来访问。
二、避免使用错误的方式退出,如 Exception 能够控制方法退出,可是 Exception 要保留 stacktrace 消耗性能;除非必要,不要使用 instanceOf 作条件判断,尽可能使用比的方式作判断;使用 java 中效率高的类,例如 ArrayList 比 Vector 性能好。
使用缓存好比 redis ,在应用启动期间先把用户数据封装为 json 格式的字符串,以 hash(哈希)类型加载到 redis 内存中。访问的时候就能够直接从 redis 中获取到用户信息,redis的性能不用质疑是很是好用的,这里就不在多说了。这样就大大减小了访问数据库的频次,从而提高系统性能。
生成静态页面我想你们应该不陌生,咱们见过不少网站在请求的时候页面地址后缀已经改变了,如:https://www.cnblogs.com/xxx/p/8026575.html,该页面实际上是一个服务器请求地址,再转换成 html 以后,访问速度将提高,由于静态页面将不带有服务器组件。请看如下介绍:
一、什么是生成静态页面
简单的说,若是咱们访问一个链接,服务器对应的模块会处理这个请求,转到对应的 jsp 页面,最后生成咱们想要看到的数据。这其中的缺点是显而易见的,由于每次请求服务器都会处理,若是有太多高并发处理,那么就会加剧服务器的压力,弄很差就把服务器 down 掉了。那么如何去避免呢,若是咱们吧 test.do 请求后的结果保存成一个 html 文件,每次用户去访问,会自动生成 test.html 而后显示给用户。
二、下面简单介绍一下页面静态化的知识点:
一、基础:URL Rewirte
URL 重写:简单的说:输入网址,但实际上访问的是:abc.com/test.action 那么我就能够说 URL重写了。这项技术应用普遍,有许多开源工具能够实现这个功能。
二、基础:Servlet web.xml
若是你还不知道 web.xml 中一个请求和 servlet 是如何匹配到一块儿的,那么请搜索一下 servlet 文档。这可不是乱说呀,有不少人认为 /zxy/*.do 这样的匹配方式能有效。
三、基本方案介绍:
请求 index -> URL Rewriter -> 请求 index.action -> 处理 .action 的 servlet -> 若是静态页面存在直接返回,不然请求 index.do 到 Struts servlet -> 生成静态页面返回。
其中,对于 URL 重写的部分,可使用开源的工具来实现,若是 URL 不是特别复杂,能够考虑在 servlet 中实现,那么就是下面这个样子:
总结:其实在开发中咱们不多考虑这种问题,直接都是现将功能实现,当一个程序员干了 1 - 2 年的时候,就会感受光实现功能不是最主要的,安全性能,可靠性,可用性,扩展性等才是一个开发人员最该关心的。
今天所说的高并发:咱们的解决思路是:一、采用分布式应用设计;二、分布式缓存数据库;三、代码优化;
再来一个 java 高并发的例子:
具体状况是:经过 java 和数据库本身实现序列自增:
id_table 表结构,主要字段:
1 id_name varchar2(16); 2 id_val number(16,0); 3 id_prefix varchar2(4);
java 代码大体以下:
1 //操做DB 2 public synchronized String nextStringValue(String id){ 3 SqlSession sqlSess = SqlSessionUtil.getSqlSession(); 4 sqlSess.update("update id_table set id_val = id_val + 1 where id_name="+id); 5 Map map = sqlSess.getOne("select id_name, id_prefix, id_val from id_table where id_name="+ id); 6 BigDecimal val = (BigDecimal) map.get("id_val"); 7 // id_val是具体数字,rePack主要是统一返回固定长度的字符串;如:Y0000001, F0000001, T0000001等 8 String idValue = rePack(val, map); 9 return idValue; 10 } 11 12 //公共方法 13 public class IdHelpTool{ 14 public static String getNextStringValue(String idName){ 15 return getXX().nextStringValue(idName); 16 } 17 }
具体使用时,都是经过相似这种方式,IdHelpTool.getNextStringValue("PAY_LOG"); 来调用。
问题:
一、当出现并发时候,有时会获取重复的ID。
二、因为服务器作了一些相关设置,有时候调用这个方法还会致使超时。
解决思路一:
一、出现重复ID,是应为脏读了,并发的时候不加 synchronized 会出现问题
二、可是加了synchronized ,会致使性能急剧降低,自己 java 就是多线程的,你把它单线程使用,不是明智的选择。还有若是分布式部署的时候,加了 synchronized 也没法控制并发。
三、调用这个方法,出现超时,说明你并发已经超过数据库的处理能力,数据库无限等待致使超时。
基于以上分析,建议采用线程池的方案,数据库 update 不是一次加 1 ,而是一次加几百甚至上千,而后取到这些序号,放在线程池里慢慢分配,能应付任意大的并发,同时保证数据库没有任何压力。
若是数据库可使用非关系型数据库,建议使用 redis incy 来实现。具体请参考 redis 文档。