幂等性:同一个接口,相同的参数,执行多次的结果应当是一样的。
一般来说 get 请求是不用管幂等性的。因为 get 请求是查询,查一次跟查 10 次都不影响数据库的数据。
但如果是 post 这种请求,用户如果手快,点了两下,不就添加两条记录进来了吗。
虽然可以靠前端解决这个问题,但是跟权限一样,前端解决是为了提高用户体验,后端解决才是真的保证数据的完整性。
但是这不冲突,前后端都要一起做。
那怎么做呢?
1、先定义一个注解,作为一个标记,将来,凡是加了注解的接口,都进行幂等性的检查,没加该注解的接口,就不用检查。
2、给需要进行幂等性处理的接口生成一个令牌。
3、每次请求的时候,都校验该令牌(看 redis 中是否有该令牌):
(1)有:如果方法携带了令牌,且 reids 中也有的时候,说明这是一个合法请求,请求通过,同时删除 redis 中的令牌。
(2)无:如果方法携带了令牌,且 redis 中没有的时候,说明这个请求可能已经执行过了,请求中止。
需要有个地方取拦截令牌并检查,所以需要有拦截器;这里有两种方式创建这个拦截器:
如果使用第一种方式,则还需要在 config 注册拦截器,看 g 小节。
截图后面还有两个方法,不用动,默认就行。
如果使用的是第一种拦截器,这里就需要注册(指的是 @Configuration 这个注解); 如果用的是第二种(aop),则这里就不需要开启(只需要注释掉 @Configuration 这个注解即可)。
后端接口是写好了,但是前端怎么使用呢?
一般会用在比较显式的地方,比如载入网页,比如刷新网页的时候就已经获得了这个 token,然后在使用 post 请求等等之类的地方的时候就使用掉这个 token。
下面的代码都有用到 博客 Redis(一)中的工具类!
下面会提出个问题,然后一步步深入;看下面的文字和截图一步步看下去即可。
这里一点一点展示不完整的代码,主要用作从 0 开始学习使用:
但是上面的代码看似没有问题,其实还是有问题。
上图中第一个角度:最核心的锁不要锁大片的代码,就跟 mysql 一样不要表锁,要行锁。这样并发性能才能上来。类似于不要锁方法,应该锁最关键的代码就 ok 了。
上图中的第二个角度,代码类似于这样(这里仅是举例子):
但是上面的代码还是有问题,因为要先获取,然后又要比较,比较完再去释放,很明显不是原子操作,在这过程中依然是有可能因为各种各样的事故导致锁没有被释放。所以是希望这部分操作具有原子性;或者同时成功同时失败。这时就需要引入 lua 脚本。
if redis.call("get",KEYS[1])==ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
接下来,可以给 Lua 脚本求一个 SHA1 和,命令如下:
redis-cli -a (这里填密码) -x script load < lua/releasewherevalueequal.lua
结果展示:
然后就在 java 代码里面通过这个字符串去调用 lua 脚本。
通过 SHA1 校验文件,一样的文件校验的结果是相同的,只要里面有一丁点不一样,那么结果都会不同,所以根据这个性质,可以用来校验文件。
也可以通过如下方式生成校验和(不推荐):
SCRIPT LOAD 'if redis.call("get",KEYS[1])==ARGV[1] then return
redis.call("del",KEYS[1]) else return 0 end'
"79b954ef3863314d4ff2324b4a520053b6dcc018"
接下来,在 Java 端调用这个脚本。
注意,如果要调用这里的代码,记得要把 Redis 博客一的工具类也带上。
public class JedisDemo6 {
public static void main(String[] args) {
new Redis().execute(jedis -> {
String value = UUID.randomUUID().toString();
String k1 = "k1";
String set = jedis.set(k1, value, new SetParams().nx().ex(5L));
if (set != null && "ok".equals(set)){
// 成功拿到锁
// do something
// 如果 value 等于 k1 的值,说明现在的锁是我自己当时上的锁
// if (value.equals(jedis.get("k1"))){
// jedis.del("k1");
// }
// 通过原子操作释放锁
jedis.evalsha("b8059ba43af6ffe8bed3db65bac35d452f8115d8",Arrays.asList(k1), Arrays.asList(value));
}else {
// 没拿到锁。暂停操作。
}
});
}
}
上面的方式还是有点麻烦,不过如果是自己写代码就是上面的方案。
为什么会这么麻烦呢?因为 jedis 不支持直接去调用分布式锁,里面没有相关的 api。
虽然 jedis 不支持,但是有个东西是支持的,那就是 Redisson。
相对于 Jedis 这种原生态的应用,Redisson 对 Redis 请求做了较多的封装,对于锁,也提供了对应的方法可以直接使用:
首先依赖导入:
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.16.8version>
dependency>
接着 java 代码:
Config config = new Config();
//配置 Redis 基本连接信息
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("123");
//获取一个 RedissonClient 对象
RedissonClient redisson = Redisson.create(config);
//获取一个锁对象实例
RLock lock = redisson.getLock("lock");
try {
//获取锁
boolean b = lock.tryLock(500, 1000, TimeUnit.MILLISECONDS);
if (b) {
//获取到锁了,开始写业务
RBucket<Object> bucket = redisson.getBucket("javaboy");
bucket.set("www.javaboy.org");
Object o = bucket.get();
System.out.println(o);
} else {
System.out.println("没拿到锁");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放锁
lock.unlock();
}
在这段代码中,核心的就是 lock.tryLock(500, 1000, TimeUnit.MILLISECONDS); ,第一个参数是尝试加锁的等待时间为 500 毫秒,第二个参数表示锁的超时时间为 1000 毫秒,也就是这个锁在 1000毫秒后会自动失效。
我们可以发现,这和我们在方案一里边配置的参数是一样的,其实思路是不变的,Redisson 只不过是将我们写的和锁相关的方法封装起来了而已。
如果是传统的企业级后台,一般流量是不会很大的。如果是互联网应用,那基本会涉及到高并发,大数据这种;那么这种时候,就需要限流了。
比如做秒杀,搞活动,那么那天的流量就会非常大,如果所有流量一起涌过来,那么肯定一下处理不了这么多。
比如秒杀,100件商品,涌进来 1000 个请求,最简单的想法就是挨个进来取走商品,等到商品没有了再返回。其实没必要用这种方式。我们可以就让 100 个请求通过就行,剩下的 900 个就直接告诉它们秒杀失败。
因为是很常见的现象,所以限流有很多工具可用,包括后面会讲到的 Spring Cloud 里面就有限流工具。
redis 做限流可以自己写,也可以使用插件来限流。
Pipeline(管道)本质上是由客户端提供的一种操作。Pipeline 通过调整指令列表的读写顺序,可以大幅度的节省 IO 时间,提高效率。
为了方便理解下面的代码,这里贴出一幅图,这幅图的含义就是下面代码要执行的思想:
public class RateLimiter {
private Jedis jedis;
public RateLimiter(Jedis jedis) {
this.jedis = jedis;
}
/**
* @param user 调用者的名称(单纯只是为了多人调用限流方法时不冲突)
* @param action 具体的操作
* @param period 这是一个周期,即一个时间窗,单位为秒
* @param maxCount 在一个时间窗内,可以处理的方法/请求数量
* @return 当前请求/方法是否被允许执行
*/
public boolean isAllowed(String user, String action, Integer period, Integer maxCount) {
//根据 user 和 action 生成 key
String key = user + "-" + action;
//Redis 中的管道操作
Pipeline pipelined = jedis.pipelined();
pipelined.multi();
//获取当前时间戳
long nowTime = System.currentTimeMillis();
System.out.println("nowTime = " + nowTime);
//移除当前时间窗之外的 key
pipelined.zremrangeByScore(key, 0, nowTime - period * 1000);
System.out.println("nowTime-period*1000 = " + (nowTime - period * 1000));
//统计当前时间窗内有多少 key
Response<Long> response = pipelined.zcard(key);
//提交管道中的命令
pipelined.exec();
//关闭管道
pipelined.close();
//要求时间窗内的请求数量少于要求的数量
boolean b = response.get() < maxCount;
// 如果小于,那么说明还能放些请求进来;否则就是不能再放请求进来。
if (b) {
//先将当前操作添加进来
jedis.zadd(key, nowTime, String.valueOf(nowTime));
}
return b;
}
}
public static void main(String[] args) {
new Redis().execute(jedis -> {
RateLimiter rateLimiter = new RateLimiter(jedis);
for (int i = 0; i < 10; i++) {
System.out.println("rateLimiter.isAllowed(\"zhangsan\",
\"publish\", 5, 3) = " + rateLimiter.isAllowed("zhangsan", "publish", 5, 1));
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
Redis4.0 开始提供了一个 Redis-Cell 模块,这个模块使用漏斗算法,提供了一个非常好用的限流指令。漏斗算法就像名字一样,是一个漏斗,请求从漏斗的大口进,然后从小口出进入到系统中,这样,无论是多大的访问量,最终进入到系统中的请求,都是固定的。
使用漏斗算法,需要我们首先安装 Redis-Cell 模块(但是一般不建议下载最新的):
http://https//github.com/brandur/redis-cell
安装步骤(可以参考用这里的。如果安装有问题,去下载下面这个版本的压缩包即可。):
wget https://github.com/brandur/redis-cell/releases/download/v0.2.4/redis-cellv0.2.4-x86_64-unknown-linux-gnu.tar.gz
tar -zxvf redis-cell-v0.2.4-x86_64-unknown-linux-gnu.tar.gz
mkdir redis-cell
mv libredis_cell.d ./redis-cell
mv libredis_cell.so ./redis-cell
loadmodule /root/redis-5.0.7/redis-cell/libredis_cell.so
然后,启动 Redis:
redis 启动成功后,如果存在 CL.THROTTLE 命令(控制台输入这个指令,如果后面有提示,说明安装成功),说明 redis-cell 已经安装成功了。
漏斗算法有一个限流的桶。
CL.THROTTLE 命令一共有五个参数:
比如产生 50 个,需要 60 秒,设置好后会均匀固定的 60 秒内放入 50 个令牌。如果请求进来能拿到令牌,说明这个令牌是被允许的,如果没有令牌了,说明请求已经超过了限流的要求了。
执行完成后,返回值也有五个:
第四个参数是 -1,意思是没有失败的意思。
Redis-Cell 没有提供依赖,所以导致 jedis 调用不了 Redis-Cell。redis 也是装了插件才能用得了。
但是可以自己自定义操作。Lettuce 里面提供了方式,可以快速自定义命令。
首先定义一个命令接口:
定义完成后,接下来,直接调用即可:
打印效果:
如果以后遇到没有学过的 redis 的插件,java代码这里又没有提供对应的 jar 包或者依赖之类的,就通过 Lettuce 快速自定义命令。