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

造成并发问题的根源:多个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仅在更新时才对比数据时间戳,同时能在多台服务器上部署;


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值