准备redis环境
用docker-compose来搭建Redis测试环境,采用单机模式,具体配置如下:
version: '3'
services:
redis:
image: registry.cn-hangzhou.aliyuncs.com/zhengqing/redis:6.0.8 # image 'redis:6.0.8'
container_name: redis # 容器名为'redis'
restart: unless-stopped # 指定容器退出后的重启策略为始终重启,但是不考虑在Docker守护进程启动时就已经停止了的容器
command: redis-server /etc/redis/redis.conf --requirepass 123456 --appendonly no # 启动redis服务并添加密码为:123456,默认不开启redis-aof方式持久化配置
# command: redis-server --requirepass 123456 --appendonly yes # 启动redis服务并添加密码为:123456,并开启redis持久化配置
environment: # 设置环境变量,相当于docker run命令中的-e
TZ: Asia/Shanghai
LANG: en_US.UTF-8
volumes: # 数据卷挂载路径设置,将本机目录映射到容器目录
- "./redis/data:/data"
- "./redis/config/redis.conf:/etc/redis/redis.conf" # `redis.conf`文件内容`http://download.redis.io/redis-stable/redis.conf`
ports: # 映射端口
- "6379:6379"
一、依赖
<!--redis client-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.9.0</version>
</dependency>
二、application.yaml
server:
port: 8088
spring:
redis:
database: 0
host: 127.0.0.1
port: 6379
password: 123456
lettuce:
pool:
min-idle: 0
max-active: 8
max-idle: 8
max-wait: -1ms
connect-timeout: 30000ms
三、redis配置类
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
四、测试
@RunWith(SpringRunner.class)
@SpringBootTest(classes = DemoApplication.class)
public class RedisTests {
private Logger log = LoggerFactory.getLogger(getClass());
@Autowired
private RedisTemplate redisTemplate;
@Test
public void save() {
redisTemplate.opsForValue().set("test","this is a test");
System.out.println(redisTemplate.opsForValue().get("test"));
}
}
Springboot-Redis分布式锁
RedisTemplate 来实现,一般步骤如下:
①、将锁资源放入 Redis (注意是当key不存在时才能放成功,所以使用 setIfAbsent 方法):
redisTemplate.opsForValue().setIfAbsent("key", "value");
②、设置过期时间
redisTemplate.expire("key", 30000, TimeUnit.MILLISECONDS);
③、释放锁
redisTemplate.delete("key");
一般情况下,这样的实现就能够满足锁的需求了.
但是如果在调用 setIfAbsent 方法之后线程挂掉了,即没有给锁定的资源设置过期时间,默认是永不过期,那么这个锁就会一直存在。
所以需要保证设置锁及其过期时间两个操作的原子性,spring data的 RedisTemplate 当中并没有这样的方法。
但是在jedis当中是有这种原子操作的方法的,需要通过 RedisTemplate 的 execute 方法获取到jedis里操作命令的对象,代码如下:
String result = redisTemplate.execute(new RedisCallback<>(){
@Override
public String doInRedis(RedisConnection connection)throws DataAccessException{
JedisCommands commands = (JedisCommands)connectio.getNativeConnection();
return commands.set(key,"锁定资源","NX","PX",expire);
}
});
【 Redis 从2.6.12版本开始 set 命令支持 NX 、 PX 这些参数来达到 setnx 、 setex 、 psetex 命令的效果。】
- NX: 表示只有当锁定资源不存在的时候才能 SET 成功。利用 Redis 的原子性,保证了只有第一个请求的线程才能获得锁,而之后的所有线程在锁定资源被释放之前都不能获得锁。
- PX: expire 表示锁定的资源的自动过期时间,单位是毫秒。
【Redis 从2.6.0开始通过内置的 Lua 解释器,可以使用 EVAL 命令对 Lua 脚本进行求值】
if redis.call("get",KEYS[1]) == ARGV[1]then
return redis.call("del",KEYS[1])
else
return 0
end
// 使用Lua脚本删除Redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
// spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本
Long result = redisTemplate.execute(new RedisCallback<Long>() {
public Long doInRedis(RedisConnection connection) throws DataAccessException {
Object nativeConnection = connection.getNativeConnection();
// 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行
// 集群模式
if (nativeConnection instanceof JedisCluster) {
return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args);
}
// 单机模式
else if (nativeConnection instanceof Jedis) {
return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args);
}
return 0L;
}
});
集群模式下存在的问题
- 线程T1获取锁成功
- Redis 的master节点挂掉,slave自动顶上
- 线程T2获取锁,会从slave节点上去判断锁是否存在,由于Redis的master slave复制是异步的,所以此时线程T2可能成功获取到锁
一、为了可以以后扩展为使用其他方式来实现分布式锁,定义了接口和抽象类,所有的源码如下:
/**
* @author fuwei.deng
* @date 2017年6月14日 下午3:11:05
* @version 1.0.0
*/
public interface DistributedLock {
public static final long TIMEOUT_MILLIS = 30000;
public static final int RETRY_TIMES = Integer.MAX_VALUE;
public static final long SLEEP_MILLIS = 500;
public boolean lock(String key);
public boolean lock(String key, int retryTimes);
public boolean lock(String key, int retryTimes, long sleepMillis);
public boolean lock(String key, long expire);
public boolean lock(String key, long expire, int retryTimes);
public boolean lock(String key, long expire, int retryTimes, long sleepMillis);
public boolean releaseLock(String key);
}
public abstract class AbstractDistributedLock implements DistributedLock {
@Override
public boolean lock(String key) {
return lock(key, TIMEOUT_MILLIS, RETRY_TIMES, SLEEP_MILLIS);
}
@Override
public boolean lock(String key, int retryTimes) {
return lock(key, TIMEOUT_MILLIS, retryTimes, SLEEP_MILLIS);
}
@Override
public boolean lock(String key, int retryTimes, long sleepMillis) {
return lock(key, TIMEOUT_MILLIS, retryTimes, sleepMillis);
}
@Override
public boolean lock(String key, long expire) {
return lock(key, expire, RETRY_TIMES, SLEEP_MILLIS);
}
@Override
public boolean lock(String key, long expire, int retryTimes) {
return lock(key, expire, retryTimes, SLEEP_MILLIS);
}
}
//分布式锁实现
public class RedisDistributedLock extends AbstractDistributedLock {
private final Logger logger = LoggerFactory.getLogger(RedisDistributedLock.class);
private RedisTemplate<Object, Object> redisTemplate;
private ThreadLocal<String> lockFlag = new ThreadLocal<String>();
public static final String UNLOCK_LUA;
static {
StringBuilder sb = new StringBuilder();
sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
sb.append("then ");
sb.append(" return redis.call(\"del\",KEYS[1]) ");
sb.append("else ");
sb.append(" return 0 ");
sb.append("end ");
UNLOCK_LUA = sb.toString();
}
public RedisDistributedLock(RedisTemplate<Object, Object> redisTemplate) {
super();
this.redisTemplate = redisTemplate;
}
@Override
public boolean lock(String key, long expire, int retryTimes, long sleepMillis) {
boolean result = setRedis(key, expire);
// 如果获取锁失败,按照传入的重试次数进行重试
while((!result) && retryTimes-- > 0){
try {
logger.debug("lock failed, retrying..." + retryTimes);
Thread.sleep(sleepMillis);
} catch (InterruptedException e) {
return false;
}
result = setRedis(key, expire);
}
return result;
}
private boolean setRedis(String key, long expire) {
try {
String result = redisTemplate.execute(new RedisCallback<String>() {
@Override
public String doInRedis(RedisConnection connection) throws DataAccessException {
JedisCommands commands = (JedisCommands) connection.getNativeConnection();
String uuid = UUID.randomUUID().toString();
lockFlag.set(uuid);
return commands.set(key, uuid, "NX", "PX", expire);
}
});
return !StringUtils.isEmpty(result);
} catch (Exception e) {
logger.error("set redis occured an exception", e);
}
return false;
}
@Override
public boolean releaseLock(String key) {
// 释放锁的时候,有可能因为持锁之后方法执行时间大于锁的有效期,此时有可能已经被另外一个线程持有锁,所以不能直接删除
try {
List<String> keys = new ArrayList<String>();
keys.add(key);
List<String> args = new ArrayList<String>();
args.add(lockFlag.get());
// 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
// spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本
Long result = redisTemplate.execute(new RedisCallback<Long>() {
public Long doInRedis(RedisConnection connection) throws DataAccessException {
Object nativeConnection = connection.getNativeConnection();
// 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行
// 集群模式
if (nativeConnection instanceof JedisCluster) {
return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args);
}
// 单机模式
else if (nativeConnection instanceof Jedis) {
return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args);
}
return 0L;
}
});
return result != null && result > 0;
} catch (Exception e) {
logger.error("release lock occured an exception", e);
}
return false;
}
}
二、基于AOP 的 Redis 分布式锁
在实际的使用过程中,分布式锁可以封装好后使用在方法级别,这样就不用每个地方都去获取锁和释放锁,使用起来更加方便。
①、定义注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface RedisLock {
/** 锁的资源,redis的key*/
String value() default "default";
/** 持锁时间,单位毫秒*/
long keepMills() default 30000;
/** 当获取失败时候动作*/
LockFailAction action() default LockFailAction.CONTINUE;
public enum LockFailAction{
/** 放弃 */
GIVEUP,
/** 继续 */
CONTINUE;
}
/** 重试的间隔时间,设置GIVEUP忽略此项*/
long sleepMills() default 200;
/** 重试次数*/
int retryTimes() default 5;
}
②、配置类
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class DistributedLockAutoConfiguration {
@Bean
@ConditionalOnBean(RedisTemplate.class)
public DistributedLock redisDistributedLock(RedisTemplate<Object, Object> redisTemplate){
return new RedisDistributedLock(redisTemplate);
}
}
③、定义切面
@Aspect
@Configuration
@ConditionalOnClass(DistributedLock.class)
@AutoConfigureAfter(DistributedLockAutoConfiguration.class)
public class DistributedLockAspectConfiguration {
private final Logger logger = LoggerFactory.getLogger(DistributedLockAspectConfiguration.class);
@Autowired
private DistributedLock distributedLock;
@Pointcut("@annotation(com.itopener.lock.redis.spring.boot.autoconfigure.annotations.RedisLock)")
private void lockPoint(){
}
@Around("lockPoint()")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
RedisLock redisLock = method.getAnnotation(RedisLock.class);
String key = redisLock.value();
if(StringUtils.isEmpty(key)){
Object[] args = pjp.getArgs();
key = Arrays.toString(args);
}
int retryTimes = redisLock.action().equals(LockFailAction.CONTINUE) ? redisLock.retryTimes() : 0;
boolean lock = distributedLock.lock(key, redisLock.keepMills(), retryTimes, redisLock.sleepMills());
if(!lock) {
logger.debug("get lock failed : " + key);
return null;
}
//得到锁,执行方法,释放锁
logger.debug("get lock success : " + key);
try {
return pjp.proceed();
} catch (Exception e) {
logger.error("execute locked method occured an exception", e);
} finally {
boolean releaseResult = distributedLock.releaseLock(key);
logger.debug("release lock : " + key + (releaseResult ? " success" : " failed"));
}
return null;
}
}
spring boot starter还需要在 resources/META-INF 中添加 spring.factories 文件
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.itopener.lock.redis.spring.boot.autoconfigure.DistributedLockAutoConfiguration,\
com.itopener.lock.redis.spring.boot.autoconfigure.DistributedLockAspectConfiguration
这样封装之后,使用spring boot开发的项目,直接依赖这个starter,就可以在方法上加 RedisLock 注解来实现分布式锁的功能了,当然如果需要自己控制,直接注入分布式锁的bean即可
@Autowired
private DistributedLock distributedLock;
如果需要使用其他的分布式锁实现,继承 AbstractDistributedLock 后实现获取锁和释放锁的方法即可
SpringBoot 使用 Redis 缓存
一、依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
二、启动类和配置文件
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=172.31.19.222
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=0
log4j日志信息
## LOG4J配置
log4j.rootCategory=DEBUG,stdout
## 控制台输出
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %5p %c{1}:%L - %m%n
@SpringBootApplication
@EnableScheduling
@EnableCaching //开启缓存功能
public class SpringbootApplication{
public static void main(String[] args) {
SpringApplication.run(SpringbootApplication.class, args);
}
}
三、RedisConfig配置
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport{
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.timeout}")
private int timeout;
//自定义缓存生成策略
@Bean
public KeyGenerator keyGenerator(){
return new KeyGenerator(){
@Override
publilc Object generate(Object target, java.lang.reflect.Method method, Object... params){
String sb = new StringBuffer();
sb.append(target.getClass().getName());
sb.append(method.getName());
return sb.toString();
}
}
}
//缓存管理器
@Bean
public CacheManager cacheManager(@SuppressWarnings("rawtypes") RedisTemplate redisTemplate){
RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
//设置缓存过期时间
cacheManager.setDefaultExpiration(1000);
return cacheManager;
}
@Bean
public RedisTemplate<String,String> redisTemplate(RedisConnectionFactory factory){
StringRedisTemplate template = new StringRedisTemplate(factory);
setSerializer(template);//设置序列化工具
template.afterPropertiesSet();
return template;
}
private void setSerializer(StringRedisTemplate template){
@SuppressWarnings({ "rawtypes", "unchecked" })
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setValueSerializer(jackson2JsonRedisSerializer);
}
}
四、Controller/Service/Mapper
@RestController
public class HelloController{
@Autowired
private UserService userService;
@RequestMapping("/adduser")
public int addUser(@RequestParam("name")String name,@RequestParam("age")String age){
return userService.addUser(name, age);
}
@RequestMapping("/findUser")
public User findUser(@RequestParam("id") String id){
return userService.findById(id);
}
@RequestMapping("/updataById")
public String updataById(@RequestParam("id") String id,@RequestParam("name") String name){
try {
userService.updataById(id, name);
} catch (Exception e) {
return "error";
}
return "success";
}
@RequestMapping("/deleteById")
public String deleteById(@RequestParam("id") String id){
try {
userService.deleteById(id);
} catch (Exception e) {
return "error";
}
return "success";
}
}
@Service
public class UserService{
@Autowired
private UserMapper userMapper;
public User findById(String id){
return userMapper.findById(id);
}
public int addUser(String name,String age){
return userMapper.addUser(name,age);
}
public void updataById(String id,String name){
userMapper.updataById(id,name);
}
public void deleteById(String id){
userMapper.deleteById(id);
}
}
- @Cacheable将查询结果缓存到redis中,(key=“#p0”)指定传入的第一个参数作为redis的key
- @CachePut,指定key,将更新的结果同步到redis中
- @CacheEvict,指定key,删除缓存数据,allEntries=true,方法调用后将立即清除缓存
@Mapper
@CacheConfig(cacheNames = "users")
public interface UserMapper{
@Insert("insert into user(name,age) values(#{name},#{age})")
int addUser(@Param("name") String name,@Param("age") String age);
@Select("select * from user where id = #{id}")
@Cacheable(key = "#p0")
User findById(@Param("id") String id);
@CachePut(key = "#p0")
@Update("update user set name = #{name} where id = #{id}")
void updateById(@Param("id")String id,@Param("name")String name);
@CacheEvict(key = "#p0",allEntries=true)
@Delete("delete from user where id=#{id}")
void deleteById(@Param("id") String id);
}
验证:
向user表总插入一条数据,数据库显示如下:
查询一下user表中id=24的数据,观擦控制台输出的信息,如下:
通过控制台输出信息我们可以知道,这次执行了数据库查询,并开启了Redis缓存查询结果。接下来我们再次查询user表中id=24的数据,观察控制台,如下:
通过控制台输出信息我们可以知道,这次并没有执行数据库查询,而是从Redis缓存中查询,并返回查询结果。我们查看redis中的信息,如下:
方法finduser方法使用了注解@Cacheable(key=“#p0”),即将id作为redis中的key值。当我们更新数据的时候,应该使用@CachePut(key=“#p0”)进行缓存数据的更新,否则将查询到脏数据。