对于我们开发的网站,如果网站的访问量非常大的话,那么我们就需要考虑相关的并发访问问题了,然而并发问题是令我们大多数程序员头疼的问题,但话又说回来了,既然逃避不掉,那我们就坦然面对吧~今天就让我们深入研究一下常见的并发和同步问题吧。
一、同步和异步的区别和联系
为了更好的理解同步和并发的问题,我们需要先掌握两个重要的概念:同步、异步
同步:可以理解为在执行完一个函数或者方法后,一直等待系统返回值或消息,这时程序是处于阻塞的状态,只有接收到系统的返回值或者消息后,才会继续往下执行。
异步:执行完函数或方法后,不必阻塞性的等待返回值或消息,只需要向系统委托一个异步过程,那么系统接收到返回值或消息时,就会自动触发委托的异步过程,从而完成一个完整的流程。
同步在一定程度上可以看做是单线程,这个线程请求一个方法后,就等待这个方法给他回复,否则不往下执行(死心眼子)。
异步在一定程度上可以看做是多线程(废话,一个线程怎么叫异步),请求一个方法后就不管了,继续执行接下来的其他方法。
同步就一件事、一件事、一件事的做。
异步就是做一件事情,不影响做其他的事情。
例如:吃饭和说话是同步的,只能一件事一件事的来,因为只有一张嘴。吃饭和听音乐是异步的,听音乐不影响我们吃饭。
对于java程序员来说,我们经常见到同步关键字 synchronized,假如这个同步的监视对象是一个类,当一个对象A在访问这个类里面的同步方法,此时另外一个对象B也想访问这个类里面的这个同步方法,就会进入阻塞,只有等待前一个对象执行完该同步方法后当前对象才能够继续执行该方法;这就是同步。
相反,如果方法前没有同步关键字修饰的话,那么不同的对象就可以在同一时间访问同一个方法,这就是异步。
再补充一下,脏数据和不可重复读的概念:
1、脏数据
脏读是指:一个事务正在访问数据,并且对数据进行了修改,而这个修改还有提交到数据库中,这时另外一个事务也访问这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的数据是脏数据(Dirty Data),脏数据所做的操作可能是不正确的。
2、不可重复读
不可重复读是指:在一个事务内,多次读同一条数据。这个事务还没有结束时,另外一个事务也访问了该数据,那么,在第一个事务中两次读数据之间,由于第二个事务的修改,导致第一个事务两次读到数据可能不一样。这样就发生了在同一个事务内,两次读到的数据是不一样的,因此称为不可重复读。
二、如何处理并发和同步
今天讲的如何处理并发和同步问题主要是通过锁机制去解决。我们需要明白锁机制有两个层面:
第一是代码层面,如java中的同步锁,典型的就是同步关键字synchronize(还有Lock等)。 感兴趣的可以参考: http://www.cnblogs.com/xiohao/p/4151408.html
第二是数据库层面上,比较典型的就是悲观锁和乐观锁,这里重点研究一下悲观锁(传统的物理锁)和乐观锁,这两个锁:
- 悲观锁(Pessimistic Locking)
悲观锁正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及外部系统的事务)的修改持保守状态,因此,在整个数据处理过程中,将数据处于锁定状态。
悲观锁的实现,一般是依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则即使在本系统中实现了加锁机制,也无法保证外部系统会修改数据)。
一个典型的依赖数据库悲观锁调用: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的加锁机制有:
1、LockMode.NONE:无锁机制
2、LockMode.WRITE:hibernate 在 insert 和 update 的时候记录会自动获取
3、LockMode.READ:hibernate在读取记录的时候会自动获取
以上这三种锁机制一般由heibernate内部使用,如hibernate为保证 update 过程中对象不会被外界修改,会在 save 方法实现中自动为目标对象加上 WRITE 锁。
4、LockMode.UPGRADE:利用数据的 for update 子句加锁
5、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
2、乐观锁 (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 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 8 <property name="dialect">org.hibernate.dialect.MySQL5InnoDBDialectproperty> 9 10 <property name="hbm2ddl.auto">updateproperty> 11 12 <property name="show_sql">trueproperty> 13 14 <property name="format_sql">falseproperty> 15 <property name="current_session_context_class">threadproperty> 16 17 18 <property name="connection.url">jdbc:mysql:///userproperty> 19 <property name="connection.username">rootproperty> 20 <property name="connection.password">123456property> 21 <property name="connection.driver_class">com.mysql.jdbc.Driverproperty> 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 异常,并指出版本检查失败,当前事务正在准备提交一个过期数据,通过捕捉这个异常,我们可以在乐观锁效验失败时候进行相应的处理。
三、常见并发同步案例分析:
案例一:订票系统案例:某航班只有一张机票,假定有1W个人打开你的网站来订票,问你如何解决并发问题(可扩展到任何高并发网站要考虑到的并发读写问题)
问题1:1W个人来访问,票没订出去前要保证大家都能看到有票,不可能一个人在看到票的时候别人就不能看了。
问题2:并发1W个人同时点击购买,总共只有一张票,到底谁能买到。
首先我们容易想到和并发相关的几个方案:
锁同步更多指的是应用程序层面,多线程进来,只能一个一个的访问,java 中使用的是 synchronized 关键字。锁也有两个层面,一个是 java 中谈到的对象锁,用于线程同步;另外一个层面是数据库锁;如果是分布式的系统,显然只能利用数据库的锁来实现。
假定我们采用了同步机制或者数据库物理机制,如何保证1W个人还能同时看到有票,显然会牺牲性能,在高并发网站中不可取。使用 Hibernate 之后我们提出了另外一个概念:乐观锁、悲观锁(传统的物理锁)。
采用乐观锁即可解决此问题,乐观锁的意思是在不锁定表的情况下,利用业务的控制来解决并发问题,这样既保证数据的并发可读性、又保证保存数据的排他性。保证性能的同时,解决了并发带来脏数据的问题。
Hibernate 中实现乐观锁的方法:
注意:在现有表中增加一个冗余字段,version 版本号,long 类型
原理:1、只有当前版本号 >= 数据库表版本号,才能提交
2、提交成功后版本号 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后,查询性能就很差了,如何保证系统性能?
再比如:中国移动有上亿的用户量,所有用户都存在一张表吗?表如何设计?所以大数据量的系统,必须考虑表拆分(表名不一样,表结构完全一样),通用的几种方式:
1、按业务拆分,比如手机号的表,可以考虑130开头的号码存在一张表,131开头的再存一张表,以此类推。
2、利用 oracle 的表拆分机制做分表
3、如果是交易系统,可以考虑按时间轴拆分,当日数据一个表,历史数据保存到其他表。这里的历史数据的报表和查询不会影响当日交易
当然、表拆分后我们应用程序非做相应的适配,单纯的 or-mapping 就得要改动了,比如业务部分得通过存储过程等。
此外、我们还要考虑缓存,这个缓存不仅仅指的是 Hibernate 提供的一、二级缓存。这个缓存独立于应用,依然是内存的读取,假如我们能减少数据库的频繁访问,那对系统性能肯定大大有利。比如电子商务系统的商品搜索,如果某个关键字的商品经常被搜索,那就可以考虑把这部分的商品列表存放到缓存中(内存中),这样不用每次访问数据库,使系统性能大大增加。
简单的缓存可以理解为一个 hashMap,把经常访问的数据作为一个key,value是第一次从数据库查出的数据,下次访问就可以从map中取值,而不读取数据库。目前可以使用memcached,redis,等。可独立部署成一个缓存服务器。
案例三:抢购秒杀业务的解决方案:
之前我们将高并发解决方案误认为可以用线程或者队列解决,因为高并发的时候有很多用户在访问,导致出现系统数据不正确,丢失数据现象。所以想到的是用队列解决。
其实队列解决的方式也可以处理,比如我们再竞拍商品、转发评论微博、秒杀商品等,同一时间访问量特别大,队列再次起到特别的作用,将所有请求放入队列,以毫秒为计时单位,有序的进行,从而不回初选数据丢失系统不正确的情况。
四、常见的高并发下提高访问效率的方法:
首先了解高并发的瓶颈在哪里
1、可能是服务器网络带宽不够:可以增加网路带宽,DNS域名解析分发多台服务器。
2、可能是web线程连接数不够:负载均衡,前置代理服务器 negix、Apache 等等。
3、可能是数据库连接查询上不去:数据库查询优化、读写分离,分表等等。
以下列出一些在高并发下经常需要处理的内容:
1、尽量使用缓存,包括用户信息缓存,常用信息缓存,多花点内存来做缓存,可以大量减少与数据库的交互,提高性能。
2、用 jprofile 等工具找出性能瓶颈,减少额外的开销。
3、优化数据库库查询语句,减少直接使用 Hibernate 等工具生成查询语句(只针对耗时长的语句做优化)。
4、数据库结构,使用索引,提高查询效率。
5、统计的功能尽量作缓存,或按每天一统计或者定时统计相关报表,避免需要时进行统计的功能。
6、能使用静态页面的地方尽量使用静态页面,减少容器的解析(尽量将动态的内容生成 html 来显示)。
7、解决以上问题之后,尽量使用服务器集群来解决单台服务器的瓶颈。
高并发的解决方案有如下两种:
第一:使用缓存;第二:生成静态页面;
还有就是从最基础的地方优化我们写的代码,减少不必要的资源浪费:
1、不要频繁 new 对象;对于在整个应用中只需要存在一个实例的类,使用单例模式;对于 String 的拼接操作,使用 StringBuffer 或者 StringBuilder;对于 utility 类型的类通过静态方法来访问。
2、避免使用错误的方式退出,如 Exception 可以控制方法退出,但是 Exception 要保留 stacktrace 消耗性能;除非必要,不要使用 instanceOf 做条件判断,尽量使用比的方式做判断;使用 java 中效率高的类,例如 ArrayList 比 Vector 性能好。
使用缓存比如 redis ,在应用启动期间先把用户数据封装为 json 格式的字符串,以 hash(哈希)类型加载到 redis 内存中。访问的时候就可以直接从 redis 中获取到用户信息,redis的性能不用质疑是非常好用的,这里就不在多说了。这样就大大减少了访问数据库的频次,从而提升系统性能。
生成静态页面我想大家应该不陌生,我们见过很多网站在请求的时候页面地址后缀已经改变了,如:https://www.cnblogs.com/xxx/p/8026575.html,该页面其实是一个服务器请求地址,再转换成 html 之后,访问速度将提升,因为静态页面将不带有服务器组件。请看以下介绍:
1、什么是生成静态页面
简单的说,如果我们访问一个连接,服务器对应的模块会处理这个请求,转到对应的 jsp 页面,最后生成我们想要看到的数据。这其中的缺点是显而易见的,因为每次请求服务器都会处理,如果有太多高并发处理,那么就会加重服务器的压力,弄不好就把服务器 down 掉了。那么如何去避免呢,如果我们吧 test.do 请求后的结果保存成一个 html 文件,每次用户去访问,会自动生成 test.html 然后显示给用户。
2、下面简单介绍一下页面静态化的知识点:
1、基础:URL Rewirte
URL 重写:简单的说:输入网址,但实际***问的是:abc.com/test.action 那么我就可以说 URL重写了。这项技术应用广泛,有许多开源工具可以实现这个功能。
2、基础:Servlet web.xml
如果你还不知道 web.xml 中一个请求和 servlet 是如何匹配到一起的,那么请搜索一下 servlet 文档。这可不是乱说呀,有很多人认为 /zxy/*.do 这样的匹配方式能有效。
3、基本方案介绍:
请求 index -> URL Rewriter -> 请求 index.action -> 处理 .action 的 servlet -> 如果静态页面存在直接返回,否则请求 index.do 到 Struts servlet -> 生成静态页面返回。
其中,对于 URL 重写的部分,可以使用开源的工具来实现,如果 URL 不是特别复杂,可以考虑在 servlet 中实现,那么就是下面这个样子:
总结:其实在开发中我们很少考虑这种问题,直接都是现将功能实现,当一个程序员干了 1 - 2 年的时候,就会感觉光实现功能不是最主要的,安全性能,可靠性,可用性,扩展性等才是一个开发人员最该关心的。
今天所说的高并发:我们的解决思路是:1、采用分布式应用设计;2、分布式缓存数据库;3、代码优化;
再来一个 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"); 来调用。
问题:
1、当出现并发时候,有时会获取重复的ID。
2、由于服务器做了一些相关设置,有时候调用这个方法还会导致超时。
解决思路一:
1、出现重复ID,是应为脏读了,并发的时候不加 synchronized 会出现问题
2、但是加了synchronized ,会导致性能急剧下降,本身 java 就是多线程的,你把它单线程使用,不是明智的选择。还有如果分布式部署的时候,加了 synchronized 也无法控制并发。
3、调用这个方法,出现超时,说明你并发已经超过数据库的处理能力,数据库无限等待导致超时。
基于以上分析,建议采用线程池的方案,数据库 update 不是一次加 1 ,而是一次加几百甚至上千,然后取到这些序号,放在线程池里慢慢分配,能应付任意大的并发,同时保证数据库没有任何压力。
如果数据库可以使用非关系型数据库,建议使用 redis incy 来实现。具体请参考 redis 文档。