@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class BootRedis01Application {
public static void main(String[] args) {
SpringApplication.run(BootRedis01Application.class);
}
}
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory){
// 新建 RedisTemplate 对象,key 为 String 对象,value 为 Serializable(可序列化的)对象
RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
// key 值使用字符串序列化器
redisTemplate.setKeySerializer(new StringRedisSerializer());
// value 值使用 json 序列化器
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
// 传入连接工厂
redisTemplate.setConnectionFactory(connectionFactory);
// 返回 redisTemplate 对象
return redisTemplate;
}
}
@RestController
public class GoodController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@GetMapping("/buy_goods")
public String buy_Goods() {
// 从 redis 中获取商品的剩余数量
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
String retStr = null;
// 商品数量大于零才能出售
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
retStr = "成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
retStr = "商品已经售罄/活动结束/调用超时" + "\t 服务器端口: " + serverPort;
}
System.out.println(retStr);
return retStr;
}
}
单机版没有加锁,并发下会出现超卖问题
加锁!但是,加到锁是Synchronized还是ReentrantLock呢?
(a)Synchronized:不见不散,其他线程等不到锁就会死等,会造成积压
(b)ReentrantLock:过时不候,需手动获取锁,为避免死锁 ,在finally释放锁
①trylock()
②trylock(long time,TimeUnit unit):可设置抢锁时间,规定时间内抢不到锁就放弃
@RestController
public class GoodController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@GetMapping("/buy_goods")
public String buy_Goods() {
synchronized (this) {
// 从 redis 中获取商品的剩余数量
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
String retStr = null;
// 商品数量大于零才能出售
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
retStr = "成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
retStr = "商品已经售罄/活动结束/调用超时" + "\t 服务器端口: " + serverPort;
}
System.out.println(retStr);
return retStr;
}
}
}
在分布式情况下,竞争的线程可能不在同一节点上,所以需要一个所有进程都可以访问到的节点来进行加锁,如Redis或Zookeeper。
value=当前请求的UUID+线程名称。
@RestController
public class GoodController {
private static final String REDIS_LOCK_KEY = "lockOneby";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@GetMapping("/buy_goods")
public String buy_Goods() {
// 当前请求的 UUID + 线程名
String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
// setIfAbsent() 就相当于 setnx,如果不存在就新建锁
Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value);
// 抢锁失败
if(lockFlag == false){
return "抢锁失败";
}
// 从 redis 中获取商品的剩余数量
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
String retStr = null;
// 商品数量大于零才能出售
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
retStr = "成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
retStr = "商品已经售罄/活动结束/调用超时" + "\t 服务器端口: " + serverPort;
}
System.out.println(retStr);
stringRedisTemplate.delete(REDIS_LOCK_KEY); // 释放分布式锁
return retStr;
}
}
上述代码在执行时,可能会出现无法释放锁的情况。
@RestController
public class GoodController {
private static final String REDIS_LOCK_KEY = "lockOneby";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@GetMapping("/buy_goods")
public String buy_Goods() {
// 当前请求的 UUID + 线程名
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
try {
// setIfAbsent() 就相当于 setnx,如果不存在就新建锁
Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value);
// 抢锁失败
if (lockFlag == false) {
return "抢锁失败";
}
// 从 redis 中获取商品的剩余数量
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
String retStr = null;
// 商品数量大于零才能出售
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
retStr = "成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
retStr = "商品已经售罄/活动结束/调用超时" + "\t 服务器端口: " + serverPort;
}
System.out.println(retStr);
return retStr;
} finally {
stringRedisTemplate.delete(REDIS_LOCK_KEY); // 释放分布式锁
}
}
}
假设部署了微服务jar包的服务器挂了,代码层面没有走到finally部分,也就无法释放锁,导致锁的key删除不了,这样其他微服务无法抢到锁。
设置一个过期时间
// 设置过期时间为 10s
stringRedisTemplate.expire(REDIS_LOCK_KEY, 10L, TimeUnit.SECONDS);
完整代码如下:
@RestController
public class GoodController {
private static final String REDIS_LOCK_KEY = "lockOneby";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@GetMapping("/buy_goods")
public String buy_Goods() {
// 当前请求的 UUID + 线程名
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
try {
// setIfAbsent() 就相当于 setnx,如果不存在就新建锁
Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value);
// 设置过期时间为 10s
stringRedisTemplate.expire(REDIS_LOCK_KEY, 10L, TimeUnit.SECONDS);
// 抢锁失败
if (lockFlag == false) {
return "抢锁失败";
}
// 从 redis 中获取商品的剩余数量
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
String retStr = null;
// 商品数量大于零才能出售
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
retStr = "成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
retStr = "商品已经售罄/活动结束/调用超时" + "\t 服务器端口: " + serverPort;
}
System.out.println(retStr);
return retStr;
} finally {
stringRedisTemplate.delete(REDIS_LOCK_KEY); // 释放分布式锁
}
}
}
加锁操作与设置过期时间为两行代码,如果服务器刚执行加锁操作就宕机了,那么锁也可能释放不了。
//该方法在加锁的同时设置过期时间,保证了原子性
stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value, 10L, TimeUnit.SECONDS)
张冠李戴,删除了别人的锁。
因为无法保证一个业务的执行时间,如果当前业务还在执行,但是锁过期了,那么久有可能会出现超卖的情况,并且可能会导致其他业务进来执行,没有执行完,而锁被释放的情况。如下图:
//释放锁之前,判断是否为自己的锁
value.equalsIgnoreCase(stringRedisTemplate.opsForValue().get(REDIS_LOCK_KEY))
完整代码:
@RestController
public class GoodController {
private static final String REDIS_LOCK_KEY = "lockOneby";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@GetMapping("/buy_goods")
public String buy_Goods() {
// 当前请求的 UUID + 线程名
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
try {
// setIfAbsent() 就相当于 setnx,如果不存在就新建锁,同时加上过期时间保证原子性
Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value, 10L, TimeUnit.SECONDS);
// 抢锁失败
if (lockFlag == false) {
return "抢锁失败";
}
// 从 redis 中获取商品的剩余数量
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
String retStr = null;
// 商品数量大于零才能出售
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
retStr = "成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
retStr = "商品已经售罄/活动结束/调用超时" + "\t 服务器端口: " + serverPort;
}
System.out.println(retStr);
return retStr;
} finally {
// 判断是否是自己加的锁
if(value.equalsIgnoreCase(stringRedisTemplate.opsForValue().get(REDIS_LOCK_KEY))){
stringRedisTemplate.delete(REDIS_LOCK_KEY); // 释放分布式锁
}
}
}
}
在finally代码块中的判断与删除并不是原子操作,假设判断if时,还是当前业务获得的锁,但是可能在执行完if之后,这把锁就被其他执行操作给释放了,出现了误删锁的情况。
开启事务,监视REDIS_LOCK_KEY,如果被修改过,就重新执行删除操作,否则解除监视。
@RestController
public class GoodController {
private static final String REDIS_LOCK_KEY = "lockOneby";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@GetMapping("/buy_goods")
public String buy_Goods() {
// 当前请求的 UUID + 线程名
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
try {
// setIfAbsent() 就相当于 setnx,如果不存在就新建锁,同时加上过期时间保证原子性
Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value, 10L, TimeUnit.SECONDS);
// 抢锁失败
if (lockFlag == false) {
return "抢锁失败";
}
// 从 redis 中获取商品的剩余数量
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
String retStr = null;
// 商品数量大于零才能出售
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
retStr = "成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
retStr = "商品已经售罄/活动结束/调用超时" + "\t 服务器端口: " + serverPort;
}
System.out.println(retStr);
return retStr;
} finally {
while (true) {
//加事务,乐观锁
stringRedisTemplate.watch(REDIS_LOCK_KEY);
// 判断是否是自己加的锁
if (value.equalsIgnoreCase(stringRedisTemplate.opsForValue().get(REDIS_LOCK_KEY))) {
// 开启事务
stringRedisTemplate.setEnableTransactionSupport(true);
stringRedisTemplate.multi();
stringRedisTemplate.delete(REDIS_LOCK_KEY);
// 判断事务是否执行成功,如果等于 null,就是没有删掉,删除失败,再回去 while 循环那再重新执行删除
List<Object> list = stringRedisTemplate.exec();
if (list == null) {
continue;
}
}
//如果删除成功,释放监控器,并且 break 跳出当前循环
stringRedisTemplate.unwatch();
break;
}
}
}
}
lua脚本
Redis可以通过eval命令保证执行的原子性。
public class RedisUtils {
private static JedisPool jedisPool;
private static String hostAddr = "192.168.152.233";
static {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(20);
jedisPoolConfig.setMaxIdle(10);
jedisPool = new JedisPool(jedisPoolConfig, hostAddr, 6379, 100000);
}
public static Jedis getJedis() throws Exception {
if (null != jedisPool) {
return jedisPool.getResource();
}
throw new Exception("Jedispool is not ok");
}
}
@RestController
public class GoodController {
private static final String REDIS_LOCK_KEY = "lockOneby";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@GetMapping("/buy_goods")
public String buy_Goods() throws Exception {
// 当前请求的 UUID + 线程名
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
try {
// setIfAbsent() 就相当于 setnx,如果不存在就新建锁,同时加上过期时间保证原子性
Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value, 10L, TimeUnit.SECONDS);
// 抢锁失败
if (lockFlag == false) {
return "抢锁失败";
}
// 从 redis 中获取商品的剩余数量
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
String retStr = null;
// 商品数量大于零才能出售
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
retStr = "成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
retStr = "商品已经售罄/活动结束/调用超时" + "\t 服务器端口: " + serverPort;
}
System.out.println(retStr);
return retStr;
} finally {
// 获取连接对象
Jedis jedis = RedisUtils.getJedis();
// lua 脚本,摘自官网
String script = "if redis.call('get', KEYS[1]) == ARGV[1]" + "then "
+ "return redis.call('del', KEYS[1])" + "else " + " return 0 " + "end";
try {
// 执行 lua 脚本
Object result = jedis.eval(script, Collections.singletonList(REDIS_LOCK_KEY), Collections.singletonList(value));
// 获取 lua 脚本的执行结果
if ("1".equals(result.toString())) {
System.out.println("------del REDIS_LOCK_KEY success");
} else {
System.out.println("------del REDIS_LOCK_KEY error");
}
} finally {
// 关闭链接
if (null != jedis) {
jedis.close();
}
}
}
}
}
无法保证业务执行时间,如果业务还在执行,但是锁过期了,就会出现误删其他锁或超卖的问题。
补充:redisson看门狗机制
官网解释
Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。
看门狗开启条件
我们可以看到,leaseTime != -1时,只执行tryLockInnerAsync方法,其它情况会执行下面的代码,而leaseTime 就是我们调用lock(10, TimeUnit.SECONDS);方法传入的时间参数。
由此可知:redisson如果只是用lock.lock();不传过期时间的话,会启动看门狗机制,传过期时间的话,就不会启动看门狗机制。
在Redis配置类中注入Redisson对象。
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Bean
public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory) {
// 新建 RedisTemplate 对象,key 为 String 对象,value 为 Serializable(可序列化的)对象
RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
// key 值使用字符串序列化器
redisTemplate.setKeySerializer(new StringRedisSerializer());
// value 值使用 json 序列化器
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
// 传入连接工厂
redisTemplate.setConnectionFactory(connectionFactory);
// 返回 redisTemplate 对象
return redisTemplate;
}
@Bean
public Redisson redisson() {
Config config = new Config();
config.useSingleServer().setAddress("redis://" + redisHost + ":6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
}
@RestController
public class GoodController {
private static final String REDIS_LOCK_KEY = "lockOneby";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@Autowired
private Redisson redisson;
@GetMapping("/buy_goods")
public String buy_Goods() throws Exception {
// 当前请求的 UUID + 线程名
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
// 获取锁
RLock redissonLock = redisson.getLock(REDIS_LOCK_KEY);
// 上锁
redissonLock.lock();
try {
// 从 redis 中获取商品的剩余数量
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
String retStr = null;
// 商品数量大于零才能出售
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
retStr = "成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
retStr = "商品已经售罄/活动结束/调用超时" + "\t 服务器端口: " + serverPort;
}
System.out.println(retStr);
return retStr;
} finally {
// 还在持有锁的状态,并且是当前线程持有的锁再解锁
if (redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()){
redissonLock.unlock();
}
}
}
}