幂等性概念 定义 幂等函数 (idempotent) 在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。
代码实例
Person { private int weight; private int age; //是幂等函数 public void setAge(int v){ this.age = v; } //不是幂等函数 public void increaseAge(){ this.age++; } //是幂等函数 public void setWeight(int v){ this.weight=v+10; } }
幂等性是系统的接口对外一种承诺(而不是实现), 承诺只要调用接口成功, 外部多次调用对系统的影响是一致的。声明为幂等的接口会认为外部调用失败是常态, 并且失败之后必然会有重试。
restful方法幂等性 GET:天然幂等 DELETE:天然幂等 PUT:天然幂等 POST:天然非幂等
幂等性实现方式 查询操作天然幂等
删除操作天然幂等
数据库唯一索引,防止新增脏数据
数据库悲观锁:
select * from table_xxx where id='xxx' for update; 数据库乐观锁
update table_xxx set name=#name#,version=version+1 where version=#version# or
update table_xxx set avai_amount=avai_amount-#subAmount# where avai_amount-#subAmount# >= 0 token机制,防止页面重复提交
数据提交前要向服务的申请token,token放到redis或jvm内存,token有效时间
提交后后台校验token,同时删除token,生成新的token返回
分布式锁
引入分布式锁,通过第三方的系统(redis或zookeeper),在业务系统插入数据或者更新数据,获取分布式锁,然后做操作,之后释放锁,这样其实是把多线程并发的锁的思路,引入多多个系统,也就是分布式系统中得解决思路。
调用方提供唯一标示
如银联提供的付款接口:需要接入商户提交付款请求时附带:source来源,seq序列号
分布式锁 分布式锁特点 互斥性。在任意时刻,只有一个客户端能持有锁。
不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
加锁 public boolean tryGetDistributedLock(String lockKey, String requestId, int expireTime) { String result = getJedis().set(getLockKey(lockKey), requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); return LOCK_SUCCESS.equalsIgnoreCase(result); } 说明:
第一个为key,我们使用key来当锁,因为key是唯一的;
第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成;
第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定;
第五个为time,与第四个参数相呼应,代表key的过期时间。
错误代码实例:
public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) { Long result = jedis.setnx(lockKey, requestId); if (result == 1) { jedis.expire(lockKey, expireTime); } } 解锁 public boolean releaseDistributedLock(String lockKey, String requestId) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = getJedis().eval(script, Collections.singletonList(getLockKey(lockKey)), Collections.singletonList(requestId)); return RELEASE_SUCCESS.equals(result); } 错误代码实例:
public static void wrongReleaseLock1(Jedis jedis, String lockKey) { jedis.del(lockKey); } public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) { // 判断加锁与解锁是不是同一个客户端 if (requestId.equals(jedis.get(lockKey))) { // 若在此时,这把锁突然不是这个客户端的,则会误解锁 jedis.del(lockKey); } }
框架用法 简单用法 添加依赖:
compile('cn.magfin:idempotent-spring-boot-starter:1.0.0-SNAPSHOT') 添加配置:
spring: idempotent: enable: true default-expire-time: 5 添加注解:
@PostMapping("/person") @Idempotent(value = "post-idempotent", express = "#p.name + #p.age") public Person test(@RequestBody Person p) { p.setAge(p.getAge() + 1); return p; } 注解的定义:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Idempotent { /** * 幂等key的分组值,例如:sale-check * * @return */ String value();
/**
* 参数的表达式,用来确定key值,例如:"#req.saleInfo.channelCode+'-'+#req.seqId"
*
* @return
*/
String express();
/**
* 失效时间,单位为秒
*
* @return
*/
long expireTime() default 0;
复制代码
}
关键点 value和express执行结果是幂等性校验关键业务标志,因此
express的结果不能有随机变化的内容,否则框架任务每次都是一个新的请求,从而幂等性验证失效
express的执行结果要反应每次业务动作的唯一标示,否则会把不相干的多次请求误认为是一次请求,从而影响真正的业务意图
expireTime设置根据实际请求设置一个大于业务代码执行实际执行时间的值
参考资料
[redis官网实现方式] redis.io/topics/dist…
[框架源码地址] git@192.168.1.8:transformer/utils.git