当部署多个服务时,会同时存在对同一数据修改,从而造成脏数据或者没有按照规则、套路出牌的场景。为解决这个问题,redis的单线程进制可以解决这个问题,来保证事务依次执行。
1、首先,创建一个spring-boot项目。
2、然后,加入redis的maven依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
3、在application.properties中加入redis配置。
# REDIS (RedisProperties)
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=500
4、编写redis锁工具类。
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
/**
* 分布式事务锁
* @date 2020/03/31
* @author zyh
*/
@Component
@Slf4j
public class RedisLockUtils {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 执行加锁
* @param key 实物id
* @param value 当前时间+失效时间
* @return
*/
private Boolean lock(String key,String value){
// 第一次加锁时
// 调用redis的setnx命令,若成功,则说明是第一次加锁;若失败,则说明事务已存在
if(redisTemplate.opsForValue().setIfAbsent(key, value)){
return true;
}
// 若已存在事务,则接着往下验证
// 接下来验证已存在的事务是否超时
String currentVal=redisTemplate.opsForValue().get(key);
Long currentTimeMillis=System.currentTimeMillis();
Long currentValLong=Long.parseLong(currentVal);
if(StringUtils.isNotEmpty(currentVal) && currentValLong.longValue()<currentTimeMillis.longValue()){
// 事务超时时间 小于 当前时间,说明已超时
// 重新设置新的时间
String oldVal=redisTemplate.opsForValue().getAndSet(key, value);
if(StringUtils.isNotEmpty(oldVal) && oldVal.equals(currentVal)){
// 这一步判断是为了避免两个线程同时走到上一步,但是redis是单线程的,
// 所以getset的返回值只有一个是和之前线程保持一致的
// 所以,只有一个线程会成功。
// 就是那个最先调用getset的
return true;
}
}
return false;
}
/**
* 加锁-加入重试机制
* @param key 实物id
* @param value 当前时间+失效时间
* @return
*/
public Boolean doLock(String key,String value){
Long currentTimeMillis=System.currentTimeMillis();
Long valueLong=Long.parseLong(value);
while(valueLong.longValue()>currentTimeMillis.longValue()){
Boolean flag=lock(key,value);
if(flag){
return flag;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
currentTimeMillis=System.currentTimeMillis();
}
return false;
}
/**
* 删除锁
* @param key 实物id
* @param value 当前时间+失效时间
*/
public void unLock(String key,String value){
String currentVal=redisTemplate.opsForValue().get(key);
if(StringUtils.isNotEmpty(currentVal) && currentVal.equals(value)){
redisTemplate.opsForValue().getOperations().delete(key);
}
}
}
5、编写测试service。
import com.study.redislock.util.RedisLockUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.text.SimpleDateFormat;
import java.util.Date;
@Slf4j
@Service
public class UserService {
@Autowired
private RedisLockUtils redisLockUtils;
private static final String USER_TABLE_NAME="user";
private static final long REDIS_LOCK_TIME_OUT = 1000 * 60;
private SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public void add(String id,String name){
log.info("id:"+id+",name:"+name+",进来。。。。"+simpleDateFormat.format(new Date()));
//加锁
long time=System.currentTimeMillis()+REDIS_LOCK_TIME_OUT;
Boolean lockFlag=redisLockUtils.doLock(USER_TABLE_NAME,String.valueOf(time));
if(!lockFlag){
log.error("id:"+id+",name:"+name);
throw new RuntimeException("操作过于频繁,请稍后重试!"+id+","+name);
}
try {
Thread.sleep(1000*30);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("id:"+id+",name:"+name);
//删除锁
redisLockUtils.unLock(USER_TABLE_NAME,String.valueOf(time));
log.info("id:"+id+",name:"+name+",结束。。。。"+simpleDateFormat.format(new Date()));
}
}
6、编写测试用的controller。
import com.study.redislock.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/add")
public Object add(
@RequestBody Map<String,Object> map
){
userService.add(String.valueOf(map.get("id")),String.valueOf(map.get("name")));
return map;
}
}
7、用postman开始测试。
8、查看控制台。
可见三个请求,差不多时间进来,但是他们的执行却是分别等前面的请求执行完。一个请求正常用时30ms,但是后两个请求大大超过了这个时间,说明分布式事务锁的作用。也可以采取debug进代码,看看。