- 浏览: 133887 次
- 性别:
- 来自: 北京
文章分类
最新评论
使用memcached进行并发控制(转)
- 博客分类:
- java
版权声明:本文为博主原创文章,未经博主允许不得转载。
引子
一个使用缓存进行并发控制的讨论,让我学习到成本与收益间的平衡,以及何为真正的可用性......
防止并发有多种方式,本文只涉及使用缓存memcached控制。
并发场景:
用例:SNS系统中具有高级会员资格的人发起活动。
业务规则:1.一个人同时只能创建一个活动。2.具有高级会员资格。
基本流程如下:
这个流程中存在明显的并发问题,当进程A校验过会员M有资格,并且为创建过活动,但为开始执行创建操作,此时另一个进程B也进行了规则判断,顺利通过,并完成创建操作,此时A继续执行,则会产生两条M的活动。(这个并发场景很简单,很普遍)
最初的解决方案:
计划利用memcached的add操作的原子性来控制并发,具体方式如下:
1.申请锁:在校验是否创建过活动前,执行add操作key为memberId,如果add操作失败,则表示有另外的进程在并发的为该memberId创建活动,返回创建失败。否则表示无并发
2.执行创建活动
3.释放锁:创建活动完成后,执行delete操作,删除该memberId。
问题:
如此实现存在一些问题:
1.memcached中存放的值有有效期,即过期后自动失效,如add过M1后,M1失效,可以在此add成功
2.即使通过配置,可以使memcached永久有效,即不设有效期,memcached有容量限制,当容量不够后会进行自动替换,即有可能add过M1后,M1被其他key值置换掉,则再次add可以成功。
3.此外,memcached是基于内存的,掉电后数据会全部丢失,导致重启后所有memberId均可重新add。
应对问题:
针对上述的几个问题,根本原因是add操作有时效性,过期,被替换,重启,都会是原来的add操作失效。解决该问题有两个方法:
1.采用持久化的缓存解决方法,如TT(Tokyo Tyrant:http://fallabs.com/tokyotyrant/)
2.减轻时效性的影响,使用memcached CAS(check and set)方式。
第一种不必解释了,很简单,原来的所有问题都是时效性惹得祸,时效性源于memcached是基于内存的,那么采用持久话存储的TT可以彻底根治这个问题。
第二种方式需要简单介绍下:
memcached中除了add操作是原子的,还有另外两个操作也是原子的:incr和decr,使用CAS模式即:
1.预先在memcached中设置一个key值,假设为CREATKEY=1
2.每次创建活动时,在规则校验前先get出CREATEKEY=x;
3.进行规则校验
4.执行incr CREATEKEY操作,检验返回值是否为所期望的x+1,如果不是,则说明在此期间有另外的进程执行了incr操作,即存在并发,放弃更新。否则
5.执行创建活动
对比这两种方法,从效果上看可以发现第一种时100%可靠的,不存在问题;第二种,可能存在误判,即本来不存在并发,却被判为并发,如缓存重启,或key值失效后,incr值可能不同于期望值,导致误判。
但是从成本上考虑,TT是持久化的缓存解决方案,完美意味着高成本,我们必须维护持久化数据,而使用memcached的CAS方式,可以以几乎0成本的方式解决时效性问题,尽管存在一点小缺陷,但这种缺陷可以通过简单的重试即可解决。考虑实际的产出比,采用memcached的CAS方式更适合实际情况。
成本与收益间的平衡,做科学与做工程的区别~】
应用场景分析:
如原来MEMCACHED中的KES的内容为A,客户端C1和客户端C2都把A取了出来,C1往准备往其中加B,C2准备往其中加C,这就会造成C1和C2执行后的CACHE KEYS要么是AB要么是AC,而不会出现我们期望的ABC。这种情况,如果不是在集群环境中,而只是单机服务器,可以通过在写CACHE KEYS时增加同步锁,就可以解决问题,可是在集群环境中,同步锁是显然解决不了问题的。
memcached是原子的吗?宏观
所有的被发送到memcached的单个命令是完全原子的。如果您针对同一份数据同时发送了一个set命令和一个get命令,它们不会影响对方。它们将被串行化、先后执行。即使在多线程模式,所有的命令都是原子的;命令序列不是原子的。如果您通过get命令获取了一个item,修改了它,然后想把它set回memcached,我们不保证这个item没有被其他进程(process,未必是操作系统中的进程)操作过。在并发的情况下,您也可能覆写了一个被其他进程set的item。
memcached 1.2.5以及更高版本,提供了gets和cas命令,它们可以解决上面的问题。如果您使用gets命令查询某个key的item,memcached会 给您返回该item当前值的唯一标识。如果您覆写了这个item并想把它写回到memcached中,您可以通过cas命令把那个唯一标识一起发送给 memcached。如果该item存放在memcached中的唯一标识与您提供的一致,您的写操作将会成功。如果另一个进程在这期间也修改了这个 item,那么该item存放在memcached中的唯一标识将会改变,您的写操作就会失败。
微观分析
memcache为了避免一些竞争,加入了一些特殊原子操作:add cas incr decr
"add" means "store this data, but only if the server *doesn't* already;
“cas” is a check and set operation which means “store this data but only if no one else has updated since I last fetched it.” ;
它们的实现原理基于CAS(check and save)模式:
下面是一个全新的CAS模式实现:
1.预先在memcached中设置一个key值,假设为CREATKEY=1
2.每次创建活动时,在规则校验前先get出CREATEKEY=x;
3.进行规则校验
4.执行incr CREATEKEY操作,检验返回值是否为所期望的x+1,如果不是,则说明在此期间有另外的进程执行了incr操作,即存在并发,放弃更新。否则
5.执行创建活动
memcached保存的key value都有一个唯一标识casUnique,在进行incr decr操作时,首先获取casUnique,执行incr,检验返回值是否casUnique+1,如果是,则更新,否则,失败不更新!
尽管这种设计在处理并发时还存在缺陷,但可以通过简单的重试来解决问题!
接口分析:
返回MemcachedItem对象:
public MemcachedItem gets(String key) {
return client.gets(key);
}
public MemcachedItem gets(String key, Integer hashCode) {
return gets(OPCODE_GET, key, hashCode, false);
}
普通的get方法,返回Value对象
public Object get(String key) {
return client.get(key);
}
casUnique:是唯一标识
public boolean cas(String key, Object value, long casUnique) {
return client.cas(key, value, casUnique);
}
public boolean cas(String key, Object value, long casUnique) {
return set(OPCODE_SET, key, value, null, null, casUnique, primitiveAsString);
}
MemcachedItem类结构:
public final class MemcachedItem {
public long casUnique;
public Object value;
}
其它约束:
32位无符号整数
下面一片文章是对memcache add原子性实际应用的使用:
原文出自:http://blog.csdn.net/jiangbo_hit/article/details/6211704
引子
一个使用缓存进行并发控制的讨论,让我学习到成本与收益间的平衡,以及何为真正的可用性......
防止并发有多种方式,本文只涉及使用缓存memcached控制。
并发场景:
用例:SNS系统中具有高级会员资格的人发起活动。
业务规则:1.一个人同时只能创建一个活动。2.具有高级会员资格。
基本流程如下:
这个流程中存在明显的并发问题,当进程A校验过会员M有资格,并且为创建过活动,但为开始执行创建操作,此时另一个进程B也进行了规则判断,顺利通过,并完成创建操作,此时A继续执行,则会产生两条M的活动。(这个并发场景很简单,很普遍)
最初的解决方案:
计划利用memcached的add操作的原子性来控制并发,具体方式如下:
1.申请锁:在校验是否创建过活动前,执行add操作key为memberId,如果add操作失败,则表示有另外的进程在并发的为该memberId创建活动,返回创建失败。否则表示无并发
2.执行创建活动
3.释放锁:创建活动完成后,执行delete操作,删除该memberId。
问题:
如此实现存在一些问题:
1.memcached中存放的值有有效期,即过期后自动失效,如add过M1后,M1失效,可以在此add成功
2.即使通过配置,可以使memcached永久有效,即不设有效期,memcached有容量限制,当容量不够后会进行自动替换,即有可能add过M1后,M1被其他key值置换掉,则再次add可以成功。
3.此外,memcached是基于内存的,掉电后数据会全部丢失,导致重启后所有memberId均可重新add。
应对问题:
针对上述的几个问题,根本原因是add操作有时效性,过期,被替换,重启,都会是原来的add操作失效。解决该问题有两个方法:
1.采用持久化的缓存解决方法,如TT(Tokyo Tyrant:http://fallabs.com/tokyotyrant/)
2.减轻时效性的影响,使用memcached CAS(check and set)方式。
第一种不必解释了,很简单,原来的所有问题都是时效性惹得祸,时效性源于memcached是基于内存的,那么采用持久话存储的TT可以彻底根治这个问题。
第二种方式需要简单介绍下:
memcached中除了add操作是原子的,还有另外两个操作也是原子的:incr和decr,使用CAS模式即:
1.预先在memcached中设置一个key值,假设为CREATKEY=1
2.每次创建活动时,在规则校验前先get出CREATEKEY=x;
3.进行规则校验
4.执行incr CREATEKEY操作,检验返回值是否为所期望的x+1,如果不是,则说明在此期间有另外的进程执行了incr操作,即存在并发,放弃更新。否则
5.执行创建活动
对比这两种方法,从效果上看可以发现第一种时100%可靠的,不存在问题;第二种,可能存在误判,即本来不存在并发,却被判为并发,如缓存重启,或key值失效后,incr值可能不同于期望值,导致误判。
但是从成本上考虑,TT是持久化的缓存解决方案,完美意味着高成本,我们必须维护持久化数据,而使用memcached的CAS方式,可以以几乎0成本的方式解决时效性问题,尽管存在一点小缺陷,但这种缺陷可以通过简单的重试即可解决。考虑实际的产出比,采用memcached的CAS方式更适合实际情况。
成本与收益间的平衡,做科学与做工程的区别~
http://blog.csdn.net/jiangbo_hit/article/details/6211704
http://zhengjunwei2007.blog.163.com/blog/static/3529794220117325112464/
引子
一个使用缓存进行并发控制的讨论,让我学习到成本与收益间的平衡,以及何为真正的可用性......
防止并发有多种方式,本文只涉及使用缓存memcached控制。
并发场景:
用例:SNS系统中具有高级会员资格的人发起活动。
业务规则:1.一个人同时只能创建一个活动。2.具有高级会员资格。
基本流程如下:
这个流程中存在明显的并发问题,当进程A校验过会员M有资格,并且为创建过活动,但为开始执行创建操作,此时另一个进程B也进行了规则判断,顺利通过,并完成创建操作,此时A继续执行,则会产生两条M的活动。(这个并发场景很简单,很普遍)
最初的解决方案:
计划利用memcached的add操作的原子性来控制并发,具体方式如下:
1.申请锁:在校验是否创建过活动前,执行add操作key为memberId,如果add操作失败,则表示有另外的进程在并发的为该memberId创建活动,返回创建失败。否则表示无并发
2.执行创建活动
3.释放锁:创建活动完成后,执行delete操作,删除该memberId。
问题:
如此实现存在一些问题:
1.memcached中存放的值有有效期,即过期后自动失效,如add过M1后,M1失效,可以在此add成功
2.即使通过配置,可以使memcached永久有效,即不设有效期,memcached有容量限制,当容量不够后会进行自动替换,即有可能add过M1后,M1被其他key值置换掉,则再次add可以成功。
3.此外,memcached是基于内存的,掉电后数据会全部丢失,导致重启后所有memberId均可重新add。
应对问题:
针对上述的几个问题,根本原因是add操作有时效性,过期,被替换,重启,都会是原来的add操作失效。解决该问题有两个方法:
1.采用持久化的缓存解决方法,如TT(Tokyo Tyrant:http://fallabs.com/tokyotyrant/)
2.减轻时效性的影响,使用memcached CAS(check and set)方式。
第一种不必解释了,很简单,原来的所有问题都是时效性惹得祸,时效性源于memcached是基于内存的,那么采用持久话存储的TT可以彻底根治这个问题。
第二种方式需要简单介绍下:
memcached中除了add操作是原子的,还有另外两个操作也是原子的:incr和decr,使用CAS模式即:
1.预先在memcached中设置一个key值,假设为CREATKEY=1
2.每次创建活动时,在规则校验前先get出CREATEKEY=x;
3.进行规则校验
4.执行incr CREATEKEY操作,检验返回值是否为所期望的x+1,如果不是,则说明在此期间有另外的进程执行了incr操作,即存在并发,放弃更新。否则
5.执行创建活动
对比这两种方法,从效果上看可以发现第一种时100%可靠的,不存在问题;第二种,可能存在误判,即本来不存在并发,却被判为并发,如缓存重启,或key值失效后,incr值可能不同于期望值,导致误判。
但是从成本上考虑,TT是持久化的缓存解决方案,完美意味着高成本,我们必须维护持久化数据,而使用memcached的CAS方式,可以以几乎0成本的方式解决时效性问题,尽管存在一点小缺陷,但这种缺陷可以通过简单的重试即可解决。考虑实际的产出比,采用memcached的CAS方式更适合实际情况。
成本与收益间的平衡,做科学与做工程的区别~】
应用场景分析:
如原来MEMCACHED中的KES的内容为A,客户端C1和客户端C2都把A取了出来,C1往准备往其中加B,C2准备往其中加C,这就会造成C1和C2执行后的CACHE KEYS要么是AB要么是AC,而不会出现我们期望的ABC。这种情况,如果不是在集群环境中,而只是单机服务器,可以通过在写CACHE KEYS时增加同步锁,就可以解决问题,可是在集群环境中,同步锁是显然解决不了问题的。
memcached是原子的吗?宏观
所有的被发送到memcached的单个命令是完全原子的。如果您针对同一份数据同时发送了一个set命令和一个get命令,它们不会影响对方。它们将被串行化、先后执行。即使在多线程模式,所有的命令都是原子的;命令序列不是原子的。如果您通过get命令获取了一个item,修改了它,然后想把它set回memcached,我们不保证这个item没有被其他进程(process,未必是操作系统中的进程)操作过。在并发的情况下,您也可能覆写了一个被其他进程set的item。
memcached 1.2.5以及更高版本,提供了gets和cas命令,它们可以解决上面的问题。如果您使用gets命令查询某个key的item,memcached会 给您返回该item当前值的唯一标识。如果您覆写了这个item并想把它写回到memcached中,您可以通过cas命令把那个唯一标识一起发送给 memcached。如果该item存放在memcached中的唯一标识与您提供的一致,您的写操作将会成功。如果另一个进程在这期间也修改了这个 item,那么该item存放在memcached中的唯一标识将会改变,您的写操作就会失败。
微观分析
memcache为了避免一些竞争,加入了一些特殊原子操作:add cas incr decr
"add" means "store this data, but only if the server *doesn't* already;
“cas” is a check and set operation which means “store this data but only if no one else has updated since I last fetched it.” ;
它们的实现原理基于CAS(check and save)模式:
下面是一个全新的CAS模式实现:
1.预先在memcached中设置一个key值,假设为CREATKEY=1
2.每次创建活动时,在规则校验前先get出CREATEKEY=x;
3.进行规则校验
4.执行incr CREATEKEY操作,检验返回值是否为所期望的x+1,如果不是,则说明在此期间有另外的进程执行了incr操作,即存在并发,放弃更新。否则
5.执行创建活动
memcached保存的key value都有一个唯一标识casUnique,在进行incr decr操作时,首先获取casUnique,执行incr,检验返回值是否casUnique+1,如果是,则更新,否则,失败不更新!
尽管这种设计在处理并发时还存在缺陷,但可以通过简单的重试来解决问题!
接口分析:
返回MemcachedItem对象:
public MemcachedItem gets(String key) {
return client.gets(key);
}
public MemcachedItem gets(String key, Integer hashCode) {
return gets(OPCODE_GET, key, hashCode, false);
}
普通的get方法,返回Value对象
public Object get(String key) {
return client.get(key);
}
casUnique:是唯一标识
public boolean cas(String key, Object value, long casUnique) {
return client.cas(key, value, casUnique);
}
public boolean cas(String key, Object value, long casUnique) {
return set(OPCODE_SET, key, value, null, null, casUnique, primitiveAsString);
}
MemcachedItem类结构:
public final class MemcachedItem {
public long casUnique;
public Object value;
}
其它约束:
32位无符号整数
下面一片文章是对memcache add原子性实际应用的使用:
原文出自:http://blog.csdn.net/jiangbo_hit/article/details/6211704
引子
一个使用缓存进行并发控制的讨论,让我学习到成本与收益间的平衡,以及何为真正的可用性......
防止并发有多种方式,本文只涉及使用缓存memcached控制。
并发场景:
用例:SNS系统中具有高级会员资格的人发起活动。
业务规则:1.一个人同时只能创建一个活动。2.具有高级会员资格。
基本流程如下:
这个流程中存在明显的并发问题,当进程A校验过会员M有资格,并且为创建过活动,但为开始执行创建操作,此时另一个进程B也进行了规则判断,顺利通过,并完成创建操作,此时A继续执行,则会产生两条M的活动。(这个并发场景很简单,很普遍)
最初的解决方案:
计划利用memcached的add操作的原子性来控制并发,具体方式如下:
1.申请锁:在校验是否创建过活动前,执行add操作key为memberId,如果add操作失败,则表示有另外的进程在并发的为该memberId创建活动,返回创建失败。否则表示无并发
2.执行创建活动
3.释放锁:创建活动完成后,执行delete操作,删除该memberId。
问题:
如此实现存在一些问题:
1.memcached中存放的值有有效期,即过期后自动失效,如add过M1后,M1失效,可以在此add成功
2.即使通过配置,可以使memcached永久有效,即不设有效期,memcached有容量限制,当容量不够后会进行自动替换,即有可能add过M1后,M1被其他key值置换掉,则再次add可以成功。
3.此外,memcached是基于内存的,掉电后数据会全部丢失,导致重启后所有memberId均可重新add。
应对问题:
针对上述的几个问题,根本原因是add操作有时效性,过期,被替换,重启,都会是原来的add操作失效。解决该问题有两个方法:
1.采用持久化的缓存解决方法,如TT(Tokyo Tyrant:http://fallabs.com/tokyotyrant/)
2.减轻时效性的影响,使用memcached CAS(check and set)方式。
第一种不必解释了,很简单,原来的所有问题都是时效性惹得祸,时效性源于memcached是基于内存的,那么采用持久话存储的TT可以彻底根治这个问题。
第二种方式需要简单介绍下:
memcached中除了add操作是原子的,还有另外两个操作也是原子的:incr和decr,使用CAS模式即:
1.预先在memcached中设置一个key值,假设为CREATKEY=1
2.每次创建活动时,在规则校验前先get出CREATEKEY=x;
3.进行规则校验
4.执行incr CREATEKEY操作,检验返回值是否为所期望的x+1,如果不是,则说明在此期间有另外的进程执行了incr操作,即存在并发,放弃更新。否则
5.执行创建活动
对比这两种方法,从效果上看可以发现第一种时100%可靠的,不存在问题;第二种,可能存在误判,即本来不存在并发,却被判为并发,如缓存重启,或key值失效后,incr值可能不同于期望值,导致误判。
但是从成本上考虑,TT是持久化的缓存解决方案,完美意味着高成本,我们必须维护持久化数据,而使用memcached的CAS方式,可以以几乎0成本的方式解决时效性问题,尽管存在一点小缺陷,但这种缺陷可以通过简单的重试即可解决。考虑实际的产出比,采用memcached的CAS方式更适合实际情况。
成本与收益间的平衡,做科学与做工程的区别~
http://blog.csdn.net/jiangbo_hit/article/details/6211704
http://zhengjunwei2007.blog.163.com/blog/static/3529794220117325112464/
发表评论
-
Lock和Synchronized的不同
2014-10-24 15:46 734在 Java 中线程的状态可以分为:新建(New),运行状 ... -
ClassLoader加载流程
2014-04-16 09:59 575[b]当运行一个程序的时候,JVM启动, 运行bootstar ... -
springmvc请求接收参数的几种方法(转载)
2014-04-14 13:37 1479通过@PathVariabl注解获取路径中传递参数 JAVA ... -
JDK里的设计模式
2014-03-10 09:52 573适配器模式: 用来把一个接口转化成另一个接口。 java ... -
include与jsp:include区别
2014-02-28 15:03 548在jsp中include有两种形式,分别是 <%@ i ... -
DBCP的配置参数
2014-02-25 15:51 710tomcatde DHCP的配置 <Resource d ... -
java 时间处理的工具类(转)
2014-01-27 15:13 614import java.text.ParseException ... -
log4j
2014-01-17 13:56 336public Logger m_log = null; ... -
关于java Date和时区的问题
2013-12-18 11:55 809java2平台为我们提供了丰富的日期时间API。如java.u ... -
hessian异常总结
2013-12-12 17:53 1213版本不兼容 com.caucho.hessian.io.Hes ... -
java异常
2013-09-06 12:01 523java.sql.SQLException: Incorrec ... -
汉字转码的java实现
2013-08-05 17:26 971package common; import java.ut ... -
验证邮件的有效性
2013-07-30 14:25 1100public static boolean checkEma ... -
优化Servlet配置
2013-04-09 10:31 612[b] 以往每添加一个Servlet就要在web. ... -
纯hessian的部署(转)
2012-11-06 17:44 798Web 服务是通过在 Web 服务器上部署 Servlet 来 ... -
Ehcache的使用
2012-07-27 15:11 823一、 配置文件ehcache.xml <ehcache& ... -
struts2上传文件活动文件名、类型
2012-05-16 08:45 898public class FileUpload { ... -
struts2乱码
2012-05-14 17:36 0Struts2.1.6 StrutsPrepareAndExe ... -
解决struts2过滤器冲突问题
2012-04-27 13:40 0struts2使用FilterDispatcher进行url过 ... -
cron表达式
2012-03-14 15:02 825所在组的定时任务用的quartz,是我第一个用的,今天有同事用 ...
相关推荐
在高并发环境下,大量的读、写请求涌向数据库,此时磁盘IO将成为瓶颈,从而导致过高的响应延迟,因此缓存应运而生。无论是单机缓存还是分布式缓存都有其适应场景和优缺点,当今存在的缓存产品也是数不胜数,最常见的...
使用乐观并发控制和悲观并发控制,查看是否可以提高性能。 包含Api验证和日志记录中间件,以及基于Swagger的文档 为什么选择goRubu? :dog_face: 想通过构建项目来学习Go和系统设计。 因此,goRubu。 基准测试 ...
并发控制:理解并合理应用锁机制(如乐观锁、悲观锁),防止并发问题。 缓存策略:在合适的地方引入缓存机制(如Redis、Memcached)提高系统性能。 分页与批量处理:对于大数据量的处理,采用分页或流式处理,减轻...
使用应用服务器集群改善网站的并发处理能力 问题: 负载均衡情况下session状态的保持? 解决方案: 基于DNS的负载均衡 反向代理 ngix JK2 数据库的读写分离 问题: 读库与写库的数据同步...
如果将ext-redis用于锁定并配置为使用igbinary进行序列化或使用lzf进行压缩,则还必须安装ext-igbinary和/或ext-lzf 。 安装 作曲家 要通过composer使用此库,请在存储库的根文件夹中运行以下终端命令。 ...
第12章 限制用户并发连接数 第13章 修改或隐藏Nginx的版本号 第14章 配置FLV服务器 第15章 Nginx的访问控制 第16章 提供FTP下载 第17章 Nginx与编码 第18章 网页压缩传输 第19章 控制Nginx如何记录日志 第20...
第12章 限制用户并发连接数 第13章 修改或隐藏Nginx的版本号 第14章 配置FLV服务器 第15章 Nginx的访问控制 第16章 提供FTP下载 第17章 Nginx与编码 第18章 网页压缩传输 第19章 控制Nginx如何记录日志 第20...
第12章 限制用户并发连接数 第13章 修改或隐藏Nginx的版本号 第14章 配置FLV服务器 第15章 Nginx的访问控制 第16章 提供FTP下载 第17章 Nginx与编码 第18章 网页压缩传输 第19章 控制Nginx如何记录日志 第20...
JCaptcha4Struts2 是一个 Struts2的插件,用来增加验证码的支持,使用时只需要用一个 JSP 标签 (<jcaptcha:image label="Type the text "/> ) 即可,直接在 struts.xml 中进行配置,使用强大的 JCaptcha来生成验证码...
它需要对程序进行变更,但由于使用廉价的服务器,可以控制成本。另外,以后只要依葫芦画瓢增加廉价服务器的数量就可以了。 不对大量数据进行处理的话就没有使用的必要吗? NoSQL数据库基本上来说为了“使大量数据...
并发控制 连接控制 延迟连接 粘滞连接 令牌验证 路由规则 配置规则 服务降级 优雅停机 主机绑定 日志适配 访问日志 服务容器 Reference Config缓存 分布式事务13-1-13 U serG uide-zh -D ubbo -A libaba O pen S ...
JCaptcha4Struts2 是一个 Struts2的插件,用来增加验证码的支持,使用时只需要用一个 JSP 标签 (<jcaptcha:image label="Type the text "/> ) 即可,直接在 struts.xml 中进行配置,使用强大的 JCaptcha来生成验证码...
JCaptcha4Struts2 是一个 Struts2的插件,用来增加验证码的支持,使用时只需要用一个 JSP 标签 (<jcaptcha:image label="Type the text "/> ) 即可,直接在 struts.xml 中进行配置,使用强大的 JCaptcha来生成验证码...
JCaptcha4Struts2 是一个 Struts2的插件,用来增加验证码的支持,使用时只需要用一个 JSP 标签 (<jcaptcha:image label="Type the text "/> ) 即可,直接在 struts.xml 中进行配置,使用强大的 JCaptcha来生成验证码...
JCaptcha4Struts2 是一个 Struts2的插件,用来增加验证码的支持,使用时只需要用一个 JSP 标签 (<jcaptcha:image label="Type the text "/> ) 即可,直接在 struts.xml 中进行配置,使用强大的 JCaptcha来生成验证码...
JCaptcha4Struts2 是一个 Struts2的插件,用来增加验证码的支持,使用时只需要用一个 JSP 标签 (<jcaptcha:image label="Type the text "/> ) 即可,直接在 struts.xml 中进行配置,使用强大的 JCaptcha来生成验证码...
JCaptcha4Struts2 是一个 Struts2的插件,用来增加验证码的支持,使用时只需要用一个 JSP 标签 (<jcaptcha:image label="Type the text "/> ) 即可,直接在 struts.xml 中进行配置,使用强大的 JCaptcha来生成验证码...
JCaptcha4Struts2 是一个 Struts2的插件,用来增加验证码的支持,使用时只需要用一个 JSP 标签 (<jcaptcha:image label="Type the text "/> ) 即可,直接在 struts.xml 中进行配置,使用强大的 JCaptcha来生成验证码...
JCaptcha4Struts2 是一个 Struts2的插件,用来增加验证码的支持,使用时只需要用一个 JSP 标签 (<jcaptcha:image label="Type the text "/> ) 即可,直接在 struts.xml 中进行配置,使用强大的 JCaptcha来生成验证码...
JCaptcha4Struts2 是一个 Struts2的插件,用来增加验证码的支持,使用时只需要用一个 JSP 标签 (<jcaptcha:image label="Type the text "/> ) 即可,直接在 struts.xml 中进行配置,使用强大的 JCaptcha来生成验证码...