您的位置:首页 > 其它

解决web环境中并发问题3种可选方案

2012-09-25 13:30 429 查看
造成并发问题的根源:多个session更改同一条数据,造成数据的不一致性;例如A,B同时查询出1000元,A从账户中取走800元,更新;此时由于B查询出的是1000,他也从账户中取走800,更新,最后账户里面得到B更新的数据,200,账户余额错误!

1.在程序中利用线程同步机制解决并发:相当于让线程排队进入一段事务,保证数据的完整性;

private static ReentrantLock lock = new ReentrantLock();//可重入锁,必须静态,全局的

//此方法必须考虑解决并发问题;很经典
public String grantOrRevoke() throws Exception{
/**
* 在程序中解决此处权限分配并发的方案:(方案一)
* 	1.在Action里面添加锁,排队进入addOrUpdateAcl方法,保证一个事务结束,不会出现并发的问题;需要注意的问题是ReentrantLock必须是类变量,
* 不能是实例变量,因为Action是prototype类型的
* 	2.在addOrUpdateAcl方法添加synchronized关键字,将其声明为线程安全的,这这个事务也能保证完整性;此处需要注意的是:不能在addOrUpdateAcl
* 方法里面添加锁,如果在里面添加锁,则不能保证一个事务完整结束
*/
/**
* 感叹一下:如果在只有一台服务器的情况下,不需要考虑多台web服务器同时对数据库并发,这个解决方案比较简单,完全不需要对数据库进行额外的事务控制,不需要添加排他锁,共享锁之类的【hibernate中类似于悲观锁,乐观锁】;测试并发的时候很完美
*/
//1.可重入锁
lock.lock();//保证每次只有一个线程进入,这样就保证了事务的完整性;很安全;此时数据库可以不添加锁的机制
try{
serviceFactory.createAcl().addOrUpdateAcl(acl.getRole().getId(),
acl.getModule().getId(), permission, permit);
}finally{
lock.unlock();
}

return null;
}


同样的道理,也可以使用synchronized代码块,例如:

private static Object syn_block_lock = new Object();

public String grantOrRevoke() throws Exception{
synchronized(syn_block_lock){
serviceFactory.createAcl().addOrUpdateAcl(acl.getRole().getId(),
acl.getModule().getId(), permission, permit);
}
return null;
}


或者,在addOrUpdateAcl方法上添加synchronized关键字,原理都是一样的;但是在addOrUpdateAcl方法中不能添加使用代码块,因为在这个程序中,事务是由spring管理的,事务的边界在addOrUpdateAcl方法上,如果在这个方法中添加代码块,并不能保证这个事务完整的结束;

此种方案优势很明显,解决方案相当简单,依靠java自身语言机制,保证一个事务完整的结束;但缺点同样很致命:1.只部署在单台服务器上,如果是多台服务器分布式的话,根本没有办法用程序的方式控制事务;2.排队等待时间比较长,排队本身就限制了并发的效率问题,如果一个事务跨度比较大,严重影响并发效率;

2.从数据库的角度控制并发:

a).使用mysql的排他锁,select * for update;对应hibernate中的悲观锁,但此时必须借助异常处理,使用悲观所,很容易造成数据库端死锁,让程序多尝试几次;

public static final int ERROR_TRY_COUNT = 5;//出错尝试次数

//此方法必须考虑解决并发问题;很经典
public String grantOrRevoke() throws Exception{
log.debug("ajax invoked grantOrRevoke ...在数据库端使用可排它锁(对应hibernate悲观锁)方案2解决并发问题(数据库方案)_2");
/**
* 在数据库端控制并发解决方案:
* 1.使用数据库的排他锁,select * for update,这个语句在查询时就对数据进行添加锁,其他事务必须等这个事务结束了才能访问数据;对应hibernate的悲观锁;同时增加role_id,module_id共同约束条件;使用排他锁很容易造成死锁,所以应该
* 对错误进行处理,增加尝试次数; 由于在addOrUpdateAcl的方法中如果出错,则根据spring的事务管理机制,这个事务会回滚,所以这个事务回滚了;然后再次进行尝试,直到成功;超过三次尝试后,权限分配失败
* 2.使用乐观锁解决,配合服务器程序和数据库的记录时间戳,每次更新时检查当前时间戳是否和数据库的时间戳相同,如果不相同,则重新查询最新记录,再次更新
*/
int errorCount = 0;//出错三次后,不再尝试
//必须加锁,保证一条记录对应四个权限
//1.使用hibernate悲观锁解决;查询的时候使用加锁查询;select * for update
do{
try{
serviceFactory.createAcl().addOrUpdateAcl(acl.getRole().getId(),
acl.getModule().getId(), permission, permit);
break;//执行成功,则跳出
}catch(Exception e){
log.debug("添加或者修改失败,再次尝试:errorCount=" + errorCount);
errorCount ++;
Thread.currentThread().sleep(1);//休息1毫秒,如果不添加这个,失败的次数多了
}
}while(errorCount < ERROR_TRY_COUNT);//继续尝试更新
log.error("分配失败!");
return null;
}


在加载权限时,需要添加hibernate UPGREADE悲观锁机制加载:

@Override
public Acl findByRoleIdAndModuleId(int roleId, int moduleId, boolean addLock) {
//这个地方必须防止并发,必须使用锁机制
Session session = sessionFactory.getCurrentSession();
Query q = session.createQuery("from " + entityClass.getSimpleName() + " o where o.role.id =:roleId and o.module.id =:moduleId");
q.setInteger("roleId", roleId);
q.setInteger("moduleId", moduleId);
if(addLock){
q.setLockMode("o", LockMode.PESSIMISTIC_WRITE); //hibernate4替换了hibernate3的 LockMode.UPGRADE
}
return (Acl) q.uniqueResult();
}


这个方案优势很明显了,多台服务器并发不存在问题了,因为数据库端使用了排他锁(mysql);但缺点是目前对数据库的性能有一定的影响;

b) 使用乐观锁:此时需要在权限实体上添加一个字段,version,这个version可为timestamp,int等类型;同时需要在配置文件中声明;

//Acl实体:

package com.train.admin.acl.bean;

import java.io.Serializable;
import java.sql.Timestamp;
import java.util.Date;

import com.train.admin.module.bean.Module;
import com.train.admin.role.bean.Role;

public class Acl implements Serializable {

public static final int PERMISSION_C = 0;//添加
public static final int PERMISSION_R = 1;//查询
public static final int PERMISSION_U = 2;//更新
public static final int PERMISSION_D = 3;//删除

private int id;
private Role role;
private Module module;
private int aclState;
private Timestamp version = new Timestamp((new Date()).getTime());

/**
* @param permission 权限标识符, CRUD
* @param permit 是否允许;true允许,false不允许
*/
public void setPermission(int permission, boolean permit){
int temp = 1;
temp = temp << permission;
if(permit){//授予权限
aclState = aclState | temp;//或
}else{//去掉权限
aclState = aclState ^ temp;//异或
}
}

/**
* @param permission 权限标识符,CRUD
* @return 是否拥有对应权限
*/
public boolean hasPermission(int permission){
int temp = 1;
aclState = aclState >> permission;
temp = aclState & temp;
return temp == 1 ? true : false;
}//省略get,set方法}


Acl.cfg.xml配置文件:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<!--
Mapping file autogenerated by MyEclipse Persistence Tools
-->
<hibernate-mapping>
<class name="com.train.admin.acl.bean.Acl" table="t_acl" optimistic-lock="version" catalog="train_ticket">
<id name="id" type="java.lang.Integer">
<column name="id" />
<generator class="native"></generator>
</id>
<timestamp name="version" column="version"></timestamp><!--必须在ID属性后面-->
<many-to-one name="role" class="com.train.admin.role.bean.Role" fetch="select">
<column name="role_id" not-null="true" />
</many-to-one>
<many-to-one name="module" class="com.train.admin.module.bean.Module" fetch="select">
<column name="module_id" not-null="true" />
</many-to-one>
<property name="aclState" type="java.lang.Integer">
<column name="acl_state" not-null="true" />
</property>
</class>
</hibernate-mapping>
处理代码和悲观所基本不变,添加错误处理机制;

public static final int ERROR_TRY_COUNT = 5;//出错尝试次数
//此方法必须考虑解决并发问题;很经典
public String grantOrRevoke() throws Exception{
log.debug("ajax invoked grantOrRevoke ...在数据库端使用可排它锁(对应hibernate悲观锁)方案2解决并发问题(数据库方案)_2");
/**
* 在数据库端控制并发解决方案:
* 1.使用数据库的排他锁,select * for update,这个语句在查询时就对数据进行添加锁,其他事务必须等这个事务结束了才能访问数据;对应hibernate的悲观锁;同时增加role_id,module_id共同约束条件;使用排他锁很容易造成死锁,所以应该
* 对错误进行处理,增加尝试次数; 由于在addOrUpdateAcl的方法中如果出错,则根据spring的事务管理机制,这个事务会回滚,所以这个事务回滚了;然后再次进行尝试,直到成功;超过三次尝试后,权限分配失败
* 2.使用乐观锁解决,配合服务器程序和数据库的记录时间戳,每次更新时检查当前时间戳是否和数据库的时间戳相同,如果不相同,则重新查询最新记录,再次更新
*/
int errorCount = 0;//出错三次后,不再尝试
//必须加锁,保证一条记录对应四个权限
//2.使用hibernate乐观锁实现,需要添加一个字段,version,用来标识hibernate更新时对比的时间戳
do{
try{
serviceFactory.createAcl().addOrUpdateAcl(acl.getRole().getId(),
acl.getModule().getId(), permission, permit);
break;//执行成功,则跳出
}catch(Exception e){
log.debug("添加或者修改失败,再次尝试:errorCount=" + errorCount);
errorCount ++;
Thread.currentThread().sleep(5);//休息5毫秒,如果不添加这个,失败的次数多了
}
}while(errorCount < ERROR_TRY_COUNT);//继续尝试更新
return null;
}


这个方案应该是比较满意的方案了,数据库端在更新时才添加锁,不会像悲观锁那样在查询的时候就加锁了,缩短了事务跨度,提高了并发,hibernate仅在更新时才对比数据时间戳,同时能在多台服务器上部署;
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: