一、管道 与 Lua 脚本
管道(Pipeline)
客户端一次性发送多个请求,不用等待服务器响应,待所有脚本执行完成后一起响应。客户端用 pipeline 方式打包命令发送,redis必须在处理完所有命令前先缓存起所有命令的处理结果。打包的命令越多,缓存消耗内存也越多。pipeline 中发送的每个命令都会被 server 立即执行,如果执行失败,将会在此后的响应中得到信息,不会影响管道里面后续其他命令的执行。
注意: 管道不是原子的,但 Redis 提供的批量操作命令(例如 MSET)是原子的
Lua脚本
Redis 2.6 版本开始推出,可以使用 Lua 语言编写脚本传到 Redis 中执行。使用 Lua脚本有如下优点:
- 减少内存开销: 多次网络请求(Redis 逻辑)可以通过一个请求完成,可以减少网络往返时延。
- 原子操作: Redis 会将整个 Lua 脚本作为一个原子操作执行,这个期间其他命令无法插入。
- 替代redis的事务功能: Redis 自带事务出错不支持回滚,十分鸡肋,Lua 脚本支持常规事务功能,Redis 官方也推荐使用 Lua 脚本代替 Redis 的事务
#分布式锁使用的一个Lua脚本,不存在即删除
#在 Lua 脚本中,可以使用redis.call()函数来执行Redis命令
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
// SpringBoot 整合RedisTemplate执行Lua脚本
// script是上面的Lua脚本
redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
注意:Redis 是单线程执行脚本的,所以不要在 Redis 中出现耗时的运算或死循环,否则会阻塞 Redis。
还可以使用 EVAL 命令对 Lua 脚本进行求值,EVAL命令的格式如下:
# script是 Lua 脚本
# numkeys 参数用于指定键名参数的个数
# key [key ...] 键名参数,表示在脚本中所用到的哪些Redis键(key)
# arg [arg ...] 不是键名的附加参数,
EVAL script numkeys key [key ...] arg [arg ...]
#使用示例
eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
#输出
1) "key1"
2) "key2"
3) "first"
4) "second"
下面是购买商品减库存示例:
// KEYS[] 对应第一个 List 集合中的数据,ARGV[] 对应第二个集合中的数据
jedis.set("product_stock_10016", "15"); //初始化商品10016的库存
String script = " local count = redis.call('get', KEYS[1]) " +
" local a = tonumber(count) " +
" local b = tonumber(ARGV[1]) " +
" if a >= b then " +
" redis.call('set', KEYS[1], a-b) " +
" return 1 " +
" end " +
" return 0 ";
Object obj = jedis.eval(script, Arrays.asList("product_stock_10016"), Arrays.asList("10"));
System.out.println(obj);
Redis 实现分布式锁
在分布式的情况下,Synchronize 等同步机制无法对不同服务器之间的对象进行加锁,为了解决这一问题,出现了分布式锁。
1.下列代码在分布式系统下失效
@Autowired
StringRedisTemplate redisTemplate;
@PostMapping(/reduce_stock)
public void reduceStock() {
synchronized(this) {
int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if(stock > 0){
int nowStock = stock - 1;
redisTemplate.opsForValue().set("stock",nowStock + "");
System.out.println("减库存成功,剩余库存:" + nowStock + "");
} else {
System.out.println("库存不足:" + nowStock + "");
}
}
}
2.上述代码锁分布式情况下失效,引入分布式锁
@Autowired
StringRedisTemplate redisTemplate;
@PostMapping(/reduce_stock)
public void reduceStock() {
String lockKey = "product_lock_01";
try {
// 占分布式锁,这两条命令非原子操作,使用下面的合体
// Boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockKey,"")
// redisTemplate.expire(lcokKey,30,TimeUnit.SECONDS);
// 占分布式锁,设置30s超时
String uuid = UUID.randomUUID().toString();
Boolean isLock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
if(isLock) {
int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if(stock > 0){
int nowStock = stock - 1;
redisTemplate.opsForValue().set("stock",nowStock + "");
System.out.println("减库存成功,剩余库存:" + nowStock + "");
} else {
System.out.println("库存不足:" + nowStock + "");
}
} else {
System.out.println("获取分布式锁失败...等待重试...");
//加锁失败...自旋重试机制
//休眠一百毫秒
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
reduceStock();
}
} finally {
// 删除锁
redisTemplate.delete(lockKey);
}
}
3.上述代码还是存在问题,锁失效,在低并发的情况下运行存在,假如我们第一个线程由于某些业务逻辑较大需要执行 35 秒,但是我们的自动过期时间是 30 秒,此时第一个线程锁失效后,第二个线程进来,发现可以设置锁成功,此时第一个线程执行完成最后的 finally 逻辑,删除的是第二个线程设置的锁 ,一直循环,分布式锁失效,造成时灵时不灵的错误。
@Autowired
StringRedisTemplate redisTemplate;
@PostMapping(/reduce_stock)
public void reduceStock() {
String lockKey = "product_lock_01";
String uuid = UUID.randomUUID().toString();
try {
// 占分布式锁,这两条命令非原子操作,使用下面的合体
// Boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockKey,"")
// redisTemplate.expire(lcokKey,30,TimeUnit.SECONDS);
// 占分布式锁,设置30s超时
Boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockKey, uuid,300,TimeUnit.SECONDS);
if(isLock) {
int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if(stock > 0){
int nowStock = stock - 1;
redisTemplate.opsForValue().set("stock",nowStock + "");
System.out.println("减库存成功,剩余库存:" + nowStock + "");
} else {
System.out.println("库存不足:" + nowStock + "");
}
} else {
System.out.println("获取分布式锁失败...等待重试...");
//加锁失败...自旋重试机制
//休眠一百毫秒
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
reduceStock();
}
} finally {
// 非原子操作,推荐使用Lua脚本
// if(uuid.equals(redisTemplate.opsForValue().get(lockKey))){
// redisTemplate.delete(lockKey);
// }
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//删除锁
redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(lockKey), uuid);
}
}
4.上述代码还是存在一些问题,但是一般互联网情况下代码实现到这种程度已经差不多了(一般设置的锁失效时间绝对大于业务代码执行时间,如果出现问题,大多是业务代码有问题)。但也存在一些极端的情况,因此可以使用锁续存,在锁过来多久未释放进行一次续存操作。此功能在 Redisson 中有实现。
Redisson
Redisson 是一个在 Redis 的基础上实现的 Java 驻内存数据网格。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中包括(BitSet、 Set、Multimap、 SortedSet、Map、BlockingQueue、List、Queue、,Deque、BlockingDeque、,Semaphore、Lock、AtomicLong、CountDownLatch、Publish / Subscribe、Bloom filter、Remote service、Spring cache、Executor service、Live Object service、Scheduler service)。
Redisson 的使用
第一步:导入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.11.1</version>
</dependency>
第二步:配置Redisson
配置Redisson 官方文档 https://github.com/redisson/redisson/wiki
@Configuration
public class MyRedissonConfig {
/**
* 所有对Redisson的操作都是通过该对象
* @return
* @throws IOException
*/
@Bean(destroyMethod="shutdown")
RedissonClient redisson() throws IOException {
// 1.创建配置
Config config = new Config();
// 单节点模式 根据Config创建出RedissonClient
config.useSingleServer().setAddress("redis://192.168.56.10:6379");
// 集群模式
// config.useClusterServers()
// .addNodeAddress("redis://127.0.0.1:7004", "redis://127.0.0.1:7001");
return Redisson.create(config);
}
}
前面减库存代码优化,使用 Redisson 加解锁,并续存
@Autowired
StringRedisTemplate redisTemplate;
@Autowired
Redisson redisson;
@PostMapping(/reduce_stock)
public void reduceStock() {
String lockKey = "product_lock_01";
RLock resissonLock = redisson.getLock(lockKey);
try {
// 占分布式锁,设置30s超时
Boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockKey, uuid,300,TimeUnit.SECONDS);
// 加锁,底层实现了锁续存
// 获取一把锁,只要锁名字一样就是同一把锁,他会自己去Reddish中创建一个key为my-lock的数据
// 底层原越是使用sexnx和Lua脚本
// 阻塞式等待(自旋),默认加锁30s
// (1) 锁的自动续期,如果业务时间超长,运行期间自动给锁续上新的30s,不用担心业务时间超长,锁自动被删除
// (2) 加锁业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s后自动过期,看门狗机制
// 可以通过修改Config.lockWatchdogTimeout来另行指定过期时间
// lock.lock(10, TimeUnit.SECONDS); 是要自己指定超时时间不会自动续期,一定要大于业务时间
// 如果传递了超时时间,就会发送给Redis执行lua脚本,并设置传递的超时时间
// 如果没有传递超时时间,自动使用30s,如果占锁成功,就会启动一个定时任务(重新给Redis设置过期时间 续期1/3超时时间)
// 30 -> 20 -> 30 ->20 每隔十秒,自动续期10秒
// 最佳方案
// (1) lock.lock(30, TimeUnit.SECONDS); 可以省去续期,业务如果超时,一般都是有问题的
redissonLock.lock();
int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if(stock > 0){
int nowStock = stock - 1;
redisTemplate.opsForValue().set("stock",nowStock + "");
System.out.println("减库存成功,剩余库存:" + nowStock + "");
} else {
System.out.println("库存不足:" + nowStock + "");
}
}
} finally {
resissonLock.unLock();
}
}
Redisson 的其他工具类
读写锁
/**
* 保证一定能读到最新数据,修改期间,写锁是一个排它锁(互斥锁、独享锁),读锁是一个共享锁
* 写锁没释放读锁必须等待
* 读 + 读 :相当于无锁,并发读,只会在Redis中记录好,所有当前的读锁。他们都会同时加锁成功
* 写 + 读 :必须等待写锁释放
* 写 + 写 :阻塞方式
* 读 + 写 :有读锁。写也需要等待
* 写锁没释放,都必须等待
* @return
*/
@GetMapping(value = "/write")
@ResponseBody
public String writeValue() {
String s = "";
RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
RLock rLock = readWriteLock.writeLock();
try {
//1、改数据加写锁,读数据加读锁
rLock.lock();
s = UUID.randomUUID().toString();
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
ops.set("writeValue",s);
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
return s;
}
@GetMapping(value = "/read")
@ResponseBody
public String readValue() {
String s = "";
RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
//加读锁
RLock rLock = readWriteLock.readLock();
try {
rLock.lock();
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
s = ops.get("writeValue");
try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
} catch (Exception e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
return s;
}
Semaphore
/**
* 车库停车
* 3车位
* 信号量也可以做分布式限流,参照 Semaphore
*/
@GetMapping(value = "/park")
@ResponseBody
public String park() throws InterruptedException {
RSemaphore park = redisson.getSemaphore("park");
// park.acquire(); //获取一个信号、获取一个值,占一个车位
boolean flag = park.tryAcquire(); // 尝试获取,不行就算了
if (flag) {
//执行业务
} else {
return "error";
}
return "ok=>" + flag;
}
/**
* 释放车位
* @return
*/
@GetMapping(value = "/go")
@ResponseBody
public String go() {
RSemaphore park = redisson.getSemaphore("park");
park.release(); //释放一个车位
return "ok";
}
CountDownLanch
/**
* 放假、锁门
* 1班没人了
* 5个班,全部走完,我们才可以锁大门
* 分布式闭锁
*/
@GetMapping(value = "/lockDoor")
@ResponseBody
public String lockDoor() throws InterruptedException {
RCountDownLatch door = redisson.getCountDownLatch("door");
door.trySetCount(5);
door.await(); //等待闭锁完成
return "放假了...";
}
@GetMapping(value = "/gogogo/{id}")
@ResponseBody
public String gogogo(@PathVariable("id") Long id) {
RCountDownLatch door = redisson.getCountDownLatch("door");
door.countDown(); //计数-1
return id + "班的人都走了...";
}
缓存里的数据如何和数据库的数据保持一致?
1)、双写模式 改完数据库同时修改缓存中数据 解决:①写操作加锁 ②如果允许暂时性不一致,加过期时间(最终一致性)
2)、失效模式 修改后删除缓存
读写锁,读多写少情况,写加分布式锁
怎么提高分布式锁的效率
参考 ConcurrentHashMap,使用分段锁实现,比如一个需要秒杀商品的库存为10000个,我们可以将它分成 5 段,每段 2000 个库存,每次请求时去某一段减库存,如果为如果某一段为 0 时,去其他段找