7.2.分布式锁
0.原则
分布式锁 要满足以下原则
1、互斥
在分布式高并发的条件下,我们最需要保证,同一时刻只能有一个线程获得锁,这是最基本的一点。
2、防止死锁
在分布式高并发的条件下,比如有个线程获得锁的同时,还没有来得及去释放锁,就因为系统故障或者其它原因使它无法执行释放锁的命令,导致其它线程都无法获得锁,造成死锁。
所以分布式非常有必要设置锁的有效时间,确保系统出现故障后,在一定时间内能够主动去释放锁,避免造成死锁的情况。
3、性能
对于访问量大的共享资源,需要考虑减少锁等待的时间,避免导致大量线程阻塞。
所以在锁的设计时,需要考虑两点。
1、锁的颗粒度要尽量小。比如你要通过锁来减库存,那这个锁的名称你可以设置成是商品的ID,而不是任取名称。这样这个锁只对当前商品有效,锁的颗粒度小。
2、锁的范围尽量要小。比如只要锁2行代码就可以解决问题的,那就不要去锁10行代码了。
4、重入
我们知道ReentrantLock是可重入锁,那它的特点就是:同一个线程可以重复拿到同一个资源的锁。重入锁非常有利于资源的高效利用。关于这点之后会做演示。
1.redis缓存
启动 应用 的一个实例(使用 StringRedisTemplate, JSONObject), 通过 jmeter分别发两 个请求
请求两次 , 分别 观察 控制台 及 最小/最大 响应时间
@Autowired
private StringRedisTemplate redisTemplate;
@RequestMapping("/query")
public List<MiddleData> query(){
List<MiddleData> list = new ArrayList<>();
String middleDataList = redisTemplate.opsForValue().get("middleDataList");
if (StringUtils.isEmpty(middleDataList)) {
list = middleDataService.list();
redisTemplate.opsForValue().set("middleDataList", JSONObject.toJSONString(list));
System.out.println(" 从数据库中取出 存储到redis里 ");
}else{
list = JSONObject.parseObject(middleDataList, new TypeReference<List<MiddleData>>(){});
System.out.println(" 从redis里取出 ");
}
return list;
}
2.并发请求
清空 redis , 将 jmeter 调整成 50 并发
观察 控制台 发现 有多次 “从数据库中取出”
解决办法 加 本地锁 synchronized, 这样只有一个请求是从数据库中查询 其它的请求都 是从redis里取
@Autowired
private StringRedisTemplate redisTemplate;
@RequestMapping("/query")
public List<MiddleData> query(){
List<MiddleData> list = new ArrayList<>();
synchronized (this) {
String middleDataList = redisTemplate.opsForValue().get("middleDataList");
if (StringUtils.isEmpty(middleDataList)) {
list = middleDataService.list();
redisTemplate.opsForValue().set("middleDataList", JSONObject.toJSONString(list));
System.out.println(" 从数据库中取出 存储到redis里 ");
}else{
list = JSONObject.parseObject(middleDataList, new TypeReference<List<MiddleData>>(){});
System.out.println(" 从redis里取出 ");
}
}
return list;
}
3.微服务多实例
为了 模拟 集群 形式
同时启动多个应用实例及通过网关并发请求
清空 redis , 将 jmeter 调整成 50 并发, 并通过 网关进行请求
出现异常
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.QueryTimeoutException: Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException: Command timed out after 3 second(s)] with root cause
io.lettuce.core.RedisCommandTimeoutException: Command timed out after 3 second(s)
更换 redis的引擎, 清空 redis 重新设置
<!--redis 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
不再报错 但每一个 应用实例 都 要进行数据库 查询
4.分布式锁
http://redis.cn/commands/set.html
@RequestMapping("/query")
public List<MiddleData> query(){
List<MiddleData> list = new ArrayList<>();
// 加锁 判断
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "lock");
if (lock) {
//synchronized (this) {
String middleDataList = redisTemplate.opsForValue().get("middleDataList");
if (StringUtils.isEmpty(middleDataList)) {
list = middleDataService.list();
redisTemplate.opsForValue().set("middleDataList", JSONObject.toJSONString(list));
System.out.println(" 从数据库中取出 存储到redis里 ");
}else{
list = JSONObject.parseObject(middleDataList, new TypeReference<List<MiddleData>>(){});
System.out.println(" 从redis里取出 ");
}
//}
// 解锁
redisTemplate.delete("lock");
}else{
// 自旋 调用
query();
}
return list;
}
5.追加失效时间解决死锁问题
设置 key-value 与 加锁 要原子执行
@RequestMapping("/query")
public List<MiddleData> query(){
List<MiddleData> list = new ArrayList<>();
// 追加 失效时间为 30 秒
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "lock", 30, TimeUnit.SECONDS);
if (lock) {
//synchronized (this) {
String middleDataList = redisTemplate.opsForValue().get("middleDataList");
if (StringUtils.isEmpty(middleDataList)) {
list = middleDataService.list();
redisTemplate.opsForValue().set("middleDataList", JSONObject.toJSONString(list));
System.out.println(" 从数据库中取出 存储到redis里 ");
}else{
list = JSONObject.parseObject(middleDataList, new TypeReference<List<MiddleData>>(){});
System.out.println(" 从redis里取出 ");
}
//}
redisTemplate.delete("lock");
}else{
query();
}
return list;
}
6.业务超时 超过 锁定时间
@RequestMapping("/query")
public List<MiddleData> query(){
List<MiddleData> list = new ArrayList<>();
// 设置一个随机的value值
String uuid = UUID.randomUUID().toString();
// 存储到redis里
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,30, TimeUnit.SECONDS);
if (lock) {
//synchronized (this) {
String middleDataList = redisTemplate.opsForValue().get("middleDataList");
if (StringUtils.isEmpty(middleDataList)) {
list = middleDataService.list();
redisTemplate.opsForValue().set("middleDataList", JSONObject.toJSONString(list));
System.out.println(" 从数据库中取出 存储到redis里 ");
}else{
list = JSONObject.parseObject(middleDataList, new TypeReference<List<MiddleData>>(){});
System.out.println(" 从redis里取出 ");
}
//}
// 取出 lock 对应的value
String lockValue = redisTemplate.opsForValue().get("lock");
// 与 uuid 比较 相同 再删除
if (uuid.equals(lockValue)) {
redisTemplate.delete("lock");
}
}else{
query();
}
return list;
}
7.取value时 对比 也要是原子性
使用 lua脚本
if redis.call("get",KEYS[1]) == ARGV[1]
then
return redis.call("del",KEYS[1])
else
return 0
end
/* 调用 脚本 */
/* 删除操作 返回 int 类型 1 成功 0 不成功*/
/* 这个是赋值给 KEYS[1] 的 */
/* 这个是赋值给 ARGV[1] 的 */
Object lockState = redisTemplate.execute(new DefaultRedisScript<Long>(luaScript, Long.class), Arrays.asList("lock"), uuid);
System.out.println("lockState = " + lockState);
8.异常处理
@Autowired
private StringRedisTemplate redisTemplate;
@RequestMapping("/query")
public List<MiddleData> query(){
List<MiddleData> list = new ArrayList<>();
// 设置一个随机的value值
String uuid = UUID.randomUUID().toString();
// 存储到redis里
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300, TimeUnit.SECONDS);
if (lock) {
//synchronized (this) {
try {
String middleDataList = redisTemplate.opsForValue().get("middleDataList");
if (StringUtils.isEmpty(middleDataList)) {
list = middleDataService.list();
redisTemplate.opsForValue().set("middleDataList", JSONObject.toJSONString(list));
System.out.println(" 从数据库中取出 存储到redis里 ");
}else{
list = JSONObject.parseObject(middleDataList, new TypeReference<List<MiddleData>>(){});
System.out.println(" 从redis里取出 ");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
取出 lock 对应的value
//String lockValue = redisTemplate.opsForValue().get("lock");
与 uuid 比较 相同 再删除
//if (uuid.equals(lockValue)) {
// redisTemplate.delete("lock");
//}
String luaScript = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
"then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
/* 删除操作 返回 int 类型 1 成功 0 不成功*/
/* 这个是赋值给 KEYS[1] 的 */
/* 这个是赋值给 ARGV[1] 的 */
Long lockState = redisTemplate.execute(new DefaultRedisScript<Long>(luaScript, Long.class), Arrays.asList("lock"), uuid);
}
//}
}else{
query();
}
return list;
}