幂等性就是指:一个幂等操作任其执行屡次所产生的影响均与一次执行的影响相同。
用数学的概念表达是这样的: f(f(x)) = f(x).
就像 nx1 = n 一样, x1 就是一个幂等操作。无论是乘以多少次结果都一样。
幂等性问题经常会是由网络问题引起的,还有重复操作引起的。
示例代码:
public void like(Article article,User user) { //检查能否点过赞 if (checkIsLike(article,user)) { //点过赞了 throw new ApiException(CodeEnums.SYSTEM_ERR);}else { //保存点赞 saveLike(article,user);}}</pre>
看上去如同没有什么问题,保存点赞之前已经检查过能否点赞了,理论上同一个人不会对同一篇文章重复点赞。但实际不是这样的。由于网络请求不是排队进来的,而是一窝蜂涌进来的。
某些时候,客户网络不好,可能很短的时间内点击了屡次,因为网络传输问题,这些请求可能会同时来到我们的服务器。
这样子,就造成了一个客户同时对一篇文章进行了屡次点赞操作。
这就是典型的幂等性问题, 操作了一次和操作了两次结果不一样,由于你多点了一次赞,按照幂等性准则 不论你点击了多少次结果都一样,只点了一次赞。
很多场景都是这样造成的,比方客户重复下单,重复评论,重复提交表单等。
那怎样处理呢?
假设网络的请求是排队进来的就不会出现这个问题了。
于是我们可以改成这样:
public synchronized void like(Article article,User user) { //检查能否点过赞if (checkIsLike(article,user)) { //点过赞了throw new ApiException(CodeEnums.SYSTEM_ERR);}else { //保存点赞saveLike(article,user);}}</pre>
synchronized 同步锁 这样我们的请求就会乖乖的排队进来了。
PS :这样做是效率比较低的做法,不建议这么做,只是举例子,synchronized 也不适合分布式集群场景。
我们系统经常需要和第三方系统打交道,比方微信充值,支付宝充值什么的,微信和支付宝常常会以回调你的接口通知你支付结果。为了保证你能收到回调,往往可能会回调屡次。
有时候我们也为了保证数据的精确性会有个定时器去查询支付结果未知的流水,并执行响应的解决。
假如定时器的轮训和回调恰好是在同时进行,这可能又出BUG了,又进行了两次重复操作。
那么问题来了:
假设我是一个充值操作, 回调回来的时候 ,会做业务解决,成功了给客户账户加钱。这是后就要保证幂等性了, 假设微信同一笔交易给你回调了两次,假如你给客户充值了两次,这显然不正当(我是老板一定扣你工资),所以要保证 不论微信回调你多少次 ,同一笔交易你只能给客户充一次钱。这就幂等性。
Redis 分布式锁:
/*** setNx** @param key* @param value* @return*/public Boolean setNx(String key,Object value) { return redisTemplate.opsForValue().setIfAbsent(key,value);}/*** @param key 锁* @param waitTime 等待时间 毫秒* @param expireTime 超时时间 毫秒* @return*/public Boolean lock(String key,Long waitTime,Long expireTime) { String vlaue = UUIDUtil.mongoObjectId(); Boolean flag = setNx(key,vlaue); //尝试获取锁 成功返回if (flag) { redisTemplate.expire(key,expireTime,TimeUnit.MILLISECONDS); return flag;}else { //失败//现在时间long newTime = System.currentTimeMillis(); //等待过期时间long loseTime = newTime + waitTime; //不断尝试获取锁成功返回while (System.currentTimeMillis() < loseTime) { Boolean testFlag = setNx(key,vlaue); if (testFlag) { redisTemplate.expire(key,expireTime,TimeUnit.MILLISECONDS); return testFlag;}//休眠100毫秒try { Thread.sleep(100);}catch (InterruptedException e) { e.printStackTrace();}}}return false;}/*** @param key* @return*/public Boolean lock(String key) { return lock(key,1000L,60 * 1000L);}/*** @param key*/public void unLock(String key) { remove(key);}</pre>
利用Redis 分布式锁 我们的代码可以改成这样:
public void like(Article article,User user) { String key = "key:like" + article.getId() + ":" + user.getUserId(); // 等待锁的时间 0 , 过期时间 一分钟防止死锁boolean flag = redisService.lock(key,0,60 * 1000L); if(!flag) { //获取锁失败 说明前面的请求已经获取了锁throw new ApiException(CodeEnums.SYSTEM_ERR);}//检查能否点过赞if (checkIsLike(article,user)) { //点过赞了throw new ApiException(CodeEnums.SYSTEM_ERR);}else { //保存点赞saveLike(article,user);}//删除锁redisService.unLock(key);}</pre>
key 的设计也很讲究:
数据不冲突的两个业务场景,key不能冲突,不同人的key也不一样,不同的文章Key也不一样。
根据场景业务设定。
一个准则: 尽可能的缩小key的范围。 这样才能加强我们的并发。
首先我们先获取锁,获取锁成功 执行完操作,保存数据 ,删除锁。获取不到锁返回失败。设置过期时间是为了防止‘死锁’,比方机器获取到了 锁,没有设置过期时间,但是他死机了,没有删除释放锁。
本文的重点是你有没有收获与成长,其他的都不重要,希望读者们能谨记这一点。同时我经过多年的收藏目前也算收集到了一套完整的学习资料,包括但不限于:分布式架构、高可扩展、高性能、高并发、Jvm性能调优、Spring,MyBatis,Nginx源码分析,Redis,ActiveMQ、、Mycat、Netty、Kafka、Mysql、Zookeeper、Tomcat、Docker、Dubbo、Nginx等多个知识点高级进阶干货,希望对想成为架构师的朋友有肯定的参考和帮助
需要更详细思维导图和以下资料的可以加一下技术交流分享群:“708 701 457”免费获取