目录
引出
1.redis中数据一致性的问题,大量抢购如何保证数据安全;
2.用java代码加锁解决一致性问题,可重入锁,以及死锁的产生;
3.采用lua脚本,让库存-1操作原子化;
4.分布式环境下setnx锁,以及存在的问题;
5.Redisson框架的使用,锁的续期,看门狗策略,1/3时续期;
Redis中的数据一致性
CAP原则:
C:一致性
A: 可用性
P: 分区容错性(*) AP CP
BASE——最终一致性
场景
redis存储库存,多个客户(线程)请求, 库存量100, 如何保证数据一致性。
用Java代码加锁解决一致性
大量请求拥挤抢购
package com.tianju.redis.service.impl;
import com.tianju.redis.service.IRushGoodsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class RushGoodsServiceImpl implements IRushGoodsService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private final String GOODS = "goods";
@Override
public String rush() {
String sNum = stringRedisTemplate.opsForValue().get(GOODS);
int nums = Integer.parseInt(sNum);
if (nums>0){
stringRedisTemplate.opsForValue().set(GOODS, String.valueOf(--nums) );
return stringRedisTemplate.opsForValue().get(GOODS);
}else {
return "error";
}
}
}
@PutMapping("/rushJmeter")
public void rushJmeter(){
String goodsNum = rushGoodsService.rush();
System.out.println("goodsNum: "+goodsNum);
}
加锁(java)
可重入锁: sychronized ReentrantLock
synchronized (this.getClass()){}
private final ReentrantLock lock = new ReentrantLock(); // 可重入锁
什么是可重入锁?
当线程获取某个锁后,还可以继续获取它,可以递归调用,而不会发生死锁;
如何保证可重入
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。
如果测试成功,表示线程已经获得了锁。
如果测试失败,则需要再测试一下Mark Word中偏向锁标志是否设置成1:没有则CAS竞争;设置了,则CAS将对象头偏向锁指向当前线程。再维护一个计数器,同个线程进入则自增1,离开再减1,直到为0才能释放
滥用锁的代价?
死锁的四个必要条件?
循环
A —> B —>C —>A
可能导致死锁
锁对象ObjLock
package com.tianju.redis.lock;
public class ObjLock {
private String name;
public ObjLock(String name){
this.name = name;
}
@Override
public String toString() {
return "ObjLock:"+this.name;
}
}
加锁释放锁方法DeadLockDemo
package com.tianju.redis.lock;
public class DeadLockDemo {
private ObjLock a;
public ObjLock b;
public DeadLockDemo(ObjLock a,ObjLock b){
this.a = a;
this.b = b;
}
public void dead(){
System.out.println("********"+a+"对象"+b+"对象都加锁**************");
System.out.println(a+"--"+b+": "+"准备给"+a+"对象加锁>>");
synchronized (a){
System.out.println(a+"--"+b+": "+a+"对象加锁成功...");
System.out.println(a+"--"+b+": "+"准备给"+b+"对象加锁>>>");
synchronized (b){
System.out.println(a+"--"+b+": "+b+"对象加锁成功");
}
System.out.println(a+"--"+b+": "+"释放"+b+"对象的锁");
}
System.out.println(a+"--"+b+": "+"释放"+b+"对象的锁");
System.out.println("****************");
}
}
测试方法
package com.tianju.redis.lock;
public class TestDeadLock {
/**
* 一个一个顺序运行
*/
public static void run(){
ObjLock a = new ObjLock("A");
ObjLock b = new ObjLock("B");
ObjLock c = new ObjLock("C");
DeadLockDemo lockDemo1 = new DeadLockDemo(a, b);
lockDemo1.dead(); // 锁住a和b
DeadLockDemo lockDemo2 = new DeadLockDemo(b, c);
lockDemo2.dead(); // 锁住a和b
DeadLockDemo lockDemo3 = new DeadLockDemo(c, a);
lockDemo3.dead(); // 锁住a和b
}
/**
* 进行线程抢,死锁
*/
public static void rushRun(){
ObjLock a = new ObjLock("A");
ObjLock b = new ObjLock("B");
ObjLock c = new ObjLock("C");
new Thread(()->{
DeadLockDemo lockDemo1 = new DeadLockDemo(a, b);
lockDemo1.dead(); // 锁住a和b
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
new Thread(()->{
DeadLockDemo lockDemo2 = new DeadLockDemo(b, c);
lockDemo2.dead(); // 锁住a和b
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
new Thread(()->{
DeadLockDemo lockDemo3 = new DeadLockDemo(c, a);
lockDemo3.dead(); // 锁住a和b
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
}
public static void main(String[] args) {
// run(); // 顺序执行加锁,解锁
rushRun(); // 线程进行抢
}
}
数据一致性改进-Lua脚本
- 锁: 多个线程抢—->每个线程按顺序完成
- Lua: 原子性
采用单独lua脚本+配置类实现
创建Lua脚本
让减库存这个操作整体是原子性的
local key = KEYS[1]
--- 判断key是否存在
local isIn = tonumber(redis.call('exists',key))
if isIn==1 then
local nums = tonumber(redis.call('get',key))
if nums>0 then
--- 减库存
redis.call('decr',key)
return redis.call('get',key)
else
return 'goods is sold out'
end
else
return 'goods not exists'
end
配置类
package com.tianju.redis.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
/**
* lua脚本的Redis配置类
*/
@Configuration
public class RedisConfig {
@Bean
public RedisScript<String> redisScript(){
DefaultRedisScript<String> redisScript = new DefaultRedisScript<>();
redisScript.setResultType(String.class); // 返回值类型
redisScript.setLocation(new ClassPathResource("lua/goods.lua")); // 设置lua脚本的路径
return redisScript;
}
}
调用lua脚本
package com.tianju.redis.service.impl;
import com.tianju.redis.service.IRushGoodsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.concurrent.locks.ReentrantLock;
@Service
public class RushGoodsServiceImpl implements IRushGoodsService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private final String GOODS = "goods";
@Autowired
private RedisScript<String> redisScript;
@Override
public String rushLua(){
return stringRedisTemplate.execute(
redisScript,
Collections.singletonList(GOODS)); // 创建只有一个元素的list
}
}
将lua代码放入java中
package com.tianju.redis.service.impl;
import com.tianju.redis.service.IRushGoodsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.concurrent.locks.ReentrantLock;
@Service
public class RushGoodsServiceImpl implements IRushGoodsService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private final String GOODS = "goods";
private final ReentrantLock lock = new ReentrantLock(); // 可重入锁
@Override
public String rush() {
lock.lock();
try{
String sNum = stringRedisTemplate.opsForValue().get(GOODS);
int nums = Integer.parseInt(sNum);
if (nums>0){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
stringRedisTemplate.opsForValue().decrement(GOODS); // 采用redis的原子减1
return stringRedisTemplate.opsForValue().get(GOODS);
}else {
return "error";
}
}finally {
lock.unlock();
}
}
@Autowired
private RedisScript<String> redisScript;
@Override
public String rushLua(){
return stringRedisTemplate.execute(
redisScript,
Collections.singletonList(GOODS)); // 创建只有一个元素的list
}
@Override
public String rushLuaJava() {
String lua = "local key = KEYS[1]\n" +
"\n" +
"--- 判断key是否存在\n" +
"local isIn = tonumber(redis.call('exists',key))\n" +
"\n" +
"if isIn==1 then\n" +
" local nums = tonumber(redis.call('get',key))\n" +
" if nums>0 then\n" +
" --- 减库存\n" +
" redis.call('decr',key)\n" +
" return redis.call('get',key)\n" +
" else\n" +
" return 'goods is sold out'\n" +
" end\n" +
"else\n" +
" return 'goods not exists'\n" +
"end";
return stringRedisTemplate.opsForValue().getOperations()
.execute(
new DefaultRedisScript<>(lua, String.class),
Collections.singletonList(GOODS)
);
}
}
分布式环境下
setnx:特点
如果key不存在,返回为1
如果key存在, 返回为0
package com.tianju.redis.service.impl;
import com.tianju.redis.service.IRushGoodsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
@Service
public class RushGoodsServiceImpl implements IRushGoodsService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private final String GOODS = "goods";
private final String LOCK = "myLock";
private final String DEFAULT = "1";
private final ReentrantLock lock = new ReentrantLock(); // 可重入锁
@Override
public String rush() {
lock.lock();
try{
String sNum = stringRedisTemplate.opsForValue().get(GOODS);
int nums = Integer.parseInt(sNum);
if (nums>0){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
stringRedisTemplate.opsForValue().decrement(GOODS); // 采用redis的原子减1
return stringRedisTemplate.opsForValue().get(GOODS);
}else {
return "error";
}
}finally {
lock.unlock();
}
}
@Autowired
private RedisScript<String> redisScript;
@Override
public String rushLua(){
return stringRedisTemplate.execute(
redisScript,
Collections.singletonList(GOODS)); // 创建只有一个元素的list
}
@Override
public String rushLuaJava() {
String lua = "local key = KEYS[1]\n" +
"\n" +
"--- 判断key是否存在\n" +
"local isIn = tonumber(redis.call('exists',key))\n" +
"\n" +
"if isIn==1 then\n" +
" local nums = tonumber(redis.call('get',key))\n" +
" if nums>0 then\n" +
" --- 减库存\n" +
" redis.call('decr',key)\n" +
" return redis.call('get',key)\n" +
" else\n" +
" return 'goods is sold out'\n" +
" end\n" +
"else\n" +
" return 'goods not exists'\n" +
"end";
return stringRedisTemplate.opsForValue().getOperations()
.execute(
new DefaultRedisScript<>(lua, String.class),
Collections.singletonList(GOODS)
);
}
@Override
public String rushSetNx() {
List<String> keys = new ArrayList<>();
keys.add(GOODS);
keys.add(LOCK);
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(LOCK, DEFAULT);
System.out.println(flag);
if (flag){ // 拿到锁
return stringRedisTemplate.execute(
redisScript,
keys); // 创建只有一个元素的list
}else {
return "锁在使用中,请稍后";
}
}
}
local key = KEYS[1]
local myLock = KEYS[2]
--- 判断key是否存在
local isIn = tonumber(redis.call('exists',key))
if isIn==1 then
local nums = tonumber(redis.call('get',key))
if nums>0 then
--- 减库存
redis.call('decr',key)
--- 删掉用于加锁的key,setnx
redis.call('del',myLock)
return redis.call('get',key)
else
return 'goods is sold out'
end
else
return 'goods not exists'
end
问题1: setnx 没有设置过期时间
问题2:执行的业务周期超过锁的有效时间
问题3: 业务没有执行完之前,锁要进行续期。
问题4: 可重入
Redisson框架
在分布环境下,redis的数据一致性解决方案.
引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.22.0</version>
</dependency>
配置类
package com.tianju.redis.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.111.130:6399");
return Redisson.create(config);
}
}
创建redis连接池
加锁和锁释放
package com.tianju.redis.service.impl;
import com.tianju.redis.service.IGoodsService;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class GoodsServiceImpl implements IGoodsService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private final String GOODS = "goods";
private final String LOCK = "myLock";
@Autowired
private RedissonClient redissonClient;
@Override
public String rushRedisson() {
RLock lock = redissonClient.getLock(LOCK);
lock.lock();// 默认的时间是30s
try {
String s = stringRedisTemplate.opsForValue().get(GOODS);
int nums = Integer.parseInt(s);
if (nums>0){
// 库存-1操作
stringRedisTemplate.opsForValue().decrement(GOODS);
return stringRedisTemplate.opsForValue().get(GOODS);
}else {
return "销售完了";
}
} finally {
// 释放锁
lock.unlock();
}
}
}
总结
1.redis中数据一致性的问题,大量抢购如何保证数据安全;
2.用java代码加锁解决一致性问题,可重入锁,以及死锁的产生;
3.采用lua脚本,让库存-1操作原子化;
4.分布式环境下setnx锁,以及存在的问题;
5.Redisson框架的使用,锁的续期,看门狗策略,1/3时续期;
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/165079.html